kirograph 1.2.3 → 1.3.0

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 (41) hide show
  1. package/README.md +4 -3
  2. package/dist/bin/commands/attack-surface.js +15 -2
  3. package/dist/bin/commands/attack-surface.js.map +2 -2
  4. package/dist/bin/commands/help.js +4 -2
  5. package/dist/bin/commands/help.js.map +2 -2
  6. package/dist/bin/commands/memory.js +23 -3
  7. package/dist/bin/commands/memory.js.map +2 -2
  8. package/dist/bin/commands/security-export.js +7 -1
  9. package/dist/bin/commands/security-export.js.map +2 -2
  10. package/dist/bin/commands/wiki.js +12 -1
  11. package/dist/bin/commands/wiki.js.map +2 -2
  12. package/dist/bin/installer/common.js +25 -0
  13. package/dist/bin/installer/common.js.map +2 -2
  14. package/dist/bin/installer/config-prompt.js +25 -1
  15. package/dist/bin/installer/config-prompt.js.map +2 -2
  16. package/dist/bin/installer/index.js +23 -0
  17. package/dist/bin/installer/index.js.map +2 -2
  18. package/dist/bin/installer/steering.js +5 -1
  19. package/dist/bin/installer/steering.js.map +2 -2
  20. package/dist/bin/kirograph.js +1 -1
  21. package/dist/config.js +63 -2
  22. package/dist/config.js.map +2 -2
  23. package/dist/mcp/handlers/memory.js +17 -5
  24. package/dist/mcp/handlers/memory.js.map +2 -2
  25. package/dist/mcp/handlers/security.js +13 -2
  26. package/dist/mcp/handlers/security.js.map +2 -2
  27. package/dist/mcp/handlers/wiki.js +11 -1
  28. package/dist/mcp/handlers/wiki.js.map +2 -2
  29. package/dist/mcp/tools.js +4 -4
  30. package/dist/mcp/tools.js.map +2 -2
  31. package/dist/memory/index.js +40 -10
  32. package/dist/memory/index.js.map +2 -2
  33. package/dist/memory/relation-display.js +37 -0
  34. package/dist/memory/relation-display.js.map +7 -0
  35. package/dist/security/attack-surface.js +20 -2
  36. package/dist/security/attack-surface.js.map +2 -2
  37. package/dist/wiki/index.js +15 -2
  38. package/dist/wiki/index.js.map +2 -2
  39. package/dist/wiki/lint.js +23 -11
  40. package/dist/wiki/lint.js.map +2 -2
  41. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/memory/index.ts"],
4
- "sourcesContent": ["/**\n * KiroGraph Memory \u2014 Public API\n *\n * MemoryManager is the single entry point for all memory operations.\n * Coordinates compression, storage, symbol detection, and embedding.\n */\n\nimport type { KiroGraphConfig } from '../config';\nimport type {\n MemObservation,\n MemObservationInput,\n MemSession,\n ScoredObservation,\n MemSearchOptions,\n MemTimelineOptions,\n MemStats,\n CompressResult,\n WatchmenReadyResult,\n MemRelation,\n MemRelationInput,\n RelationType,\n MemPrompt,\n} from './types';\nimport { MemoryDatabase } from './database';\nimport { compressObservation, type CavemanMode } from './compress';\nimport { detectSymbols } from './symbols';\nimport { MemoryVectorManager } from './vectors';\nimport { WatchmenChecker } from '../watchmen';\nimport { logDebug } from '../errors';\nimport { validateObservationInput, validateObservationFields } from './schema';\nimport * as path from 'path';\n\nexport { MemoryDatabase } from './database';\nexport { compressObservation, extractIdentifiers, type CavemanMode } from './compress';\nexport { detectSymbols } from './symbols';\nexport { MemoryVectorManager } from './vectors';\nexport { MemorySchemaError, OBSERVATION_KINDS, validateObservationInput, validateObservationFields } from './schema';\nexport * from './types';\n\n// \u2500\u2500 MemoryManager \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 MemoryManager {\n private memDb: MemoryDatabase;\n private vectorMgr: MemoryVectorManager;\n private config: KiroGraphConfig;\n private db: any; // raw sqlite handle for symbol detection\n private cavemanMode: CavemanMode;\n private excludePatterns: string[];\n private watchmenChecker: WatchmenChecker | null;\n private projectRoot: string;\n\n constructor(config: KiroGraphConfig, db: any, projectRoot = process.cwd()) {\n this.config = config;\n this.db = db;\n this.projectRoot = projectRoot;\n this.memDb = new MemoryDatabase(db);\n const kirographDir = require('path').join(projectRoot, '.kirograph');\n this.vectorMgr = new MemoryVectorManager(config, this.memDb, kirographDir);\n this.cavemanMode = (config as any).cavemanMode ?? 'off';\n this.excludePatterns = (config as any).memoryExcludePatterns ?? [];\n this.watchmenChecker = config.enableWatchmen\n ? new WatchmenChecker(config.watchmenThreshold)\n : null;\n }\n\n /**\n * Initialize memory tables. Must be called before any operations.\n */\n initialize(): void {\n this.memDb.initialize();\n }\n\n /**\n * Check if memory is enabled in config.\n */\n static isEnabled(config: KiroGraphConfig): boolean {\n return !!(config as any).enableMemory;\n }\n\n // \u2500\u2500 Store \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Store an observation. Handles:\n * 1. Privacy stripping (<private> blocks)\n * 2. Exclude pattern checking\n * 3. Caveman compression (if enabled)\n * 4. Deduplication (content hash)\n * 5. Symbol detection and linking\n * 6. Embedding (if enabled)\n * 7. Watchmen threshold check (if enabled) \u2014 returns WatchmenReadyResult when synthesis should run\n *\n * Returns the observation ID, a WatchmenReadyResult, or null if skipped (duplicate or excluded).\n * @throws MemorySchemaError if `memoryStrictWrites` is enabled and the input doesn't conform.\n */\n async store(input: MemObservationInput, ide = 'kiro'): Promise<string | WatchmenReadyResult | null> {\n // 0. Opt-in schema validation \u2014 rejects instead of silently corrupting the store.\n if ((this.config as any).memoryStrictWrites) {\n validateObservationInput(input);\n }\n if ((this.config as any).memorySchemaValidation) {\n validateObservationFields(input, path.join(this.projectRoot, '.kirograph', 'memory-schemas'));\n }\n\n let text = input.content;\n\n // 1. Strip <private> blocks\n text = MemoryDatabase.stripPrivate(text);\n if (!text.trim()) return null;\n\n // 2. Check exclude patterns\n if (this.shouldExclude(text)) return null;\n\n // 3. Compress (if caveman is on)\n let contentRaw: string | undefined;\n if (this.cavemanMode !== 'off') {\n const result = compressObservation(text, this.cavemanMode);\n contentRaw = text;\n text = result.compressed;\n }\n\n // 4. Get or create session\n const cwd = process.cwd();\n const sessionTimeout = (this.config as any).memorySessionTimeout ?? 7200;\n const sessionId = this.memDb.getOrCreateSession(ide, cwd, sessionTimeout);\n\n // 5. Insert observation (deduplicates via content_hash)\n const id = this.memDb.insertObservation(text, {\n contentRaw: this.cavemanMode !== 'off' ? contentRaw : undefined,\n kind: input.kind ?? 'note',\n source: input.source ?? 'manual',\n tags: input.tags,\n sessionId,\n topicKey: input.topicKey,\n reviewAfter: input.reviewAfter,\n fields: input.fields,\n });\n\n if (!id) {\n logDebug('Memory: duplicate observation skipped');\n return null;\n }\n\n // 6. Detect and link symbols\n const symbols = detectSymbols(text, this.db);\n if (symbols.length > 0) {\n this.memDb.linkToSymbols(id, symbols.map(s => s.qualifiedName));\n }\n\n // Also detect in raw text if different\n if (contentRaw) {\n const rawSymbols = detectSymbols(contentRaw, this.db);\n const extraSymbols = rawSymbols.filter(\n s => !symbols.some(existing => existing.qualifiedName === s.qualifiedName)\n );\n if (extraSymbols.length > 0) {\n this.memDb.linkToSymbols(id, extraSymbols.map(s => s.qualifiedName));\n }\n }\n\n // 7. Embed (async, non-blocking for the store operation)\n const obs = this.memDb.getObservation(id);\n if (obs) {\n await this.vectorMgr.embedObservation(obs);\n }\n\n // 8. Watchmen threshold check\n if (this.watchmenChecker && input.kind !== 'summary') {\n const check = this.watchmenChecker.shouldSynthesize(this.memDb);\n if (check.ready) {\n return this.watchmenChecker.buildReadyResponse(id, check.pendingCount, this.projectRoot);\n }\n }\n\n return id;\n }\n\n // \u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Hybrid search: combines FTS5 and vector search with configurable alpha.\n */\n async search(query: string, opts: MemSearchOptions = {}): Promise<ScoredObservation[]> {\n const limit = opts.limit ?? 10;\n const alpha = opts.alpha ?? (this.config as any).memorySearchAlpha ?? 0.5;\n\n // FTS search\n const ftsResults = this.memDb.searchFTS(query, { ...opts, limit: limit * 2 });\n\n // Vector search (if enabled and no model mismatch)\n let vectorResults: ScoredObservation[] = [];\n if (this.vectorMgr.isEnabled() && !this.vectorMgr.hasModelMismatch()) {\n vectorResults = await this.vectorMgr.search(query, limit * 2);\n }\n\n // If only one source has results, return it directly\n if (vectorResults.length === 0) {\n const sliced = ftsResults.slice(0, limit);\n const ids = sliced.map(r => r.observation.id);\n const relMap = this.memDb.getRelationsForObservations(ids);\n for (const r of sliced) {\n const rels = relMap.get(r.observation.id);\n if (rels && rels.length > 0) r.relations = rels;\n }\n return sliced;\n }\n if (ftsResults.length === 0) {\n const sliced = vectorResults.slice(0, limit);\n const ids = sliced.map(r => r.observation.id);\n const relMap = this.memDb.getRelationsForObservations(ids);\n for (const r of sliced) {\n const rels = relMap.get(r.observation.id);\n if (rels && rels.length > 0) r.relations = rels;\n }\n return sliced;\n }\n\n // Hybrid merge: normalize scores and blend\n const merged = this.mergeResults(ftsResults, vectorResults, alpha);\n const sliced = merged.slice(0, limit);\n\n // Enrich with relations\n const ids = sliced.map(r => r.observation.id);\n const relMap = this.memDb.getRelationsForObservations(ids);\n for (const r of sliced) {\n const rels = relMap.get(r.observation.id);\n if (rels && rels.length > 0) r.relations = rels;\n }\n\n return sliced;\n }\n\n /**\n * Get observations linked to specific symbols.\n * Used by kirograph_context and kirograph_impact.\n */\n getLinkedObservations(qualifiedNames: string[], limit = 3, threshold = 0.3): ScoredObservation[] {\n const results = this.memDb.getLinkedObservationsForSymbols(qualifiedNames, limit * 2);\n return results.filter(r => r.score >= threshold).slice(0, limit);\n }\n\n // \u2500\u2500 Timeline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * List sessions with their observation counts.\n */\n timeline(opts: MemTimelineOptions = {}): { sessions: MemSession[]; observations: Map<string, MemObservation[]> } {\n const limit = opts.limit ?? 5;\n\n if (opts.sessionId) {\n const session = this.memDb.getSession(opts.sessionId);\n if (!session) return { sessions: [], observations: new Map() };\n const obs = this.memDb.getObservationsBySession(opts.sessionId);\n const map = new Map<string, MemObservation[]>();\n map.set(opts.sessionId, obs);\n return { sessions: [session], observations: map };\n }\n\n const sessions = this.memDb.listSessions(limit);\n const observations = new Map<string, MemObservation[]>();\n for (const session of sessions) {\n const obs = this.memDb.getObservationsBySession(session.id, 10);\n observations.set(session.id, obs);\n }\n return { sessions, observations };\n }\n\n // \u2500\u2500 Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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(): MemStats {\n const modelName = (this.config as any).embeddingModel ?? 'nomic-ai/nomic-embed-text-v1.5';\n return this.memDb.getStats(modelName);\n }\n\n // \u2500\u2500 Session management \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 endCurrentSession(ide = 'kiro'): void {\n const cwd = process.cwd();\n const sessionTimeout = (this.config as any).memorySessionTimeout ?? 7200;\n // Find active session and close it\n const sessionId = this.memDb.getOrCreateSession(ide, cwd, sessionTimeout);\n this.memDb.endSession(sessionId);\n }\n\n // \u2500\u2500 Maintenance \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 prune(olderThanMs: number): number {\n const cutoff = Date.now() - olderThanMs;\n return this.memDb.prune(cutoff);\n }\n\n async reembed(batchSize = 32): Promise<number> {\n return this.vectorMgr.reembed(batchSize);\n }\n\n lint(): { staleLinks: number; modelMismatch: boolean; staleSessions: number } {\n const staleLinks = this.memDb.findStaleLinks().length;\n const modelMismatch = this.vectorMgr.hasModelMismatch();\n const sessionTimeout = ((this.config as any).memorySessionTimeout ?? 7200) * 1000;\n const staleSessions = this.memDb.closeStaleSessionsOlderThan(sessionTimeout);\n return { staleLinks, modelMismatch, staleSessions };\n }\n\n removeStaleLinks(): number {\n return this.memDb.removeStaleLinks();\n }\n\n // \u2500\u2500 Relations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 compareObservations(input: MemRelationInput): string {\n // Resolve by topic_key if not found by ID\n let obsA = input.observationA;\n let obsB = input.observationB;\n if (!this.memDb.getObservation(obsA)) {\n const byKey = this.memDb.resolveObservationByTopicKey(obsA);\n if (byKey) obsA = byKey.id;\n }\n if (!this.memDb.getObservation(obsB)) {\n const byKey = this.memDb.resolveObservationByTopicKey(obsB);\n if (byKey) obsB = byKey.id;\n }\n return this.memDb.insertRelation({ ...input, observationA: obsA, observationB: obsB });\n }\n\n judgeRelation(relationId: string, relation: RelationType, confidence: number, reason?: string, evidence?: string): void {\n this.memDb.judgeRelation(relationId, relation, confidence, reason, evidence);\n }\n\n ignoreRelation(relationId: string): void {\n this.memDb.ignoreRelation(relationId);\n }\n\n getPendingRelations(limit = 20): MemRelation[] {\n return this.memDb.getPendingRelations(limit);\n }\n\n // \u2500\u2500 Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 getObservationsForReview(limit = 20): MemObservation[] {\n return this.memDb.getObservationsForReview(limit);\n }\n\n markReviewed(id: string): void {\n this.memDb.markReviewed(id);\n }\n\n // \u2500\u2500 Prompts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 async savePrompt(content: string, ide = 'kiro'): Promise<string> {\n const cwd = process.cwd();\n const sessionTimeout = (this.config as any).memorySessionTimeout ?? 7200;\n const sessionId = this.memDb.getOrCreateSession(ide, cwd, sessionTimeout);\n return this.memDb.insertPrompt(sessionId, content);\n }\n\n // \u2500\u2500 Passive capture \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 capturePassive(content: string, sessionId?: string): Array<{ id: string | null; kind: string; content: string }> {\n const kindMap: Record<string, string> = {\n 'key learnings': 'pattern',\n 'learnings': 'pattern',\n 'observations': 'note',\n 'decisions': 'decision',\n 'key changes': 'pattern',\n 'errors': 'error',\n 'patterns': 'pattern',\n };\n\n const results: Array<{ id: string | null; kind: string; content: string }> = [];\n const lines = content.split('\\n');\n let currentKind: string | null = null;\n\n for (const line of lines) {\n const headingMatch = line.match(/^#{1,3}\\s+(.+)/);\n if (headingMatch) {\n const heading = headingMatch[1].toLowerCase().trim();\n currentKind = kindMap[heading] ?? null;\n continue;\n }\n if (!currentKind) continue;\n const bulletMatch = line.match(/^[\\s]*[-*\u2022]\\s+(.+)|^[\\s]*\\d+[.)]\\s+(.+)/);\n if (bulletMatch) {\n const text = (bulletMatch[1] || bulletMatch[2]).trim();\n if (text.length < 5) continue;\n const id = this.memDb.insertObservation(text, { kind: currentKind, source: 'passive', sessionId });\n const symbols = detectSymbols(text, this.db);\n if (id && symbols.length > 0) this.memDb.linkToSymbols(id, symbols.map(s => s.qualifiedName));\n results.push({ id, kind: currentKind, content: text });\n }\n }\n\n return results;\n }\n\n // \u2500\u2500 Topic key \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 suggestTopicKey(kind: string, title: string): string {\n const slug = title\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '')\n .trim()\n .split(/\\s+/)\n .slice(0, 6)\n .join('-');\n return `${kind}/${slug}`;\n }\n\n // \u2500\u2500 Conflict scan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 scanConflicts(limit = 50): Array<{ observationA: MemObservation; observationB: MemObservation; similarity: number }> {\n const recent = this.memDb.getObservationsByTimeRange(0, Date.now(), limit);\n const candidates: Array<{ observationA: MemObservation; observationB: MemObservation; similarity: number }> = [];\n const seen = new Set<string>();\n\n for (const obs of recent) {\n if (!obs.content || obs.content.length < 20) continue;\n const words = obs.content.split(/\\s+/).slice(0, 8).join(' ');\n const similar = this.memDb.searchFTS(words, { limit: 5 });\n for (const match of similar) {\n if (match.observation.id === obs.id) continue;\n const pairKey = [obs.id, match.observation.id].sort().join('|');\n if (seen.has(pairKey)) continue;\n seen.add(pairKey);\n const existing = this.memDb.getRelationsForObservations([obs.id]);\n const alreadyLinked = existing.get(obs.id)?.some(r => r.observationA === match.observation.id || r.observationB === match.observation.id);\n if (!alreadyLinked && match.score > 0.3) {\n candidates.push({ observationA: obs, observationB: match.observation, similarity: match.score });\n }\n }\n }\n\n return candidates.slice(0, 20);\n }\n\n // \u2500\u2500 Private \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 private shouldExclude(text: string): boolean {\n if (this.excludePatterns.length === 0) return false;\n\n // Simple check: if any exclude pattern appears as a substring in the text\n // This is a basic implementation; could use picomatch for glob matching\n for (const pattern of this.excludePatterns) {\n const normalized = pattern.replace(/\\*/g, '');\n if (normalized && text.includes(normalized)) return true;\n }\n return false;\n }\n\n private mergeResults(\n ftsResults: ScoredObservation[],\n vectorResults: ScoredObservation[],\n alpha: number\n ): ScoredObservation[] {\n // Normalize FTS scores (rank is negative in FTS5, lower = better)\n const maxFts = Math.max(...ftsResults.map(r => r.score), 1);\n const normalizedFts = new Map<string, number>();\n for (const r of ftsResults) {\n normalizedFts.set(r.observation.id, r.score / maxFts);\n }\n\n // Vector scores are already 0-1 (cosine similarity)\n const normalizedVec = new Map<string, number>();\n for (const r of vectorResults) {\n normalizedVec.set(r.observation.id, r.score);\n }\n\n // Merge: collect all unique observation IDs\n const allIds = new Set([...normalizedFts.keys(), ...normalizedVec.keys()]);\n const merged: ScoredObservation[] = [];\n\n // Build a lookup for observations\n const obsMap = new Map<string, MemObservation>();\n for (const r of ftsResults) obsMap.set(r.observation.id, r.observation);\n for (const r of vectorResults) obsMap.set(r.observation.id, r.observation);\n\n for (const id of allIds) {\n const ftsScore = normalizedFts.get(id) ?? 0;\n const vecScore = normalizedVec.get(id) ?? 0;\n const hybridScore = (1 - alpha) * ftsScore + alpha * vecScore;\n const obs = obsMap.get(id)!;\n merged.push({ observation: obs, score: hybridScore, scoreSource: 'hybrid' });\n }\n\n merged.sort((a, b) => b.score - a.score);\n return merged;\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * KiroGraph Memory \u2014 Public API\n *\n * MemoryManager is the single entry point for all memory operations.\n * Coordinates compression, storage, symbol detection, and embedding.\n */\n\nimport type { KiroGraphConfig } from '../config';\nimport type {\n MemObservation,\n MemObservationInput,\n MemSession,\n ScoredObservation,\n MemSearchOptions,\n MemTimelineOptions,\n MemStats,\n CompressResult,\n WatchmenReadyResult,\n MemRelation,\n MemRelationInput,\n RelationType,\n MemPrompt,\n} from './types';\nimport { MemoryDatabase } from './database';\nimport { compressObservation, type CavemanMode } from './compress';\nimport { detectSymbols } from './symbols';\nimport { MemoryVectorManager } from './vectors';\nimport { WatchmenChecker } from '../watchmen';\nimport { logDebug } from '../errors';\nimport { validateObservationInput, validateObservationFields } from './schema';\nimport * as path from 'path';\n\nexport { MemoryDatabase } from './database';\nexport { compressObservation, extractIdentifiers, type CavemanMode } from './compress';\nexport { detectSymbols } from './symbols';\nexport { MemoryVectorManager } from './vectors';\nexport { MemorySchemaError, OBSERVATION_KINDS, validateObservationInput, validateObservationFields } from './schema';\nexport * from './types';\n\n// \u2500\u2500 MemoryManager \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 MemoryManager {\n private memDb: MemoryDatabase;\n private vectorMgr: MemoryVectorManager;\n private config: KiroGraphConfig;\n private db: any; // raw sqlite handle for symbol detection\n private cavemanMode: CavemanMode;\n private excludePatterns: string[];\n private watchmenChecker: WatchmenChecker | null;\n private projectRoot: string;\n\n constructor(config: KiroGraphConfig, db: any, projectRoot = process.cwd()) {\n this.config = config;\n this.db = db;\n this.projectRoot = projectRoot;\n this.memDb = new MemoryDatabase(db);\n const kirographDir = require('path').join(projectRoot, '.kirograph');\n this.vectorMgr = new MemoryVectorManager(config, this.memDb, kirographDir);\n this.cavemanMode = (config as any).cavemanMode ?? 'off';\n this.excludePatterns = (config as any).memoryExcludePatterns ?? [];\n this.watchmenChecker = config.enableWatchmen\n ? new WatchmenChecker(config.watchmenThreshold)\n : null;\n }\n\n /**\n * Initialize memory tables. Must be called before any operations.\n */\n initialize(): void {\n this.memDb.initialize();\n }\n\n /**\n * Check if memory is enabled in config.\n */\n static isEnabled(config: KiroGraphConfig): boolean {\n return !!(config as any).enableMemory;\n }\n\n // \u2500\u2500 Store \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Store an observation. Handles:\n * 1. Privacy stripping (<private> blocks)\n * 2. Exclude pattern checking\n * 3. Caveman compression (if enabled)\n * 4. Deduplication (content hash)\n * 5. Symbol detection and linking\n * 6. Embedding (if enabled)\n * 7. Watchmen threshold check (if enabled) \u2014 returns WatchmenReadyResult when synthesis should run\n *\n * Returns the observation ID, a WatchmenReadyResult, or null if skipped (duplicate or excluded).\n * @throws MemorySchemaError if `memoryStrictWrites` is enabled and the input doesn't conform.\n */\n async store(input: MemObservationInput, ide = 'kiro'): Promise<string | WatchmenReadyResult | null> {\n // 0. Opt-in schema validation \u2014 rejects instead of silently corrupting the store.\n if ((this.config as any).memoryStrictWrites) {\n validateObservationInput(input);\n }\n if ((this.config as any).memorySchemaValidation) {\n validateObservationFields(input, path.join(this.projectRoot, '.kirograph', 'memory-schemas'));\n }\n\n let text = input.content;\n\n // 1. Strip <private> blocks\n text = MemoryDatabase.stripPrivate(text);\n if (!text.trim()) return null;\n\n // 2. Check exclude patterns\n if (this.shouldExclude(text)) return null;\n\n // 3. Compress (if caveman is on)\n let contentRaw: string | undefined;\n if (this.cavemanMode !== 'off') {\n const result = compressObservation(text, this.cavemanMode);\n contentRaw = text;\n text = result.compressed;\n }\n\n // 4. Get or create session\n const cwd = process.cwd();\n const sessionTimeout = (this.config as any).memorySessionTimeout ?? 7200;\n const sessionId = this.memDb.getOrCreateSession(ide, cwd, sessionTimeout);\n\n // 5. Insert observation (deduplicates via content_hash)\n const id = this.memDb.insertObservation(text, {\n contentRaw: this.cavemanMode !== 'off' ? contentRaw : undefined,\n kind: input.kind ?? 'note',\n source: input.source ?? 'manual',\n tags: input.tags,\n sessionId,\n topicKey: input.topicKey,\n reviewAfter: input.reviewAfter,\n fields: input.fields,\n });\n\n if (!id) {\n logDebug('Memory: duplicate observation skipped');\n return null;\n }\n\n // 6. Detect and link symbols\n const symbols = detectSymbols(text, this.db);\n if (symbols.length > 0) {\n this.memDb.linkToSymbols(id, symbols.map(s => s.qualifiedName));\n }\n\n // Also detect in raw text if different\n if (contentRaw) {\n const rawSymbols = detectSymbols(contentRaw, this.db);\n const extraSymbols = rawSymbols.filter(\n s => !symbols.some(existing => existing.qualifiedName === s.qualifiedName)\n );\n if (extraSymbols.length > 0) {\n this.memDb.linkToSymbols(id, extraSymbols.map(s => s.qualifiedName));\n }\n }\n\n // 7. Embed (async, non-blocking for the store operation)\n const obs = this.memDb.getObservation(id);\n if (obs) {\n await this.vectorMgr.embedObservation(obs);\n }\n\n // 8. Watchmen threshold check\n if (this.watchmenChecker && input.kind !== 'summary') {\n const check = this.watchmenChecker.shouldSynthesize(this.memDb);\n if (check.ready) {\n return this.watchmenChecker.buildReadyResponse(id, check.pendingCount, this.projectRoot);\n }\n }\n\n return id;\n }\n\n // \u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Hybrid search: combines FTS5 and vector search with configurable alpha.\n */\n async search(query: string, opts: MemSearchOptions = {}): Promise<ScoredObservation[]> {\n const limit = opts.limit ?? 10;\n const alpha = opts.alpha ?? (this.config as any).memorySearchAlpha ?? 0.5;\n\n // FTS search\n const ftsResults = this.memDb.searchFTS(query, { ...opts, limit: limit * 2 });\n\n // Vector search (if enabled and no model mismatch)\n let vectorResults: ScoredObservation[] = [];\n if (this.vectorMgr.isEnabled() && !this.vectorMgr.hasModelMismatch()) {\n vectorResults = await this.vectorMgr.search(query, limit * 2);\n }\n\n // If only one source has results, return it directly\n if (vectorResults.length === 0) {\n const sliced = ftsResults.slice(0, limit);\n const ids = sliced.map(r => r.observation.id);\n const relMap = this.memDb.getRelationsForObservations(ids);\n for (const r of sliced) {\n const rels = relMap.get(r.observation.id);\n if (rels && rels.length > 0) r.relations = rels;\n }\n return sliced;\n }\n if (ftsResults.length === 0) {\n const sliced = vectorResults.slice(0, limit);\n const ids = sliced.map(r => r.observation.id);\n const relMap = this.memDb.getRelationsForObservations(ids);\n for (const r of sliced) {\n const rels = relMap.get(r.observation.id);\n if (rels && rels.length > 0) r.relations = rels;\n }\n return sliced;\n }\n\n // Hybrid merge: normalize scores and blend\n const merged = this.mergeResults(ftsResults, vectorResults, alpha);\n const sliced = merged.slice(0, limit);\n\n // Enrich with relations\n const ids = sliced.map(r => r.observation.id);\n const relMap = this.memDb.getRelationsForObservations(ids);\n for (const r of sliced) {\n const rels = relMap.get(r.observation.id);\n if (rels && rels.length > 0) r.relations = rels;\n }\n\n return sliced;\n }\n\n /**\n * Get observations linked to specific symbols.\n * Used by kirograph_context and kirograph_impact.\n */\n getLinkedObservations(qualifiedNames: string[], limit = 3, threshold = 0.3): ScoredObservation[] {\n const results = this.memDb.getLinkedObservationsForSymbols(qualifiedNames, limit * 2);\n return results.filter(r => r.score >= threshold).slice(0, limit);\n }\n\n // \u2500\u2500 Timeline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * List sessions with their observation counts.\n */\n timeline(opts: MemTimelineOptions = {}): { sessions: MemSession[]; observations: Map<string, MemObservation[]> } {\n const limit = opts.limit ?? 5;\n\n if (opts.sessionId) {\n const session = this.memDb.getSession(opts.sessionId);\n if (!session) return { sessions: [], observations: new Map() };\n const obs = this.memDb.getObservationsBySession(opts.sessionId);\n const map = new Map<string, MemObservation[]>();\n map.set(opts.sessionId, obs);\n return { sessions: [session], observations: map };\n }\n\n const sessions = this.memDb.listSessions(limit);\n const observations = new Map<string, MemObservation[]>();\n for (const session of sessions) {\n const obs = this.memDb.getObservationsBySession(session.id, 10);\n observations.set(session.id, obs);\n }\n return { sessions, observations };\n }\n\n // \u2500\u2500 Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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(): MemStats {\n const modelName = (this.config as any).embeddingModel ?? 'nomic-ai/nomic-embed-text-v1.5';\n return this.memDb.getStats(modelName);\n }\n\n // \u2500\u2500 Session management \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 endCurrentSession(ide = 'kiro'): void {\n const cwd = process.cwd();\n const sessionTimeout = (this.config as any).memorySessionTimeout ?? 7200;\n // Find active session and close it\n const sessionId = this.memDb.getOrCreateSession(ide, cwd, sessionTimeout);\n this.memDb.endSession(sessionId);\n }\n\n // \u2500\u2500 Maintenance \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 prune(olderThanMs: number): number {\n const cutoff = Date.now() - olderThanMs;\n return this.memDb.prune(cutoff);\n }\n\n async reembed(batchSize = 32): Promise<number> {\n return this.vectorMgr.reembed(batchSize);\n }\n\n lint(): { staleLinks: number; modelMismatch: boolean; staleSessions: number } {\n const staleLinks = this.memDb.findStaleLinks().length;\n const modelMismatch = this.vectorMgr.hasModelMismatch();\n const sessionTimeout = ((this.config as any).memorySessionTimeout ?? 7200) * 1000;\n const staleSessions = this.memDb.closeStaleSessionsOlderThan(sessionTimeout);\n return { staleLinks, modelMismatch, staleSessions };\n }\n\n removeStaleLinks(): number {\n return this.memDb.removeStaleLinks();\n }\n\n // \u2500\u2500 Relations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 /** Resolve an observation reference (ID or topic_key) to its ID. Returns the input unchanged if neither resolves. */\n private resolveObservationId(ref: string): string {\n if (this.memDb.getObservation(ref)) return ref;\n const byKey = this.memDb.resolveObservationByTopicKey(ref);\n return byKey ? byKey.id : ref;\n }\n\n compareObservations(input: MemRelationInput): string {\n const obsA = this.resolveObservationId(input.observationA);\n const obsB = this.resolveObservationId(input.observationB);\n return this.memDb.insertRelation({ ...input, observationA: obsA, observationB: obsB });\n }\n\n /**\n * Classify the relation between two observations via jev instead of\n * requiring the caller to supply `relation`/`confidence` itself. Only\n * used when `memoryRelationMode: 'jev'`. Auto-judges the relation when\n * jev's confidence is at or above `memoryRelationConfidenceThreshold`;\n * otherwise leaves it `pending` for review via the existing judge flow.\n */\n async autoCompareObservations(observationA: string, observationB: string): Promise<{\n relationId: string;\n relation: RelationType;\n confidence: number;\n autoJudged: boolean;\n }> {\n const obsAId = this.resolveObservationId(observationA);\n const obsBId = this.resolveObservationId(observationB);\n const obsA = this.memDb.getObservation(obsAId);\n const obsB = this.memDb.getObservation(obsBId);\n if (!obsA) throw new Error(`Observation not found: ${observationA}`);\n if (!obsB) throw new Error(`Observation not found: ${observationB}`);\n\n const { createJevClientFromConfig } = await Promise.resolve().then(() => require('../jev/client.js'));\n const { classifyRelationWithJev } = await Promise.resolve().then(() => require('./relation-jev.js'));\n const client = createJevClientFromConfig(this.config as unknown as { jevApiKey?: string; jevBaseUrl?: string; jevModel?: string });\n const { relation, confidence } = await classifyRelationWithJev(client, obsA.content, obsB.content);\n\n const relationId = this.memDb.insertRelation({\n observationA: obsAId,\n observationB: obsBId,\n relation,\n confidence,\n reason: 'Auto-classified by jev',\n });\n\n const threshold = (this.config as unknown as { memoryRelationConfidenceThreshold?: number }).memoryRelationConfidenceThreshold ?? 0.8;\n const autoJudged = confidence >= threshold;\n if (autoJudged) {\n this.memDb.judgeRelation(relationId, relation, confidence, 'Auto-judged by jev (confidence >= memoryRelationConfidenceThreshold)');\n }\n\n return { relationId, relation, confidence, autoJudged };\n }\n\n judgeRelation(relationId: string, relation: RelationType, confidence: number, reason?: string, evidence?: string): void {\n this.memDb.judgeRelation(relationId, relation, confidence, reason, evidence);\n }\n\n ignoreRelation(relationId: string): void {\n this.memDb.ignoreRelation(relationId);\n }\n\n getPendingRelations(limit = 20): MemRelation[] {\n return this.memDb.getPendingRelations(limit);\n }\n\n // \u2500\u2500 Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 getObservationsForReview(limit = 20): MemObservation[] {\n return this.memDb.getObservationsForReview(limit);\n }\n\n markReviewed(id: string): void {\n this.memDb.markReviewed(id);\n }\n\n // \u2500\u2500 Prompts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 async savePrompt(content: string, ide = 'kiro'): Promise<string> {\n const cwd = process.cwd();\n const sessionTimeout = (this.config as any).memorySessionTimeout ?? 7200;\n const sessionId = this.memDb.getOrCreateSession(ide, cwd, sessionTimeout);\n return this.memDb.insertPrompt(sessionId, content);\n }\n\n // \u2500\u2500 Passive capture \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 capturePassive(content: string, sessionId?: string): Array<{ id: string | null; kind: string; content: string }> {\n const kindMap: Record<string, string> = {\n 'key learnings': 'pattern',\n 'learnings': 'pattern',\n 'observations': 'note',\n 'decisions': 'decision',\n 'key changes': 'pattern',\n 'errors': 'error',\n 'patterns': 'pattern',\n };\n\n const results: Array<{ id: string | null; kind: string; content: string }> = [];\n const lines = content.split('\\n');\n let currentKind: string | null = null;\n\n for (const line of lines) {\n const headingMatch = line.match(/^#{1,3}\\s+(.+)/);\n if (headingMatch) {\n const heading = headingMatch[1].toLowerCase().trim();\n currentKind = kindMap[heading] ?? null;\n continue;\n }\n if (!currentKind) continue;\n const bulletMatch = line.match(/^[\\s]*[-*\u2022]\\s+(.+)|^[\\s]*\\d+[.)]\\s+(.+)/);\n if (bulletMatch) {\n const text = (bulletMatch[1] || bulletMatch[2]).trim();\n if (text.length < 5) continue;\n const id = this.memDb.insertObservation(text, { kind: currentKind, source: 'passive', sessionId });\n const symbols = detectSymbols(text, this.db);\n if (id && symbols.length > 0) this.memDb.linkToSymbols(id, symbols.map(s => s.qualifiedName));\n results.push({ id, kind: currentKind, content: text });\n }\n }\n\n return results;\n }\n\n // \u2500\u2500 Topic key \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 suggestTopicKey(kind: string, title: string): string {\n const slug = title\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '')\n .trim()\n .split(/\\s+/)\n .slice(0, 6)\n .join('-');\n return `${kind}/${slug}`;\n }\n\n // \u2500\u2500 Conflict scan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 scanConflicts(limit = 50): Array<{ observationA: MemObservation; observationB: MemObservation; similarity: number }> {\n const recent = this.memDb.getObservationsByTimeRange(0, Date.now(), limit);\n const candidates: Array<{ observationA: MemObservation; observationB: MemObservation; similarity: number }> = [];\n const seen = new Set<string>();\n\n for (const obs of recent) {\n if (!obs.content || obs.content.length < 20) continue;\n const words = obs.content.split(/\\s+/).slice(0, 8).join(' ');\n const similar = this.memDb.searchFTS(words, { limit: 5 });\n for (const match of similar) {\n if (match.observation.id === obs.id) continue;\n const pairKey = [obs.id, match.observation.id].sort().join('|');\n if (seen.has(pairKey)) continue;\n seen.add(pairKey);\n const existing = this.memDb.getRelationsForObservations([obs.id]);\n const alreadyLinked = existing.get(obs.id)?.some(r => r.observationA === match.observation.id || r.observationB === match.observation.id);\n if (!alreadyLinked && match.score > 0.3) {\n candidates.push({ observationA: obs, observationB: match.observation, similarity: match.score });\n }\n }\n }\n\n return candidates.slice(0, 20);\n }\n\n // \u2500\u2500 Private \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 private shouldExclude(text: string): boolean {\n if (this.excludePatterns.length === 0) return false;\n\n // Simple check: if any exclude pattern appears as a substring in the text\n // This is a basic implementation; could use picomatch for glob matching\n for (const pattern of this.excludePatterns) {\n const normalized = pattern.replace(/\\*/g, '');\n if (normalized && text.includes(normalized)) return true;\n }\n return false;\n }\n\n private mergeResults(\n ftsResults: ScoredObservation[],\n vectorResults: ScoredObservation[],\n alpha: number\n ): ScoredObservation[] {\n // Normalize FTS scores (rank is negative in FTS5, lower = better)\n const maxFts = Math.max(...ftsResults.map(r => r.score), 1);\n const normalizedFts = new Map<string, number>();\n for (const r of ftsResults) {\n normalizedFts.set(r.observation.id, r.score / maxFts);\n }\n\n // Vector scores are already 0-1 (cosine similarity)\n const normalizedVec = new Map<string, number>();\n for (const r of vectorResults) {\n normalizedVec.set(r.observation.id, r.score);\n }\n\n // Merge: collect all unique observation IDs\n const allIds = new Set([...normalizedFts.keys(), ...normalizedVec.keys()]);\n const merged: ScoredObservation[] = [];\n\n // Build a lookup for observations\n const obsMap = new Map<string, MemObservation>();\n for (const r of ftsResults) obsMap.set(r.observation.id, r.observation);\n for (const r of vectorResults) obsMap.set(r.observation.id, r.observation);\n\n for (const id of allIds) {\n const ftsScore = normalizedFts.get(id) ?? 0;\n const vecScore = normalizedVec.get(id) ?? 0;\n const hybridScore = (1 - alpha) * ftsScore + alpha * vecScore;\n const obs = obsMap.get(id)!;\n merged.push({ observation: obs, score: hybridScore, scoreSource: 'hybrid' });\n }\n\n merged.sort((a, b) => b.score - a.score);\n return merged;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["import_database", "import_compress", "import_symbols", "import_vectors", "import_schema", "sliced", "ids", "relMap"]
7
7
  }
@@ -0,0 +1,37 @@
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 relation_display_exports = {};
20
+ __export(relation_display_exports, {
21
+ describeMemRelation: () => describeMemRelation
22
+ });
23
+ module.exports = __toCommonJS(relation_display_exports);
24
+ function describeMemRelation(rel, observationId) {
25
+ const otherId = rel.observationA === observationId ? rel.observationB : rel.observationA;
26
+ if (rel.relation === "supersedes") {
27
+ const isOutdatedSide = rel.observationA === observationId;
28
+ return isOutdatedSide ? { icon: "\u26A0", label: "superseded by", otherId } : { icon: "\u21A9", label: "supersedes", otherId };
29
+ }
30
+ const icon = rel.relation === "conflicts_with" ? "\u26A1" : "~";
31
+ return { icon, label: rel.relation, otherId };
32
+ }
33
+ // Annotate the CommonJS export names for ESM import in node:
34
+ 0 && (module.exports = {
35
+ describeMemRelation
36
+ });
37
+ //# sourceMappingURL=relation-display.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/memory/relation-display.ts"],
4
+ "sourcesContent": ["import type { MemRelation } from './types';\n\n/**\n * Human-readable, direction-aware description of how `observationId` relates\n * to the other observation in `rel`.\n *\n * `supersedes` is directional \u2014 per its classification criteria (see\n * relation-jev.ts), Observation B replaces Observation A, so A is the\n * outdated side and B is the replacing side. Rendering both sides of the\n * same relation with the identical \"supersedes\" label (as the CLI/MCP output\n * used to) makes a directional relation look symmetric and misleading \u2014 a\n * still-current observation and the exact one it just replaced show up with\n * the same annotation. Every other relation type is symmetric and needs no\n * such distinction.\n */\nexport function describeMemRelation(rel: MemRelation, observationId: string): { icon: string; label: string; otherId: string } {\n const otherId = rel.observationA === observationId ? rel.observationB : rel.observationA;\n\n if (rel.relation === 'supersedes') {\n const isOutdatedSide = rel.observationA === observationId;\n return isOutdatedSide\n ? { icon: '\u26A0', label: 'superseded by', otherId }\n : { icon: '\u21A9', label: 'supersedes', otherId };\n }\n\n const icon = rel.relation === 'conflicts_with' ? '\u26A1' : '~';\n return { icon, label: rel.relation, otherId };\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAeO,SAAS,oBAAoB,KAAkB,eAAyE;AAC7H,QAAM,UAAU,IAAI,iBAAiB,gBAAgB,IAAI,eAAe,IAAI;AAE5E,MAAI,IAAI,aAAa,cAAc;AACjC,UAAM,iBAAiB,IAAI,iBAAiB;AAC5C,WAAO,iBACH,EAAE,MAAM,UAAK,OAAO,iBAAiB,QAAQ,IAC7C,EAAE,MAAM,UAAK,OAAO,cAAc,QAAQ;AAAA,EAChD;AAEA,QAAM,OAAO,IAAI,aAAa,mBAAmB,WAAM;AACvD,SAAO,EAAE,MAAM,OAAO,IAAI,UAAU,QAAQ;AAC9C;",
6
+ "names": []
7
+ }
@@ -36,11 +36,17 @@ const AUTH_PATTERNS = [
36
36
  const PUBLIC_ROUTE_PATTERNS = ["/api/", "/v1/", "/v2/", "/v3/", "/graphql", "/rest/"];
37
37
  const INTERNAL_ROUTE_PATTERNS = ["/internal/", "/admin/"];
38
38
  class AttackSurfaceAnalyzer {
39
- constructor(db) {
39
+ constructor(db, opts = {}) {
40
40
  this.db = db;
41
+ this.opts = opts;
41
42
  }
42
43
  async analyze() {
43
44
  const rawDb = this.db.getRawDb();
45
+ let jevClient;
46
+ if (this.opts.authDetectionMode === "jev") {
47
+ const { createJevClientFromConfig } = await Promise.resolve().then(() => require("../jev/client.js"));
48
+ jevClient = createJevClientFromConfig(this.opts);
49
+ }
44
50
  const routeRows = rawDb.all(
45
51
  `SELECT id, name, file_path FROM nodes WHERE kind = 'route'`
46
52
  );
@@ -142,10 +148,22 @@ class AttackSurfaceAnalyzer {
142
148
  WHERE n.name IS NOT NULL`,
143
149
  [route.id]
144
150
  );
145
- const isAuthenticated = pathNodeNames.some((row) => {
151
+ let isAuthenticated = pathNodeNames.some((row) => {
146
152
  const lowerName = (row.name ?? "").toLowerCase();
147
153
  return AUTH_PATTERNS.some((pattern) => lowerName.includes(pattern));
148
154
  });
155
+ if (!isAuthenticated && jevClient) {
156
+ const { checkAuthWithJev } = await Promise.resolve().then(() => require("./auth-detection-jev.js"));
157
+ const jevResult = await checkAuthWithJev(
158
+ jevClient,
159
+ route.name ?? route.id,
160
+ pathNodeNames.map((r) => r.name).filter((n) => !!n)
161
+ );
162
+ const threshold = this.opts.authConfidenceThreshold ?? 0.6;
163
+ if (jevResult.authenticated && jevResult.confidence >= threshold) {
164
+ isAuthenticated = true;
165
+ }
166
+ }
149
167
  const routeName = (route.name ?? "").toLowerCase();
150
168
  let exposureLevel;
151
169
  if (INTERNAL_ROUTE_PATTERNS.some((p) => routeName.includes(p))) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/security/attack-surface.ts"],
4
- "sourcesContent": ["/**\n * KiroGraph-Sec Attack Surface Analyzer\n *\n * Maps all route nodes in the knowledge graph to their reachable vulnerable\n * dependencies, computing exposure levels and risk scores per route.\n */\n\nimport type { GraphDatabase } from '../db/database';\n\n/** Auth-related name patterns for heuristic isAuthenticated detection. */\nconst AUTH_PATTERNS = [\n 'auth',\n 'authenticate',\n 'middleware',\n 'guard',\n 'protect',\n 'requirelogin',\n 'isloggedin',\n 'verifytoken',\n 'checkauth',\n 'ensureauth',\n] as const;\n\n/** Route path patterns that indicate a public-facing API. */\nconst PUBLIC_ROUTE_PATTERNS = ['/api/', '/v1/', '/v2/', '/v3/', '/graphql', '/rest/'];\n\n/** Route path patterns that indicate an internal/admin route. */\nconst INTERNAL_ROUTE_PATTERNS = ['/internal/', '/admin/'];\n\nexport interface AttackSurfaceEntry {\n route: string;\n nodeId: string;\n filePath: string;\n isAuthenticated: boolean;\n exposureLevel: 'public' | 'authenticated' | 'internal' | 'unknown';\n vulnerableDeps: Array<{\n cveId: string;\n packageName: string;\n severity: number | null;\n epssScore: number | null;\n riskScore: number | null;\n verdict: string | null;\n hopCount: number;\n }>;\n patternMatches: Array<{\n patternId: string;\n severity: string;\n line: number;\n symbolNodeId: string;\n symbolName: string;\n }>;\n patternRiskLevel: 'none' | 'low' | 'medium' | 'high' | 'critical';\n riskScore: number;\n}\n\nexport interface AttackSurfaceResult {\n totalRoutes: number;\n publicRoutes: number;\n authenticatedRoutes: number;\n routesWithVulns: number;\n criticalPaths: AttackSurfaceEntry[];\n allRoutes: AttackSurfaceEntry[];\n}\n\nexport class AttackSurfaceAnalyzer {\n constructor(private readonly db: GraphDatabase) {}\n\n async analyze(): Promise<AttackSurfaceResult> {\n const rawDb = this.db.getRawDb();\n\n // Step 1: Query all route nodes\n const routeRows: Array<{ id: string; name: string; file_path: string }> = rawDb.all(\n `SELECT id, name, file_path FROM nodes WHERE kind = 'route'`,\n );\n\n if (routeRows.length === 0) {\n return {\n totalRoutes: 0,\n publicRoutes: 0,\n authenticatedRoutes: 0,\n routesWithVulns: 0,\n criticalPaths: [],\n allRoutes: [],\n };\n }\n\n const entries: AttackSurfaceEntry[] = [];\n\n for (const route of routeRows) {\n // Step 2: BFS via SQL to find reachable dependency nodes (max 5 hops)\n const reachableDeps: Array<{\n node_id: string;\n package_name: string;\n hop_count: number;\n }> = rawDb.all(\n `WITH RECURSIVE reachable(node_id, depth) AS (\n SELECT target, 1 FROM edges WHERE source = ? AND kind IN ('calls', 'imports', 'references')\n UNION\n SELECT e.target, r.depth + 1\n FROM edges e\n JOIN reachable r ON e.source = r.node_id\n WHERE r.depth < 5 AND e.kind IN ('calls', 'imports', 'references')\n )\n SELECT d.node_id, d.package_name, MIN(r.depth) as hop_count\n FROM reachable r\n JOIN sec_dependencies d ON d.node_id = r.node_id\n GROUP BY d.node_id, d.package_name`,\n [route.id],\n );\n\n // Step 3: For each reachable dep, find affected/under_investigation vulns\n const vulnerableDeps: AttackSurfaceEntry['vulnerableDeps'] = [];\n\n for (const dep of reachableDeps) {\n const vulnRows: Array<{\n cve_id: string;\n package_name: string;\n severity_score: number | null;\n epss_score: number | null;\n risk_score: number | null;\n verdict: string | null;\n }> = rawDb.all(\n `SELECT v.cve_id, d.package_name, v.severity_score, v.epss_score, v.risk_score, r.verdict\n FROM sec_vulnerabilities v\n JOIN edges e ON e.target = v.node_id AND e.kind = 'has_vulnerability'\n JOIN sec_dependencies d ON d.node_id = e.source\n LEFT JOIN sec_reachability r ON r.vulnerability_node_id = v.node_id\n WHERE d.node_id = ?\n AND (r.verdict IN ('affected', 'under_investigation') OR r.verdict IS NULL)`,\n [dep.node_id],\n );\n\n for (const vuln of vulnRows) {\n vulnerableDeps.push({\n cveId: vuln.cve_id,\n packageName: vuln.package_name ?? dep.package_name,\n severity: vuln.severity_score,\n epssScore: vuln.epss_score,\n riskScore: vuln.risk_score,\n verdict: vuln.verdict,\n hopCount: dep.hop_count,\n });\n }\n }\n\n // Step 3b: Find pattern matches on the call path (table may not exist)\n const routeNodeId = route.id;\n let patternRows: Array<{\n symbol_node_id: string;\n symbol_name: string;\n pattern_id: string;\n severity: string;\n line: number;\n }> = [];\n try {\n patternRows = rawDb.all(\n `WITH RECURSIVE reachable(node_id, depth) AS (\n SELECT target, 1 FROM edges WHERE source = ? AND kind IN ('calls', 'imports', 'references')\n UNION\n SELECT e.target, r.depth + 1\n FROM edges e\n JOIN reachable r ON e.source = r.node_id\n WHERE r.depth < 5 AND e.kind IN ('calls', 'imports', 'references')\n )\n SELECT DISTINCT pm.symbol_node_id, n.name as symbol_name, pm.pattern_id, pm.severity, pm.line\n FROM reachable r\n JOIN pattern_matches pm ON pm.symbol_node_id = r.node_id\n JOIN nodes n ON n.id = pm.symbol_node_id\n WHERE pm.symbol_node_id IS NOT NULL\n ORDER BY CASE pm.severity WHEN 'critical' THEN 4 WHEN 'high' THEN 3 WHEN 'medium' THEN 2 ELSE 1 END DESC\n LIMIT 10`,\n [routeNodeId],\n );\n } catch {\n // pattern_matches table does not exist yet \u2014 degrade silently\n patternRows = [];\n }\n\n const patternMatches: AttackSurfaceEntry['patternMatches'] = patternRows.map(r => ({\n patternId: r.pattern_id,\n severity: r.severity,\n line: r.line,\n symbolNodeId: r.symbol_node_id,\n symbolName: r.symbol_name,\n }));\n\n const patternRiskLevel: AttackSurfaceEntry['patternRiskLevel'] =\n patternMatches.length === 0\n ? 'none'\n : (patternMatches[0].severity as AttackSurfaceEntry['patternRiskLevel']);\n\n // Step 4: Heuristic \u2014 isAuthenticated via path node names\n const pathNodeNames: Array<{ name: string }> = rawDb.all(\n `WITH RECURSIVE reachable(node_id, depth) AS (\n SELECT target, 1 FROM edges WHERE source = ? AND kind IN ('calls', 'imports', 'references')\n UNION\n SELECT e.target, r.depth + 1\n FROM edges e\n JOIN reachable r ON e.source = r.node_id\n WHERE r.depth < 5 AND e.kind IN ('calls', 'imports', 'references')\n )\n SELECT DISTINCT n.name\n FROM reachable r\n JOIN nodes n ON n.id = r.node_id\n WHERE n.name IS NOT NULL`,\n [route.id],\n );\n\n const isAuthenticated = pathNodeNames.some(row => {\n const lowerName = (row.name ?? '').toLowerCase();\n return AUTH_PATTERNS.some(pattern => lowerName.includes(pattern));\n });\n\n // Step 5: Determine exposure level\n const routeName = (route.name ?? '').toLowerCase();\n let exposureLevel: AttackSurfaceEntry['exposureLevel'];\n\n if (INTERNAL_ROUTE_PATTERNS.some(p => routeName.includes(p))) {\n exposureLevel = 'internal';\n } else if (isAuthenticated) {\n exposureLevel = 'authenticated';\n } else if (PUBLIC_ROUTE_PATTERNS.some(p => routeName.includes(p))) {\n exposureLevel = 'public';\n } else {\n exposureLevel = 'unknown';\n }\n\n // Step 6: riskScore = max risk_score of reachable affected vulns, plus pattern bonus\n const affectedVulns = vulnerableDeps.filter(v => v.verdict === 'affected' || v.verdict === 'under_investigation');\n const baseRiskScore = affectedVulns.reduce((max, v) => {\n if (v.riskScore != null && v.riskScore > max) return v.riskScore;\n return max;\n }, 0);\n\n const patternBonus: Record<AttackSurfaceEntry['patternRiskLevel'], number> = {\n none: 0, low: 0.3, medium: 0.6, high: 0.8, critical: 1.0,\n };\n const riskScore = Math.min(10, baseRiskScore + (patternBonus[patternRiskLevel] ?? 0));\n\n entries.push({\n route: route.name ?? route.id,\n nodeId: route.id,\n filePath: route.file_path ?? '',\n isAuthenticated,\n exposureLevel,\n vulnerableDeps,\n patternMatches,\n patternRiskLevel,\n riskScore,\n });\n }\n\n // Compute summary stats\n const publicRoutes = entries.filter(e => e.exposureLevel === 'public').length;\n const authenticatedRoutes = entries.filter(\n e => e.exposureLevel === 'authenticated' || e.exposureLevel === 'internal',\n ).length;\n const routesWithVulns = entries.filter(e => e.vulnerableDeps.length > 0).length;\n\n // Critical paths: routes with affected/under_investigation vulns, sorted by riskScore desc\n const criticalPaths = entries\n .filter(e => e.vulnerableDeps.length > 0)\n .sort((a, b) => b.riskScore - a.riskScore);\n\n const allRoutes = entries.sort((a, b) => b.riskScore - a.riskScore);\n\n return {\n totalRoutes: entries.length,\n publicRoutes,\n authenticatedRoutes,\n routesWithVulns,\n criticalPaths,\n allRoutes,\n };\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,MAAM,wBAAwB,CAAC,SAAS,QAAQ,QAAQ,QAAQ,YAAY,QAAQ;AAGpF,MAAM,0BAA0B,CAAC,cAAc,SAAS;AAqCjD,MAAM,sBAAsB;AAAA,EACjC,YAA6B,IAAmB;AAAnB;AAAA,EAAoB;AAAA,EAEjD,MAAM,UAAwC;AAC5C,UAAM,QAAQ,KAAK,GAAG,SAAS;AAG/B,UAAM,YAAoE,MAAM;AAAA,MAC9E;AAAA,IACF;AAEA,QAAI,UAAU,WAAW,GAAG;AAC1B,aAAO;AAAA,QACL,aAAa;AAAA,QACb,cAAc;AAAA,QACd,qBAAqB;AAAA,QACrB,iBAAiB;AAAA,QACjB,eAAe,CAAC;AAAA,QAChB,WAAW,CAAC;AAAA,MACd;AAAA,IACF;AAEA,UAAM,UAAgC,CAAC;AAEvC,eAAW,SAAS,WAAW;AAE7B,YAAM,gBAID,MAAM;AAAA,QACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAYA,CAAC,MAAM,EAAE;AAAA,MACX;AAGA,YAAM,iBAAuD,CAAC;AAE9D,iBAAW,OAAO,eAAe;AAC/B,cAAM,WAOD,MAAM;AAAA,UACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAOA,CAAC,IAAI,OAAO;AAAA,QACd;AAEA,mBAAW,QAAQ,UAAU;AAC3B,yBAAe,KAAK;AAAA,YAClB,OAAO,KAAK;AAAA,YACZ,aAAa,KAAK,gBAAgB,IAAI;AAAA,YACtC,UAAU,KAAK;AAAA,YACf,WAAW,KAAK;AAAA,YAChB,WAAW,KAAK;AAAA,YAChB,SAAS,KAAK;AAAA,YACd,UAAU,IAAI;AAAA,UAChB,CAAC;AAAA,QACH;AAAA,MACF;AAGA,YAAM,cAAc,MAAM;AAC1B,UAAI,cAMC,CAAC;AACN,UAAI;AACF,sBAAc,MAAM;AAAA,UAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAeA,CAAC,WAAW;AAAA,QACd;AAAA,MACF,QAAQ;AAEN,sBAAc,CAAC;AAAA,MACjB;AAEA,YAAM,iBAAuD,YAAY,IAAI,QAAM;AAAA,QACjF,WAAW,EAAE;AAAA,QACb,UAAU,EAAE;AAAA,QACZ,MAAM,EAAE;AAAA,QACR,cAAc,EAAE;AAAA,QAChB,YAAY,EAAE;AAAA,MAChB,EAAE;AAEF,YAAM,mBACJ,eAAe,WAAW,IACtB,SACC,eAAe,CAAC,EAAE;AAGzB,YAAM,gBAAyC,MAAM;AAAA,QACnD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAYA,CAAC,MAAM,EAAE;AAAA,MACX;AAEA,YAAM,kBAAkB,cAAc,KAAK,SAAO;AAChD,cAAM,aAAa,IAAI,QAAQ,IAAI,YAAY;AAC/C,eAAO,cAAc,KAAK,aAAW,UAAU,SAAS,OAAO,CAAC;AAAA,MAClE,CAAC;AAGD,YAAM,aAAa,MAAM,QAAQ,IAAI,YAAY;AACjD,UAAI;AAEJ,UAAI,wBAAwB,KAAK,OAAK,UAAU,SAAS,CAAC,CAAC,GAAG;AAC5D,wBAAgB;AAAA,MAClB,WAAW,iBAAiB;AAC1B,wBAAgB;AAAA,MAClB,WAAW,sBAAsB,KAAK,OAAK,UAAU,SAAS,CAAC,CAAC,GAAG;AACjE,wBAAgB;AAAA,MAClB,OAAO;AACL,wBAAgB;AAAA,MAClB;AAGA,YAAM,gBAAgB,eAAe,OAAO,OAAK,EAAE,YAAY,cAAc,EAAE,YAAY,qBAAqB;AAChH,YAAM,gBAAgB,cAAc,OAAO,CAAC,KAAK,MAAM;AACrD,YAAI,EAAE,aAAa,QAAQ,EAAE,YAAY,IAAK,QAAO,EAAE;AACvD,eAAO;AAAA,MACT,GAAG,CAAC;AAEJ,YAAM,eAAuE;AAAA,QAC3E,MAAM;AAAA,QAAG,KAAK;AAAA,QAAK,QAAQ;AAAA,QAAK,MAAM;AAAA,QAAK,UAAU;AAAA,MACvD;AACA,YAAM,YAAY,KAAK,IAAI,IAAI,iBAAiB,aAAa,gBAAgB,KAAK,EAAE;AAEpF,cAAQ,KAAK;AAAA,QACX,OAAO,MAAM,QAAQ,MAAM;AAAA,QAC3B,QAAQ,MAAM;AAAA,QACd,UAAU,MAAM,aAAa;AAAA,QAC7B;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAGA,UAAM,eAAe,QAAQ,OAAO,OAAK,EAAE,kBAAkB,QAAQ,EAAE;AACvE,UAAM,sBAAsB,QAAQ;AAAA,MAClC,OAAK,EAAE,kBAAkB,mBAAmB,EAAE,kBAAkB;AAAA,IAClE,EAAE;AACF,UAAM,kBAAkB,QAAQ,OAAO,OAAK,EAAE,eAAe,SAAS,CAAC,EAAE;AAGzE,UAAM,gBAAgB,QACnB,OAAO,OAAK,EAAE,eAAe,SAAS,CAAC,EACvC,KAAK,CAAC,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS;AAE3C,UAAM,YAAY,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS;AAElE,WAAO;AAAA,MACL,aAAa,QAAQ;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["/**\n * KiroGraph-Sec Attack Surface Analyzer\n *\n * Maps all route nodes in the knowledge graph to their reachable vulnerable\n * dependencies, computing exposure levels and risk scores per route.\n */\n\nimport type { GraphDatabase } from '../db/database';\n\n/** Auth-related name patterns for heuristic isAuthenticated detection. */\nconst AUTH_PATTERNS = [\n 'auth',\n 'authenticate',\n 'middleware',\n 'guard',\n 'protect',\n 'requirelogin',\n 'isloggedin',\n 'verifytoken',\n 'checkauth',\n 'ensureauth',\n] as const;\n\n/** Route path patterns that indicate a public-facing API. */\nconst PUBLIC_ROUTE_PATTERNS = ['/api/', '/v1/', '/v2/', '/v3/', '/graphql', '/rest/'];\n\n/** Route path patterns that indicate an internal/admin route. */\nconst INTERNAL_ROUTE_PATTERNS = ['/internal/', '/admin/'];\n\nexport interface AttackSurfaceEntry {\n route: string;\n nodeId: string;\n filePath: string;\n isAuthenticated: boolean;\n exposureLevel: 'public' | 'authenticated' | 'internal' | 'unknown';\n vulnerableDeps: Array<{\n cveId: string;\n packageName: string;\n severity: number | null;\n epssScore: number | null;\n riskScore: number | null;\n verdict: string | null;\n hopCount: number;\n }>;\n patternMatches: Array<{\n patternId: string;\n severity: string;\n line: number;\n symbolNodeId: string;\n symbolName: string;\n }>;\n patternRiskLevel: 'none' | 'low' | 'medium' | 'high' | 'critical';\n riskScore: number;\n}\n\nexport interface AttackSurfaceResult {\n totalRoutes: number;\n publicRoutes: number;\n authenticatedRoutes: number;\n routesWithVulns: number;\n criticalPaths: AttackSurfaceEntry[];\n allRoutes: AttackSurfaceEntry[];\n}\n\nexport interface AttackSurfaceAnalyzerOptions {\n /**\n * 'heuristic' (default): substring match only.\n * 'jev': when the heuristic finds no auth pattern, ask jev to confirm/override.\n */\n authDetectionMode?: 'heuristic' | 'jev';\n /** Confidence (0.0\u20131.0) above which a jev auth judgment overrides the heuristic's \"not authenticated\" default. Default: 0.6. */\n authConfidenceThreshold?: number;\n jevApiKey?: string;\n jevBaseUrl?: string;\n jevModel?: string;\n}\n\nexport class AttackSurfaceAnalyzer {\n constructor(\n private readonly db: GraphDatabase,\n private readonly opts: AttackSurfaceAnalyzerOptions = {},\n ) {}\n\n async analyze(): Promise<AttackSurfaceResult> {\n const rawDb = this.db.getRawDb();\n\n let jevClient: import('../jev/client').JevClient | undefined;\n if (this.opts.authDetectionMode === 'jev') {\n const { createJevClientFromConfig } = await Promise.resolve().then(() => require('../jev/client.js'));\n jevClient = createJevClientFromConfig(this.opts);\n }\n\n // Step 1: Query all route nodes\n const routeRows: Array<{ id: string; name: string; file_path: string }> = rawDb.all(\n `SELECT id, name, file_path FROM nodes WHERE kind = 'route'`,\n );\n\n if (routeRows.length === 0) {\n return {\n totalRoutes: 0,\n publicRoutes: 0,\n authenticatedRoutes: 0,\n routesWithVulns: 0,\n criticalPaths: [],\n allRoutes: [],\n };\n }\n\n const entries: AttackSurfaceEntry[] = [];\n\n for (const route of routeRows) {\n // Step 2: BFS via SQL to find reachable dependency nodes (max 5 hops)\n const reachableDeps: Array<{\n node_id: string;\n package_name: string;\n hop_count: number;\n }> = rawDb.all(\n `WITH RECURSIVE reachable(node_id, depth) AS (\n SELECT target, 1 FROM edges WHERE source = ? AND kind IN ('calls', 'imports', 'references')\n UNION\n SELECT e.target, r.depth + 1\n FROM edges e\n JOIN reachable r ON e.source = r.node_id\n WHERE r.depth < 5 AND e.kind IN ('calls', 'imports', 'references')\n )\n SELECT d.node_id, d.package_name, MIN(r.depth) as hop_count\n FROM reachable r\n JOIN sec_dependencies d ON d.node_id = r.node_id\n GROUP BY d.node_id, d.package_name`,\n [route.id],\n );\n\n // Step 3: For each reachable dep, find affected/under_investigation vulns\n const vulnerableDeps: AttackSurfaceEntry['vulnerableDeps'] = [];\n\n for (const dep of reachableDeps) {\n const vulnRows: Array<{\n cve_id: string;\n package_name: string;\n severity_score: number | null;\n epss_score: number | null;\n risk_score: number | null;\n verdict: string | null;\n }> = rawDb.all(\n `SELECT v.cve_id, d.package_name, v.severity_score, v.epss_score, v.risk_score, r.verdict\n FROM sec_vulnerabilities v\n JOIN edges e ON e.target = v.node_id AND e.kind = 'has_vulnerability'\n JOIN sec_dependencies d ON d.node_id = e.source\n LEFT JOIN sec_reachability r ON r.vulnerability_node_id = v.node_id\n WHERE d.node_id = ?\n AND (r.verdict IN ('affected', 'under_investigation') OR r.verdict IS NULL)`,\n [dep.node_id],\n );\n\n for (const vuln of vulnRows) {\n vulnerableDeps.push({\n cveId: vuln.cve_id,\n packageName: vuln.package_name ?? dep.package_name,\n severity: vuln.severity_score,\n epssScore: vuln.epss_score,\n riskScore: vuln.risk_score,\n verdict: vuln.verdict,\n hopCount: dep.hop_count,\n });\n }\n }\n\n // Step 3b: Find pattern matches on the call path (table may not exist)\n const routeNodeId = route.id;\n let patternRows: Array<{\n symbol_node_id: string;\n symbol_name: string;\n pattern_id: string;\n severity: string;\n line: number;\n }> = [];\n try {\n patternRows = rawDb.all(\n `WITH RECURSIVE reachable(node_id, depth) AS (\n SELECT target, 1 FROM edges WHERE source = ? AND kind IN ('calls', 'imports', 'references')\n UNION\n SELECT e.target, r.depth + 1\n FROM edges e\n JOIN reachable r ON e.source = r.node_id\n WHERE r.depth < 5 AND e.kind IN ('calls', 'imports', 'references')\n )\n SELECT DISTINCT pm.symbol_node_id, n.name as symbol_name, pm.pattern_id, pm.severity, pm.line\n FROM reachable r\n JOIN pattern_matches pm ON pm.symbol_node_id = r.node_id\n JOIN nodes n ON n.id = pm.symbol_node_id\n WHERE pm.symbol_node_id IS NOT NULL\n ORDER BY CASE pm.severity WHEN 'critical' THEN 4 WHEN 'high' THEN 3 WHEN 'medium' THEN 2 ELSE 1 END DESC\n LIMIT 10`,\n [routeNodeId],\n );\n } catch {\n // pattern_matches table does not exist yet \u2014 degrade silently\n patternRows = [];\n }\n\n const patternMatches: AttackSurfaceEntry['patternMatches'] = patternRows.map(r => ({\n patternId: r.pattern_id,\n severity: r.severity,\n line: r.line,\n symbolNodeId: r.symbol_node_id,\n symbolName: r.symbol_name,\n }));\n\n const patternRiskLevel: AttackSurfaceEntry['patternRiskLevel'] =\n patternMatches.length === 0\n ? 'none'\n : (patternMatches[0].severity as AttackSurfaceEntry['patternRiskLevel']);\n\n // Step 4: Heuristic \u2014 isAuthenticated via path node names\n const pathNodeNames: Array<{ name: string }> = rawDb.all(\n `WITH RECURSIVE reachable(node_id, depth) AS (\n SELECT target, 1 FROM edges WHERE source = ? AND kind IN ('calls', 'imports', 'references')\n UNION\n SELECT e.target, r.depth + 1\n FROM edges e\n JOIN reachable r ON e.source = r.node_id\n WHERE r.depth < 5 AND e.kind IN ('calls', 'imports', 'references')\n )\n SELECT DISTINCT n.name\n FROM reachable r\n JOIN nodes n ON n.id = r.node_id\n WHERE n.name IS NOT NULL`,\n [route.id],\n );\n\n let isAuthenticated = pathNodeNames.some(row => {\n const lowerName = (row.name ?? '').toLowerCase();\n return AUTH_PATTERNS.some(pattern => lowerName.includes(pattern));\n });\n\n // jev backstop: only consulted when the heuristic found nothing, to\n // catch custom-named auth wrappers without re-checking every route.\n if (!isAuthenticated && jevClient) {\n const { checkAuthWithJev } = await Promise.resolve().then(() => require('./auth-detection-jev.js'));\n const jevResult = await checkAuthWithJev(\n jevClient,\n route.name ?? route.id,\n pathNodeNames.map(r => r.name).filter((n): n is string => !!n),\n );\n const threshold = this.opts.authConfidenceThreshold ?? 0.6;\n if (jevResult.authenticated && jevResult.confidence >= threshold) {\n isAuthenticated = true;\n }\n }\n\n // Step 5: Determine exposure level\n const routeName = (route.name ?? '').toLowerCase();\n let exposureLevel: AttackSurfaceEntry['exposureLevel'];\n\n if (INTERNAL_ROUTE_PATTERNS.some(p => routeName.includes(p))) {\n exposureLevel = 'internal';\n } else if (isAuthenticated) {\n exposureLevel = 'authenticated';\n } else if (PUBLIC_ROUTE_PATTERNS.some(p => routeName.includes(p))) {\n exposureLevel = 'public';\n } else {\n exposureLevel = 'unknown';\n }\n\n // Step 6: riskScore = max risk_score of reachable affected vulns, plus pattern bonus\n const affectedVulns = vulnerableDeps.filter(v => v.verdict === 'affected' || v.verdict === 'under_investigation');\n const baseRiskScore = affectedVulns.reduce((max, v) => {\n if (v.riskScore != null && v.riskScore > max) return v.riskScore;\n return max;\n }, 0);\n\n const patternBonus: Record<AttackSurfaceEntry['patternRiskLevel'], number> = {\n none: 0, low: 0.3, medium: 0.6, high: 0.8, critical: 1.0,\n };\n const riskScore = Math.min(10, baseRiskScore + (patternBonus[patternRiskLevel] ?? 0));\n\n entries.push({\n route: route.name ?? route.id,\n nodeId: route.id,\n filePath: route.file_path ?? '',\n isAuthenticated,\n exposureLevel,\n vulnerableDeps,\n patternMatches,\n patternRiskLevel,\n riskScore,\n });\n }\n\n // Compute summary stats\n const publicRoutes = entries.filter(e => e.exposureLevel === 'public').length;\n const authenticatedRoutes = entries.filter(\n e => e.exposureLevel === 'authenticated' || e.exposureLevel === 'internal',\n ).length;\n const routesWithVulns = entries.filter(e => e.vulnerableDeps.length > 0).length;\n\n // Critical paths: routes with affected/under_investigation vulns, sorted by riskScore desc\n const criticalPaths = entries\n .filter(e => e.vulnerableDeps.length > 0)\n .sort((a, b) => b.riskScore - a.riskScore);\n\n const allRoutes = entries.sort((a, b) => b.riskScore - a.riskScore);\n\n return {\n totalRoutes: entries.length,\n publicRoutes,\n authenticatedRoutes,\n routesWithVulns,\n criticalPaths,\n allRoutes,\n };\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -46,6 +46,9 @@ class KiroGraphWiki {
46
46
  this.wikiDir = path.join(kirographDir, "wiki");
47
47
  this.wikiSchemasDir = path.join(kirographDir, "wiki-schemas");
48
48
  this.autoResolveConflicts = opts.autoResolveConflicts ?? false;
49
+ this.contradictionMode = opts.wikiContradictionMode ?? "heuristic";
50
+ this.contradictionConfidenceThreshold = opts.wikiContradictionConfidenceThreshold ?? 0.7;
51
+ this.jevConfig = { jevApiKey: opts.jevApiKey, jevBaseUrl: opts.jevBaseUrl, jevModel: opts.jevModel };
49
52
  this.typedPages = opts.typedPages ?? false;
50
53
  }
51
54
  initialize() {
@@ -119,9 +122,19 @@ class KiroGraphWiki {
119
122
  return this.wikiDb.listPages();
120
123
  }
121
124
  // ── Lint ───────────────────────────────────────────────────────────────────
122
- lint() {
125
+ async lint() {
123
126
  this.initialize();
124
- return (0, import_lint.lintWiki)(this.wikiDb, this.typedPages ? { wikiSchemasDir: this.wikiSchemasDir } : {});
127
+ let jevClient;
128
+ if (this.contradictionMode === "jev") {
129
+ const { createJevClientFromConfig } = await Promise.resolve().then(() => require("../jev/client.js"));
130
+ jevClient = createJevClientFromConfig(this.jevConfig);
131
+ }
132
+ return (0, import_lint.lintWiki)(this.wikiDb, {
133
+ contradictionMode: this.contradictionMode,
134
+ contradictionConfidenceThreshold: this.contradictionConfidenceThreshold,
135
+ jevClient,
136
+ ...this.typedPages ? { wikiSchemasDir: this.wikiSchemasDir } : {}
137
+ });
125
138
  }
126
139
  // ── Links ──────────────────────────────────────────────────────────────────
127
140
  /** Outgoing/broken/incoming `[[slug]]` links for one page. Null if the slug is unknown. */
@@ -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 { 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 wikiSchemasDir: string;\n private autoResolveConflicts: boolean;\n private typedPages: boolean;\n\n constructor(\n db: any,\n kirographDir: string,\n opts: { autoResolveConflicts?: boolean; typedPages?: boolean } = {}\n ) {\n this.wikiDb = new WikiDatabase(db);\n this.wikiDir = path.join(kirographDir, 'wiki');\n this.wikiSchemasDir = path.join(kirographDir, 'wiki-schemas');\n this.autoResolveConflicts = opts.autoResolveConflicts ?? false;\n this.typedPages = opts.typedPages ?? 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, this.typedPages ? { wikiSchemasDir: this.wikiSchemasDir } : {});\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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",
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 wikiSchemasDir: string;\n private autoResolveConflicts: boolean;\n private contradictionMode: 'heuristic' | 'jev';\n private contradictionConfidenceThreshold: number;\n private jevConfig: { jevApiKey?: string; jevBaseUrl?: string; jevModel?: string };\n private typedPages: boolean;\n\n constructor(\n db: any,\n kirographDir: string,\n opts: {\n autoResolveConflicts?: boolean;\n wikiContradictionMode?: 'heuristic' | 'jev';\n wikiContradictionConfidenceThreshold?: number;\n jevApiKey?: string;\n jevBaseUrl?: string;\n jevModel?: string;\n typedPages?: boolean;\n } = {}\n ) {\n this.wikiDb = new WikiDatabase(db);\n this.wikiDir = path.join(kirographDir, 'wiki');\n this.wikiSchemasDir = path.join(kirographDir, 'wiki-schemas');\n this.autoResolveConflicts = opts.autoResolveConflicts ?? false;\n this.contradictionMode = opts.wikiContradictionMode ?? 'heuristic';\n this.contradictionConfidenceThreshold = opts.wikiContradictionConfidenceThreshold ?? 0.7;\n this.jevConfig = { jevApiKey: opts.jevApiKey, jevBaseUrl: opts.jevBaseUrl, jevModel: opts.jevModel };\n this.typedPages = opts.typedPages ?? 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 async lint(): Promise<WikiLintIssue[]> {\n this.initialize();\n let jevClient: import('../jev/client').JevClient | undefined;\n if (this.contradictionMode === 'jev') {\n const { createJevClientFromConfig } = await Promise.resolve().then(() => require('../jev/client.js'));\n jevClient = createJevClientFromConfig(this.jevConfig);\n }\n return lintWiki(this.wikiDb, {\n contradictionMode: this.contradictionMode,\n contradictionConfidenceThreshold: this.contradictionConfidenceThreshold,\n jevClient,\n ...(this.typedPages ? { wikiSchemasDir: this.wikiSchemasDir } : {}),\n });\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
  }
package/dist/wiki/lint.js CHANGED
@@ -23,7 +23,7 @@ __export(lint_exports, {
23
23
  module.exports = __toCommonJS(lint_exports);
24
24
  var import_links = require("./links");
25
25
  var import_typed_pages = require("./typed-pages");
26
- function lintWiki(wikiDb, opts = {}) {
26
+ async function lintWiki(wikiDb, opts = {}) {
27
27
  const issues = [];
28
28
  const pages = wikiDb.listPages();
29
29
  const slugSet = new Set(pages.map((p) => p.slug));
@@ -74,23 +74,35 @@ function lintWiki(wikiDb, opts = {}) {
74
74
  const similar = wikiDb.search(page.title, 5);
75
75
  for (const { page: other } of similar) {
76
76
  if (other.slug === page.slug) continue;
77
- const contradictionSignals = ["instead of", "not", "replaced by", "superseded", "deprecated"];
78
- const bothMentionSignal = contradictionSignals.some(
79
- (sig) => page.content.toLowerCase().includes(sig) && other.content.toLowerCase().includes(sig)
77
+ const alreadyReported = issues.some(
78
+ (i) => i.kind === "contradiction" && i.detail.includes(other.slug)
80
79
  );
81
- if (bothMentionSignal) {
82
- const key = [page.slug, other.slug].sort().join("|");
83
- const alreadyReported = issues.some(
84
- (i) => i.kind === "contradiction" && i.detail.includes(other.slug)
85
- );
86
- if (!alreadyReported) {
80
+ if (alreadyReported) continue;
81
+ if (opts.contradictionMode === "jev" && opts.jevClient) {
82
+ const { checkContradictionWithJev } = await Promise.resolve().then(() => require("./contradiction-jev.js"));
83
+ const result = await checkContradictionWithJev(opts.jevClient, page.content, other.content);
84
+ const threshold = opts.contradictionConfidenceThreshold ?? 0.7;
85
+ if (result.contradicts && result.confidence >= threshold) {
87
86
  issues.push({
88
87
  kind: "contradiction",
89
88
  slug: page.slug,
90
- detail: `Possible contradiction with [[${other.slug}]] \u2014 both pages contain negation signals on shared topics`,
89
+ detail: `Possible contradiction with [[${other.slug}]] \u2014 jev confidence: ${result.confidence.toFixed(2)}`,
91
90
  relatedSlug: other.slug
92
91
  });
93
92
  }
93
+ continue;
94
+ }
95
+ const contradictionSignals = ["instead of", "not", "replaced by", "superseded", "deprecated"];
96
+ const bothMentionSignal = contradictionSignals.some(
97
+ (sig) => page.content.toLowerCase().includes(sig) && other.content.toLowerCase().includes(sig)
98
+ );
99
+ if (bothMentionSignal) {
100
+ issues.push({
101
+ kind: "contradiction",
102
+ slug: page.slug,
103
+ detail: `Possible contradiction with [[${other.slug}]] \u2014 both pages contain negation signals on shared topics`,
104
+ relatedSlug: other.slug
105
+ });
94
106
  }
95
107
  }
96
108
  }
@@ -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';\nimport { extractLinks } from './links';\nimport { parseFrontmatter, validatePageSchema } from './typed-pages';\n\nexport interface LintWikiOptions {\n /**\n * When set, pages declaring `_type` in their frontmatter are validated\n * against `.kirograph/wiki-schemas/<type>.schema.json` (wikiTypedPages).\n */\n wikiSchemasDir?: string;\n}\n\nexport function lintWiki(wikiDb: WikiDatabase, opts: LintWikiOptions = {}): 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 // Typed-page schema validation (opt-in via wikiTypedPages)\n if (opts.wikiSchemasDir) {\n const { type, fields } = parseFrontmatter(page.content);\n if (type) {\n for (const violation of validatePageSchema(fields, type, opts.wikiSchemasDir)) {\n issues.push({\n kind: 'schema_error',\n slug: page.slug,\n detail: `[${type}] ${violation}`,\n });\n }\n }\n }\n\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;AAC7B,yBAAqD;AAU9C,SAAS,SAAS,QAAsB,OAAwB,CAAC,GAAoB;AAC1F,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,KAAK,gBAAgB;AACvB,YAAM,EAAE,MAAM,OAAO,QAAI,qCAAiB,KAAK,OAAO;AACtD,UAAI,MAAM;AACR,mBAAW,iBAAa,uCAAmB,QAAQ,MAAM,KAAK,cAAc,GAAG;AAC7E,iBAAO,KAAK;AAAA,YACV,MAAM;AAAA,YACN,MAAM,KAAK;AAAA,YACX,QAAQ,IAAI,IAAI,KAAK,SAAS;AAAA,UAChC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAGA,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;",
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 type { JevClient } from '../jev/client';\nimport { extractLinks } from './links';\nimport { parseFrontmatter, validatePageSchema } from './typed-pages';\n\nexport interface LintWikiOptions {\n /** 'heuristic' (default): keyword co-occurrence. 'jev': ask jev to judge each FTS-similar pair. */\n contradictionMode?: 'heuristic' | 'jev';\n /** Confidence (0.0\u20131.0) above which a jev contradiction judgment is reported. Default: 0.7. */\n contradictionConfidenceThreshold?: number;\n /** Required when contradictionMode is 'jev'. */\n jevClient?: JevClient;\n /**\n * When set, pages declaring `_type` in their frontmatter are validated\n * against `.kirograph/wiki-schemas/<type>.schema.json` (wikiTypedPages).\n */\n wikiSchemasDir?: string;\n}\n\nexport async function lintWiki(wikiDb: WikiDatabase, opts: LintWikiOptions = {}): Promise<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 // Typed-page schema validation (opt-in via wikiTypedPages)\n if (opts.wikiSchemasDir) {\n const { type, fields } = parseFrontmatter(page.content);\n if (type) {\n for (const violation of validatePageSchema(fields, type, opts.wikiSchemasDir)) {\n issues.push({\n kind: 'schema_error',\n slug: page.slug,\n detail: `[${type}] ${violation}`,\n });\n }\n }\n }\n\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 const similar = wikiDb.search(page.title, 5);\n for (const { page: other } of similar) {\n if (other.slug === page.slug) continue;\n const alreadyReported = issues.some(\n i => i.kind === 'contradiction' && i.detail.includes(other.slug)\n );\n if (alreadyReported) continue;\n\n if (opts.contradictionMode === 'jev' && opts.jevClient) {\n const { checkContradictionWithJev } = await Promise.resolve().then(() => require('./contradiction-jev.js'));\n const result = await checkContradictionWithJev(opts.jevClient, page.content, other.content);\n const threshold = opts.contradictionConfidenceThreshold ?? 0.7;\n if (result.contradicts && result.confidence >= threshold) {\n issues.push({\n kind: 'contradiction',\n slug: page.slug,\n detail: `Possible contradiction with [[${other.slug}]] \u2014 jev confidence: ${result.confidence.toFixed(2)}`,\n relatedSlug: other.slug,\n });\n }\n continue;\n }\n\n // Heuristic (default): co-occurring negation keywords on both pages\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 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 return issues;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kirograph",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Semantic code knowledge graph for Kiro: fewer tool calls, instant symbol lookups, 100% local.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",