kirograph 1.0.0 → 1.1.1

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.
Files changed (84) hide show
  1. package/README.md +3 -2
  2. package/dist/architecture/manifest/python.js +2 -2
  3. package/dist/architecture/manifest/python.js.map +2 -2
  4. package/dist/bin/commands/help.js +3 -1
  5. package/dist/bin/commands/help.js.map +2 -2
  6. package/dist/bin/commands/memory.js +18 -8
  7. package/dist/bin/commands/memory.js.map +2 -2
  8. package/dist/bin/commands/reachability.js +7 -2
  9. package/dist/bin/commands/reachability.js.map +2 -2
  10. package/dist/bin/commands/vulns.js +16 -1
  11. package/dist/bin/commands/vulns.js.map +2 -2
  12. package/dist/bin/commands/wiki.js +56 -0
  13. package/dist/bin/commands/wiki.js.map +2 -2
  14. package/dist/bin/installer/steering.js +9 -0
  15. package/dist/bin/installer/steering.js.map +2 -2
  16. package/dist/bin/kirograph.js +1 -1
  17. package/dist/config.js +4 -0
  18. package/dist/config.js.map +2 -2
  19. package/dist/extraction/extractor.js +46 -41
  20. package/dist/extraction/extractor.js.map +2 -2
  21. package/dist/extraction/notebook.js +15 -7
  22. package/dist/extraction/notebook.js.map +2 -2
  23. package/dist/jev/client.js +99 -0
  24. package/dist/jev/client.js.map +7 -0
  25. package/dist/jev/types.js +17 -0
  26. package/dist/jev/types.js.map +7 -0
  27. package/dist/mcp/handlers/memory.js +14 -8
  28. package/dist/mcp/handlers/memory.js.map +2 -2
  29. package/dist/mcp/handlers/security.js +5 -3
  30. package/dist/mcp/handlers/security.js.map +2 -2
  31. package/dist/mcp/tools.js +1 -1
  32. package/dist/mcp/tools.js.map +1 -1
  33. package/dist/memory/index.js +13 -1
  34. package/dist/memory/index.js.map +3 -3
  35. package/dist/memory/relation-jev.js +59 -0
  36. package/dist/memory/relation-jev.js.map +7 -0
  37. package/dist/memory/schema.js +57 -0
  38. package/dist/memory/schema.js.map +7 -0
  39. package/dist/security/auth-detection-jev.js +47 -0
  40. package/dist/security/auth-detection-jev.js.map +7 -0
  41. package/dist/security/manifest/adapter.js +17 -3
  42. package/dist/security/manifest/adapter.js.map +3 -3
  43. package/dist/security/manifest/plugins/cargo.js +23 -0
  44. package/dist/security/manifest/plugins/cargo.js.map +2 -2
  45. package/dist/security/manifest/plugins/composer.js +12 -0
  46. package/dist/security/manifest/plugins/composer.js.map +2 -2
  47. package/dist/security/manifest/plugins/gradle.js +12 -0
  48. package/dist/security/manifest/plugins/gradle.js.map +2 -2
  49. package/dist/security/manifest/plugins/hex.js +12 -0
  50. package/dist/security/manifest/plugins/hex.js.map +2 -2
  51. package/dist/security/manifest/plugins/maven.js +68 -1
  52. package/dist/security/manifest/plugins/maven.js.map +2 -2
  53. package/dist/security/manifest/plugins/npm.js +86 -0
  54. package/dist/security/manifest/plugins/npm.js.map +2 -2
  55. package/dist/security/manifest/plugins/nuget.js +13 -0
  56. package/dist/security/manifest/plugins/nuget.js.map +2 -2
  57. package/dist/security/manifest/plugins/pubspec.js +12 -0
  58. package/dist/security/manifest/plugins/pubspec.js.map +2 -2
  59. package/dist/security/manifest/plugins/pyproject.js +12 -0
  60. package/dist/security/manifest/plugins/pyproject.js.map +2 -2
  61. package/dist/security/manifest/plugins/rubygems.js +12 -0
  62. package/dist/security/manifest/plugins/rubygems.js.map +2 -2
  63. package/dist/security/pipeline.js +1 -1
  64. package/dist/security/pipeline.js.map +2 -2
  65. package/dist/security/reachability.js +54 -5
  66. package/dist/security/reachability.js.map +3 -3
  67. package/dist/security/types.js.map +1 -1
  68. package/dist/security/vuln/client.js +45 -1
  69. package/dist/security/vuln/client.js.map +2 -2
  70. package/dist/security/vuln/osv-adapter.js +45 -1
  71. package/dist/security/vuln/osv-adapter.js.map +2 -2
  72. package/dist/wiki/contradiction-jev.js +49 -0
  73. package/dist/wiki/contradiction-jev.js.map +7 -0
  74. package/dist/wiki/index.js +13 -0
  75. package/dist/wiki/index.js.map +2 -2
  76. package/dist/wiki/ingest.js +2 -0
  77. package/dist/wiki/ingest.js.map +2 -2
  78. package/dist/wiki/links.js +53 -0
  79. package/dist/wiki/links.js.map +7 -0
  80. package/dist/wiki/lint.js +3 -6
  81. package/dist/wiki/lint.js.map +2 -2
  82. package/dist/wiki/rename.js +103 -0
  83. package/dist/wiki/rename.js.map +7 -0
  84. package/package.json +1 -1
@@ -49,8 +49,10 @@ const ECOSYSTEM_MAP = {
49
49
  };
50
50
  const OSV_API_URL = "https://api.osv.dev/v1/query";
51
51
  const OSV_BATCH_API_URL = "https://api.osv.dev/v1/querybatch";
52
+ const OSV_VULN_DETAIL_URL = "https://api.osv.dev/v1/vulns";
52
53
  const DEFAULT_TIMEOUT_MS = 3e4;
53
54
  const OSV_BATCH_MAX_QUERIES = 1e3;
55
+ const OSV_DETAIL_FETCH_CONCURRENCY = 10;
54
56
  const MAX_SUMMARY_LENGTH = 500;
55
57
  function parseCvssScore(scoreStr) {
56
58
  if (!scoreStr) return null;
@@ -120,6 +122,7 @@ class OsvAdapter {
120
122
  this.name = "OSV";
121
123
  this.apiUrl = options?.apiUrl ?? OSV_API_URL;
122
124
  this.batchApiUrl = options?.batchApiUrl ?? OSV_BATCH_API_URL;
125
+ this.vulnDetailUrl = options?.vulnDetailUrl ?? OSV_VULN_DETAIL_URL;
123
126
  this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
124
127
  }
125
128
  /**
@@ -180,14 +183,55 @@ class OsvAdapter {
180
183
  } finally {
181
184
  clearTimeout(timeoutId);
182
185
  }
186
+ const uniqueIds = /* @__PURE__ */ new Set();
187
+ for (const result of batchResults) {
188
+ for (const vuln of result.vulns ?? []) {
189
+ uniqueIds.add(vuln.id);
190
+ }
191
+ }
192
+ const detailsById = await this.fetchVulnDetails([...uniqueIds], combinedSignal);
183
193
  return queries.map((_, i) => {
184
194
  const osvIdx = indexMap[i];
185
195
  if (osvIdx === null) return [];
186
196
  const resultEntry = batchResults[osvIdx];
187
197
  if (!resultEntry) return [];
188
- return this.parseResponse({ vulns: resultEntry.vulns });
198
+ const fullVulns = (resultEntry.vulns ?? []).map((stub) => detailsById.get(stub.id) ?? stub);
199
+ return this.parseResponse({ vulns: fullVulns });
189
200
  });
190
201
  }
202
+ /**
203
+ * Fetch full vulnerability records for a set of IDs via OSV's per-ID detail
204
+ * endpoint (the batch endpoint doesn't include this data). Runs with
205
+ * bounded concurrency; a failure on one ID is logged and that ID falls
206
+ * back to its stub (still usable, just without severity/EPSS-relevant data)
207
+ * rather than failing the whole batch.
208
+ */
209
+ async fetchVulnDetails(ids, signal) {
210
+ const results = /* @__PURE__ */ new Map();
211
+ if (ids.length === 0) return results;
212
+ let cursor = 0;
213
+ const worker = async () => {
214
+ while (cursor < ids.length) {
215
+ const id = ids[cursor++];
216
+ try {
217
+ const response = await fetch(`${this.vulnDetailUrl}/${encodeURIComponent(id)}`, { signal });
218
+ if (!response.ok) {
219
+ (0, import_errors2.logWarn)(`OSV: failed to fetch details for ${id}: HTTP ${response.status}`);
220
+ continue;
221
+ }
222
+ const vuln = await response.json();
223
+ results.set(id, vuln);
224
+ } catch (error) {
225
+ if (isAbortError(error)) return;
226
+ const msg = error instanceof Error ? error.message : String(error);
227
+ (0, import_errors2.logWarn)(`OSV: failed to fetch details for ${id}: ${msg}`);
228
+ }
229
+ }
230
+ };
231
+ const workerCount = Math.min(OSV_DETAIL_FETCH_CONCURRENCY, ids.length);
232
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
233
+ return results;
234
+ }
191
235
  async query(ecosystem, packageName, version, signal) {
192
236
  const osvEcosystem = ECOSYSTEM_MAP[ecosystem.toLowerCase()];
193
237
  if (!osvEcosystem) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/security/vuln/osv-adapter.ts"],
4
- "sourcesContent": ["/**\n * OSV API Adapter\n *\n * Queries the OSV (Open Source Vulnerabilities) database for known CVEs\n * affecting project dependencies. Implements the VulnDatabaseAdapter interface.\n */\n\nimport { CVERecord, VersionRange } from '../types';\nimport type { VulnDatabaseAdapter, BatchQuery } from './types';\nimport { VulnDatabaseError } from '../errors';\nimport { logWarn, logError } from '../../errors';\n\nexport type { VulnDatabaseAdapter, BatchQuery };\n\n// \u2500\u2500 Ecosystem Mapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ECOSYSTEM_MAP: Record<string, string> = {\n npm: 'npm',\n maven: 'Maven',\n go: 'Go',\n pypi: 'PyPI',\n python: 'PyPI', // manifest parser sends 'python' for requirements.txt\n cargo: 'crates.io',\n pyproject: 'PyPI', // pyproject.toml (Poetry/Hatch/PDM/PEP 621)\n nuget: 'NuGet',\n csproj: 'NuGet', // manifest parser sends 'csproj' for .csproj files\n gradle: 'Maven', // Gradle projects use Maven Central\n rubygems: 'RubyGems',\n composer: 'Packagist',\n swift: 'SwiftURL',\n pub: 'Pub',\n hex: 'Hex',\n};\n\n// \u2500\u2500 OSV Request/Response Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface OsvSeverity {\n type: string;\n score: string;\n}\n\ninterface OsvAffectedRange {\n type: string;\n events: Array<{ introduced?: string; fixed?: string; last_affected?: string }>;\n}\n\ninterface OsvAffected {\n package?: { name?: string; ecosystem?: string };\n ranges?: OsvAffectedRange[];\n versions?: string[];\n}\n\ninterface OsvVulnerability {\n id: string;\n aliases?: string[];\n summary?: string;\n details?: string;\n severity?: OsvSeverity[];\n affected?: OsvAffected[];\n /** OSV per-database extras. GHSA records expose a coarse qualitative severity here. */\n database_specific?: { severity?: string;[key: string]: unknown };\n}\n\ninterface OsvQueryResponse {\n vulns?: OsvVulnerability[];\n}\n\ninterface OsvBatchQueryItem {\n package: { name: string; ecosystem: string };\n version: string;\n}\n\ninterface OsvBatchResponse {\n results: Array<{ vulns?: OsvVulnerability[] }>;\n}\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst OSV_API_URL = 'https://api.osv.dev/v1/query';\nconst OSV_BATCH_API_URL = 'https://api.osv.dev/v1/querybatch';\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst OSV_BATCH_MAX_QUERIES = 1000;\nconst MAX_SUMMARY_LENGTH = 500;\n\n// \u2500\u2500 CVSS parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Parse an OSV severity `score` field into a CVSS base score (0.0\u201310.0).\n *\n * The field is either a plain number (rare) or a CVSS vector string such as\n * \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\". For v3.0/3.1 vectors we compute\n * the base score from the metrics per the FIRST CVSS specification. Returns null\n * when the value can't be interpreted.\n */\nexport function parseCvssScore(scoreStr: string | undefined): number | null {\n if (!scoreStr) return null;\n\n const trimmed = scoreStr.trim();\n\n // Plain numeric score.\n const num = parseFloat(trimmed);\n if (!isNaN(num) && String(num) === trimmed && num >= 0 && num <= 10) {\n return num;\n }\n\n // CVSS vector string.\n if (/^CVSS:3\\.[01]\\//i.test(trimmed)) {\n return computeCvssV3BaseScore(trimmed);\n }\n\n return null;\n}\n\n/** Round up to one decimal place, per the CVSS spec's roundup() function. */\nfunction cvssRoundUp(value: number): number {\n const intInput = Math.round(value * 100000);\n if (intInput % 10000 === 0) return intInput / 100000;\n return (Math.floor(intInput / 10000) + 1) / 10;\n}\n\n/**\n * Compute the CVSS v3.0/3.1 base score from a vector string.\n * Implements the base-score equations from the FIRST CVSS v3.1 specification.\n * Returns null if required base metrics are missing.\n */\nexport function computeCvssV3BaseScore(vector: string): number | null {\n const metrics: Record<string, string> = {};\n for (const part of vector.split('/')) {\n const [k, v] = part.split(':');\n if (k && v) metrics[k.toUpperCase()] = v.toUpperCase();\n }\n\n // Required base metrics.\n const AV = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 }[metrics.AV];\n const AC = { L: 0.77, H: 0.44 }[metrics.AC];\n const UI = { N: 0.85, R: 0.62 }[metrics.UI];\n const scopeChanged = metrics.S === 'C';\n // Privileges Required depends on Scope.\n const PR = ((): number | undefined => {\n if (metrics.PR === 'N') return 0.85;\n if (metrics.PR === 'L') return scopeChanged ? 0.68 : 0.62;\n if (metrics.PR === 'H') return scopeChanged ? 0.5 : 0.27;\n return undefined;\n })();\n const impactVal = { H: 0.56, L: 0.22, N: 0.0 };\n const C = impactVal[metrics.C as keyof typeof impactVal];\n const I = impactVal[metrics.I as keyof typeof impactVal];\n const A = impactVal[metrics.A as keyof typeof impactVal];\n\n if ([AV, AC, UI, PR, C, I, A].some(v => v === undefined)) {\n return null;\n }\n\n const iscBase = 1 - (1 - C!) * (1 - I!) * (1 - A!);\n const impact = scopeChanged\n ? 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase - 0.02, 15)\n : 6.42 * iscBase;\n const exploitability = 8.22 * AV! * AC! * PR! * UI!;\n\n if (impact <= 0) return 0.0;\n\n const raw = scopeChanged\n ? Math.min(1.08 * (impact + exploitability), 10)\n : Math.min(impact + exploitability, 10);\n\n return cvssRoundUp(raw);\n}\n\n/**\n * Map a coarse qualitative severity label (GHSA `database_specific.severity`,\n * or a CVSS qualitative rating) to a representative numeric score. Used only as\n * a fallback when no CVSS vector is available. Returns null for unknown labels.\n */\nexport function qualitativeToScore(label: string | undefined): number | null {\n if (!label) return null;\n switch (label.trim().toUpperCase()) {\n case 'CRITICAL': return 9.8;\n case 'HIGH': return 7.5;\n case 'MODERATE':\n case 'MEDIUM': return 5.5;\n case 'LOW': return 3.1;\n default: return null;\n }\n}\n\n// \u2500\u2500 OsvAdapter Implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class OsvAdapter implements VulnDatabaseAdapter {\n public readonly name = 'OSV';\n\n private readonly apiUrl: string;\n private readonly batchApiUrl: string;\n private readonly timeoutMs: number;\n\n constructor(options?: { apiUrl?: string; batchApiUrl?: string; timeoutMs?: number }) {\n this.apiUrl = options?.apiUrl ?? OSV_API_URL;\n this.batchApiUrl = options?.batchApiUrl ?? OSV_BATCH_API_URL;\n this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n }\n\n /**\n * Batch query up to OSV_BATCH_MAX_QUERIES packages in a single HTTP request.\n * Results are returned in the same order as the input queries.\n * Queries for unsupported ecosystems produce empty results without an HTTP call.\n */\n async queryBatch(\n queries: BatchQuery[],\n signal?: AbortSignal,\n ): Promise<Array<CVERecord[]>> {\n if (queries.length === 0) return [];\n\n // Map each query to its OSV ecosystem \u2014 track indices of unsupported ones\n const osvQueries: OsvBatchQueryItem[] = [];\n const indexMap: Array<number | null> = []; // index into osvQueries, or null if unsupported\n\n for (const q of queries) {\n const osvEcosystem = ECOSYSTEM_MAP[q.ecosystem.toLowerCase()];\n if (!osvEcosystem) {\n logWarn(`OSV adapter: unsupported ecosystem \"${q.ecosystem}\", skipping batch entry`);\n indexMap.push(null);\n } else {\n indexMap.push(osvQueries.length);\n osvQueries.push({\n package: { name: q.packageName, ecosystem: osvEcosystem },\n version: q.version,\n });\n }\n }\n\n if (osvQueries.length === 0) {\n return queries.map(() => []);\n }\n\n const timeoutController = new AbortController();\n const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);\n const combinedSignal = signal\n ? combineAbortSignals(signal, timeoutController.signal)\n : timeoutController.signal;\n\n let batchResults: Array<{ vulns?: OsvVulnerability[] }>;\n\n try {\n const response = await fetch(this.batchApiUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ queries: osvQueries }),\n signal: combinedSignal,\n });\n\n if (!response.ok) {\n throw new VulnDatabaseError(\n `OSV batch API returned HTTP ${response.status}: ${response.statusText}`,\n 'OSV',\n response.status,\n );\n }\n\n const data = (await response.json()) as OsvBatchResponse;\n batchResults = data.results ?? [];\n } catch (error: unknown) {\n if (error instanceof VulnDatabaseError) throw error;\n if (isAbortError(error)) {\n const msg = timeoutController.signal.aborted\n ? `OSV batch query timed out after ${this.timeoutMs}ms (${osvQueries.length} packages)`\n : `OSV batch query aborted`;\n logError(msg);\n throw new VulnDatabaseError(msg, 'OSV');\n }\n const msg = error instanceof Error ? error.message : String(error);\n logError(`OSV batch query failed: ${msg}`);\n throw new VulnDatabaseError(`Network error in OSV batch query: ${msg}`, 'OSV');\n } finally {\n clearTimeout(timeoutId);\n }\n\n // Rebuild full results array aligned with input queries\n return queries.map((_, i) => {\n const osvIdx = indexMap[i];\n if (osvIdx === null) return [];\n const resultEntry = batchResults[osvIdx];\n if (!resultEntry) return [];\n return this.parseResponse({ vulns: resultEntry.vulns });\n });\n }\n\n async query(\n ecosystem: string,\n packageName: string,\n version: string,\n signal?: AbortSignal,\n ): Promise<CVERecord[]> {\n const osvEcosystem = ECOSYSTEM_MAP[ecosystem.toLowerCase()];\n if (!osvEcosystem) {\n logWarn(`OSV adapter: unsupported ecosystem \"${ecosystem}\", skipping query`);\n return [];\n }\n\n const body = JSON.stringify({\n package: {\n name: packageName,\n ecosystem: osvEcosystem,\n },\n version,\n });\n\n // Create a timeout abort signal and combine with any external signal\n const timeoutController = new AbortController();\n const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);\n\n const combinedSignal = signal\n ? combineAbortSignals(signal, timeoutController.signal)\n : timeoutController.signal;\n\n try {\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n signal: combinedSignal,\n });\n\n if (!response.ok) {\n throw new VulnDatabaseError(\n `OSV API returned HTTP ${response.status}: ${response.statusText}`,\n 'OSV',\n response.status,\n );\n }\n\n const data = (await response.json()) as OsvQueryResponse;\n return this.parseResponse(data);\n } catch (error: unknown) {\n if (error instanceof VulnDatabaseError) {\n throw error;\n }\n\n if (isAbortError(error)) {\n const isTimeout = timeoutController.signal.aborted;\n const message = isTimeout\n ? `OSV query timed out after ${this.timeoutMs}ms for ${packageName}@${version}`\n : `OSV query aborted for ${packageName}@${version}`;\n logError(message);\n throw new VulnDatabaseError(message, 'OSV');\n }\n\n const errorMessage = error instanceof Error ? error.message : String(error);\n logError(`OSV query failed for ${packageName}@${version}: ${errorMessage}`);\n throw new VulnDatabaseError(\n `Network error querying OSV for ${packageName}@${version}: ${errorMessage}`,\n 'OSV',\n );\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Parse the OSV API response into CVERecord objects.\n */\n private parseResponse(data: OsvQueryResponse): CVERecord[] {\n if (!data.vulns || data.vulns.length === 0) {\n return [];\n }\n\n const records: CVERecord[] = [];\n\n for (const vuln of data.vulns) {\n const cveId = this.extractCveId(vuln);\n if (!cveId) {\n // Skip vulnerabilities without a CVE identifier\n continue;\n }\n\n const severity = this.extractSeverity(vuln);\n const { ranges, fixedVersion } = this.extractAffectedRanges(vuln);\n const summary = this.extractSummary(vuln);\n\n records.push({\n id: cveId,\n severity,\n affectedVersionRanges: ranges,\n fixedVersion,\n summary,\n });\n }\n\n return records;\n }\n\n /**\n * Extract CVE ID from the vulnerability's aliases or use the OSV ID.\n * Prefers CVE-* identifiers from the aliases array.\n */\n private extractCveId(vuln: OsvVulnerability): string | undefined {\n // Look for a CVE alias first\n if (vuln.aliases) {\n const cveAlias = vuln.aliases.find((alias) => alias.startsWith('CVE-'));\n if (cveAlias) {\n return cveAlias;\n }\n }\n\n // Fall back to the OSV ID (e.g., GHSA-xxxx-xxxx-xxxx)\n return vuln.id;\n }\n\n /**\n * Extract a CVSS base score (0.0\u201310.0) for the vulnerability.\n *\n * OSV encodes severity as an array of `{ type, score }`. For CVSS_V3/CVSS_V4\n * the `score` field is a *vector string* (e.g. \"CVSS:3.1/AV:N/AC:L/...\") \u2014 NOT\n * a plain number \u2014 so the base score must be computed from the vector.\n *\n * Resolution order:\n * 1. CVSS_V3 vector \u2192 computed base score\n * 2. CVSS_V4 vector \u2192 computed base score\n * 3. Any severity entry that happens to be a plain number\n * 4. Coarse qualitative `database_specific.severity` (GHSA) \u2192 representative score\n *\n * Returns null when no severity data is available (so callers can distinguish\n * \"unknown\" from a genuine 0.0 score).\n */\n private extractSeverity(vuln: OsvVulnerability): number | null {\n if (vuln.severity && vuln.severity.length > 0) {\n // Prefer CVSS v3, then v4, then any parseable entry.\n const byType = (type: string) =>\n vuln.severity!.filter(s => s.type === type);\n\n for (const sev of [...byType('CVSS_V3'), ...byType('CVSS_V4')]) {\n const score = parseCvssScore(sev.score);\n if (score !== null) return score;\n }\n for (const sev of vuln.severity) {\n const score = parseCvssScore(sev.score);\n if (score !== null) return score;\n }\n }\n\n // Fallback: GHSA advisories expose a coarse qualitative severity here even\n // when a CVSS vector is absent.\n const qualitative = qualitativeToScore(vuln.database_specific?.severity);\n if (qualitative !== null) return qualitative;\n\n return null;\n }\n\n /**\n * Extract affected version ranges and the first fixed version from the vulnerability.\n */\n private extractAffectedRanges(vuln: OsvVulnerability): {\n ranges: VersionRange[];\n fixedVersion?: string;\n } {\n const ranges: VersionRange[] = [];\n let fixedVersion: string | undefined;\n\n if (!vuln.affected) {\n return { ranges, fixedVersion };\n }\n\n for (const affected of vuln.affected) {\n if (!affected.ranges) {\n continue;\n }\n\n for (const range of affected.ranges) {\n if (!range.events || range.events.length === 0) {\n continue;\n }\n\n const versionRange: VersionRange = {};\n\n for (const event of range.events) {\n if (event.introduced) {\n versionRange.introduced = event.introduced;\n }\n if (event.fixed) {\n versionRange.fixed = event.fixed;\n // Capture the first fixed version we encounter\n if (!fixedVersion) {\n fixedVersion = event.fixed;\n }\n }\n if (event.last_affected) {\n versionRange.lastAffected = event.last_affected;\n }\n }\n\n // Only add ranges that have at least one meaningful field\n if (versionRange.introduced || versionRange.fixed || versionRange.lastAffected) {\n ranges.push(versionRange);\n }\n }\n }\n\n return { ranges, fixedVersion };\n }\n\n /**\n * Extract and truncate the summary from the vulnerability.\n * Prefers the summary field, falls back to details.\n */\n private extractSummary(vuln: OsvVulnerability): string {\n const text = vuln.summary || vuln.details || '';\n if (text.length <= MAX_SUMMARY_LENGTH) {\n return text;\n }\n return text.slice(0, MAX_SUMMARY_LENGTH - 3) + '...';\n }\n}\n\n// \u2500\u2500 Utility Functions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Combine multiple AbortSignals into one that aborts when any of them aborts.\n */\nfunction combineAbortSignals(...signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort(signal.reason);\n return controller.signal;\n }\n\n signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });\n }\n\n return controller.signal;\n}\n\n/**\n * Check if an error is an abort/timeout error.\n */\nfunction isAbortError(error: unknown): boolean {\n if (error instanceof Error) {\n return error.name === 'AbortError' || error.name === 'TimeoutError';\n }\n return false;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * OSV API Adapter\n *\n * Queries the OSV (Open Source Vulnerabilities) database for known CVEs\n * affecting project dependencies. Implements the VulnDatabaseAdapter interface.\n */\n\nimport { CVERecord, VersionRange } from '../types';\nimport type { VulnDatabaseAdapter, BatchQuery } from './types';\nimport { VulnDatabaseError } from '../errors';\nimport { logWarn, logError } from '../../errors';\n\nexport type { VulnDatabaseAdapter, BatchQuery };\n\n// \u2500\u2500 Ecosystem Mapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ECOSYSTEM_MAP: Record<string, string> = {\n npm: 'npm',\n maven: 'Maven',\n go: 'Go',\n pypi: 'PyPI',\n python: 'PyPI', // manifest parser sends 'python' for requirements.txt\n cargo: 'crates.io',\n pyproject: 'PyPI', // pyproject.toml (Poetry/Hatch/PDM/PEP 621)\n nuget: 'NuGet',\n csproj: 'NuGet', // manifest parser sends 'csproj' for .csproj files\n gradle: 'Maven', // Gradle projects use Maven Central\n rubygems: 'RubyGems',\n composer: 'Packagist',\n swift: 'SwiftURL',\n pub: 'Pub',\n hex: 'Hex',\n};\n\n// \u2500\u2500 OSV Request/Response Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface OsvSeverity {\n type: string;\n score: string;\n}\n\ninterface OsvAffectedRange {\n type: string;\n events: Array<{ introduced?: string; fixed?: string; last_affected?: string }>;\n}\n\ninterface OsvAffected {\n package?: { name?: string; ecosystem?: string };\n ranges?: OsvAffectedRange[];\n versions?: string[];\n}\n\ninterface OsvVulnerability {\n id: string;\n aliases?: string[];\n summary?: string;\n details?: string;\n severity?: OsvSeverity[];\n affected?: OsvAffected[];\n /** OSV per-database extras. GHSA records expose a coarse qualitative severity here. */\n database_specific?: { severity?: string;[key: string]: unknown };\n}\n\ninterface OsvQueryResponse {\n vulns?: OsvVulnerability[];\n}\n\ninterface OsvBatchQueryItem {\n package: { name: string; ecosystem: string };\n version: string;\n}\n\ninterface OsvBatchResponse {\n results: Array<{ vulns?: OsvVulnerability[] }>;\n}\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst OSV_API_URL = 'https://api.osv.dev/v1/query';\nconst OSV_BATCH_API_URL = 'https://api.osv.dev/v1/querybatch';\nconst OSV_VULN_DETAIL_URL = 'https://api.osv.dev/v1/vulns';\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst OSV_BATCH_MAX_QUERIES = 1000;\nconst OSV_DETAIL_FETCH_CONCURRENCY = 10;\nconst MAX_SUMMARY_LENGTH = 500;\n\n// \u2500\u2500 CVSS parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Parse an OSV severity `score` field into a CVSS base score (0.0\u201310.0).\n *\n * The field is either a plain number (rare) or a CVSS vector string such as\n * \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\". For v3.0/3.1 vectors we compute\n * the base score from the metrics per the FIRST CVSS specification. Returns null\n * when the value can't be interpreted.\n */\nexport function parseCvssScore(scoreStr: string | undefined): number | null {\n if (!scoreStr) return null;\n\n const trimmed = scoreStr.trim();\n\n // Plain numeric score.\n const num = parseFloat(trimmed);\n if (!isNaN(num) && String(num) === trimmed && num >= 0 && num <= 10) {\n return num;\n }\n\n // CVSS vector string.\n if (/^CVSS:3\\.[01]\\//i.test(trimmed)) {\n return computeCvssV3BaseScore(trimmed);\n }\n\n return null;\n}\n\n/** Round up to one decimal place, per the CVSS spec's roundup() function. */\nfunction cvssRoundUp(value: number): number {\n const intInput = Math.round(value * 100000);\n if (intInput % 10000 === 0) return intInput / 100000;\n return (Math.floor(intInput / 10000) + 1) / 10;\n}\n\n/**\n * Compute the CVSS v3.0/3.1 base score from a vector string.\n * Implements the base-score equations from the FIRST CVSS v3.1 specification.\n * Returns null if required base metrics are missing.\n */\nexport function computeCvssV3BaseScore(vector: string): number | null {\n const metrics: Record<string, string> = {};\n for (const part of vector.split('/')) {\n const [k, v] = part.split(':');\n if (k && v) metrics[k.toUpperCase()] = v.toUpperCase();\n }\n\n // Required base metrics.\n const AV = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 }[metrics.AV];\n const AC = { L: 0.77, H: 0.44 }[metrics.AC];\n const UI = { N: 0.85, R: 0.62 }[metrics.UI];\n const scopeChanged = metrics.S === 'C';\n // Privileges Required depends on Scope.\n const PR = ((): number | undefined => {\n if (metrics.PR === 'N') return 0.85;\n if (metrics.PR === 'L') return scopeChanged ? 0.68 : 0.62;\n if (metrics.PR === 'H') return scopeChanged ? 0.5 : 0.27;\n return undefined;\n })();\n const impactVal = { H: 0.56, L: 0.22, N: 0.0 };\n const C = impactVal[metrics.C as keyof typeof impactVal];\n const I = impactVal[metrics.I as keyof typeof impactVal];\n const A = impactVal[metrics.A as keyof typeof impactVal];\n\n if ([AV, AC, UI, PR, C, I, A].some(v => v === undefined)) {\n return null;\n }\n\n const iscBase = 1 - (1 - C!) * (1 - I!) * (1 - A!);\n const impact = scopeChanged\n ? 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase - 0.02, 15)\n : 6.42 * iscBase;\n const exploitability = 8.22 * AV! * AC! * PR! * UI!;\n\n if (impact <= 0) return 0.0;\n\n const raw = scopeChanged\n ? Math.min(1.08 * (impact + exploitability), 10)\n : Math.min(impact + exploitability, 10);\n\n return cvssRoundUp(raw);\n}\n\n/**\n * Map a coarse qualitative severity label (GHSA `database_specific.severity`,\n * or a CVSS qualitative rating) to a representative numeric score. Used only as\n * a fallback when no CVSS vector is available. Returns null for unknown labels.\n */\nexport function qualitativeToScore(label: string | undefined): number | null {\n if (!label) return null;\n switch (label.trim().toUpperCase()) {\n case 'CRITICAL': return 9.8;\n case 'HIGH': return 7.5;\n case 'MODERATE':\n case 'MEDIUM': return 5.5;\n case 'LOW': return 3.1;\n default: return null;\n }\n}\n\n// \u2500\u2500 OsvAdapter Implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class OsvAdapter implements VulnDatabaseAdapter {\n public readonly name = 'OSV';\n\n private readonly apiUrl: string;\n private readonly batchApiUrl: string;\n private readonly vulnDetailUrl: string;\n private readonly timeoutMs: number;\n\n constructor(options?: { apiUrl?: string; batchApiUrl?: string; vulnDetailUrl?: string; timeoutMs?: number }) {\n this.apiUrl = options?.apiUrl ?? OSV_API_URL;\n this.batchApiUrl = options?.batchApiUrl ?? OSV_BATCH_API_URL;\n this.vulnDetailUrl = options?.vulnDetailUrl ?? OSV_VULN_DETAIL_URL;\n this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n }\n\n /**\n * Batch query up to OSV_BATCH_MAX_QUERIES packages in a single HTTP request.\n * Results are returned in the same order as the input queries.\n * Queries for unsupported ecosystems produce empty results without an HTTP call.\n */\n async queryBatch(\n queries: BatchQuery[],\n signal?: AbortSignal,\n ): Promise<Array<CVERecord[]>> {\n if (queries.length === 0) return [];\n\n // Map each query to its OSV ecosystem \u2014 track indices of unsupported ones\n const osvQueries: OsvBatchQueryItem[] = [];\n const indexMap: Array<number | null> = []; // index into osvQueries, or null if unsupported\n\n for (const q of queries) {\n const osvEcosystem = ECOSYSTEM_MAP[q.ecosystem.toLowerCase()];\n if (!osvEcosystem) {\n logWarn(`OSV adapter: unsupported ecosystem \"${q.ecosystem}\", skipping batch entry`);\n indexMap.push(null);\n } else {\n indexMap.push(osvQueries.length);\n osvQueries.push({\n package: { name: q.packageName, ecosystem: osvEcosystem },\n version: q.version,\n });\n }\n }\n\n if (osvQueries.length === 0) {\n return queries.map(() => []);\n }\n\n const timeoutController = new AbortController();\n const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);\n const combinedSignal = signal\n ? combineAbortSignals(signal, timeoutController.signal)\n : timeoutController.signal;\n\n let batchResults: Array<{ vulns?: OsvVulnerability[] }>;\n\n try {\n const response = await fetch(this.batchApiUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ queries: osvQueries }),\n signal: combinedSignal,\n });\n\n if (!response.ok) {\n throw new VulnDatabaseError(\n `OSV batch API returned HTTP ${response.status}: ${response.statusText}`,\n 'OSV',\n response.status,\n );\n }\n\n const data = (await response.json()) as OsvBatchResponse;\n batchResults = data.results ?? [];\n } catch (error: unknown) {\n if (error instanceof VulnDatabaseError) throw error;\n if (isAbortError(error)) {\n const msg = timeoutController.signal.aborted\n ? `OSV batch query timed out after ${this.timeoutMs}ms (${osvQueries.length} packages)`\n : `OSV batch query aborted`;\n logError(msg);\n throw new VulnDatabaseError(msg, 'OSV');\n }\n const msg = error instanceof Error ? error.message : String(error);\n logError(`OSV batch query failed: ${msg}`);\n throw new VulnDatabaseError(`Network error in OSV batch query: ${msg}`, 'OSV');\n } finally {\n clearTimeout(timeoutId);\n }\n\n // OSV's batch endpoint only ever returns { id, modified } stubs \u2014 no\n // severity, affected ranges, summary, or fixed version. Fetch full\n // details for every unique vulnerability ID found, deduplicated since\n // many packages in the same batch often share the same advisory.\n const uniqueIds = new Set<string>();\n for (const result of batchResults) {\n for (const vuln of result.vulns ?? []) {\n uniqueIds.add(vuln.id);\n }\n }\n const detailsById = await this.fetchVulnDetails([...uniqueIds], combinedSignal);\n\n // Rebuild full results array aligned with input queries\n return queries.map((_, i) => {\n const osvIdx = indexMap[i];\n if (osvIdx === null) return [];\n const resultEntry = batchResults[osvIdx];\n if (!resultEntry) return [];\n const fullVulns = (resultEntry.vulns ?? []).map(stub => detailsById.get(stub.id) ?? stub);\n return this.parseResponse({ vulns: fullVulns });\n });\n }\n\n /**\n * Fetch full vulnerability records for a set of IDs via OSV's per-ID detail\n * endpoint (the batch endpoint doesn't include this data). Runs with\n * bounded concurrency; a failure on one ID is logged and that ID falls\n * back to its stub (still usable, just without severity/EPSS-relevant data)\n * rather than failing the whole batch.\n */\n private async fetchVulnDetails(\n ids: string[],\n signal: AbortSignal,\n ): Promise<Map<string, OsvVulnerability>> {\n const results = new Map<string, OsvVulnerability>();\n if (ids.length === 0) return results;\n\n let cursor = 0;\n const worker = async (): Promise<void> => {\n while (cursor < ids.length) {\n const id = ids[cursor++];\n try {\n const response = await fetch(`${this.vulnDetailUrl}/${encodeURIComponent(id)}`, { signal });\n if (!response.ok) {\n logWarn(`OSV: failed to fetch details for ${id}: HTTP ${response.status}`);\n continue;\n }\n const vuln = (await response.json()) as OsvVulnerability;\n results.set(id, vuln);\n } catch (error: unknown) {\n if (isAbortError(error)) return;\n const msg = error instanceof Error ? error.message : String(error);\n logWarn(`OSV: failed to fetch details for ${id}: ${msg}`);\n }\n }\n };\n\n const workerCount = Math.min(OSV_DETAIL_FETCH_CONCURRENCY, ids.length);\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n return results;\n }\n\n async query(\n ecosystem: string,\n packageName: string,\n version: string,\n signal?: AbortSignal,\n ): Promise<CVERecord[]> {\n const osvEcosystem = ECOSYSTEM_MAP[ecosystem.toLowerCase()];\n if (!osvEcosystem) {\n logWarn(`OSV adapter: unsupported ecosystem \"${ecosystem}\", skipping query`);\n return [];\n }\n\n const body = JSON.stringify({\n package: {\n name: packageName,\n ecosystem: osvEcosystem,\n },\n version,\n });\n\n // Create a timeout abort signal and combine with any external signal\n const timeoutController = new AbortController();\n const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);\n\n const combinedSignal = signal\n ? combineAbortSignals(signal, timeoutController.signal)\n : timeoutController.signal;\n\n try {\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n signal: combinedSignal,\n });\n\n if (!response.ok) {\n throw new VulnDatabaseError(\n `OSV API returned HTTP ${response.status}: ${response.statusText}`,\n 'OSV',\n response.status,\n );\n }\n\n const data = (await response.json()) as OsvQueryResponse;\n return this.parseResponse(data);\n } catch (error: unknown) {\n if (error instanceof VulnDatabaseError) {\n throw error;\n }\n\n if (isAbortError(error)) {\n const isTimeout = timeoutController.signal.aborted;\n const message = isTimeout\n ? `OSV query timed out after ${this.timeoutMs}ms for ${packageName}@${version}`\n : `OSV query aborted for ${packageName}@${version}`;\n logError(message);\n throw new VulnDatabaseError(message, 'OSV');\n }\n\n const errorMessage = error instanceof Error ? error.message : String(error);\n logError(`OSV query failed for ${packageName}@${version}: ${errorMessage}`);\n throw new VulnDatabaseError(\n `Network error querying OSV for ${packageName}@${version}: ${errorMessage}`,\n 'OSV',\n );\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Parse the OSV API response into CVERecord objects.\n */\n private parseResponse(data: OsvQueryResponse): CVERecord[] {\n if (!data.vulns || data.vulns.length === 0) {\n return [];\n }\n\n const records: CVERecord[] = [];\n\n for (const vuln of data.vulns) {\n const cveId = this.extractCveId(vuln);\n if (!cveId) {\n // Skip vulnerabilities without a CVE identifier\n continue;\n }\n\n const severity = this.extractSeverity(vuln);\n const { ranges, fixedVersion } = this.extractAffectedRanges(vuln);\n const summary = this.extractSummary(vuln);\n\n records.push({\n id: cveId,\n severity,\n affectedVersionRanges: ranges,\n fixedVersion,\n summary,\n });\n }\n\n return records;\n }\n\n /**\n * Extract CVE ID from the vulnerability's aliases or use the OSV ID.\n * Prefers CVE-* identifiers from the aliases array.\n */\n private extractCveId(vuln: OsvVulnerability): string | undefined {\n // Look for a CVE alias first\n if (vuln.aliases) {\n const cveAlias = vuln.aliases.find((alias) => alias.startsWith('CVE-'));\n if (cveAlias) {\n return cveAlias;\n }\n }\n\n // Fall back to the OSV ID (e.g., GHSA-xxxx-xxxx-xxxx)\n return vuln.id;\n }\n\n /**\n * Extract a CVSS base score (0.0\u201310.0) for the vulnerability.\n *\n * OSV encodes severity as an array of `{ type, score }`. For CVSS_V3/CVSS_V4\n * the `score` field is a *vector string* (e.g. \"CVSS:3.1/AV:N/AC:L/...\") \u2014 NOT\n * a plain number \u2014 so the base score must be computed from the vector.\n *\n * Resolution order:\n * 1. CVSS_V3 vector \u2192 computed base score\n * 2. CVSS_V4 vector \u2192 computed base score\n * 3. Any severity entry that happens to be a plain number\n * 4. Coarse qualitative `database_specific.severity` (GHSA) \u2192 representative score\n *\n * Returns null when no severity data is available (so callers can distinguish\n * \"unknown\" from a genuine 0.0 score).\n */\n private extractSeverity(vuln: OsvVulnerability): number | null {\n if (vuln.severity && vuln.severity.length > 0) {\n // Prefer CVSS v3, then v4, then any parseable entry.\n const byType = (type: string) =>\n vuln.severity!.filter(s => s.type === type);\n\n for (const sev of [...byType('CVSS_V3'), ...byType('CVSS_V4')]) {\n const score = parseCvssScore(sev.score);\n if (score !== null) return score;\n }\n for (const sev of vuln.severity) {\n const score = parseCvssScore(sev.score);\n if (score !== null) return score;\n }\n }\n\n // Fallback: GHSA advisories expose a coarse qualitative severity here even\n // when a CVSS vector is absent.\n const qualitative = qualitativeToScore(vuln.database_specific?.severity);\n if (qualitative !== null) return qualitative;\n\n return null;\n }\n\n /**\n * Extract affected version ranges and the first fixed version from the vulnerability.\n */\n private extractAffectedRanges(vuln: OsvVulnerability): {\n ranges: VersionRange[];\n fixedVersion?: string;\n } {\n const ranges: VersionRange[] = [];\n let fixedVersion: string | undefined;\n\n if (!vuln.affected) {\n return { ranges, fixedVersion };\n }\n\n for (const affected of vuln.affected) {\n if (!affected.ranges) {\n continue;\n }\n\n for (const range of affected.ranges) {\n if (!range.events || range.events.length === 0) {\n continue;\n }\n\n const versionRange: VersionRange = {};\n\n for (const event of range.events) {\n if (event.introduced) {\n versionRange.introduced = event.introduced;\n }\n if (event.fixed) {\n versionRange.fixed = event.fixed;\n // Capture the first fixed version we encounter\n if (!fixedVersion) {\n fixedVersion = event.fixed;\n }\n }\n if (event.last_affected) {\n versionRange.lastAffected = event.last_affected;\n }\n }\n\n // Only add ranges that have at least one meaningful field\n if (versionRange.introduced || versionRange.fixed || versionRange.lastAffected) {\n ranges.push(versionRange);\n }\n }\n }\n\n return { ranges, fixedVersion };\n }\n\n /**\n * Extract and truncate the summary from the vulnerability.\n * Prefers the summary field, falls back to details.\n */\n private extractSummary(vuln: OsvVulnerability): string {\n const text = vuln.summary || vuln.details || '';\n if (text.length <= MAX_SUMMARY_LENGTH) {\n return text;\n }\n return text.slice(0, MAX_SUMMARY_LENGTH - 3) + '...';\n }\n}\n\n// \u2500\u2500 Utility Functions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Combine multiple AbortSignals into one that aborts when any of them aborts.\n */\nfunction combineAbortSignals(...signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort(signal.reason);\n return controller.signal;\n }\n\n signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });\n }\n\n return controller.signal;\n}\n\n/**\n * Check if an error is an abort/timeout error.\n */\nfunction isAbortError(error: unknown): boolean {\n if (error instanceof Error) {\n return error.name === 'AbortError' || error.name === 'TimeoutError';\n }\n return false;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["import_errors", "msg"]
7
7
  }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var contradiction_jev_exports = {};
20
+ __export(contradiction_jev_exports, {
21
+ checkContradictionWithJev: () => checkContradictionWithJev
22
+ });
23
+ module.exports = __toCommonJS(contradiction_jev_exports);
24
+ async function checkContradictionWithJev(client, contentA, contentB) {
25
+ const state = `Page A:
26
+ ${contentA}
27
+
28
+ Page B:
29
+ ${contentB}`;
30
+ const response = await client.ask(state, {
31
+ contradicts: {
32
+ type: "noul",
33
+ instructions: "Page A and Page B make claims that directly contradict each other about the same subject \u2014 not just related, actually incompatible."
34
+ }
35
+ });
36
+ const answer = response.answers.contradicts;
37
+ if (!answer || answer.type !== "noul") {
38
+ throw new Error('jev did not return a noul answer for "contradicts"');
39
+ }
40
+ return {
41
+ contradicts: answer.noul >= 0.5,
42
+ confidence: answer.confidence ?? answer.noul
43
+ };
44
+ }
45
+ // Annotate the CommonJS export names for ESM import in node:
46
+ 0 && (module.exports = {
47
+ checkContradictionWithJev
48
+ });
49
+ //# sourceMappingURL=contradiction-jev.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/wiki/contradiction-jev.ts"],
4
+ "sourcesContent": ["/**\n * KiroGraph Wiki \u2014 jev-based contradiction detection (opt-in, wikiContradictionMode: 'jev')\n *\n * Replaces the keyword-heuristic contradiction check (co-occurring negation\n * words like \"instead of\"/\"superseded\" on FTS-similar pages) with an actual\n * judgment call, still restricted to the same FTS-similar candidate pairs.\n */\nimport type { JevClient } from '../jev/client';\n\nexport interface JevContradictionResult {\n contradicts: boolean;\n confidence: number;\n}\n\nexport async function checkContradictionWithJev(\n client: JevClient,\n contentA: string,\n contentB: string,\n): Promise<JevContradictionResult> {\n const state = `Page A:\\n${contentA}\\n\\nPage B:\\n${contentB}`;\n const response = await client.ask(state, {\n contradicts: {\n type: 'noul',\n instructions: 'Page A and Page B make claims that directly contradict each other about the same subject \u2014 not just related, actually incompatible.',\n },\n });\n\n const answer = response.answers.contradicts;\n if (!answer || answer.type !== 'noul') {\n throw new Error('jev did not return a noul answer for \"contradicts\"');\n }\n\n return {\n contradicts: answer.noul >= 0.5,\n confidence: answer.confidence ?? answer.noul,\n };\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA,eAAsB,0BACpB,QACA,UACA,UACiC;AACjC,QAAM,QAAQ;AAAA,EAAY,QAAQ;AAAA;AAAA;AAAA,EAAgB,QAAQ;AAC1D,QAAM,WAAW,MAAM,OAAO,IAAI,OAAO;AAAA,IACvC,aAAa;AAAA,MACX,MAAM;AAAA,MACN,cAAc;AAAA,IAChB;AAAA,EACF,CAAC;AAED,QAAM,SAAS,SAAS,QAAQ;AAChC,MAAI,CAAC,UAAU,OAAO,SAAS,QAAQ;AACrC,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE;AAEA,SAAO;AAAA,IACL,aAAa,OAAO,QAAQ;AAAA,IAC5B,YAAY,OAAO,cAAc,OAAO;AAAA,EAC1C;AACF;",
6
+ "names": []
7
+ }
@@ -38,6 +38,8 @@ var import_schema = require("./schema");
38
38
  var import_schema2 = require("./schema");
39
39
  var import_ingest = require("./ingest");
40
40
  var import_lint = require("./lint");
41
+ var import_links = require("./links");
42
+ var import_rename = require("./rename");
41
43
  class KiroGraphWiki {
42
44
  constructor(db, kirographDir, opts = {}) {
43
45
  this.wikiDb = new import_database.WikiDatabase(db);
@@ -119,6 +121,17 @@ class KiroGraphWiki {
119
121
  this.initialize();
120
122
  return (0, import_lint.lintWiki)(this.wikiDb);
121
123
  }
124
+ // ── Links ──────────────────────────────────────────────────────────────────
125
+ /** Outgoing/broken/incoming `[[slug]]` links for one page. Null if the slug is unknown. */
126
+ getLinks(slug) {
127
+ this.initialize();
128
+ return (0, import_links.getPageLinks)(this.wikiDb, slug);
129
+ }
130
+ /** Rename a page's slug, moving its file and rewriting every `[[slug]]` reference to it. */
131
+ rename(fromSlug, toSlug) {
132
+ this.initialize();
133
+ return (0, import_rename.renamePage)(this.wikiDir, this.wikiDb, fromSlug, toSlug);
134
+ }
122
135
  // ── Reindex ────────────────────────────────────────────────────────────────
123
136
  reindex() {
124
137
  this.initialize();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/wiki/index.ts"],
4
- "sourcesContent": ["/**\n * KiroGraph Wiki \u2014 Public API\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { WikiDatabase } from './database';\nimport { DEFAULT_SCHEMA } from './schema';\nimport { parseWikiDiff } from './schema';\nimport { buildIngestPrompt, applyDiff, reindexFromDisk, updateManifest } from './ingest';\nimport { lintWiki } from './lint';\nimport type {\n WikiPage,\n ScoredWikiPage,\n WikiDiff,\n WikiLintIssue,\n WikiStats,\n} from './types';\n\nexport type { WikiPage, ScoredWikiPage, WikiDiff, WikiLintIssue, WikiStats };\n\nexport class KiroGraphWiki {\n private wikiDb: WikiDatabase;\n private wikiDir: string;\n private autoResolveConflicts: boolean;\n\n constructor(\n db: any,\n kirographDir: string,\n opts: { autoResolveConflicts?: boolean } = {}\n ) {\n this.wikiDb = new WikiDatabase(db);\n this.wikiDir = path.join(kirographDir, 'wiki');\n this.autoResolveConflicts = opts.autoResolveConflicts ?? false;\n }\n\n initialize(): void {\n this.wikiDb.initialize();\n fs.mkdirSync(this.wikiDir, { recursive: true });\n }\n\n // \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n initWiki(): void {\n this.initialize();\n const schemaPath = path.join(this.wikiDir, 'SCHEMA.md');\n if (!fs.existsSync(schemaPath)) {\n fs.writeFileSync(schemaPath, DEFAULT_SCHEMA, 'utf8');\n }\n const manifestPath = path.join(this.wikiDir, 'MANIFEST.md');\n if (!fs.existsSync(manifestPath)) {\n fs.writeFileSync(manifestPath, '# Wiki Manifest\\n\\n(empty)\\n', 'utf8');\n }\n }\n\n // \u2500\u2500 Ingest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Returns the ingest prompt for the LLM (agent mode).\n * The agent uses this to produce a WIKI_DIFF, then calls applyDiff.\n */\n getIngestPrompt(source: string, sourceName = 'source'): string {\n this.initialize();\n return buildIngestPrompt(this.wikiDir, source, sourceName);\n }\n\n // \u2500\u2500 Queue (local synthesis mode) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n queueSource(sourceText: string, sourceName: string): void {\n this.initialize();\n this.wikiDb.queueSource(sourceName, sourceText);\n }\n\n getPendingQueue(): Array<{ id: number; sourceName: string; sourceText: string }> {\n this.initialize();\n return this.wikiDb.getPendingQueue();\n }\n\n clearQueue(): void {\n this.initialize();\n this.wikiDb.clearQueue();\n }\n\n getQueueCount(): number {\n this.initialize();\n return this.wikiDb.getQueueCount();\n }\n\n async synthesize(modelName: string, quiet = false) {\n this.initialize();\n const { runWikiLocalSynthesis } = await Promise.resolve().then(() => require('./synthesize.js'));\n return runWikiLocalSynthesis(this, modelName, quiet);\n }\n\n /**\n * Parse and apply a WIKI_DIFF string produced by the LLM.\n */\n applyDiff(rawDiff: string) {\n this.initialize();\n const diff = parseWikiDiff(rawDiff);\n return applyDiff(diff, this.wikiDir, this.wikiDb, {\n autoResolveConflicts: this.autoResolveConflicts,\n });\n }\n\n // \u2500\u2500 Search & Read \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n search(query: string, limit = 10): ScoredWikiPage[] {\n this.initialize();\n return this.wikiDb.search(query, limit);\n }\n\n getPage(slug: string): WikiPage | null {\n this.initialize();\n return this.wikiDb.getPage(slug);\n }\n\n listPages(): WikiPage[] {\n this.initialize();\n return this.wikiDb.listPages();\n }\n\n // \u2500\u2500 Lint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n lint(): WikiLintIssue[] {\n this.initialize();\n return lintWiki(this.wikiDb);\n }\n\n // \u2500\u2500 Reindex \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n reindex(): number {\n this.initialize();\n return reindexFromDisk(this.wikiDir, this.wikiDb);\n }\n\n // \u2500\u2500 Stats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n getStats(): WikiStats {\n this.initialize();\n return this.wikiDb.getStats();\n }\n\n // \u2500\u2500 Context enrichment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Returns wiki pages relevant to a query, for inclusion in kirograph_context.\n * Returns up to `limit` pages with score >= threshold.\n */\n getContextPages(query: string, limit = 3, threshold = 0.4): WikiPage[] {\n this.initialize();\n return this.wikiDb\n .search(query, limit * 2)\n .filter(r => r.score >= threshold)\n .slice(0, limit)\n .map(r => r.page);\n }\n\n get dir(): string {\n return this.wikiDir;\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,SAAoB;AACpB,WAAsB;AACtB,sBAA6B;AAC7B,oBAA+B;AAC/B,IAAAA,iBAA8B;AAC9B,oBAA8E;AAC9E,kBAAyB;AAWlB,MAAM,cAAc;AAAA,EAKzB,YACE,IACA,cACA,OAA2C,CAAC,GAC5C;AACA,SAAK,SAAS,IAAI,6BAAa,EAAE;AACjC,SAAK,UAAU,KAAK,KAAK,cAAc,MAAM;AAC7C,SAAK,uBAAuB,KAAK,wBAAwB;AAAA,EAC3D;AAAA,EAEA,aAAmB;AACjB,SAAK,OAAO,WAAW;AACvB,OAAG,UAAU,KAAK,SAAS,EAAE,WAAW,KAAK,CAAC;AAAA,EAChD;AAAA;AAAA,EAIA,WAAiB;AACf,SAAK,WAAW;AAChB,UAAM,aAAa,KAAK,KAAK,KAAK,SAAS,WAAW;AACtD,QAAI,CAAC,GAAG,WAAW,UAAU,GAAG;AAC9B,SAAG,cAAc,YAAY,8BAAgB,MAAM;AAAA,IACrD;AACA,UAAM,eAAe,KAAK,KAAK,KAAK,SAAS,aAAa;AAC1D,QAAI,CAAC,GAAG,WAAW,YAAY,GAAG;AAChC,SAAG,cAAc,cAAc,gCAAgC,MAAM;AAAA,IACvE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,gBAAgB,QAAgB,aAAa,UAAkB;AAC7D,SAAK,WAAW;AAChB,eAAO,iCAAkB,KAAK,SAAS,QAAQ,UAAU;AAAA,EAC3D;AAAA;AAAA,EAIA,YAAY,YAAoB,YAA0B;AACxD,SAAK,WAAW;AAChB,SAAK,OAAO,YAAY,YAAY,UAAU;AAAA,EAChD;AAAA,EAEA,kBAAiF;AAC/E,SAAK,WAAW;AAChB,WAAO,KAAK,OAAO,gBAAgB;AAAA,EACrC;AAAA,EAEA,aAAmB;AACjB,SAAK,WAAW;AAChB,SAAK,OAAO,WAAW;AAAA,EACzB;AAAA,EAEA,gBAAwB;AACtB,SAAK,WAAW;AAChB,WAAO,KAAK,OAAO,cAAc;AAAA,EACnC;AAAA,EAEA,MAAM,WAAW,WAAmB,QAAQ,OAAO;AACjD,SAAK,WAAW;AAChB,UAAM,EAAE,sBAAsB,IAAI,MAAM,QAAQ,QAAQ,EAAE,KAAK,MAAM,QAAQ,iBAAiB,CAAC;AAC/F,WAAO,sBAAsB,MAAM,WAAW,KAAK;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,SAAiB;AACzB,SAAK,WAAW;AAChB,UAAM,WAAO,8BAAc,OAAO;AAClC,eAAO,yBAAU,MAAM,KAAK,SAAS,KAAK,QAAQ;AAAA,MAChD,sBAAsB,KAAK;AAAA,IAC7B,CAAC;AAAA,EACH;AAAA;AAAA,EAIA,OAAO,OAAe,QAAQ,IAAsB;AAClD,SAAK,WAAW;AAChB,WAAO,KAAK,OAAO,OAAO,OAAO,KAAK;AAAA,EACxC;AAAA,EAEA,QAAQ,MAA+B;AACrC,SAAK,WAAW;AAChB,WAAO,KAAK,OAAO,QAAQ,IAAI;AAAA,EACjC;AAAA,EAEA,YAAwB;AACtB,SAAK,WAAW;AAChB,WAAO,KAAK,OAAO,UAAU;AAAA,EAC/B;AAAA;AAAA,EAIA,OAAwB;AACtB,SAAK,WAAW;AAChB,eAAO,sBAAS,KAAK,MAAM;AAAA,EAC7B;AAAA;AAAA,EAIA,UAAkB;AAChB,SAAK,WAAW;AAChB,eAAO,+BAAgB,KAAK,SAAS,KAAK,MAAM;AAAA,EAClD;AAAA;AAAA,EAIA,WAAsB;AACpB,SAAK,WAAW;AAChB,WAAO,KAAK,OAAO,SAAS;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,gBAAgB,OAAe,QAAQ,GAAG,YAAY,KAAiB;AACrE,SAAK,WAAW;AAChB,WAAO,KAAK,OACT,OAAO,OAAO,QAAQ,CAAC,EACvB,OAAO,OAAK,EAAE,SAAS,SAAS,EAChC,MAAM,GAAG,KAAK,EACd,IAAI,OAAK,EAAE,IAAI;AAAA,EACpB;AAAA,EAEA,IAAI,MAAc;AAChB,WAAO,KAAK;AAAA,EACd;AACF;",
4
+ "sourcesContent": ["/**\n * KiroGraph Wiki \u2014 Public API\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { WikiDatabase } from './database';\nimport { DEFAULT_SCHEMA } from './schema';\nimport { parseWikiDiff } from './schema';\nimport { buildIngestPrompt, applyDiff, reindexFromDisk, updateManifest } from './ingest';\nimport { lintWiki } from './lint';\nimport { getPageLinks, type WikiPageLinks } from './links';\nimport { renamePage, type RenamePageResult } from './rename';\nimport type {\n WikiPage,\n ScoredWikiPage,\n WikiDiff,\n WikiLintIssue,\n WikiStats,\n} from './types';\n\nexport type { WikiPage, ScoredWikiPage, WikiDiff, WikiLintIssue, WikiStats, WikiPageLinks, RenamePageResult };\n\nexport class KiroGraphWiki {\n private wikiDb: WikiDatabase;\n private wikiDir: string;\n private autoResolveConflicts: boolean;\n\n constructor(\n db: any,\n kirographDir: string,\n opts: { autoResolveConflicts?: boolean } = {}\n ) {\n this.wikiDb = new WikiDatabase(db);\n this.wikiDir = path.join(kirographDir, 'wiki');\n this.autoResolveConflicts = opts.autoResolveConflicts ?? false;\n }\n\n initialize(): void {\n this.wikiDb.initialize();\n fs.mkdirSync(this.wikiDir, { recursive: true });\n }\n\n // \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n initWiki(): void {\n this.initialize();\n const schemaPath = path.join(this.wikiDir, 'SCHEMA.md');\n if (!fs.existsSync(schemaPath)) {\n fs.writeFileSync(schemaPath, DEFAULT_SCHEMA, 'utf8');\n }\n const manifestPath = path.join(this.wikiDir, 'MANIFEST.md');\n if (!fs.existsSync(manifestPath)) {\n fs.writeFileSync(manifestPath, '# Wiki Manifest\\n\\n(empty)\\n', 'utf8');\n }\n }\n\n // \u2500\u2500 Ingest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Returns the ingest prompt for the LLM (agent mode).\n * The agent uses this to produce a WIKI_DIFF, then calls applyDiff.\n */\n getIngestPrompt(source: string, sourceName = 'source'): string {\n this.initialize();\n return buildIngestPrompt(this.wikiDir, source, sourceName);\n }\n\n // \u2500\u2500 Queue (local synthesis mode) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n queueSource(sourceText: string, sourceName: string): void {\n this.initialize();\n this.wikiDb.queueSource(sourceName, sourceText);\n }\n\n getPendingQueue(): Array<{ id: number; sourceName: string; sourceText: string }> {\n this.initialize();\n return this.wikiDb.getPendingQueue();\n }\n\n clearQueue(): void {\n this.initialize();\n this.wikiDb.clearQueue();\n }\n\n getQueueCount(): number {\n this.initialize();\n return this.wikiDb.getQueueCount();\n }\n\n async synthesize(modelName: string, quiet = false) {\n this.initialize();\n const { runWikiLocalSynthesis } = await Promise.resolve().then(() => require('./synthesize.js'));\n return runWikiLocalSynthesis(this, modelName, quiet);\n }\n\n /**\n * Parse and apply a WIKI_DIFF string produced by the LLM.\n */\n applyDiff(rawDiff: string) {\n this.initialize();\n const diff = parseWikiDiff(rawDiff);\n return applyDiff(diff, this.wikiDir, this.wikiDb, {\n autoResolveConflicts: this.autoResolveConflicts,\n });\n }\n\n // \u2500\u2500 Search & Read \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n search(query: string, limit = 10): ScoredWikiPage[] {\n this.initialize();\n return this.wikiDb.search(query, limit);\n }\n\n getPage(slug: string): WikiPage | null {\n this.initialize();\n return this.wikiDb.getPage(slug);\n }\n\n listPages(): WikiPage[] {\n this.initialize();\n return this.wikiDb.listPages();\n }\n\n // \u2500\u2500 Lint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n lint(): WikiLintIssue[] {\n this.initialize();\n return lintWiki(this.wikiDb);\n }\n\n // \u2500\u2500 Links \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Outgoing/broken/incoming `[[slug]]` links for one page. Null if the slug is unknown. */\n getLinks(slug: string): WikiPageLinks | null {\n this.initialize();\n return getPageLinks(this.wikiDb, slug);\n }\n\n /** Rename a page's slug, moving its file and rewriting every `[[slug]]` reference to it. */\n rename(fromSlug: string, toSlug: string): RenamePageResult {\n this.initialize();\n return renamePage(this.wikiDir, this.wikiDb, fromSlug, toSlug);\n }\n\n // \u2500\u2500 Reindex \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n reindex(): number {\n this.initialize();\n return reindexFromDisk(this.wikiDir, this.wikiDb);\n }\n\n // \u2500\u2500 Stats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n getStats(): WikiStats {\n this.initialize();\n return this.wikiDb.getStats();\n }\n\n // \u2500\u2500 Context enrichment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Returns wiki pages relevant to a query, for inclusion in kirograph_context.\n * Returns up to `limit` pages with score >= threshold.\n */\n getContextPages(query: string, limit = 3, threshold = 0.4): WikiPage[] {\n this.initialize();\n return this.wikiDb\n .search(query, limit * 2)\n .filter(r => r.score >= threshold)\n .slice(0, limit)\n .map(r => r.page);\n }\n\n get dir(): string {\n return this.wikiDir;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["import_schema"]
7
7
  }
@@ -31,6 +31,7 @@ __export(ingest_exports, {
31
31
  applyDiff: () => applyDiff,
32
32
  buildIngestPrompt: () => buildIngestPrompt,
33
33
  reindexFromDisk: () => reindexFromDisk,
34
+ slugToFilePath: () => slugToFilePath,
34
35
  updateManifest: () => updateManifest
35
36
  });
36
37
  module.exports = __toCommonJS(ingest_exports);
@@ -254,6 +255,7 @@ function reindexFromDisk(wikiDir, wikiDb) {
254
255
  applyDiff,
255
256
  buildIngestPrompt,
256
257
  reindexFromDisk,
258
+ slugToFilePath,
257
259
  updateManifest
258
260
  });
259
261
  //# sourceMappingURL=ingest.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/wiki/ingest.ts"],
4
- "sourcesContent": ["/**\n * KiroGraph Wiki \u2014 Ingest & apply-diff logic\n *\n * applyDiff: writes markdown files + updates SQLite from a parsed WIKI_DIFF\n * buildIngestPrompt: returns the prompt for the LLM (SCHEMA + MANIFEST + source)\n * resolveConflicts: deterministic auto-resolve by source date\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { WikiDiff, WikiDiffEntry, WikiDiffConflict, WikiPage } from './types';\nimport type { WikiDatabase } from './database';\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction slugToFilePath(slug: string): string {\n return slug.replace(/\\//g, path.sep) + '.md';\n}\n\nfunction ensureDir(p: string): void {\n fs.mkdirSync(path.dirname(p), { recursive: true });\n}\n\nfunction upsertSection(content: string, section: string, newContent: string, mode: 'replace' | 'append'): string {\n const heading = `## ${section}`;\n const idx = content.indexOf(heading);\n\n if (idx === -1) {\n // Section doesn't exist \u2014 append at end\n return content.trimEnd() + `\\n\\n${heading}\\n${newContent.trimEnd()}\\n`;\n }\n\n // Find end of section (next ## heading or EOF)\n const afterHeading = idx + heading.length;\n const nextHeading = content.indexOf('\\n## ', afterHeading);\n const sectionEnd = nextHeading === -1 ? content.length : nextHeading;\n\n if (mode === 'replace') {\n return content.slice(0, idx) + heading + '\\n' + newContent.trimEnd() + '\\n' + content.slice(sectionEnd);\n } else {\n // append: insert before next heading\n const sectionContent = content.slice(afterHeading, sectionEnd).trimEnd();\n return content.slice(0, idx) + heading + sectionContent + '\\n' + newContent.trimEnd() + '\\n' + content.slice(sectionEnd);\n }\n}\n\n// \u2500\u2500 buildIngestPrompt \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function buildIngestPrompt(\n wikiDir: string,\n sourceContent: string,\n sourceName: string,\n): string {\n const schemaPath = path.join(wikiDir, 'SCHEMA.md');\n const manifestPath = path.join(wikiDir, 'MANIFEST.md');\n\n const schema = fs.existsSync(schemaPath)\n ? fs.readFileSync(schemaPath, 'utf8')\n : '(no SCHEMA.md found \u2014 use default wiki page structure)';\n\n const manifest = fs.existsSync(manifestPath)\n ? fs.readFileSync(manifestPath, 'utf8')\n : '(wiki is empty \u2014 no pages yet)';\n\n return `You are maintaining a project wiki following the schema below.\n\n## SCHEMA\n${schema}\n\n## MANIFEST (existing pages)\n${manifest}\n\n## SOURCE TO INGEST\nSource name: ${sourceName}\n${sourceContent}\n\n## INSTRUCTIONS\n\nRead the source above. Following the schema:\n1. Identify entities, decisions, and patterns mentioned\n2. For existing pages (listed in MANIFEST): update relevant sections\n3. For new entities: create pages using the standard structure\n4. Emit one WIKI_DIFF block per page operation\n5. If incoming info contradicts existing content, emit a WIKI_DIFF_CONFLICTS block\n\nProduce ONLY the WIKI_DIFF blocks and WIKI_DIFF_CONFLICTS blocks. No other output.`;\n}\n\n// \u2500\u2500 applyDiff \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ApplyDiffResult {\n created: string[];\n updated: string[];\n conflictsResolved: string[];\n conflictsPending: WikiDiffConflict[];\n}\n\nexport function applyDiff(\n diff: WikiDiff,\n wikiDir: string,\n wikiDb: WikiDatabase,\n opts: { autoResolveConflicts?: boolean } = {}\n): ApplyDiffResult {\n const result: ApplyDiffResult = {\n created: [],\n updated: [],\n conflictsResolved: [],\n conflictsPending: [],\n };\n\n for (const entry of diff.entries) {\n if (!entry.page) continue;\n\n const relPath = slugToFilePath(entry.page);\n const absPath = path.join(wikiDir, relPath);\n\n if (entry.action === 'create') {\n ensureDir(absPath);\n const title = entry.title ?? entry.page;\n const fileContent = `# ${title}\\n\\n${entry.content.trimEnd()}\\n`;\n fs.writeFileSync(absPath, fileContent, 'utf8');\n wikiDb.upsertPage({\n slug: entry.page,\n title,\n content: fileContent,\n filePath: relPath,\n sourceCount: 1,\n });\n result.created.push(entry.page);\n\n } else if (entry.action === 'upsert') {\n let current = '';\n let isNew = false;\n if (fs.existsSync(absPath)) {\n current = fs.readFileSync(absPath, 'utf8');\n } else {\n ensureDir(absPath);\n current = `# ${entry.page}\\n\\n`;\n isNew = true;\n }\n\n const updated = entry.section\n ? upsertSection(current, entry.section, entry.content, entry.mode ?? 'append')\n : current.trimEnd() + '\\n\\n' + entry.content.trimEnd() + '\\n';\n\n fs.writeFileSync(absPath, updated, 'utf8');\n const titleMatch = updated.match(/^#\\s+(.+)$/m);\n wikiDb.upsertPage({\n slug: entry.page,\n title: titleMatch?.[1] ?? entry.page,\n content: updated,\n filePath: relPath,\n sourceCount: 1,\n });\n if (isNew) {\n result.created.push(entry.page);\n } else {\n result.updated.push(entry.page);\n }\n\n } else if (entry.action === 'append') {\n let current = '';\n if (fs.existsSync(absPath)) {\n current = fs.readFileSync(absPath, 'utf8');\n } else {\n ensureDir(absPath);\n current = `# ${entry.page}\\n\\n`;\n }\n const appended = current.trimEnd() + '\\n\\n' + entry.content.trimEnd() + '\\n';\n fs.writeFileSync(absPath, appended, 'utf8');\n const titleMatch = appended.match(/^#\\s+(.+)$/m);\n wikiDb.upsertPage({\n slug: entry.page,\n title: titleMatch?.[1] ?? entry.page,\n content: appended,\n filePath: relPath,\n sourceCount: 1,\n });\n result.updated.push(entry.page);\n }\n }\n\n // Handle conflicts\n for (const conflict of diff.conflicts) {\n if (opts.autoResolveConflicts) {\n const resolved = resolveConflictByDate(conflict, wikiDir, wikiDb);\n if (resolved) {\n result.conflictsResolved.push(conflict.page);\n continue;\n }\n }\n result.conflictsPending.push(conflict);\n }\n\n updateManifest(wikiDir, wikiDb);\n\n return result;\n}\n\n// \u2500\u2500 resolveConflictByDate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction resolveConflictByDate(\n conflict: WikiDiffConflict,\n wikiDir: string,\n wikiDb: WikiDatabase,\n): boolean {\n const existingDate = conflict.existingDate ? new Date(conflict.existingDate).getTime() : 0;\n const incomingDate = conflict.incomingDate ? new Date(conflict.incomingDate).getTime() : 0;\n\n // Incoming is newer \u2014 replace existing claim and add superseded note\n if (incomingDate >= existingDate && conflict.page && conflict.section) {\n const relPath = slugToFilePath(conflict.page);\n const absPath = path.join(wikiDir, relPath);\n if (!fs.existsSync(absPath)) return false;\n\n let content = fs.readFileSync(absPath, 'utf8');\n const supersededNote = `\\n> \u26A0 Superseded by ${conflict.source} on ${conflict.incomingDate ?? 'unknown date'}\\n`;\n const oldClaim = conflict.existing;\n content = content.replace(oldClaim, conflict.incoming + supersededNote);\n fs.writeFileSync(absPath, content, 'utf8');\n\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n wikiDb.upsertPage({\n slug: conflict.page,\n title: titleMatch?.[1] ?? conflict.page,\n content,\n filePath: relPath,\n sourceCount: 0,\n });\n return true;\n }\n\n return false;\n}\n\n// \u2500\u2500 updateManifest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function updateManifest(wikiDir: string, wikiDb: WikiDatabase): void {\n const pages = wikiDb.listPages();\n if (pages.length === 0) return;\n\n const rows = pages\n .map(p => `| [${p.slug}](${p.filePath}) | ${p.title} | ${p.sourceCount} | ${new Date(p.updatedAt).toISOString().slice(0, 10)} |`)\n .join('\\n');\n\n const manifest = `# Wiki Manifest\\n\\n| Slug | Title | Sources | Updated |\\n|------|-------|---------|----------|\\n${rows}\\n`;\n fs.writeFileSync(path.join(wikiDir, 'MANIFEST.md'), manifest, 'utf8');\n}\n\n// \u2500\u2500 reindex \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function reindexFromDisk(wikiDir: string, wikiDb: WikiDatabase): number {\n wikiDb.clearAll();\n\n let count = 0;\n\n function walkDir(dir: string): void {\n if (!fs.existsSync(dir)) return;\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const abs = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n walkDir(abs);\n } else if (entry.name.endsWith('.md') && entry.name !== 'SCHEMA.md' && entry.name !== 'MANIFEST.md') {\n const rel = path.relative(wikiDir, abs);\n const slug = rel.replace(/\\.md$/, '').replace(/\\\\/g, '/');\n const content = fs.readFileSync(abs, 'utf8');\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n const stat = fs.statSync(abs);\n wikiDb.upsertPage({\n slug,\n title: titleMatch?.[1] ?? slug,\n content,\n filePath: rel,\n updatedAt: stat.mtimeMs,\n sourceCount: 0,\n });\n count++;\n }\n }\n }\n\n walkDir(wikiDir);\n updateManifest(wikiDir, wikiDb);\n return count;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * KiroGraph Wiki \u2014 Ingest & apply-diff logic\n *\n * applyDiff: writes markdown files + updates SQLite from a parsed WIKI_DIFF\n * buildIngestPrompt: returns the prompt for the LLM (SCHEMA + MANIFEST + source)\n * resolveConflicts: deterministic auto-resolve by source date\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { WikiDiff, WikiDiffEntry, WikiDiffConflict, WikiPage } from './types';\nimport type { WikiDatabase } from './database';\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function slugToFilePath(slug: string): string {\n return slug.replace(/\\//g, path.sep) + '.md';\n}\n\nfunction ensureDir(p: string): void {\n fs.mkdirSync(path.dirname(p), { recursive: true });\n}\n\nfunction upsertSection(content: string, section: string, newContent: string, mode: 'replace' | 'append'): string {\n const heading = `## ${section}`;\n const idx = content.indexOf(heading);\n\n if (idx === -1) {\n // Section doesn't exist \u2014 append at end\n return content.trimEnd() + `\\n\\n${heading}\\n${newContent.trimEnd()}\\n`;\n }\n\n // Find end of section (next ## heading or EOF)\n const afterHeading = idx + heading.length;\n const nextHeading = content.indexOf('\\n## ', afterHeading);\n const sectionEnd = nextHeading === -1 ? content.length : nextHeading;\n\n if (mode === 'replace') {\n return content.slice(0, idx) + heading + '\\n' + newContent.trimEnd() + '\\n' + content.slice(sectionEnd);\n } else {\n // append: insert before next heading\n const sectionContent = content.slice(afterHeading, sectionEnd).trimEnd();\n return content.slice(0, idx) + heading + sectionContent + '\\n' + newContent.trimEnd() + '\\n' + content.slice(sectionEnd);\n }\n}\n\n// \u2500\u2500 buildIngestPrompt \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function buildIngestPrompt(\n wikiDir: string,\n sourceContent: string,\n sourceName: string,\n): string {\n const schemaPath = path.join(wikiDir, 'SCHEMA.md');\n const manifestPath = path.join(wikiDir, 'MANIFEST.md');\n\n const schema = fs.existsSync(schemaPath)\n ? fs.readFileSync(schemaPath, 'utf8')\n : '(no SCHEMA.md found \u2014 use default wiki page structure)';\n\n const manifest = fs.existsSync(manifestPath)\n ? fs.readFileSync(manifestPath, 'utf8')\n : '(wiki is empty \u2014 no pages yet)';\n\n return `You are maintaining a project wiki following the schema below.\n\n## SCHEMA\n${schema}\n\n## MANIFEST (existing pages)\n${manifest}\n\n## SOURCE TO INGEST\nSource name: ${sourceName}\n${sourceContent}\n\n## INSTRUCTIONS\n\nRead the source above. Following the schema:\n1. Identify entities, decisions, and patterns mentioned\n2. For existing pages (listed in MANIFEST): update relevant sections\n3. For new entities: create pages using the standard structure\n4. Emit one WIKI_DIFF block per page operation\n5. If incoming info contradicts existing content, emit a WIKI_DIFF_CONFLICTS block\n\nProduce ONLY the WIKI_DIFF blocks and WIKI_DIFF_CONFLICTS blocks. No other output.`;\n}\n\n// \u2500\u2500 applyDiff \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ApplyDiffResult {\n created: string[];\n updated: string[];\n conflictsResolved: string[];\n conflictsPending: WikiDiffConflict[];\n}\n\nexport function applyDiff(\n diff: WikiDiff,\n wikiDir: string,\n wikiDb: WikiDatabase,\n opts: { autoResolveConflicts?: boolean } = {}\n): ApplyDiffResult {\n const result: ApplyDiffResult = {\n created: [],\n updated: [],\n conflictsResolved: [],\n conflictsPending: [],\n };\n\n for (const entry of diff.entries) {\n if (!entry.page) continue;\n\n const relPath = slugToFilePath(entry.page);\n const absPath = path.join(wikiDir, relPath);\n\n if (entry.action === 'create') {\n ensureDir(absPath);\n const title = entry.title ?? entry.page;\n const fileContent = `# ${title}\\n\\n${entry.content.trimEnd()}\\n`;\n fs.writeFileSync(absPath, fileContent, 'utf8');\n wikiDb.upsertPage({\n slug: entry.page,\n title,\n content: fileContent,\n filePath: relPath,\n sourceCount: 1,\n });\n result.created.push(entry.page);\n\n } else if (entry.action === 'upsert') {\n let current = '';\n let isNew = false;\n if (fs.existsSync(absPath)) {\n current = fs.readFileSync(absPath, 'utf8');\n } else {\n ensureDir(absPath);\n current = `# ${entry.page}\\n\\n`;\n isNew = true;\n }\n\n const updated = entry.section\n ? upsertSection(current, entry.section, entry.content, entry.mode ?? 'append')\n : current.trimEnd() + '\\n\\n' + entry.content.trimEnd() + '\\n';\n\n fs.writeFileSync(absPath, updated, 'utf8');\n const titleMatch = updated.match(/^#\\s+(.+)$/m);\n wikiDb.upsertPage({\n slug: entry.page,\n title: titleMatch?.[1] ?? entry.page,\n content: updated,\n filePath: relPath,\n sourceCount: 1,\n });\n if (isNew) {\n result.created.push(entry.page);\n } else {\n result.updated.push(entry.page);\n }\n\n } else if (entry.action === 'append') {\n let current = '';\n if (fs.existsSync(absPath)) {\n current = fs.readFileSync(absPath, 'utf8');\n } else {\n ensureDir(absPath);\n current = `# ${entry.page}\\n\\n`;\n }\n const appended = current.trimEnd() + '\\n\\n' + entry.content.trimEnd() + '\\n';\n fs.writeFileSync(absPath, appended, 'utf8');\n const titleMatch = appended.match(/^#\\s+(.+)$/m);\n wikiDb.upsertPage({\n slug: entry.page,\n title: titleMatch?.[1] ?? entry.page,\n content: appended,\n filePath: relPath,\n sourceCount: 1,\n });\n result.updated.push(entry.page);\n }\n }\n\n // Handle conflicts\n for (const conflict of diff.conflicts) {\n if (opts.autoResolveConflicts) {\n const resolved = resolveConflictByDate(conflict, wikiDir, wikiDb);\n if (resolved) {\n result.conflictsResolved.push(conflict.page);\n continue;\n }\n }\n result.conflictsPending.push(conflict);\n }\n\n updateManifest(wikiDir, wikiDb);\n\n return result;\n}\n\n// \u2500\u2500 resolveConflictByDate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction resolveConflictByDate(\n conflict: WikiDiffConflict,\n wikiDir: string,\n wikiDb: WikiDatabase,\n): boolean {\n const existingDate = conflict.existingDate ? new Date(conflict.existingDate).getTime() : 0;\n const incomingDate = conflict.incomingDate ? new Date(conflict.incomingDate).getTime() : 0;\n\n // Incoming is newer \u2014 replace existing claim and add superseded note\n if (incomingDate >= existingDate && conflict.page && conflict.section) {\n const relPath = slugToFilePath(conflict.page);\n const absPath = path.join(wikiDir, relPath);\n if (!fs.existsSync(absPath)) return false;\n\n let content = fs.readFileSync(absPath, 'utf8');\n const supersededNote = `\\n> \u26A0 Superseded by ${conflict.source} on ${conflict.incomingDate ?? 'unknown date'}\\n`;\n const oldClaim = conflict.existing;\n content = content.replace(oldClaim, conflict.incoming + supersededNote);\n fs.writeFileSync(absPath, content, 'utf8');\n\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n wikiDb.upsertPage({\n slug: conflict.page,\n title: titleMatch?.[1] ?? conflict.page,\n content,\n filePath: relPath,\n sourceCount: 0,\n });\n return true;\n }\n\n return false;\n}\n\n// \u2500\u2500 updateManifest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function updateManifest(wikiDir: string, wikiDb: WikiDatabase): void {\n const pages = wikiDb.listPages();\n if (pages.length === 0) return;\n\n const rows = pages\n .map(p => `| [${p.slug}](${p.filePath}) | ${p.title} | ${p.sourceCount} | ${new Date(p.updatedAt).toISOString().slice(0, 10)} |`)\n .join('\\n');\n\n const manifest = `# Wiki Manifest\\n\\n| Slug | Title | Sources | Updated |\\n|------|-------|---------|----------|\\n${rows}\\n`;\n fs.writeFileSync(path.join(wikiDir, 'MANIFEST.md'), manifest, 'utf8');\n}\n\n// \u2500\u2500 reindex \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function reindexFromDisk(wikiDir: string, wikiDb: WikiDatabase): number {\n wikiDb.clearAll();\n\n let count = 0;\n\n function walkDir(dir: string): void {\n if (!fs.existsSync(dir)) return;\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const abs = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n walkDir(abs);\n } else if (entry.name.endsWith('.md') && entry.name !== 'SCHEMA.md' && entry.name !== 'MANIFEST.md') {\n const rel = path.relative(wikiDir, abs);\n const slug = rel.replace(/\\.md$/, '').replace(/\\\\/g, '/');\n const content = fs.readFileSync(abs, 'utf8');\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n const stat = fs.statSync(abs);\n wikiDb.upsertPage({\n slug,\n title: titleMatch?.[1] ?? slug,\n content,\n filePath: rel,\n updatedAt: stat.mtimeMs,\n sourceCount: 0,\n });\n count++;\n }\n }\n }\n\n walkDir(wikiDir);\n updateManifest(wikiDir, wikiDb);\n return count;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var links_exports = {};
20
+ __export(links_exports, {
21
+ WIKI_LINK_RE: () => WIKI_LINK_RE,
22
+ extractLinks: () => extractLinks,
23
+ getPageLinks: () => getPageLinks
24
+ });
25
+ module.exports = __toCommonJS(links_exports);
26
+ const WIKI_LINK_RE = /\[\[([^\]]+)\]\]/g;
27
+ function extractLinks(content) {
28
+ const seen = /* @__PURE__ */ new Set();
29
+ const re = new RegExp(WIKI_LINK_RE.source, "g");
30
+ let m;
31
+ while ((m = re.exec(content)) !== null) {
32
+ seen.add(m[1].trim());
33
+ }
34
+ return [...seen];
35
+ }
36
+ function getPageLinks(wikiDb, slug) {
37
+ const page = wikiDb.getPage(slug);
38
+ if (!page) return null;
39
+ const pages = wikiDb.listPages();
40
+ const slugSet = new Set(pages.map((p) => p.slug));
41
+ const linked = extractLinks(page.content);
42
+ const outgoing = linked.filter((s) => slugSet.has(s));
43
+ const broken = linked.filter((s) => !slugSet.has(s));
44
+ const incoming = pages.filter((p) => p.slug !== slug && extractLinks(p.content).includes(slug)).map((p) => p.slug);
45
+ return { outgoing, broken, incoming };
46
+ }
47
+ // Annotate the CommonJS export names for ESM import in node:
48
+ 0 && (module.exports = {
49
+ WIKI_LINK_RE,
50
+ extractLinks,
51
+ getPageLinks
52
+ });
53
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/wiki/links.ts"],
4
+ "sourcesContent": ["/**\n * KiroGraph Wiki \u2014 Link graph\n *\n * Read-only queries over the `[[slug]]` cross-reference graph between wiki\n * pages. Shared by lint.ts (broken-link/orphan detection) and the\n * `kirograph wiki links`/`wiki rename` CLI commands.\n */\n\nimport type { WikiDatabase } from './database';\n\nexport const WIKI_LINK_RE = /\\[\\[([^\\]]+)\\]\\]/g;\n\n/** Distinct slugs referenced via `[[slug]]` in `content`, in order of first appearance. */\nexport function extractLinks(content: string): string[] {\n const seen = new Set<string>();\n const re = new RegExp(WIKI_LINK_RE.source, 'g');\n let m: RegExpExecArray | null;\n while ((m = re.exec(content)) !== null) {\n seen.add(m[1].trim());\n }\n return [...seen];\n}\n\nexport interface WikiPageLinks {\n /** Slugs this page links to that exist. */\n outgoing: string[];\n /** Slugs this page links to that don't exist as a page. */\n broken: string[];\n /** Slugs of other pages that link to this one. */\n incoming: string[];\n}\n\n/**\n * Outgoing/broken/incoming `[[slug]]` links for one page.\n * Returns null if the slug isn't a known page.\n */\nexport function getPageLinks(wikiDb: WikiDatabase, slug: string): WikiPageLinks | null {\n const page = wikiDb.getPage(slug);\n if (!page) return null;\n\n const pages = wikiDb.listPages();\n const slugSet = new Set(pages.map(p => p.slug));\n\n const linked = extractLinks(page.content);\n const outgoing = linked.filter(s => slugSet.has(s));\n const broken = linked.filter(s => !slugSet.has(s));\n\n const incoming = pages\n .filter(p => p.slug !== slug && extractLinks(p.content).includes(slug))\n .map(p => p.slug);\n\n return { outgoing, broken, incoming };\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUO,MAAM,eAAe;AAGrB,SAAS,aAAa,SAA2B;AACtD,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,KAAK,IAAI,OAAO,aAAa,QAAQ,GAAG;AAC9C,MAAI;AACJ,UAAQ,IAAI,GAAG,KAAK,OAAO,OAAO,MAAM;AACtC,SAAK,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC;AAAA,EACtB;AACA,SAAO,CAAC,GAAG,IAAI;AACjB;AAeO,SAAS,aAAa,QAAsB,MAAoC;AACrF,QAAM,OAAO,OAAO,QAAQ,IAAI;AAChC,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,QAAQ,OAAO,UAAU;AAC/B,QAAM,UAAU,IAAI,IAAI,MAAM,IAAI,OAAK,EAAE,IAAI,CAAC;AAE9C,QAAM,SAAS,aAAa,KAAK,OAAO;AACxC,QAAM,WAAW,OAAO,OAAO,OAAK,QAAQ,IAAI,CAAC,CAAC;AAClD,QAAM,SAAS,OAAO,OAAO,OAAK,CAAC,QAAQ,IAAI,CAAC,CAAC;AAEjD,QAAM,WAAW,MACd,OAAO,OAAK,EAAE,SAAS,QAAQ,aAAa,EAAE,OAAO,EAAE,SAAS,IAAI,CAAC,EACrE,IAAI,OAAK,EAAE,IAAI;AAElB,SAAO,EAAE,UAAU,QAAQ,SAAS;AACtC;",
6
+ "names": []
7
+ }
package/dist/wiki/lint.js CHANGED
@@ -21,16 +21,13 @@ __export(lint_exports, {
21
21
  lintWiki: () => lintWiki
22
22
  });
23
23
  module.exports = __toCommonJS(lint_exports);
24
- const LINK_RE = /\[\[([^\]]+)\]\]/g;
24
+ var import_links = require("./links");
25
25
  function lintWiki(wikiDb) {
26
26
  const issues = [];
27
27
  const pages = wikiDb.listPages();
28
28
  const slugSet = new Set(pages.map((p) => p.slug));
29
29
  for (const page of pages) {
30
- let m;
31
- const re = new RegExp(LINK_RE.source, "g");
32
- while ((m = re.exec(page.content)) !== null) {
33
- const linked = m[1].trim();
30
+ for (const linked of (0, import_links.extractLinks)(page.content)) {
34
31
  if (!slugSet.has(linked)) {
35
32
  issues.push({
36
33
  kind: "broken_link",
@@ -41,7 +38,7 @@ function lintWiki(wikiDb) {
41
38
  }
42
39
  }
43
40
  const hasRelated = /^## Related/m.test(page.content);
44
- const hasIncoming = pages.some((p) => p.slug !== page.slug && p.content.includes(`[[${page.slug}]]`));
41
+ const hasIncoming = pages.some((p) => p.slug !== page.slug && (0, import_links.extractLinks)(p.content).includes(page.slug));
45
42
  if (!hasRelated && !hasIncoming && pages.length > 1) {
46
43
  issues.push({
47
44
  kind: "orphan",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/wiki/lint.ts"],
4
- "sourcesContent": ["/**\n * KiroGraph Wiki \u2014 Lint\n *\n * Health checks: contradictions (FTS similarity), orphan pages,\n * broken [[slug]] cross-references.\n */\n\nimport * as path from 'path';\nimport type { WikiLintIssue } from './types';\nimport type { WikiDatabase } from './database';\n\nconst LINK_RE = /\\[\\[([^\\]]+)\\]\\]/g;\n\nexport function lintWiki(wikiDb: WikiDatabase): WikiLintIssue[] {\n const issues: WikiLintIssue[] = [];\n const pages = wikiDb.listPages();\n const slugSet = new Set(pages.map(p => p.slug));\n\n for (const page of pages) {\n // Broken [[slug]] links\n let m: RegExpExecArray | null;\n const re = new RegExp(LINK_RE.source, 'g');\n while ((m = re.exec(page.content)) !== null) {\n const linked = m[1].trim();\n if (!slugSet.has(linked)) {\n issues.push({\n kind: 'broken_link',\n slug: page.slug,\n detail: `Broken link to [[${linked}]] \u2014 page does not exist`,\n relatedSlug: linked,\n });\n }\n }\n\n // Orphan: no ## Related section and no incoming links from other pages\n const hasRelated = /^## Related/m.test(page.content);\n const hasIncoming = pages.some(p => p.slug !== page.slug && p.content.includes(`[[${page.slug}]]`));\n if (!hasRelated && !hasIncoming && pages.length > 1) {\n issues.push({\n kind: 'orphan',\n slug: page.slug,\n detail: 'Page has no ## Related section and no incoming links from other pages',\n });\n }\n\n // Stale: ## Sources section present but no date found (can't verify freshness)\n const sourcesMatch = page.content.match(/## Sources\\n([\\s\\S]*?)(?=\\n## |\\n*$)/);\n if (sourcesMatch) {\n const sourcesBlock = sourcesMatch[1];\n const hasDates = /\\d{4}-\\d{2}-\\d{2}/.test(sourcesBlock);\n if (!hasDates) {\n issues.push({\n kind: 'stale',\n slug: page.slug,\n detail: '## Sources section has no dates \u2014 cannot verify freshness',\n });\n }\n }\n\n // Contradictions: FTS similarity with other pages on the same topic\n // Simple heuristic: search for the page title and flag pages with conflicting signals\n const similar = wikiDb.search(page.title, 5);\n for (const { page: other } of similar) {\n if (other.slug === page.slug) continue;\n // Check for negation keywords close to shared terms\n const contradictionSignals = ['instead of', 'not', 'replaced by', 'superseded', 'deprecated'];\n const bothMentionSignal = contradictionSignals.some(\n sig => page.content.toLowerCase().includes(sig) && other.content.toLowerCase().includes(sig)\n );\n if (bothMentionSignal) {\n const key = [page.slug, other.slug].sort().join('|');\n const alreadyReported = issues.some(\n i => i.kind === 'contradiction' && i.detail.includes(other.slug)\n );\n if (!alreadyReported) {\n issues.push({\n kind: 'contradiction',\n slug: page.slug,\n detail: `Possible contradiction with [[${other.slug}]] \u2014 both pages contain negation signals on shared topics`,\n relatedSlug: other.slug,\n });\n }\n }\n }\n }\n\n return issues;\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,MAAM,UAAU;AAET,SAAS,SAAS,QAAuC;AAC9D,QAAM,SAA0B,CAAC;AACjC,QAAM,QAAQ,OAAO,UAAU;AAC/B,QAAM,UAAU,IAAI,IAAI,MAAM,IAAI,OAAK,EAAE,IAAI,CAAC;AAE9C,aAAW,QAAQ,OAAO;AAExB,QAAI;AACJ,UAAM,KAAK,IAAI,OAAO,QAAQ,QAAQ,GAAG;AACzC,YAAQ,IAAI,GAAG,KAAK,KAAK,OAAO,OAAO,MAAM;AAC3C,YAAM,SAAS,EAAE,CAAC,EAAE,KAAK;AACzB,UAAI,CAAC,QAAQ,IAAI,MAAM,GAAG;AACxB,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN,MAAM,KAAK;AAAA,UACX,QAAQ,oBAAoB,MAAM;AAAA,UAClC,aAAa;AAAA,QACf,CAAC;AAAA,MACH;AAAA,IACF;AAGA,UAAM,aAAa,eAAe,KAAK,KAAK,OAAO;AACnD,UAAM,cAAc,MAAM,KAAK,OAAK,EAAE,SAAS,KAAK,QAAQ,EAAE,QAAQ,SAAS,KAAK,KAAK,IAAI,IAAI,CAAC;AAClG,QAAI,CAAC,cAAc,CAAC,eAAe,MAAM,SAAS,GAAG;AACnD,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,MAAM,KAAK;AAAA,QACX,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAGA,UAAM,eAAe,KAAK,QAAQ,MAAM,sCAAsC;AAC9E,QAAI,cAAc;AAChB,YAAM,eAAe,aAAa,CAAC;AACnC,YAAM,WAAW,oBAAoB,KAAK,YAAY;AACtD,UAAI,CAAC,UAAU;AACb,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN,MAAM,KAAK;AAAA,UACX,QAAQ;AAAA,QACV,CAAC;AAAA,MACH;AAAA,IACF;AAIA,UAAM,UAAU,OAAO,OAAO,KAAK,OAAO,CAAC;AAC3C,eAAW,EAAE,MAAM,MAAM,KAAK,SAAS;AACrC,UAAI,MAAM,SAAS,KAAK,KAAM;AAE9B,YAAM,uBAAuB,CAAC,cAAc,OAAO,eAAe,cAAc,YAAY;AAC5F,YAAM,oBAAoB,qBAAqB;AAAA,QAC7C,SAAO,KAAK,QAAQ,YAAY,EAAE,SAAS,GAAG,KAAK,MAAM,QAAQ,YAAY,EAAE,SAAS,GAAG;AAAA,MAC7F;AACA,UAAI,mBAAmB;AACrB,cAAM,MAAM,CAAC,KAAK,MAAM,MAAM,IAAI,EAAE,KAAK,EAAE,KAAK,GAAG;AACnD,cAAM,kBAAkB,OAAO;AAAA,UAC7B,OAAK,EAAE,SAAS,mBAAmB,EAAE,OAAO,SAAS,MAAM,IAAI;AAAA,QACjE;AACA,YAAI,CAAC,iBAAiB;AACpB,iBAAO,KAAK;AAAA,YACV,MAAM;AAAA,YACN,MAAM,KAAK;AAAA,YACX,QAAQ,iCAAiC,MAAM,IAAI;AAAA,YACnD,aAAa,MAAM;AAAA,UACrB,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;",
4
+ "sourcesContent": ["/**\n * KiroGraph Wiki \u2014 Lint\n *\n * Health checks: contradictions (FTS similarity), orphan pages,\n * broken [[slug]] cross-references.\n */\n\nimport * as path from 'path';\nimport type { WikiLintIssue } from './types';\nimport type { WikiDatabase } from './database';\nimport { extractLinks } from './links';\n\nexport function lintWiki(wikiDb: WikiDatabase): WikiLintIssue[] {\n const issues: WikiLintIssue[] = [];\n const pages = wikiDb.listPages();\n const slugSet = new Set(pages.map(p => p.slug));\n\n for (const page of pages) {\n // Broken [[slug]] links\n for (const linked of extractLinks(page.content)) {\n if (!slugSet.has(linked)) {\n issues.push({\n kind: 'broken_link',\n slug: page.slug,\n detail: `Broken link to [[${linked}]] \u2014 page does not exist`,\n relatedSlug: linked,\n });\n }\n }\n\n // Orphan: no ## Related section and no incoming links from other pages\n const hasRelated = /^## Related/m.test(page.content);\n const hasIncoming = pages.some(p => p.slug !== page.slug && extractLinks(p.content).includes(page.slug));\n if (!hasRelated && !hasIncoming && pages.length > 1) {\n issues.push({\n kind: 'orphan',\n slug: page.slug,\n detail: 'Page has no ## Related section and no incoming links from other pages',\n });\n }\n\n // Stale: ## Sources section present but no date found (can't verify freshness)\n const sourcesMatch = page.content.match(/## Sources\\n([\\s\\S]*?)(?=\\n## |\\n*$)/);\n if (sourcesMatch) {\n const sourcesBlock = sourcesMatch[1];\n const hasDates = /\\d{4}-\\d{2}-\\d{2}/.test(sourcesBlock);\n if (!hasDates) {\n issues.push({\n kind: 'stale',\n slug: page.slug,\n detail: '## Sources section has no dates \u2014 cannot verify freshness',\n });\n }\n }\n\n // Contradictions: FTS similarity with other pages on the same topic\n // Simple heuristic: search for the page title and flag pages with conflicting signals\n const similar = wikiDb.search(page.title, 5);\n for (const { page: other } of similar) {\n if (other.slug === page.slug) continue;\n // Check for negation keywords close to shared terms\n const contradictionSignals = ['instead of', 'not', 'replaced by', 'superseded', 'deprecated'];\n const bothMentionSignal = contradictionSignals.some(\n sig => page.content.toLowerCase().includes(sig) && other.content.toLowerCase().includes(sig)\n );\n if (bothMentionSignal) {\n const key = [page.slug, other.slug].sort().join('|');\n const alreadyReported = issues.some(\n i => i.kind === 'contradiction' && i.detail.includes(other.slug)\n );\n if (!alreadyReported) {\n issues.push({\n kind: 'contradiction',\n slug: page.slug,\n detail: `Possible contradiction with [[${other.slug}]] \u2014 both pages contain negation signals on shared topics`,\n relatedSlug: other.slug,\n });\n }\n }\n }\n }\n\n return issues;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,mBAA6B;AAEtB,SAAS,SAAS,QAAuC;AAC9D,QAAM,SAA0B,CAAC;AACjC,QAAM,QAAQ,OAAO,UAAU;AAC/B,QAAM,UAAU,IAAI,IAAI,MAAM,IAAI,OAAK,EAAE,IAAI,CAAC;AAE9C,aAAW,QAAQ,OAAO;AAExB,eAAW,cAAU,2BAAa,KAAK,OAAO,GAAG;AAC/C,UAAI,CAAC,QAAQ,IAAI,MAAM,GAAG;AACxB,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN,MAAM,KAAK;AAAA,UACX,QAAQ,oBAAoB,MAAM;AAAA,UAClC,aAAa;AAAA,QACf,CAAC;AAAA,MACH;AAAA,IACF;AAGA,UAAM,aAAa,eAAe,KAAK,KAAK,OAAO;AACnD,UAAM,cAAc,MAAM,KAAK,OAAK,EAAE,SAAS,KAAK,YAAQ,2BAAa,EAAE,OAAO,EAAE,SAAS,KAAK,IAAI,CAAC;AACvG,QAAI,CAAC,cAAc,CAAC,eAAe,MAAM,SAAS,GAAG;AACnD,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,MAAM,KAAK;AAAA,QACX,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAGA,UAAM,eAAe,KAAK,QAAQ,MAAM,sCAAsC;AAC9E,QAAI,cAAc;AAChB,YAAM,eAAe,aAAa,CAAC;AACnC,YAAM,WAAW,oBAAoB,KAAK,YAAY;AACtD,UAAI,CAAC,UAAU;AACb,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN,MAAM,KAAK;AAAA,UACX,QAAQ;AAAA,QACV,CAAC;AAAA,MACH;AAAA,IACF;AAIA,UAAM,UAAU,OAAO,OAAO,KAAK,OAAO,CAAC;AAC3C,eAAW,EAAE,MAAM,MAAM,KAAK,SAAS;AACrC,UAAI,MAAM,SAAS,KAAK,KAAM;AAE9B,YAAM,uBAAuB,CAAC,cAAc,OAAO,eAAe,cAAc,YAAY;AAC5F,YAAM,oBAAoB,qBAAqB;AAAA,QAC7C,SAAO,KAAK,QAAQ,YAAY,EAAE,SAAS,GAAG,KAAK,MAAM,QAAQ,YAAY,EAAE,SAAS,GAAG;AAAA,MAC7F;AACA,UAAI,mBAAmB;AACrB,cAAM,MAAM,CAAC,KAAK,MAAM,MAAM,IAAI,EAAE,KAAK,EAAE,KAAK,GAAG;AACnD,cAAM,kBAAkB,OAAO;AAAA,UAC7B,OAAK,EAAE,SAAS,mBAAmB,EAAE,OAAO,SAAS,MAAM,IAAI;AAAA,QACjE;AACA,YAAI,CAAC,iBAAiB;AACpB,iBAAO,KAAK;AAAA,YACV,MAAM;AAAA,YACN,MAAM,KAAK;AAAA,YACX,QAAQ,iCAAiC,MAAM,IAAI;AAAA,YACnD,aAAa,MAAM;AAAA,UACrB,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;",
6
6
  "names": []
7
7
  }