@swarmvaultai/engine 0.1.30 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -184,6 +184,7 @@ This matters because many "OpenAI-compatible" backends only implement part of th
184
184
 
185
185
  - `compileVault(rootDir, { approve })` writes wiki pages, graph data, and search state using the vault schema as guidance, or stages a review bundle
186
186
  - compile also writes graph orientation pages such as `wiki/graph/report.md`, `wiki/graph/report.json`, and `wiki/graph/communities/<community>.md`
187
+ - compile propagates semantic tags onto page frontmatter and source-backed graph nodes, and records deterministic `contradicts` edges plus a Contradictions section in the graph report when conflicting claims are found
187
188
  - `benchmarkVault(rootDir, { questions })` writes `state/benchmark.json` and folds the latest benchmark summary into `wiki/graph/report.md` and `wiki/graph/report.json`
188
189
  - semantic graph query and embedding-backed similarity enrichment cache vectors under `state/embeddings.json` so graph-semantic refresh stays incremental
189
190
  - `queryVault(rootDir, { question, save, format, review })` answers against the compiled vault using the same schema layer and saves by default
@@ -207,7 +208,7 @@ This matters because many "OpenAI-compatible" backends only implement part of th
207
208
  - `syncTrackedReposForWatch(rootDir)` is the repo-watch sync path that defers non-code semantic refresh into `state/watch/`
208
209
  - `installGitHooks(rootDir)`, `uninstallGitHooks(rootDir)`, and `getGitHookStatus(rootDir)` manage local `post-commit` and `post-checkout` hook blocks for the nearest git repository
209
210
  - `installAgent(rootDir, agent, { hook })` writes agent instructions and returns the primary `target`, all touched `targets`, and optional merge warnings for agents such as Aider
210
- - `lintVault(rootDir, options)` runs structural lint, optional deep lint, and optional web-augmented evidence gathering
211
+ - `lintVault(rootDir, options)` runs structural lint, optional deep lint, optional contradiction-only filtering through `{ conflicts: true }`, and optional web-augmented evidence gathering
211
212
  - `listSchedules(rootDir)`, `runSchedule(rootDir, jobId)`, and `serveSchedules(rootDir)` manage recurring local jobs from config
212
213
  - compile, query, explore, lint, and watch also write canonical markdown session artifacts to `state/sessions/`
213
214
  - scheduled `query` and `explore` jobs stage saved outputs through approvals when they write artifacts
package/dist/index.d.ts CHANGED
@@ -386,6 +386,7 @@ interface SourceAnalysis {
386
386
  entities: AnalyzedTerm[];
387
387
  claims: SourceClaim[];
388
388
  questions: string[];
389
+ tags: string[];
389
390
  rationales: SourceRationale[];
390
391
  code?: CodeAnalysis;
391
392
  producedAt: string;
@@ -407,6 +408,7 @@ interface GraphNode {
407
408
  degree?: number;
408
409
  bridgeScore?: number;
409
410
  isGodNode?: boolean;
411
+ tags?: string[];
410
412
  }
411
413
  interface GraphEdge {
412
414
  id: string;
@@ -549,6 +551,8 @@ interface ApprovalSummary {
549
551
  interface ApprovalEntryDetail extends ApprovalEntry {
550
552
  currentContent?: string;
551
553
  stagedContent?: string;
554
+ changeSummary?: string;
555
+ diff?: string;
552
556
  }
553
557
  interface ApprovalDetail extends ApprovalSummary {
554
558
  entries: ApprovalEntryDetail[];
@@ -731,6 +735,7 @@ interface CompileState {
731
735
  interface LintOptions {
732
736
  deep?: boolean;
733
737
  web?: boolean;
738
+ conflicts?: boolean;
734
739
  }
735
740
  interface ExploreOptions {
736
741
  question: string;
@@ -992,6 +997,13 @@ interface GraphReportArtifact {
992
997
  sourceType: SourceCaptureType;
993
998
  updatedAt: string;
994
999
  }>;
1000
+ contradictions: Array<{
1001
+ sourceIdA: string;
1002
+ sourceIdB: string;
1003
+ claimA: string;
1004
+ claimB: string;
1005
+ confidenceDelta: number;
1006
+ }>;
995
1007
  }
996
1008
  interface ScheduledCompileTask {
997
1009
  type: "compile";
@@ -1166,7 +1178,9 @@ declare function loadVaultSchemas(rootDir: string): Promise<LoadedVaultSchemas>;
1166
1178
  declare function loadVaultSchema(rootDir: string): Promise<VaultSchema>;
1167
1179
 
1168
1180
  declare function listApprovals(rootDir: string): Promise<ApprovalSummary[]>;
1169
- declare function readApproval(rootDir: string, approvalId: string): Promise<ApprovalDetail>;
1181
+ declare function readApproval(rootDir: string, approvalId: string, options?: {
1182
+ diff?: boolean;
1183
+ }): Promise<ApprovalDetail>;
1170
1184
  declare function acceptApproval(rootDir: string, approvalId: string, targets?: string[]): Promise<ReviewActionResult>;
1171
1185
  declare function rejectApproval(rootDir: string, approvalId: string, targets?: string[]): Promise<ReviewActionResult>;
1172
1186
  declare function listCandidates(rootDir: string): Promise<CandidateRecord[]>;
package/dist/index.js CHANGED
@@ -3910,7 +3910,7 @@ async function analyzeCodeSource(manifest, extractedText, schemaHash) {
3910
3910
  const language = manifest.language ?? inferCodeLanguage(manifest.originalPath ?? manifest.storedPath, manifest.mimeType) ?? "typescript";
3911
3911
  const { code, rationales } = language === "javascript" || language === "jsx" || language === "typescript" || language === "tsx" ? analyzeTypeScriptLikeCode(manifest, extractedText) : await analyzeTreeSitterCode(manifest, extractedText, language);
3912
3912
  return {
3913
- analysisVersion: 5,
3913
+ analysisVersion: 6,
3914
3914
  sourceId: manifest.sourceId,
3915
3915
  sourceHash: manifest.contentHash,
3916
3916
  extractionHash: manifest.extractionHash,
@@ -3921,6 +3921,7 @@ async function analyzeCodeSource(manifest, extractedText, schemaHash) {
3921
3921
  entities: [],
3922
3922
  claims: codeClaims(manifest, code),
3923
3923
  questions: codeQuestions(manifest, code),
3924
+ tags: [],
3924
3925
  rationales,
3925
3926
  code,
3926
3927
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -6323,7 +6324,7 @@ import { z as z7 } from "zod";
6323
6324
  // src/analysis.ts
6324
6325
  import path13 from "path";
6325
6326
  import { z as z2 } from "zod";
6326
- var ANALYSIS_FORMAT_VERSION = 5;
6327
+ var ANALYSIS_FORMAT_VERSION = 6;
6327
6328
  var sourceAnalysisSchema = z2.object({
6328
6329
  title: z2.string().min(1),
6329
6330
  summary: z2.string().min(1),
@@ -6338,7 +6339,8 @@ var sourceAnalysisSchema = z2.object({
6338
6339
  citation: z2.string().min(1)
6339
6340
  })
6340
6341
  ).max(8).default([]),
6341
- questions: z2.array(z2.string()).max(6).default([])
6342
+ questions: z2.array(z2.string()).max(6).default([]),
6343
+ tags: z2.array(z2.string()).max(5).default([])
6342
6344
  });
6343
6345
  var STOPWORDS = /* @__PURE__ */ new Set([
6344
6346
  "about",
@@ -6442,6 +6444,7 @@ function heuristicAnalysis(manifest, text, schemaHash) {
6442
6444
  citation: manifest.sourceId
6443
6445
  })),
6444
6446
  questions: concepts.slice(0, 3).map((term) => `How does ${term.name} relate to ${manifest.title}?`),
6447
+ tags: [],
6445
6448
  rationales: [],
6446
6449
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
6447
6450
  };
@@ -6454,6 +6457,8 @@ async function providerAnalysis(manifest, text, provider, schema) {
6454
6457
  "",
6455
6458
  "Follow the vault schema when choosing titles, categories, relationships, and summaries.",
6456
6459
  "",
6460
+ "Return up to 5 broad domain tags that categorize this source. Tags should be lowercase kebab-case (e.g., cryptography, distributed-systems, machine-learning). These are broader categories, not specific concepts or entity names.",
6461
+ "",
6457
6462
  `Vault schema path: ${schema.path}`,
6458
6463
  "",
6459
6464
  "Vault schema instructions:",
@@ -6497,6 +6502,7 @@ ${truncate(text, 18e3)}`
6497
6502
  citation: claim.citation
6498
6503
  })),
6499
6504
  questions: parsed.questions,
6505
+ tags: parsed.tags,
6500
6506
  rationales: [],
6501
6507
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
6502
6508
  };
@@ -6532,6 +6538,7 @@ function analysisFromVisionExtraction(manifest, extraction, schemaHash) {
6532
6538
  citation: manifest.sourceId
6533
6539
  })),
6534
6540
  questions: extraction.vision.questions,
6541
+ tags: [],
6535
6542
  rationales: [],
6536
6543
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
6537
6544
  };
@@ -6571,6 +6578,7 @@ async function analyzeSource(manifest, extractedText, provider, paths, schema) {
6571
6578
  entities: [],
6572
6579
  claims: [],
6573
6580
  questions: [],
6581
+ tags: [],
6574
6582
  rationales: [],
6575
6583
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
6576
6584
  };
@@ -6596,6 +6604,7 @@ async function analyzeSource(manifest, extractedText, provider, paths, schema) {
6596
6604
  entities: [],
6597
6605
  claims: [],
6598
6606
  questions: [],
6607
+ tags: [],
6599
6608
  rationales: [],
6600
6609
  producedAt: (/* @__PURE__ */ new Date()).toISOString()
6601
6610
  };
@@ -6967,7 +6976,14 @@ var deepLintResponseSchema = z5.object({
6967
6976
  findings: z5.array(
6968
6977
  z5.object({
6969
6978
  severity: z5.string().optional().default("info"),
6970
- code: z5.enum(["coverage_gap", "contradiction_candidate", "missing_citation", "candidate_page", "follow_up_question"]),
6979
+ code: z5.enum([
6980
+ "coverage_gap",
6981
+ "contradiction_candidate",
6982
+ "contradiction",
6983
+ "missing_citation",
6984
+ "candidate_page",
6985
+ "follow_up_question"
6986
+ ]),
6971
6987
  message: z5.string().min(1),
6972
6988
  relatedSourceIds: z5.array(z5.string()).default([]),
6973
6989
  relatedPageIds: z5.array(z5.string()).default([]),
@@ -7100,6 +7116,7 @@ async function runDeepLint(rootDir, structuralFindings, options = {}) {
7100
7116
  system: "You are an auditor for a local-first LLM knowledge vault. Return advisory findings only. Do not propose direct file edits.",
7101
7117
  prompt: [
7102
7118
  "Review this SwarmVault state and return high-signal advisory findings.",
7119
+ "Look for claims that contradict each other across different sources. When you find a genuine contradiction, use code 'contradiction' and include both source IDs in relatedSourceIds.",
7103
7120
  "",
7104
7121
  "Schema:",
7105
7122
  schema.content,
@@ -8962,6 +8979,13 @@ function buildGraphReportArtifact(input) {
8962
8979
  title: page.title,
8963
8980
  sourceType: page.sourceType,
8964
8981
  updatedAt: page.updatedAt
8982
+ })),
8983
+ contradictions: (input.contradictions ?? []).map((c) => ({
8984
+ sourceIdA: c.sourceIdA,
8985
+ sourceIdB: c.sourceIdB,
8986
+ claimA: c.claimA.text,
8987
+ claimB: c.claimB.text,
8988
+ confidenceDelta: Math.abs(c.claimA.confidence - c.claimB.confidence)
8965
8989
  }))
8966
8990
  };
8967
8991
  }
@@ -9081,6 +9105,12 @@ function buildGraphReportPage(input) {
9081
9105
  return `- ${sourceLabel} ${connection.relation} ${targetLabel} (${connection.evidenceClass}, ${connection.confidence.toFixed(2)}). Why: ${connection.why}. ${connection.explanation} Path: ${connection.pathSummary}.`;
9082
9106
  }) : ["- No cross-community links detected."],
9083
9107
  "",
9108
+ "## Contradictions",
9109
+ "",
9110
+ ...input.report.contradictions.length ? input.report.contradictions.map(
9111
+ (c) => `- **${c.claimA}** vs **${c.claimB}** (sources: \`${c.sourceIdA}\`, \`${c.sourceIdB}\`, confidence delta: ${c.confidenceDelta.toFixed(2)})`
9112
+ ) : ["- No contradictions detected."],
9113
+ "",
9084
9114
  "## Group Patterns",
9085
9115
  "",
9086
9116
  ...input.report.groupPatterns.length ? input.report.groupPatterns.map((hyperedge) => {
@@ -10983,21 +11013,64 @@ function buildGoPackageSymbolLookups(analyses, manifestsById) {
10983
11013
  ])
10984
11014
  );
10985
11015
  }
11016
+ function claimTokens(text) {
11017
+ return new Set(
11018
+ text.toLowerCase().match(/[a-z][a-z0-9-]{2,}/g)?.filter((t) => !(/* @__PURE__ */ new Set(["the", "and", "for", "that", "this", "with", "are", "was", "from", "has", "not", "all", "but"])).has(t)) ?? []
11019
+ );
11020
+ }
11021
+ function claimJaccardSimilarity(a, b) {
11022
+ if (a.size === 0 && b.size === 0) return 0;
11023
+ let intersection = 0;
11024
+ for (const token of a) {
11025
+ if (b.has(token)) intersection++;
11026
+ }
11027
+ return intersection / (a.size + b.size - intersection);
11028
+ }
11029
+ function detectContradictions(analyses) {
11030
+ const contradictions = [];
11031
+ const claimsWithTokens = analyses.flatMap(
11032
+ (analysis) => analysis.claims.filter((c) => c.polarity === "positive" || c.polarity === "negative").map((c) => ({ sourceId: analysis.sourceId, claim: c, tokens: claimTokens(c.text) }))
11033
+ );
11034
+ for (let i = 0; i < claimsWithTokens.length; i++) {
11035
+ for (let j = i + 1; j < claimsWithTokens.length; j++) {
11036
+ const a = claimsWithTokens[i];
11037
+ const b = claimsWithTokens[j];
11038
+ if (a.sourceId === b.sourceId) continue;
11039
+ if (a.claim.polarity === b.claim.polarity) continue;
11040
+ const similarity = claimJaccardSimilarity(a.tokens, b.tokens);
11041
+ if (similarity >= 0.3) {
11042
+ contradictions.push({
11043
+ sourceIdA: a.sourceId,
11044
+ sourceIdB: b.sourceId,
11045
+ claimA: { text: a.claim.text, confidence: a.claim.confidence },
11046
+ claimB: { text: b.claim.text, confidence: b.claim.confidence },
11047
+ similarity
11048
+ });
11049
+ }
11050
+ }
11051
+ }
11052
+ return contradictions;
11053
+ }
10986
11054
  function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
10987
11055
  const manifestsById = new Map(manifests.map((manifest) => [manifest.sourceId, manifest]));
10988
11056
  const goPackageSymbolLookups = buildGoPackageSymbolLookups(analyses, manifestsById);
10989
- const sourceNodes = manifests.map((manifest) => ({
10990
- id: `source:${manifest.sourceId}`,
10991
- type: "source",
10992
- label: manifest.title,
10993
- pageId: `source:${manifest.sourceId}`,
10994
- freshness: "fresh",
10995
- confidence: 1,
10996
- sourceIds: [manifest.sourceId],
10997
- projectIds: scopedProjectIdsFromSources([manifest.sourceId], sourceProjects),
10998
- sourceClass: manifest.sourceClass,
10999
- language: manifest.language
11000
- }));
11057
+ const analysesBySourceId = new Map(analyses.map((analysis) => [analysis.sourceId, analysis]));
11058
+ const sourceNodes = manifests.map((manifest) => {
11059
+ const analysis = analysesBySourceId.get(manifest.sourceId);
11060
+ return {
11061
+ id: `source:${manifest.sourceId}`,
11062
+ type: "source",
11063
+ label: manifest.title,
11064
+ pageId: `source:${manifest.sourceId}`,
11065
+ freshness: "fresh",
11066
+ confidence: 1,
11067
+ sourceIds: [manifest.sourceId],
11068
+ projectIds: scopedProjectIdsFromSources([manifest.sourceId], sourceProjects),
11069
+ sourceClass: manifest.sourceClass,
11070
+ language: manifest.language,
11071
+ tags: analysis?.tags ?? []
11072
+ };
11073
+ });
11001
11074
  const conceptMap = /* @__PURE__ */ new Map();
11002
11075
  const entityMap = /* @__PURE__ */ new Map();
11003
11076
  const moduleMap = /* @__PURE__ */ new Map();
@@ -11012,7 +11085,6 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
11012
11085
  edgesById.add(edge.id);
11013
11086
  edges.push(edge);
11014
11087
  };
11015
- const analysesBySourceId = new Map(analyses.map((analysis) => [analysis.sourceId, analysis]));
11016
11088
  for (const analysis of analyses) {
11017
11089
  for (const concept of analysis.concepts) {
11018
11090
  const existing = conceptMap.get(concept.id);
@@ -11362,7 +11434,7 @@ function recentResearchSourcePages(graph, previousCompiledAt) {
11362
11434
  sourceType: page.sourceType
11363
11435
  }));
11364
11436
  }
11365
- async function buildGraphOrientationPages(graph, paths, schemaHash, previousCompiledAt) {
11437
+ async function buildGraphOrientationPages(graph, paths, schemaHash, previousCompiledAt, contradictions = []) {
11366
11438
  const benchmark = await readJsonFile(paths.benchmarkPath);
11367
11439
  const communityRecords = [];
11368
11440
  for (const community of graph.communities ?? []) {
@@ -11392,7 +11464,8 @@ async function buildGraphOrientationPages(graph, paths, schemaHash, previousComp
11392
11464
  benchmark,
11393
11465
  benchmarkStale: benchmark ? benchmark.graphHash !== graphHash(graph) : false,
11394
11466
  recentResearchSources: recentResearchSourcePages(graph, previousCompiledAt),
11395
- graphHash: graphHash(graph)
11467
+ graphHash: graphHash(graph),
11468
+ contradictions
11396
11469
  });
11397
11470
  const reportAbsolutePath = path21.join(paths.wikiDir, "graph", "report.md");
11398
11471
  const reportRecord = await buildManagedGraphPage(
@@ -11656,7 +11729,7 @@ async function syncVaultArtifacts(rootDir, input) {
11656
11729
  modulePreview ?? void 0,
11657
11730
  {
11658
11731
  projectIds: sourceProjectIds,
11659
- extraTags: sourceCategoryTags,
11732
+ extraTags: [...sourceCategoryTags, ...analysis.tags ?? []],
11660
11733
  sourceClass: manifest.sourceClass
11661
11734
  }
11662
11735
  )
@@ -11702,7 +11775,7 @@ async function syncVaultArtifacts(rootDir, input) {
11702
11775
  localModules,
11703
11776
  relatedOutputs: relatedOutputsForPage(modulePreview, input.outputPages),
11704
11777
  projectIds: sourceProjectIds,
11705
- extraTags: sourceCategoryTags
11778
+ extraTags: [...sourceCategoryTags, ...analysis.tags ?? []]
11706
11779
  })
11707
11780
  )
11708
11781
  );
@@ -11782,6 +11855,22 @@ async function syncVaultArtifacts(rootDir, input) {
11782
11855
  const compiledPages = records.map((record) => record.page);
11783
11856
  const basePages = [...compiledPages, ...input.outputPages, ...input.insightPages];
11784
11857
  const structuralGraph = buildGraph(input.manifests, input.analyses, basePages, input.sourceProjects, input.codeIndex);
11858
+ const contradictions = detectContradictions(input.analyses);
11859
+ for (const contradiction of contradictions) {
11860
+ const edgeId = `contradiction:${contradiction.sourceIdA}->${contradiction.sourceIdB}`;
11861
+ if (!structuralGraph.edges.some((e) => e.id === edgeId)) {
11862
+ structuralGraph.edges.push({
11863
+ id: edgeId,
11864
+ source: `source:${contradiction.sourceIdA}`,
11865
+ target: `source:${contradiction.sourceIdB}`,
11866
+ relation: "contradicts",
11867
+ status: "conflicted",
11868
+ evidenceClass: "ambiguous",
11869
+ confidence: Math.abs(contradiction.claimA.confidence - contradiction.claimB.confidence),
11870
+ provenance: [contradiction.sourceIdA, contradiction.sourceIdB]
11871
+ });
11872
+ }
11873
+ }
11785
11874
  const embeddingEdges = await embeddingSimilarityEdges(rootDir, structuralGraph).catch(() => []);
11786
11875
  const baseGraph = embeddingEdges.length > 0 ? (() => {
11787
11876
  const edges = uniqueBy([...structuralGraph.edges, ...embeddingEdges], (edge) => edge.id).sort(
@@ -11795,7 +11884,13 @@ async function syncVaultArtifacts(rootDir, input) {
11795
11884
  communities: metrics.communities
11796
11885
  };
11797
11886
  })() : structuralGraph;
11798
- const graphOrientation = await buildGraphOrientationPages(baseGraph, paths, globalSchemaHash, input.previousState?.generatedAt);
11887
+ const graphOrientation = await buildGraphOrientationPages(
11888
+ baseGraph,
11889
+ paths,
11890
+ globalSchemaHash,
11891
+ input.previousState?.generatedAt,
11892
+ contradictions
11893
+ );
11799
11894
  records.push(...graphOrientation.records);
11800
11895
  const allPages = [...basePages, ...graphOrientation.records.map((record) => record.page)];
11801
11896
  const graph = {
@@ -12466,6 +12561,58 @@ function updateCandidateHistory(compileState, page, deleted = false) {
12466
12561
  function sortGraphPages(pages) {
12467
12562
  return [...pages].sort((left, right) => left.path.localeCompare(right.path) || left.title.localeCompare(right.title));
12468
12563
  }
12564
+ function computeUnifiedDiff(current, staged, label) {
12565
+ const currentLines = current.split("\n");
12566
+ const stagedLines = staged.split("\n");
12567
+ const output = [`--- a/${label}`, `+++ b/${label}`];
12568
+ const n = currentLines.length;
12569
+ const m = stagedLines.length;
12570
+ const dp = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
12571
+ for (let i2 = n - 1; i2 >= 0; i2--) {
12572
+ for (let j2 = m - 1; j2 >= 0; j2--) {
12573
+ dp[i2][j2] = currentLines[i2] === stagedLines[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
12574
+ }
12575
+ }
12576
+ let i = 0;
12577
+ let j = 0;
12578
+ while (i < n || j < m) {
12579
+ if (i < n && j < m && currentLines[i] === stagedLines[j]) {
12580
+ output.push(` ${currentLines[i]}`);
12581
+ i++;
12582
+ j++;
12583
+ } else if (j < m && (i >= n || dp[i][j + 1] >= dp[i + 1][j])) {
12584
+ output.push(`+${stagedLines[j]}`);
12585
+ j++;
12586
+ } else {
12587
+ output.push(`-${currentLines[i]}`);
12588
+ i++;
12589
+ }
12590
+ }
12591
+ return output.join("\n");
12592
+ }
12593
+ function computeChangeSummary(current, staged, changeType) {
12594
+ if (changeType === "create") return "New page";
12595
+ if (changeType === "delete") return "Removed page";
12596
+ if (changeType === "promote") return "Promoted from candidate";
12597
+ if (!current || !staged) return "Updated page";
12598
+ const currentParsed = matter9(current);
12599
+ const stagedParsed = matter9(staged);
12600
+ const changes = [];
12601
+ const currentTags = currentParsed.data.tags ?? [];
12602
+ const stagedTags = stagedParsed.data.tags ?? [];
12603
+ const addedTags = stagedTags.filter((t) => !currentTags.includes(t));
12604
+ const removedTags = currentTags.filter((t) => !stagedTags.includes(t));
12605
+ if (addedTags.length) changes.push(`added ${addedTags.length} tag(s)`);
12606
+ if (removedTags.length) changes.push(`removed ${removedTags.length} tag(s)`);
12607
+ if (currentParsed.data.title !== stagedParsed.data.title) changes.push("updated title");
12608
+ const currentLines = currentParsed.content.trim().split("\n").length;
12609
+ const stagedLines = stagedParsed.content.trim().split("\n").length;
12610
+ const lineDelta = stagedLines - currentLines;
12611
+ if (lineDelta > 0) changes.push(`added ${lineDelta} line(s)`);
12612
+ else if (lineDelta < 0) changes.push(`removed ${Math.abs(lineDelta)} line(s)`);
12613
+ else if (currentParsed.content !== stagedParsed.content) changes.push("modified content");
12614
+ return changes.length ? changes.join(", ") : "no visible changes";
12615
+ }
12469
12616
  async function listApprovals(rootDir) {
12470
12617
  const { paths } = await loadVaultConfig(rootDir);
12471
12618
  const manifests = await Promise.all(
@@ -12479,7 +12626,7 @@ async function listApprovals(rootDir) {
12479
12626
  );
12480
12627
  return manifests.filter((manifest) => Boolean(manifest)).map(approvalSummary).sort((left, right) => right.createdAt.localeCompare(left.createdAt));
12481
12628
  }
12482
- async function readApproval(rootDir, approvalId) {
12629
+ async function readApproval(rootDir, approvalId, options) {
12483
12630
  const { paths } = await loadVaultConfig(rootDir);
12484
12631
  const manifest = await readApprovalManifest(paths, approvalId);
12485
12632
  const details = await Promise.all(
@@ -12487,11 +12634,16 @@ async function readApproval(rootDir, approvalId) {
12487
12634
  const currentPath = entry.previousPath ?? entry.nextPath;
12488
12635
  const currentContent = currentPath ? await fs17.readFile(path21.join(paths.wikiDir, currentPath), "utf8").catch(() => void 0) : void 0;
12489
12636
  const stagedContent = entry.nextPath ? await fs17.readFile(path21.join(paths.approvalsDir, approvalId, "wiki", entry.nextPath), "utf8").catch(() => void 0) : void 0;
12490
- return {
12637
+ const detail = {
12491
12638
  ...entry,
12492
12639
  currentContent,
12493
12640
  stagedContent
12494
12641
  };
12642
+ detail.changeSummary = computeChangeSummary(detail.currentContent, detail.stagedContent, detail.changeType);
12643
+ if (options?.diff && detail.currentContent && detail.stagedContent) {
12644
+ detail.diff = computeUnifiedDiff(detail.currentContent, detail.stagedContent, detail.nextPath ?? detail.pageId);
12645
+ }
12646
+ return detail;
12495
12647
  })
12496
12648
  );
12497
12649
  return {
@@ -13687,11 +13839,43 @@ async function lintVault(rootDir, options = {}) {
13687
13839
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13688
13840
  success: true,
13689
13841
  lintFindingCount: findings2.length,
13690
- lines: [`findings=${findings2.length}`, `deep=${Boolean(options.deep)}`, `web=${Boolean(options.web)}`]
13842
+ lines: [
13843
+ `findings=${findings2.length}`,
13844
+ `deep=${Boolean(options.deep)}`,
13845
+ `web=${Boolean(options.web)}`,
13846
+ `conflicts=${Boolean(options.conflicts)}`
13847
+ ]
13691
13848
  });
13692
13849
  return findings2;
13693
13850
  }
13851
+ const contradictionFindings = options.conflicts ? graph.edges.filter((edge) => edge.relation === "contradicts").map((edge) => {
13852
+ const sourceIdA = edge.provenance[0] ?? edge.source.replace(/^source:/, "");
13853
+ const sourceIdB = edge.provenance[1] ?? edge.target.replace(/^source:/, "");
13854
+ return {
13855
+ severity: "warning",
13856
+ code: "contradiction",
13857
+ message: `Contradicting claims detected between source "${sourceIdA}" and source "${sourceIdB}".`,
13858
+ relatedSourceIds: [sourceIdA, sourceIdB]
13859
+ };
13860
+ }) : [];
13861
+ if (options.conflicts && !options.deep) {
13862
+ await recordSession(rootDir, {
13863
+ operation: "lint",
13864
+ title: `Linted ${graph.pages.length} page(s)`,
13865
+ startedAt,
13866
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13867
+ success: true,
13868
+ relatedPageIds: graph.pages.map((page) => page.id),
13869
+ relatedSourceIds: uniqueStrings3(graph.pages.flatMap((page) => page.sourceIds)),
13870
+ lintFindingCount: contradictionFindings.length,
13871
+ lines: [`findings=${contradictionFindings.length}`, `deep=false`, `web=false`, `conflicts=true`]
13872
+ });
13873
+ return contradictionFindings;
13874
+ }
13694
13875
  const findings = await structuralLintFindings(rootDir, paths, graph, schemas, manifests, sourceProjects);
13876
+ if (options.conflicts) {
13877
+ findings.push(...contradictionFindings);
13878
+ }
13695
13879
  if (options.deep) {
13696
13880
  findings.push(...await runDeepLint(rootDir, findings, { web: options.web }));
13697
13881
  }
@@ -13706,7 +13890,12 @@ async function lintVault(rootDir, options = {}) {
13706
13890
  relatedPageIds: graph.pages.map((page) => page.id),
13707
13891
  relatedSourceIds: uniqueStrings3(graph.pages.flatMap((page) => page.sourceIds)),
13708
13892
  lintFindingCount: findings.length,
13709
- lines: [`findings=${findings.length}`, `deep=${Boolean(options.deep)}`, `web=${Boolean(options.web)}`]
13893
+ lines: [
13894
+ `findings=${findings.length}`,
13895
+ `deep=${Boolean(options.deep)}`,
13896
+ `web=${Boolean(options.web)}`,
13897
+ `conflicts=${Boolean(options.conflicts)}`
13898
+ ]
13710
13899
  });
13711
13900
  return findings;
13712
13901
  }
@@ -13724,7 +13913,7 @@ async function bootstrapDemo(rootDir, input) {
13724
13913
  }
13725
13914
 
13726
13915
  // src/mcp.ts
13727
- var SERVER_VERSION = "0.1.30";
13916
+ var SERVER_VERSION = "0.1.31";
13728
13917
  async function createMcpServer(rootDir) {
13729
13918
  const server = new McpServer({
13730
13919
  name: "swarmvault",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmvaultai/engine",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "description": "Core engine for SwarmVault: ingest, compile, query, lint, and provider abstractions.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",