kirograph 1.2.3 → 1.4.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.
- package/README.md +5 -3
- package/dist/bin/commands/attack-surface.js +17 -2
- package/dist/bin/commands/attack-surface.js.map +2 -2
- package/dist/bin/commands/help.js +4 -2
- package/dist/bin/commands/help.js.map +2 -2
- package/dist/bin/commands/memory.js +23 -3
- package/dist/bin/commands/memory.js.map +2 -2
- package/dist/bin/commands/security-export.js +9 -1
- package/dist/bin/commands/security-export.js.map +2 -2
- package/dist/bin/commands/wiki.js +14 -1
- package/dist/bin/commands/wiki.js.map +2 -2
- package/dist/bin/installer/common.js +25 -0
- package/dist/bin/installer/common.js.map +2 -2
- package/dist/bin/installer/config-prompt.js +29 -1
- package/dist/bin/installer/config-prompt.js.map +2 -2
- package/dist/bin/installer/index.js +49 -0
- package/dist/bin/installer/index.js.map +2 -2
- package/dist/bin/installer/steering.js +5 -1
- package/dist/bin/installer/steering.js.map +2 -2
- package/dist/bin/kirograph.js +1 -1
- package/dist/config.js +73 -2
- package/dist/config.js.map +2 -2
- package/dist/jev/client.js.map +2 -2
- package/dist/jev/factory.js +36 -0
- package/dist/jev/factory.js.map +7 -0
- package/dist/jev/strands-client.js +89 -0
- package/dist/jev/strands-client.js.map +7 -0
- package/dist/jev/types.js.map +1 -1
- package/dist/mcp/handlers/memory.js +18 -5
- package/dist/mcp/handlers/memory.js.map +2 -2
- package/dist/mcp/handlers/security.js +15 -2
- package/dist/mcp/handlers/security.js.map +2 -2
- package/dist/mcp/handlers/wiki.js +13 -1
- package/dist/mcp/handlers/wiki.js.map +2 -2
- package/dist/mcp/tools.js +4 -4
- package/dist/mcp/tools.js.map +2 -2
- package/dist/memory/index.js +42 -10
- package/dist/memory/index.js.map +2 -2
- package/dist/memory/relation-display.js +37 -0
- package/dist/memory/relation-display.js.map +7 -0
- package/dist/memory/relation-jev.js.map +1 -1
- package/dist/security/attack-surface.js +20 -2
- package/dist/security/attack-surface.js.map +2 -2
- package/dist/security/auth-detection-jev.js.map +1 -1
- package/dist/wiki/contradiction-jev.js.map +1 -1
- package/dist/wiki/index.js +21 -2
- package/dist/wiki/index.js.map +2 -2
- package/dist/wiki/lint.js +23 -11
- package/dist/wiki/lint.js.map +2 -2
- package/package.json +1 -1
package/dist/memory/index.js.map
CHANGED
|
@@ -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 { createDecisionClient } = await Promise.resolve().then(() => require('../jev/factory.js'));\n const { classifyRelationWithJev } = await Promise.resolve().then(() => require('./relation-jev.js'));\n const cfg = this.config as unknown as {\n memoryRelationMode?: 'agent' | 'jev' | 'strands';\n jevApiKey?: string; jevBaseUrl?: string; jevModel?: string;\n strandsBaseUrl?: string; strandsModel?: string;\n };\n const backend = cfg.memoryRelationMode === 'strands' ? 'strands' : 'jev';\n const client = await createDecisionClient(backend, cfg);\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 ${backend}`,\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 ${backend} (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
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/memory/relation-jev.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * KiroGraph Memory \u2014 jev-based relation classification (opt-in, memoryRelationMode: 'jev')\n *\n * Classifies the relation between two observations via a single jev Choice\n * question instead of requiring the calling agent to reason it out manually\n * and supply `relation`/`confidence` itself.\n */\nimport type {
|
|
4
|
+
"sourcesContent": ["/**\n * KiroGraph Memory \u2014 jev-based relation classification (opt-in, memoryRelationMode: 'jev')\n *\n * Classifies the relation between two observations via a single jev Choice\n * question instead of requiring the calling agent to reason it out manually\n * and supply `relation`/`confidence` itself.\n */\nimport type { DecisionClient } from '../jev/types';\nimport type { RelationType } from './types';\n\nconst RELATION_CRITERIA: Record<RelationType, string> = {\n supersedes: 'Observation B replaces or invalidates Observation A as the current decision or fact \u2014 A is now outdated.',\n conflicts_with: 'A and B make contradictory claims about the same thing that cannot both be true at the same time.',\n compatible: 'A and B are both true and can coexist without contradiction, even if they discuss related things.',\n scoped: 'A and B both apply, but in different, non-overlapping contexts or scopes (e.g. different environments, services, or time periods).',\n related: 'A and B are on the same topic and worth cross-referencing, but neither conflicts with nor supersedes the other.',\n not_conflict: 'A and B only appear related on the surface (shared keywords) but are not meaningfully connected.',\n};\n\nexport interface JevRelationClassification {\n relation: RelationType;\n confidence: number;\n}\n\n/** Classify how observation B relates to observation A. */\nexport async function classifyRelationWithJev(\n client: DecisionClient,\n contentA: string,\n contentB: string,\n): Promise<JevRelationClassification> {\n const state = `Observation A: ${contentA}\\n\\nObservation B: ${contentB}`;\n const response = await client.ask(state, {\n relation: {\n type: 'choice',\n instructions: 'How does Observation B relate to Observation A?',\n criteria: RELATION_CRITERIA,\n },\n });\n\n const answer = response.answers.relation;\n if (!answer || answer.type !== 'choice') {\n throw new Error('jev did not return a choice answer for \"relation\"');\n }\n if (!(answer.choice in RELATION_CRITERIA)) {\n throw new Error(`jev returned an unrecognized relation \"${answer.choice}\"`);\n }\n\n return {\n relation: answer.choice as RelationType,\n confidence: answer.confidence ?? 0,\n };\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,MAAM,oBAAkD;AAAA,EACtD,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,cAAc;AAChB;AAQA,eAAsB,wBACpB,QACA,UACA,UACoC;AACpC,QAAM,QAAQ,kBAAkB,QAAQ;AAAA;AAAA,iBAAsB,QAAQ;AACtE,QAAM,WAAW,MAAM,OAAO,IAAI,OAAO;AAAA,IACvC,UAAU;AAAA,MACR,MAAM;AAAA,MACN,cAAc;AAAA,MACd,UAAU;AAAA,IACZ;AAAA,EACF,CAAC;AAED,QAAM,SAAS,SAAS,QAAQ;AAChC,MAAI,CAAC,UAAU,OAAO,SAAS,UAAU;AACvC,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACrE;AACA,MAAI,EAAE,OAAO,UAAU,oBAAoB;AACzC,UAAM,IAAI,MAAM,0CAA0C,OAAO,MAAM,GAAG;AAAA,EAC5E;AAEA,SAAO;AAAA,IACL,UAAU,OAAO;AAAA,IACjB,YAAY,OAAO,cAAc;AAAA,EACnC;AACF;",
|
|
6
6
|
"names": []
|
|
7
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 decisionClient;
|
|
46
|
+
if (this.opts.authDetectionMode === "jev" || this.opts.authDetectionMode === "strands") {
|
|
47
|
+
const { createDecisionClient } = await Promise.resolve().then(() => require("../jev/factory.js"));
|
|
48
|
+
decisionClient = await createDecisionClient(this.opts.authDetectionMode, 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
|
-
|
|
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 && decisionClient) {
|
|
156
|
+
const { checkAuthWithJev } = await Promise.resolve().then(() => require("./auth-detection-jev.js"));
|
|
157
|
+
const jevResult = await checkAuthWithJev(
|
|
158
|
+
decisionClient,
|
|
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;
|
|
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 (cloud) to confirm/override.\n * 'strands': same backstop, judged by a local strands-decider server.\n */\n authDetectionMode?: 'heuristic' | 'jev' | 'strands';\n /** Confidence (0.0\u20131.0) above which a model 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 strandsBaseUrl?: string;\n strandsModel?: 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 decisionClient: import('../jev/types').DecisionClient | undefined;\n if (this.opts.authDetectionMode === 'jev' || this.opts.authDetectionMode === 'strands') {\n const { createDecisionClient } = await Promise.resolve().then(() => require('../jev/factory.js'));\n decisionClient = await createDecisionClient(this.opts.authDetectionMode, 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 && decisionClient) {\n const { checkAuthWithJev } = await Promise.resolve().then(() => require('./auth-detection-jev.js'));\n const jevResult = await checkAuthWithJev(\n decisionClient,\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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/security/auth-detection-jev.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * KiroGraph-Sec \u2014 jev-based auth detection (opt-in, securityAuthDetectionMode: 'jev')\n *\n * Backstop for AttackSurfaceAnalyzer's isAuthenticated heuristic: only called\n * when the substring-match heuristic finds no auth pattern on the call path,\n * to catch custom-named auth wrappers the heuristic would otherwise silently\n * misclassify as unauthenticated. The heuristic's positive matches are\n * trusted as-is and never re-checked, keeping this cheap.\n */\nimport type {
|
|
4
|
+
"sourcesContent": ["/**\n * KiroGraph-Sec \u2014 jev-based auth detection (opt-in, securityAuthDetectionMode: 'jev')\n *\n * Backstop for AttackSurfaceAnalyzer's isAuthenticated heuristic: only called\n * when the substring-match heuristic finds no auth pattern on the call path,\n * to catch custom-named auth wrappers the heuristic would otherwise silently\n * misclassify as unauthenticated. The heuristic's positive matches are\n * trusted as-is and never re-checked, keeping this cheap.\n */\nimport type { DecisionClient } from '../jev/types';\n\nexport interface JevAuthResult {\n authenticated: boolean;\n confidence: number;\n}\n\nexport async function checkAuthWithJev(\n client: DecisionClient,\n routeName: string,\n callPathNames: string[],\n): Promise<JevAuthResult> {\n const state = `Route: ${routeName}\\n\\nFunctions/middleware on its call path: ${callPathNames.length > 0 ? callPathNames.join(', ') : '(none found)'}`;\n const response = await client.ask(state, {\n authenticated: {\n type: 'noul',\n instructions: 'This route is protected by authentication or authorization before its handler logic runs (a session check, token verification, login guard, permission check, etc. somewhere on its call path).',\n },\n });\n\n const answer = response.answers.authenticated;\n if (!answer || answer.type !== 'noul') {\n throw new Error('jev did not return a noul answer for \"authenticated\"');\n }\n\n return {\n authenticated: answer.noul >= 0.5,\n confidence: answer.confidence ?? answer.noul,\n };\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,eAAsB,iBACpB,QACA,WACA,eACwB;AACxB,QAAM,QAAQ,UAAU,SAAS;AAAA;AAAA,yCAA8C,cAAc,SAAS,IAAI,cAAc,KAAK,IAAI,IAAI,cAAc;AACnJ,QAAM,WAAW,MAAM,OAAO,IAAI,OAAO;AAAA,IACvC,eAAe;AAAA,MACb,MAAM;AAAA,MACN,cAAc;AAAA,IAChB;AAAA,EACF,CAAC;AAED,QAAM,SAAS,SAAS,QAAQ;AAChC,MAAI,CAAC,UAAU,OAAO,SAAS,QAAQ;AACrC,UAAM,IAAI,MAAM,sDAAsD;AAAA,EACxE;AAEA,SAAO;AAAA,IACL,eAAe,OAAO,QAAQ;AAAA,IAC9B,YAAY,OAAO,cAAc,OAAO;AAAA,EAC1C;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/wiki/contradiction-jev.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * KiroGraph Wiki \u2014 jev-based contradiction detection (opt-in, wikiContradictionMode: 'jev')\n *\n * Replaces the keyword-heuristic contradiction check (co-occurring negation\n * words like \"instead of\"/\"superseded\" on FTS-similar pages) with an actual\n * judgment call, still restricted to the same FTS-similar candidate pairs.\n */\nimport type {
|
|
4
|
+
"sourcesContent": ["/**\n * KiroGraph Wiki \u2014 jev-based contradiction detection (opt-in, wikiContradictionMode: 'jev')\n *\n * Replaces the keyword-heuristic contradiction check (co-occurring negation\n * words like \"instead of\"/\"superseded\" on FTS-similar pages) with an actual\n * judgment call, still restricted to the same FTS-similar candidate pairs.\n */\nimport type { DecisionClient } from '../jev/types';\n\nexport interface JevContradictionResult {\n contradicts: boolean;\n confidence: number;\n}\n\nexport async function checkContradictionWithJev(\n client: DecisionClient,\n contentA: string,\n contentB: string,\n): Promise<JevContradictionResult> {\n const state = `Page A:\\n${contentA}\\n\\nPage B:\\n${contentB}`;\n const response = await client.ask(state, {\n contradicts: {\n type: 'noul',\n instructions: 'Page A and Page B make claims that directly contradict each other about the same subject \u2014 not just related, actually incompatible.',\n },\n });\n\n const answer = response.answers.contradicts;\n if (!answer || answer.type !== 'noul') {\n throw new Error('jev did not return a noul answer for \"contradicts\"');\n }\n\n return {\n contradicts: answer.noul >= 0.5,\n confidence: answer.confidence ?? answer.noul,\n };\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA,eAAsB,0BACpB,QACA,UACA,UACiC;AACjC,QAAM,QAAQ;AAAA,EAAY,QAAQ;AAAA;AAAA;AAAA,EAAgB,QAAQ;AAC1D,QAAM,WAAW,MAAM,OAAO,IAAI,OAAO;AAAA,IACvC,aAAa;AAAA,MACX,MAAM;AAAA,MACN,cAAc;AAAA,IAChB;AAAA,EACF,CAAC;AAED,QAAM,SAAS,SAAS,QAAQ;AAChC,MAAI,CAAC,UAAU,OAAO,SAAS,QAAQ;AACrC,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE;AAEA,SAAO;AAAA,IACL,aAAa,OAAO,QAAQ;AAAA,IAC5B,YAAY,OAAO,cAAc,OAAO;AAAA,EAC1C;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/wiki/index.js
CHANGED
|
@@ -46,6 +46,15 @@ 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.decisionConfig = {
|
|
52
|
+
jevApiKey: opts.jevApiKey,
|
|
53
|
+
jevBaseUrl: opts.jevBaseUrl,
|
|
54
|
+
jevModel: opts.jevModel,
|
|
55
|
+
strandsBaseUrl: opts.strandsBaseUrl,
|
|
56
|
+
strandsModel: opts.strandsModel
|
|
57
|
+
};
|
|
49
58
|
this.typedPages = opts.typedPages ?? false;
|
|
50
59
|
}
|
|
51
60
|
initialize() {
|
|
@@ -119,9 +128,19 @@ class KiroGraphWiki {
|
|
|
119
128
|
return this.wikiDb.listPages();
|
|
120
129
|
}
|
|
121
130
|
// ── Lint ───────────────────────────────────────────────────────────────────
|
|
122
|
-
lint() {
|
|
131
|
+
async lint() {
|
|
123
132
|
this.initialize();
|
|
124
|
-
|
|
133
|
+
let decisionClient;
|
|
134
|
+
if (this.contradictionMode === "jev" || this.contradictionMode === "strands") {
|
|
135
|
+
const { createDecisionClient } = await Promise.resolve().then(() => require("../jev/factory.js"));
|
|
136
|
+
decisionClient = await createDecisionClient(this.contradictionMode, this.decisionConfig);
|
|
137
|
+
}
|
|
138
|
+
return (0, import_lint.lintWiki)(this.wikiDb, {
|
|
139
|
+
contradictionMode: this.contradictionMode,
|
|
140
|
+
contradictionConfidenceThreshold: this.contradictionConfidenceThreshold,
|
|
141
|
+
decisionClient,
|
|
142
|
+
...this.typedPages ? { wikiSchemasDir: this.wikiSchemasDir } : {}
|
|
143
|
+
});
|
|
125
144
|
}
|
|
126
145
|
// ── Links ──────────────────────────────────────────────────────────────────
|
|
127
146
|
/** Outgoing/broken/incoming `[[slug]]` links for one page. Null if the slug is unknown. */
|
package/dist/wiki/index.js.map
CHANGED
|
@@ -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: {
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,SAAoB;AACpB,WAAsB;AACtB,sBAA6B;AAC7B,oBAA+B;AAC/B,IAAAA,iBAA8B;AAC9B,oBAA8E;AAC9E,kBAAyB;AACzB,mBAAiD;AACjD,oBAAkD;AAW3C,MAAM,cAAc;AAAA,
|
|
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' | 'strands';\n private contradictionConfidenceThreshold: number;\n private decisionConfig: {\n jevApiKey?: string; jevBaseUrl?: string; jevModel?: string;\n strandsBaseUrl?: string; strandsModel?: string;\n };\n private typedPages: boolean;\n\n constructor(\n db: any,\n kirographDir: string,\n opts: {\n autoResolveConflicts?: boolean;\n wikiContradictionMode?: 'heuristic' | 'jev' | 'strands';\n wikiContradictionConfidenceThreshold?: number;\n jevApiKey?: string;\n jevBaseUrl?: string;\n jevModel?: string;\n strandsBaseUrl?: string;\n strandsModel?: 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.decisionConfig = {\n jevApiKey: opts.jevApiKey, jevBaseUrl: opts.jevBaseUrl, jevModel: opts.jevModel,\n strandsBaseUrl: opts.strandsBaseUrl, strandsModel: opts.strandsModel,\n };\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 decisionClient: import('../jev/types').DecisionClient | undefined;\n if (this.contradictionMode === 'jev' || this.contradictionMode === 'strands') {\n const { createDecisionClient } = await Promise.resolve().then(() => require('../jev/factory.js'));\n decisionClient = await createDecisionClient(this.contradictionMode, this.decisionConfig);\n }\n return lintWiki(this.wikiDb, {\n contradictionMode: this.contradictionMode,\n contradictionConfidenceThreshold: this.contradictionConfidenceThreshold,\n decisionClient,\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
|
|
78
|
-
|
|
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 (
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
80
|
+
if (alreadyReported) continue;
|
|
81
|
+
if ((opts.contradictionMode === "jev" || opts.contradictionMode === "strands") && opts.decisionClient) {
|
|
82
|
+
const { checkContradictionWithJev } = await Promise.resolve().then(() => require("./contradiction-jev.js"));
|
|
83
|
+
const result = await checkContradictionWithJev(opts.decisionClient, 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
|
|
89
|
+
detail: `Possible contradiction with [[${other.slug}]] \u2014 ${opts.contradictionMode} 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
|
}
|