@cleocode/core 2026.4.29 → 2026.4.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/bootstrap.d.ts +35 -0
  2. package/dist/bootstrap.d.ts.map +1 -1
  3. package/dist/code/index.d.ts +8 -4
  4. package/dist/code/index.d.ts.map +1 -1
  5. package/dist/code/parser.d.ts +22 -9
  6. package/dist/code/parser.d.ts.map +1 -1
  7. package/dist/hooks/handlers/session-hooks.d.ts +11 -4
  8. package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
  9. package/dist/hooks/payload-schemas.d.ts +6 -6
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +3859 -3008
  13. package/dist/index.js.map +4 -4
  14. package/dist/internal.d.ts +10 -7
  15. package/dist/internal.d.ts.map +1 -1
  16. package/dist/lib/tree-sitter-languages.d.ts +11 -7
  17. package/dist/lib/tree-sitter-languages.d.ts.map +1 -1
  18. package/dist/memory/auto-extract.d.ts +27 -15
  19. package/dist/memory/auto-extract.d.ts.map +1 -1
  20. package/dist/memory/brain-backfill.d.ts +59 -0
  21. package/dist/memory/brain-backfill.d.ts.map +1 -0
  22. package/dist/memory/brain-purge.d.ts +51 -0
  23. package/dist/memory/brain-purge.d.ts.map +1 -0
  24. package/dist/memory/brain-retrieval.d.ts.map +1 -1
  25. package/dist/memory/brain-search.d.ts.map +1 -1
  26. package/dist/memory/decisions.d.ts.map +1 -1
  27. package/dist/memory/engine-compat.d.ts +71 -0
  28. package/dist/memory/engine-compat.d.ts.map +1 -1
  29. package/dist/memory/graph-auto-populate.d.ts +65 -0
  30. package/dist/memory/graph-auto-populate.d.ts.map +1 -0
  31. package/dist/memory/graph-queries.d.ts +127 -0
  32. package/dist/memory/graph-queries.d.ts.map +1 -0
  33. package/dist/memory/learnings.d.ts +2 -0
  34. package/dist/memory/learnings.d.ts.map +1 -1
  35. package/dist/memory/patterns.d.ts +2 -0
  36. package/dist/memory/patterns.d.ts.map +1 -1
  37. package/dist/memory/quality-scoring.d.ts +90 -0
  38. package/dist/memory/quality-scoring.d.ts.map +1 -0
  39. package/dist/sessions/session-memory-bridge.d.ts +16 -10
  40. package/dist/sessions/session-memory-bridge.d.ts.map +1 -1
  41. package/dist/store/brain-accessor.d.ts +7 -0
  42. package/dist/store/brain-accessor.d.ts.map +1 -1
  43. package/dist/store/brain-schema.d.ts +185 -11
  44. package/dist/store/brain-schema.d.ts.map +1 -1
  45. package/dist/store/brain-sqlite.d.ts.map +1 -1
  46. package/dist/store/nexus-schema.d.ts +480 -2
  47. package/dist/store/nexus-schema.d.ts.map +1 -1
  48. package/dist/store/tasks-schema.d.ts +9 -9
  49. package/dist/store/validation-schemas.d.ts +44 -28
  50. package/dist/store/validation-schemas.d.ts.map +1 -1
  51. package/dist/system/dependencies.d.ts +43 -0
  52. package/dist/system/dependencies.d.ts.map +1 -0
  53. package/dist/system/health.d.ts +3 -0
  54. package/dist/system/health.d.ts.map +1 -1
  55. package/dist/tasks/complete.d.ts.map +1 -1
  56. package/package.json +19 -19
  57. package/src/bootstrap.ts +124 -0
  58. package/src/code/index.ts +20 -4
  59. package/src/code/parser.ts +310 -110
  60. package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +19 -45
  61. package/src/hooks/handlers/__tests__/session-hooks.test.ts +42 -54
  62. package/src/hooks/handlers/session-hooks.ts +11 -33
  63. package/src/index.ts +14 -0
  64. package/src/internal.ts +37 -7
  65. package/src/lib/tree-sitter-languages.ts +11 -7
  66. package/src/memory/__tests__/auto-extract.test.ts +20 -82
  67. package/src/memory/__tests__/embedding-pipeline.test.ts +389 -0
  68. package/src/memory/auto-extract.ts +34 -120
  69. package/src/memory/brain-backfill.ts +471 -0
  70. package/src/memory/brain-purge.ts +315 -0
  71. package/src/memory/brain-retrieval.ts +43 -2
  72. package/src/memory/brain-search.ts +23 -6
  73. package/src/memory/decisions.ts +76 -3
  74. package/src/memory/engine-compat.ts +168 -0
  75. package/src/memory/graph-auto-populate.ts +173 -0
  76. package/src/memory/graph-queries.ts +424 -0
  77. package/src/memory/learnings.ts +55 -7
  78. package/src/memory/patterns.ts +66 -13
  79. package/src/memory/quality-scoring.ts +173 -0
  80. package/src/sessions/__tests__/session-memory-bridge.test.ts +27 -49
  81. package/src/sessions/session-memory-bridge.ts +19 -47
  82. package/src/store/__tests__/brain-accessor-pageindex.test.ts +93 -22
  83. package/src/store/brain-accessor.ts +48 -2
  84. package/src/store/brain-schema.ts +165 -13
  85. package/src/store/brain-sqlite.ts +35 -0
  86. package/src/store/nexus-schema.ts +257 -3
  87. package/src/system/dependencies.ts +534 -0
  88. package/src/system/health.ts +126 -22
  89. package/src/tasks/complete.ts +40 -0
@@ -94,6 +94,12 @@ export const brainDecisions = sqliteTable(
94
94
  contextEpicId: text('context_epic_id'), // soft FK to tasks.id in tasks.db
95
95
  contextTaskId: text('context_task_id'), // soft FK to tasks.id in tasks.db
96
96
  contextPhase: text('context_phase'),
97
+ /**
98
+ * Quality score: 0.0 (noise) – 1.0 (canonical). Null for legacy entries.
99
+ * Computed at insert time from confidence, content richness, and context.
100
+ * Entries below 0.3 are excluded from search results (T531).
101
+ */
102
+ qualityScore: real('quality_score'),
97
103
  createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
98
104
  updatedAt: text('updated_at'),
99
105
  },
@@ -103,6 +109,7 @@ export const brainDecisions = sqliteTable(
103
109
  index('idx_brain_decisions_outcome').on(table.outcome),
104
110
  index('idx_brain_decisions_context_epic').on(table.contextEpicId),
105
111
  index('idx_brain_decisions_context_task').on(table.contextTaskId),
112
+ index('idx_brain_decisions_quality').on(table.qualityScore),
106
113
  ],
107
114
  );
108
115
 
@@ -123,11 +130,18 @@ export const brainPatterns = sqliteTable(
123
130
  examplesJson: text('examples_json').default('[]'),
124
131
  extractedAt: text('extracted_at').notNull().default(sql`(datetime('now'))`),
125
132
  updatedAt: text('updated_at'),
133
+ /**
134
+ * Quality score: 0.0 (noise) – 1.0 (canonical). Null for legacy entries.
135
+ * Computed at insert time from type, content richness, and examples.
136
+ * Entries below 0.3 are excluded from search results (T531).
137
+ */
138
+ qualityScore: real('quality_score'),
126
139
  },
127
140
  (table) => [
128
141
  index('idx_brain_patterns_type').on(table.type),
129
142
  index('idx_brain_patterns_impact').on(table.impact),
130
143
  index('idx_brain_patterns_frequency').on(table.frequency),
144
+ index('idx_brain_patterns_quality').on(table.qualityScore),
131
145
  ],
132
146
  );
133
147
 
@@ -145,10 +159,17 @@ export const brainLearnings = sqliteTable(
145
159
  applicableTypesJson: text('applicable_types_json'),
146
160
  createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
147
161
  updatedAt: text('updated_at'),
162
+ /**
163
+ * Quality score: 0.0 (noise) – 1.0 (canonical). Null for legacy entries.
164
+ * Computed at insert time from confidence, actionability, and content richness.
165
+ * Entries below 0.3 are excluded from search results (T531).
166
+ */
167
+ qualityScore: real('quality_score'),
148
168
  },
149
169
  (table) => [
150
170
  index('idx_brain_learnings_confidence').on(table.confidence),
151
171
  index('idx_brain_learnings_actionable').on(table.actionable),
172
+ index('idx_brain_learnings_quality').on(table.qualityScore),
152
173
  ],
153
174
  );
154
175
 
@@ -176,6 +197,12 @@ export const brainObservations = sqliteTable(
176
197
  agent: text('agent'), // nullable — null for legacy observations
177
198
  contentHash: text('content_hash'), // SHA-256 prefix for dedup
178
199
  discoveryTokens: integer('discovery_tokens'), // cost to produce this observation
200
+ /**
201
+ * Quality score: 0.0 (noise) – 1.0 (canonical). Null for legacy entries.
202
+ * Computed at insert time from content richness and title length.
203
+ * Entries below 0.3 are excluded from search results (T531).
204
+ */
205
+ qualityScore: real('quality_score'),
179
206
  createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
180
207
  updatedAt: text('updated_at'),
181
208
  },
@@ -191,6 +218,8 @@ export const brainObservations = sqliteTable(
191
218
  index('idx_brain_observations_type_project').on(table.type, table.project),
192
219
  // T417: agent provenance index for memory.find --agent filter
193
220
  index('idx_brain_observations_agent').on(table.agent),
221
+ // T531: quality score filter index
222
+ index('idx_brain_observations_quality').on(table.qualityScore),
194
223
  ],
195
224
  );
196
225
 
@@ -244,41 +273,163 @@ export const brainSchemaMeta = sqliteTable('brain_schema_meta', {
244
273
  value: text('value').notNull(),
245
274
  });
246
275
 
247
- // === PAGEINDEX GRAPH TABLES (T5160) ===
276
+ // === PAGEINDEX GRAPH TABLES (T5160, expanded T528) ===
248
277
 
249
- /** Node types for PageIndex graph. */
250
- export const BRAIN_NODE_TYPES = ['task', 'doc', 'file', 'concept'] as const;
278
+ /**
279
+ * Node types for the graph-native memory model.
280
+ * Mirrors typed tables (decision, pattern, learning, observation, sticky),
281
+ * adds task provenance (task, session, epic), codebase bridging (file, symbol),
282
+ * and abstract/synthesized types (concept, summary).
283
+ */
284
+ export const BRAIN_NODE_TYPES = [
285
+ // Memory entity types (mirror typed tables)
286
+ 'decision',
287
+ 'pattern',
288
+ 'learning',
289
+ 'observation',
290
+ 'sticky',
291
+ // Task provenance (soft FK into tasks.db)
292
+ 'task',
293
+ 'session',
294
+ 'epic',
295
+ // Codebase integration (bridge to nexus.db code_index)
296
+ 'file',
297
+ 'symbol',
298
+ // Abstract / synthesized
299
+ 'concept',
300
+ 'summary',
301
+ ] as const;
251
302
 
252
- /** Edge types for PageIndex graph. */
253
- export const BRAIN_EDGE_TYPES = ['depends_on', 'relates_to', 'implements', 'documents'] as const;
303
+ /** Discriminated union of all supported brain graph node types. */
304
+ export type BrainNodeType = (typeof BRAIN_NODE_TYPES)[number];
254
305
 
255
- /** Documents/concepts as graph nodes for cross-document linking. */
306
+ /**
307
+ * Edge types for the graph-native memory model.
308
+ * Covers provenance/derivation, semantic relationships, structural links,
309
+ * and graph bridging between memory entities and codebase nodes.
310
+ */
311
+ export const BRAIN_EDGE_TYPES = [
312
+ // Provenance / derivation
313
+ 'derived_from', // learning ← derived_from ← observation
314
+ 'produced_by', // observation ← produced_by ← session
315
+ 'informed_by', // decision ← informed_by ← pattern
316
+ // Semantic relationship
317
+ 'supports', // observation → supports → decision
318
+ 'contradicts', // observation → contradicts → decision
319
+ 'supersedes', // decision → supersedes → decision (older)
320
+ 'applies_to', // decision/pattern → applies_to → task/file/symbol
321
+ // Structural
322
+ 'documents', // observation → documents → symbol/file
323
+ 'summarizes', // summary → summarizes → observation (consolidation)
324
+ 'part_of', // task → part_of → epic
325
+ // Graph bridging (memory ↔ code)
326
+ 'references', // observation → references → symbol
327
+ 'modified_by', // file → modified_by → session
328
+ ] as const;
329
+
330
+ /** Discriminated union of all supported brain graph edge types. */
331
+ export type BrainEdgeType = (typeof BRAIN_EDGE_TYPES)[number];
332
+
333
+ /**
334
+ * Graph nodes table — the traversable knowledge graph layer.
335
+ *
336
+ * Every entity row in a typed table (decisions, patterns, learnings,
337
+ * observations) gets a corresponding node here. The typed table row is
338
+ * the source of truth; the graph node is the index entry for traversal
339
+ * and cross-entity reasoning.
340
+ *
341
+ * Node ID convention: '<type>:<source-id>'
342
+ * Examples: 'decision:D-abc123', 'observation:O-mntphoj6-0',
343
+ * 'task:T523', 'symbol:src/store/brain-schema.ts::brainPageNodes'
344
+ */
256
345
  export const brainPageNodes = sqliteTable(
257
346
  'brain_page_nodes',
258
347
  {
259
- id: text('id').primaryKey(), // 'task:T5241', 'doc:BRAIN-SPEC', 'file:src/store/brain-schema.ts'
348
+ /** Stable composite ID: '<type>:<source-id>' */
349
+ id: text('id').primaryKey(),
350
+
351
+ /** Discriminated type from BRAIN_NODE_TYPES. */
260
352
  nodeType: text('node_type', { enum: BRAIN_NODE_TYPES }).notNull(),
353
+
354
+ /** Human-readable label (title, name, or generated summary). */
261
355
  label: text('label').notNull(),
262
- metadataJson: text('metadata_json'), // JSON blob for extensible metadata
356
+
357
+ /**
358
+ * Quality score: 0.0 (noise) – 1.0 (canonical).
359
+ * Derived from: source confidence, edge density, age decay, agent provenance.
360
+ * Default 0.5 for unknown provenance; 0.0 triggers exclusion from traversal.
361
+ */
362
+ qualityScore: real('quality_score').notNull().default(0.5),
363
+
364
+ /**
365
+ * SHA-256 prefix (first 16 hex chars) of the canonical content.
366
+ * Computed at insert time; duplicate hashes are rejected.
367
+ * Null for external references (task, session, symbol nodes).
368
+ */
369
+ contentHash: text('content_hash'),
370
+
371
+ /**
372
+ * ISO 8601 timestamp of last activity on this node.
373
+ * Updated when new edges are added, quality changes, or content is revised.
374
+ */
375
+ lastActivityAt: text('last_activity_at').notNull().default(sql`(datetime('now'))`),
376
+
377
+ /**
378
+ * Extensible JSON metadata blob — type-specific payload.
379
+ * decision: { type, confidence, outcome }
380
+ * observation: { sourceType, agent, sessionId }
381
+ * symbol: { filePath, kind, startLine, endLine, language }
382
+ * task: { status, priority, epicId }
383
+ */
384
+ metadataJson: text('metadata_json'),
385
+
263
386
  createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
387
+ updatedAt: text('updated_at'),
264
388
  },
265
- (table) => [index('idx_brain_nodes_type').on(table.nodeType)],
389
+ (table) => [
390
+ index('idx_brain_nodes_type').on(table.nodeType),
391
+ index('idx_brain_nodes_quality').on(table.qualityScore),
392
+ index('idx_brain_nodes_content_hash').on(table.contentHash),
393
+ index('idx_brain_nodes_last_activity').on(table.lastActivityAt),
394
+ ],
266
395
  );
267
396
 
268
- /** Directed links between graph nodes. */
397
+ /**
398
+ * Graph edges table — directed, typed, weighted, provenance-aware links
399
+ * between brain_page_nodes entries (or external nexus node IDs).
400
+ *
401
+ * The composite primary key (fromId, toId, edgeType) prevents duplicate
402
+ * edges of the same type between the same pair of nodes.
403
+ */
269
404
  export const brainPageEdges = sqliteTable(
270
405
  'brain_page_edges',
271
406
  {
272
- fromId: text('from_id').notNull(),
273
- toId: text('to_id').notNull(),
407
+ fromId: text('from_id').notNull(), // brain_page_nodes.id
408
+ toId: text('to_id').notNull(), // brain_page_nodes.id or nexus node id
274
409
  edgeType: text('edge_type', { enum: BRAIN_EDGE_TYPES }).notNull(),
275
- weight: real('weight').default(1.0),
410
+
411
+ /**
412
+ * Edge weight / confidence: 0.0 – 1.0.
413
+ * Semantic edges use extractor confidence (similarity score).
414
+ * Structural edges use 1.0 (deterministic).
415
+ * Contradiction edges store the overlap score that triggered detection.
416
+ */
417
+ weight: real('weight').notNull().default(1.0),
418
+
419
+ /**
420
+ * Human-readable note on why this edge was emitted.
421
+ * Examples: 'auto:task-complete' | 'auto:session-end' |
422
+ * 'auto:contradiction-detected' | 'auto:consolidation' | 'manual'
423
+ */
424
+ provenance: text('provenance'),
425
+
276
426
  createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
277
427
  },
278
428
  (table) => [
279
429
  primaryKey({ columns: [table.fromId, table.toId, table.edgeType] }),
280
430
  index('idx_brain_edges_from').on(table.fromId),
281
431
  index('idx_brain_edges_to').on(table.toId),
432
+ index('idx_brain_edges_type').on(table.edgeType),
282
433
  ],
283
434
  );
284
435
 
@@ -300,3 +451,4 @@ export type BrainPageEdgeRow = typeof brainPageEdges.$inferSelect;
300
451
  export type NewBrainPageEdgeRow = typeof brainPageEdges.$inferInsert;
301
452
  export type BrainStickyNoteRow = typeof brainStickyNotes.$inferSelect;
302
453
  export type NewBrainStickyNoteRow = typeof brainStickyNotes.$inferInsert;
454
+ // BrainNodeType and BrainEdgeType are declared alongside their enum arrays above.
@@ -142,6 +142,30 @@ export function isBrainVecLoaded(): boolean {
142
142
  return _vecLoaded;
143
143
  }
144
144
 
145
+ /**
146
+ * Initialize the default embedding provider when brain.embedding.enabled is true.
147
+ *
148
+ * Called asynchronously after getBrainDb() completes its synchronous setup.
149
+ * Uses dynamic import to avoid circular dependencies and keep the heavy
150
+ * @huggingface/transformers bundle out of the critical startup path.
151
+ *
152
+ * Best-effort: errors are swallowed by the caller so DB access is never blocked.
153
+ *
154
+ * @task T539
155
+ */
156
+ async function initEmbeddingProvider(cwd?: string): Promise<void> {
157
+ try {
158
+ const { loadConfig } = await import('../config.js');
159
+ const config = await loadConfig(cwd);
160
+ if (config.brain?.embedding?.enabled) {
161
+ const { initDefaultProvider } = await import('../memory/brain-embedding.js');
162
+ await initDefaultProvider();
163
+ }
164
+ } catch {
165
+ // Config load or provider init failed — non-fatal, embedding stays unavailable
166
+ }
167
+ }
168
+
145
169
  /**
146
170
  * Initialize the brain.db SQLite database (lazy, singleton).
147
171
  * Creates the database file and tables if they don't exist.
@@ -200,6 +224,17 @@ export async function getBrainDb(cwd?: string): Promise<NodeSQLiteDatabase<typeo
200
224
 
201
225
  // Set singleton only after migrations complete
202
226
  _db = db;
227
+
228
+ // Wire the default embedding provider when vec is loaded and embedding is enabled.
229
+ // Best-effort, async, never blocks DB access. (T539)
230
+ if (_vecLoaded) {
231
+ setImmediate(() => {
232
+ initEmbeddingProvider(cwd).catch(() => {
233
+ // Non-fatal — embedding will be unavailable until next startup
234
+ });
235
+ });
236
+ }
237
+
203
238
  return db;
204
239
  })();
205
240
 
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * Drizzle ORM schema for CLEO nexus.db (SQLite via node:sqlite + sqlite-proxy).
3
3
  *
4
- * Tables: project_registry, nexus_audit_log, nexus_schema_meta
5
- * Stores cross-project registry and audit infrastructure for the Nexus domain.
4
+ * Tables: project_registry, nexus_audit_log, nexus_schema_meta,
5
+ * nexus_nodes, nexus_relations
6
+ * Stores cross-project registry and audit infrastructure for the Nexus domain,
7
+ * plus the code intelligence graph layer (nodes + directed edges).
6
8
  *
7
9
  * @task T5365
10
+ * @task T529
8
11
  */
9
12
 
10
13
  import { sql } from 'drizzle-orm';
11
- import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
14
+ import { index, integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
12
15
 
13
16
  // === PROJECT_REGISTRY TABLE ===
14
17
 
@@ -75,6 +78,253 @@ export const nexusSchemaMeta = sqliteTable('nexus_schema_meta', {
75
78
  value: text('value').notNull(),
76
79
  });
77
80
 
81
+ // === NEXUS_NODES TABLE ===
82
+
83
+ /**
84
+ * All node kind values — matches GraphNodeKind in @cleocode/contracts.
85
+ *
86
+ * Kept as a const tuple for use in Drizzle enum column definitions.
87
+ * The ordering is intentional: structural → module → callable → type →
88
+ * value-level → language-specific → graph-level → legacy.
89
+ */
90
+ export const NEXUS_NODE_KINDS = [
91
+ // Structural
92
+ 'file',
93
+ 'folder',
94
+ // Module-level
95
+ 'module',
96
+ 'namespace',
97
+ // Callable
98
+ 'function',
99
+ 'method',
100
+ 'constructor',
101
+ // Type hierarchy
102
+ 'class',
103
+ 'interface',
104
+ 'struct',
105
+ 'trait',
106
+ 'impl',
107
+ 'type_alias',
108
+ 'enum',
109
+ // Value-level
110
+ 'property',
111
+ 'constant',
112
+ 'variable',
113
+ 'static',
114
+ 'record',
115
+ 'delegate',
116
+ // Language-specific constructs
117
+ 'macro',
118
+ 'union',
119
+ 'typedef',
120
+ 'annotation',
121
+ 'template',
122
+ // Graph-level (synthetic nodes from analysis phases)
123
+ 'community',
124
+ 'process',
125
+ 'route',
126
+ // External references
127
+ 'tool',
128
+ 'section',
129
+ // Legacy (kept for T506 compatibility)
130
+ 'import',
131
+ 'export',
132
+ 'type',
133
+ ] as const;
134
+
135
+ /** TypeScript type derived from NEXUS_NODE_KINDS. */
136
+ export type NexusNodeKind = (typeof NEXUS_NODE_KINDS)[number];
137
+
138
+ /**
139
+ * Graph nodes table — one row per symbol or structural element.
140
+ *
141
+ * Stores all code intelligence graph nodes indexed per project.
142
+ * Synthetic nodes (community, process) share this table with
143
+ * source-derived nodes (function, class, file).
144
+ *
145
+ * Both this table and `code_index` are populated from the same parse pass.
146
+ * They serve complementary roles — do NOT merge them.
147
+ *
148
+ * @task T529
149
+ */
150
+ export const nexusNodes = sqliteTable(
151
+ 'nexus_nodes',
152
+ {
153
+ /** Stable node ID. Format: `<filePath>::<name>` for symbols,
154
+ * `<filePath>` for file nodes, `community:<n>` for community nodes,
155
+ * `process:<slug>` for execution flow nodes. */
156
+ id: text('id').primaryKey(),
157
+
158
+ /** Foreign key to project_registry.project_id. Scopes the node. */
159
+ projectId: text('project_id').notNull(),
160
+
161
+ /** Node kind from GraphNodeKind union. */
162
+ kind: text('kind', { enum: NEXUS_NODE_KINDS }).notNull(),
163
+
164
+ /** Human-readable label for display. For symbols, same as name.
165
+ * For communities, the inferred folder label. For processes, the
166
+ * entry point function name. */
167
+ label: text('label').notNull(),
168
+
169
+ /** Symbol name as it appears in source code. Null for file/folder nodes. */
170
+ name: text('name'),
171
+
172
+ /** File path relative to project root. Null for community/process nodes. */
173
+ filePath: text('file_path'),
174
+
175
+ /** Start line in source file (1-based). Null for structural nodes. */
176
+ startLine: integer('start_line'),
177
+
178
+ /** End line in source file (1-based). Null for structural nodes. */
179
+ endLine: integer('end_line'),
180
+
181
+ /** Source language (typescript, python, go, rust, etc.). */
182
+ language: text('language'),
183
+
184
+ /** Whether the symbol is publicly exported from its module. */
185
+ isExported: integer('is_exported', { mode: 'boolean' }).notNull().default(false),
186
+
187
+ /** Parent node ID for nested symbols (e.g., method inside class).
188
+ * References nexus_nodes.id in the same project. Soft FK. */
189
+ parentId: text('parent_id'),
190
+
191
+ /** JSON array of parameter name strings for functions/methods.
192
+ * Stored as `["param1","param2"]`. Null if not applicable. */
193
+ parametersJson: text('parameters_json'),
194
+
195
+ /** Return type annotation text (e.g., "Promise<void>"). */
196
+ returnType: text('return_type'),
197
+
198
+ /** First line of the TSDoc/JSDoc comment for this symbol. */
199
+ docSummary: text('doc_summary'),
200
+
201
+ /** Community membership ID — references the community node's id.
202
+ * Set during Phase 4.5 community detection. Null until then. */
203
+ communityId: text('community_id'),
204
+
205
+ /** JSON blob for kind-specific metadata.
206
+ * For `process` nodes: `{"stepCount": 7, "entryScore": 0.92}`.
207
+ * For `community` nodes: `{"memberCount": 14, "topFolders": ["src/core"]}`.
208
+ * For `route` nodes: `{"method": "GET", "path": "/api/v1/tasks"}`.
209
+ * For all others: null. */
210
+ metaJson: text('meta_json'),
211
+
212
+ /** ISO 8601 timestamp when this node was last indexed. */
213
+ indexedAt: text('indexed_at').notNull().default(sql`(datetime('now'))`),
214
+ },
215
+ (table) => [
216
+ index('idx_nexus_nodes_project').on(table.projectId),
217
+ index('idx_nexus_nodes_kind').on(table.kind),
218
+ index('idx_nexus_nodes_file').on(table.filePath),
219
+ index('idx_nexus_nodes_name').on(table.name),
220
+ index('idx_nexus_nodes_project_kind').on(table.projectId, table.kind),
221
+ index('idx_nexus_nodes_project_file').on(table.projectId, table.filePath),
222
+ index('idx_nexus_nodes_community').on(table.communityId),
223
+ index('idx_nexus_nodes_parent').on(table.parentId),
224
+ index('idx_nexus_nodes_exported').on(table.isExported),
225
+ ],
226
+ );
227
+
228
+ // === NEXUS_RELATIONS TABLE ===
229
+
230
+ /**
231
+ * All relation type values — matches GraphRelationType in @cleocode/contracts.
232
+ *
233
+ * Kept as a const tuple for use in Drizzle enum column definitions.
234
+ */
235
+ export const NEXUS_RELATION_TYPES = [
236
+ // Structural
237
+ 'contains',
238
+ // Definition / usage
239
+ 'defines',
240
+ 'imports',
241
+ 'accesses',
242
+ // Callable
243
+ 'calls',
244
+ // Type hierarchy
245
+ 'extends',
246
+ 'implements',
247
+ 'method_overrides',
248
+ 'method_implements',
249
+ // Class structure
250
+ 'has_method',
251
+ 'has_property',
252
+ // Graph-level (synthetic, from analysis phases)
253
+ 'member_of', // symbol → community
254
+ 'step_in_process', // symbol → process
255
+ // Web / API
256
+ 'handles_route', // function → route node
257
+ 'fetches', // function → external API
258
+ // Tool / agent
259
+ 'handles_tool',
260
+ 'entry_point_of', // function → process
261
+ // Wrapping / delegation
262
+ 'wraps',
263
+ // Data access
264
+ 'queries',
265
+ // Cross-graph (brain link)
266
+ 'documents', // brain_page_node → nexus_nodes
267
+ 'applies_to', // brain_page_node → nexus_nodes
268
+ ] as const;
269
+
270
+ /** TypeScript type derived from NEXUS_RELATION_TYPES. */
271
+ export type NexusRelationType = (typeof NEXUS_RELATION_TYPES)[number];
272
+
273
+ /**
274
+ * Graph relations table — one row per directed edge.
275
+ *
276
+ * All graph traversal (impact, context, process detection) reads from
277
+ * this table after ingestion completes.
278
+ *
279
+ * Source and target reference nexus_nodes.id. They are soft FKs —
280
+ * unresolved targets (e.g., external packages) are stored as raw specifiers.
281
+ *
282
+ * @task T529
283
+ */
284
+ export const nexusRelations = sqliteTable(
285
+ 'nexus_relations',
286
+ {
287
+ /** UUID v4 row identifier. */
288
+ id: text('id').primaryKey(),
289
+
290
+ /** Foreign key to project_registry.project_id. */
291
+ projectId: text('project_id').notNull(),
292
+
293
+ /** Source node ID (nexus_nodes.id). */
294
+ sourceId: text('source_id').notNull(),
295
+
296
+ /** Target node ID (nexus_nodes.id) or raw module specifier for
297
+ * unresolved imports. Example: `@cleocode/contracts` or
298
+ * `src/core/parser.ts::parseFile`. */
299
+ targetId: text('target_id').notNull(),
300
+
301
+ /** Semantic relation type. */
302
+ type: text('type', { enum: NEXUS_RELATION_TYPES }).notNull(),
303
+
304
+ /** Extractor confidence (0.0 to 1.0). */
305
+ confidence: real('confidence').notNull(),
306
+
307
+ /** Human-readable note explaining why this relation was emitted. */
308
+ reason: text('reason'),
309
+
310
+ /** Step index within an execution flow (for step_in_process relations). */
311
+ step: integer('step'),
312
+
313
+ /** ISO 8601 timestamp when this relation was last indexed. */
314
+ indexedAt: text('indexed_at').notNull().default(sql`(datetime('now'))`),
315
+ },
316
+ (table) => [
317
+ index('idx_nexus_relations_project').on(table.projectId),
318
+ index('idx_nexus_relations_source').on(table.sourceId),
319
+ index('idx_nexus_relations_target').on(table.targetId),
320
+ index('idx_nexus_relations_type').on(table.type),
321
+ index('idx_nexus_relations_project_type').on(table.projectId, table.type),
322
+ index('idx_nexus_relations_source_type').on(table.sourceId, table.type),
323
+ index('idx_nexus_relations_target_type').on(table.targetId, table.type),
324
+ index('idx_nexus_relations_confidence').on(table.confidence),
325
+ ],
326
+ );
327
+
78
328
  // === TYPE EXPORTS ===
79
329
 
80
330
  export type ProjectRegistryRow = typeof projectRegistry.$inferSelect;
@@ -83,3 +333,7 @@ export type NexusAuditLogRow = typeof nexusAuditLog.$inferSelect;
83
333
  export type NewNexusAuditLogRow = typeof nexusAuditLog.$inferInsert;
84
334
  export type NexusSchemaMetaRow = typeof nexusSchemaMeta.$inferSelect;
85
335
  export type NewNexusSchemaMetaRow = typeof nexusSchemaMeta.$inferInsert;
336
+ export type NexusNodeRow = typeof nexusNodes.$inferSelect;
337
+ export type NewNexusNodeRow = typeof nexusNodes.$inferInsert;
338
+ export type NexusRelationRow = typeof nexusRelations.$inferSelect;
339
+ export type NewNexusRelationRow = typeof nexusRelations.$inferInsert;