@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
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Quality scoring for CLEO BRAIN memory entries.
3
+ *
4
+ * Computes a 0.0–1.0 quality score at insert time for each typed brain table entry.
5
+ * Entries with a score below the minimum threshold are excluded from search results.
6
+ *
7
+ * Score formula per CA1 spec Section 3: source reliability * content richness * recency signals.
8
+ * Each helper clamps the result to [0.0, 1.0].
9
+ *
10
+ * Exported for use in backfill (T530) and future scoring hooks.
11
+ *
12
+ * @task T531
13
+ * @epic T523
14
+ */
15
+
16
+ /** Minimum quality score for inclusion in search results. */
17
+ export const QUALITY_SCORE_THRESHOLD = 0.3;
18
+
19
+ // ============================================================================
20
+ // Pattern quality
21
+ // ============================================================================
22
+
23
+ /** Input shape for pattern quality computation — mirrors StorePatternParams. */
24
+ export interface PatternQualityInput {
25
+ type: string;
26
+ pattern: string;
27
+ context?: string | null;
28
+ examples_json?: string | null;
29
+ }
30
+
31
+ /**
32
+ * Compute quality score for a brain_patterns row.
33
+ *
34
+ * Base: 0.4 (auto-generated patterns start lower).
35
+ * Bonuses:
36
+ * +0.10 for 'workflow' type (structured operational knowledge)
37
+ * +0.05 for 'success' type (validated positive pattern)
38
+ * +0.10 if pattern text exceeds 100 chars (richer description)
39
+ * +0.10 if context exceeds 50 chars (contextual detail present)
40
+ * +0.10 if examples_json contains more than 3 items (empirically validated)
41
+ *
42
+ * Result clamped to [0.0, 1.0].
43
+ */
44
+ export function computePatternQuality(params: PatternQualityInput): number {
45
+ let score = 0.4;
46
+
47
+ if (params.type === 'workflow') score += 0.1;
48
+ if (params.type === 'success') score += 0.05;
49
+
50
+ if (params.pattern.length > 100) score += 0.1;
51
+ if (params.context && params.context.length > 50) score += 0.1;
52
+
53
+ if (params.examples_json) {
54
+ try {
55
+ const examples = JSON.parse(params.examples_json) as unknown[];
56
+ if (Array.isArray(examples) && examples.length > 3) score += 0.1;
57
+ } catch {
58
+ // Malformed JSON — no bonus
59
+ }
60
+ }
61
+
62
+ return clamp(score);
63
+ }
64
+
65
+ // ============================================================================
66
+ // Learning quality
67
+ // ============================================================================
68
+
69
+ /** Input shape for learning quality computation — mirrors StoreLearningParams. */
70
+ export interface LearningQualityInput {
71
+ confidence: number;
72
+ actionable?: boolean | null;
73
+ insight: string;
74
+ application?: string | null;
75
+ }
76
+
77
+ /**
78
+ * Compute quality score for a brain_learnings row.
79
+ *
80
+ * Base: confidence value (already 0.0–1.0, caller-provided).
81
+ * Bonuses:
82
+ * +0.10 if actionable (directly applicable guidance)
83
+ * +0.10 if insight text exceeds 100 chars (detailed insight)
84
+ * +0.10 if application exceeds 20 chars (concrete application context)
85
+ *
86
+ * Result clamped to [0.0, 1.0].
87
+ */
88
+ export function computeLearningQuality(params: LearningQualityInput): number {
89
+ let score = params.confidence ?? 0.5;
90
+
91
+ if (params.actionable) score += 0.1;
92
+ if (params.insight.length > 100) score += 0.1;
93
+ if (params.application && params.application.length > 20) score += 0.1;
94
+
95
+ return clamp(score);
96
+ }
97
+
98
+ // ============================================================================
99
+ // Decision quality
100
+ // ============================================================================
101
+
102
+ /** Input shape for decision quality computation — mirrors StoreDecisionParams. */
103
+ export interface DecisionQualityInput {
104
+ confidence: 'low' | 'medium' | 'high';
105
+ rationale?: string | null;
106
+ contextTaskId?: string | null;
107
+ }
108
+
109
+ /** Numeric value map from confidence level. */
110
+ const CONFIDENCE_SCORE_MAP: Record<'low' | 'medium' | 'high', number> = {
111
+ high: 0.9,
112
+ medium: 0.7,
113
+ low: 0.5,
114
+ };
115
+
116
+ /**
117
+ * Compute quality score for a brain_decisions row.
118
+ *
119
+ * Base: mapped from confidence level (high=0.9, medium=0.7, low=0.5).
120
+ * Bonuses:
121
+ * +0.10 if rationale exceeds 50 chars (substantiated reasoning)
122
+ * +0.05 if linked to a specific task (anchored in real work)
123
+ *
124
+ * Result clamped to [0.0, 1.0].
125
+ */
126
+ export function computeDecisionQuality(params: DecisionQualityInput): number {
127
+ let score = CONFIDENCE_SCORE_MAP[params.confidence] ?? 0.5;
128
+
129
+ if (params.rationale && params.rationale.length > 50) score += 0.1;
130
+ if (params.contextTaskId) score += 0.05;
131
+
132
+ return clamp(score);
133
+ }
134
+
135
+ // ============================================================================
136
+ // Observation quality
137
+ // ============================================================================
138
+
139
+ /** Input shape for observation quality computation — mirrors ObserveBrainParams. */
140
+ export interface ObservationQualityInput {
141
+ text: string;
142
+ title?: string | null;
143
+ }
144
+
145
+ /**
146
+ * Compute quality score for a brain_observations row.
147
+ *
148
+ * Base: 0.6 (manual observations start higher than auto-extracted entries).
149
+ * Bonuses:
150
+ * +0.10 if text exceeds 200 chars (rich narrative)
151
+ * +0.05 if title exceeds 10 chars (meaningful label)
152
+ *
153
+ * Result clamped to [0.0, 1.0].
154
+ */
155
+ export function computeObservationQuality(params: ObservationQualityInput): number {
156
+ let score = 0.6;
157
+
158
+ if (params.text && params.text.length > 200) score += 0.1;
159
+ if (params.title && params.title.length > 10) score += 0.05;
160
+
161
+ return clamp(score);
162
+ }
163
+
164
+ // ============================================================================
165
+ // Helpers
166
+ // ============================================================================
167
+
168
+ /**
169
+ * Clamp a score to the valid [0.0, 1.0] range.
170
+ */
171
+ function clamp(score: number): number {
172
+ return Math.min(1.0, Math.max(0.0, score));
173
+ }
@@ -1,61 +1,39 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const mocks = vi.hoisted(() => ({
4
- observeBrain: vi.fn(),
5
- }));
6
-
7
- vi.mock('../../memory/brain-retrieval.js', () => ({
8
- observeBrain: mocks.observeBrain,
9
- }));
1
+ import { describe, expect, it } from 'vitest';
10
2
 
11
3
  import { bridgeSessionToMemory } from '../session-memory-bridge.js';
12
4
 
5
+ /**
6
+ * T527: bridgeSessionToMemory is now a no-op. Session data already lives in
7
+ * the sessions table; the duplicate observeBrain write and extractSessionEndMemory
8
+ * call were removed to reduce brain.db noise.
9
+ *
10
+ * These tests verify the function remains safe to call from sessions/index.ts
11
+ * without throwing.
12
+ */
13
13
  describe('bridgeSessionToMemory', () => {
14
- beforeEach(() => {
15
- mocks.observeBrain.mockReset();
16
- });
17
-
18
- it('records a session summary observation on success', async () => {
19
- mocks.observeBrain.mockResolvedValue(undefined);
20
-
21
- await bridgeSessionToMemory('/tmp/project', {
22
- sessionId: 'session-100',
23
- scope: 'epic:T5417',
24
- tasksCompleted: ['T5464', 'T5466'],
25
- duration: 125,
26
- });
27
-
28
- expect(mocks.observeBrain).toHaveBeenCalledTimes(1);
29
- expect(mocks.observeBrain).toHaveBeenCalledWith('/tmp/project', {
30
- text: 'Session session-100 ended. Scope: epic:T5417. Duration: 2 min. Tasks completed: T5464, T5466.',
31
- title: 'Session summary: session-100',
32
- type: 'change',
33
- sourceSessionId: 'session-100',
34
- sourceType: 'agent',
35
- });
14
+ it('resolves without throwing for a normal session', async () => {
15
+ await expect(
16
+ bridgeSessionToMemory('/tmp/project', {
17
+ sessionId: 'session-100',
18
+ scope: 'epic:T5417',
19
+ tasksCompleted: ['T5464', 'T5466'],
20
+ duration: 125,
21
+ }),
22
+ ).resolves.toBeUndefined();
36
23
  });
37
24
 
38
- it('uses "none" for empty task completion lists', async () => {
39
- mocks.observeBrain.mockResolvedValue(undefined);
40
-
41
- await bridgeSessionToMemory('/tmp/project', {
42
- sessionId: 'session-101',
43
- scope: 'global',
44
- tasksCompleted: [],
45
- duration: 59,
46
- });
47
-
48
- expect(mocks.observeBrain).toHaveBeenCalledWith(
49
- '/tmp/project',
50
- expect.objectContaining({
51
- text: 'Session session-101 ended. Scope: global. Duration: 1 min. Tasks completed: none.',
25
+ it('resolves without throwing for empty task completion list', async () => {
26
+ await expect(
27
+ bridgeSessionToMemory('/tmp/project', {
28
+ sessionId: 'session-101',
29
+ scope: 'global',
30
+ tasksCompleted: [],
31
+ duration: 59,
52
32
  }),
53
- );
33
+ ).resolves.toBeUndefined();
54
34
  });
55
35
 
56
- it('swallows persistence errors to preserve session-end flow', async () => {
57
- mocks.observeBrain.mockRejectedValue(new Error('database is locked'));
58
-
36
+ it('resolves without throwing even when called with minimal data', async () => {
59
37
  await expect(
60
38
  bridgeSessionToMemory('/tmp/project', {
61
39
  sessionId: 'session-102',
@@ -1,16 +1,19 @@
1
1
  /**
2
- * Memory-session bridge — records session summaries as brain observations.
2
+ * Memory-session bridge — no-op placeholder retained for call-site compatibility.
3
3
  *
4
- * Hooks into the session end flow to persist a summary of the session's
5
- * work (tasks completed, scope, duration) into brain.db as an observation.
6
- * This is best-effort: failures do not affect the session end operation.
4
+ * Previously this function wrote a session summary observation to brain.db and
5
+ * triggered auto-extraction of structured memory. Both were removed in T527
6
+ * because session data already lives in the sessions table; duplicating it to
7
+ * brain_observations was pure noise.
8
+ *
9
+ * The function is kept (as a no-op) so callers in sessions/index.ts do not need
10
+ * to be updated in this task.
7
11
  *
8
12
  * @task T5392
9
13
  * @epic T5149
14
+ * @see T527 — removal of duplicate session observation writes
10
15
  */
11
16
 
12
- import { observeBrain } from '../memory/brain-retrieval.js';
13
-
14
17
  /** Session data needed to create a memory bridge observation. */
15
18
  export interface SessionBridgeData {
16
19
  sessionId: string;
@@ -20,50 +23,19 @@ export interface SessionBridgeData {
20
23
  }
21
24
 
22
25
  /**
23
- * Bridge session end data to brain.db as an observation.
26
+ * Bridge session end data currently a no-op.
24
27
  *
25
- * Builds a summary text from the session metadata and saves it
26
- * as a 'change' observation with source_type 'agent'.
28
+ * Retained for call-site compatibility. Previously wrote a duplicate summary
29
+ * observation to brain.db and triggered extractSessionEndMemory; both were
30
+ * removed in T527 to reduce brain.db noise.
27
31
  *
28
- * @param projectRoot - Project root directory for brain.db resolution
29
- * @param sessionData - Session metadata to record
32
+ * @param _projectRoot - Project root directory (unused)
33
+ * @param _sessionData - Session metadata (unused)
30
34
  */
31
35
  export async function bridgeSessionToMemory(
32
- projectRoot: string,
33
- sessionData: SessionBridgeData,
36
+ _projectRoot: string,
37
+ _sessionData: SessionBridgeData,
34
38
  ): Promise<void> {
35
- try {
36
- const taskList =
37
- sessionData.tasksCompleted.length > 0 ? sessionData.tasksCompleted.join(', ') : 'none';
38
-
39
- const durationMinutes = Math.round(sessionData.duration / 60);
40
-
41
- const summary = [
42
- `Session ${sessionData.sessionId} ended.`,
43
- `Scope: ${sessionData.scope}.`,
44
- `Duration: ${durationMinutes} min.`,
45
- `Tasks completed: ${taskList}.`,
46
- ].join(' ');
47
-
48
- await observeBrain(projectRoot, {
49
- text: summary,
50
- title: `Session summary: ${sessionData.sessionId}`,
51
- type: 'change',
52
- sourceSessionId: sessionData.sessionId,
53
- sourceType: 'agent',
54
- });
55
-
56
- // Auto-extract structured memory from session end (best-effort)
57
- try {
58
- const { extractSessionEndMemory, resolveTaskDetails } = await import(
59
- '../memory/auto-extract.js'
60
- );
61
- const taskDetails = await resolveTaskDetails(projectRoot, sessionData.tasksCompleted);
62
- await extractSessionEndMemory(projectRoot, sessionData, taskDetails);
63
- } catch {
64
- /* Session memory extraction is best-effort */
65
- }
66
- } catch {
67
- // Best-effort: session bridge must never fail the session end flow
68
- }
39
+ // T527: Intentional no-op. Session data is already in the sessions table.
40
+ // Removed: observeBrain duplicate write and extractSessionEndMemory call.
69
41
  }
@@ -5,6 +5,10 @@
5
5
  * addPageEdge, getPageEdges, getNeighbors, and removePageEdge using
6
6
  * in-memory brain.db instances.
7
7
  *
8
+ * Updated for T528: uses expanded BRAIN_NODE_TYPES and BRAIN_EDGE_TYPES.
9
+ * Old values mapped per T528 cross-validation report:
10
+ * 'doc' → 'concept', 'depends_on' → 'derived_from', 'relates_to' → 'informed_by'
11
+ *
8
12
  * @epic T5149
9
13
  * @task T5384
10
14
  */
@@ -49,6 +53,10 @@ describe('BrainDataAccessor PageIndex', () => {
49
53
  expect(node.label).toBe('BRAIN/NEXUS Cognitive Infrastructure');
50
54
  expect(node.metadataJson).toBe('{"priority":"critical"}');
51
55
  expect(node.createdAt).toBeTruthy();
56
+ // New fields with defaults
57
+ expect(node.qualityScore).toBe(0.5);
58
+ expect(node.contentHash).toBeNull();
59
+ expect(node.lastActivityAt).toBeTruthy();
52
60
  });
53
61
 
54
62
  it('getPageNode returns node by ID', async () => {
@@ -57,16 +65,17 @@ describe('BrainDataAccessor PageIndex', () => {
57
65
  closeBrainDb();
58
66
 
59
67
  const accessor = await getBrainAccessor();
68
+ // 'doc' was removed from BRAIN_NODE_TYPES in T528; use 'concept' instead
60
69
  await accessor.addPageNode({
61
- id: 'doc:BRAIN-SPEC',
62
- nodeType: 'doc',
70
+ id: 'concept:BRAIN-SPEC',
71
+ nodeType: 'concept',
63
72
  label: 'CLEO BRAIN Specification',
64
73
  });
65
74
 
66
- const fetched = await accessor.getPageNode('doc:BRAIN-SPEC');
75
+ const fetched = await accessor.getPageNode('concept:BRAIN-SPEC');
67
76
  expect(fetched).not.toBeNull();
68
- expect(fetched!.id).toBe('doc:BRAIN-SPEC');
69
- expect(fetched!.nodeType).toBe('doc');
77
+ expect(fetched!.id).toBe('concept:BRAIN-SPEC');
78
+ expect(fetched!.nodeType).toBe('concept');
70
79
  expect(fetched!.label).toBe('CLEO BRAIN Specification');
71
80
  });
72
81
 
@@ -87,7 +96,8 @@ describe('BrainDataAccessor PageIndex', () => {
87
96
 
88
97
  const accessor = await getBrainAccessor();
89
98
  await accessor.addPageNode({ id: 'task:T1', nodeType: 'task', label: 'Task 1' });
90
- await accessor.addPageNode({ id: 'doc:D1', nodeType: 'doc', label: 'Doc 1' });
99
+ // 'doc' removed in T528; use 'concept'
100
+ await accessor.addPageNode({ id: 'concept:D1', nodeType: 'concept', label: 'Concept 1' });
91
101
  await accessor.addPageNode({ id: 'task:T2', nodeType: 'task', label: 'Task 2' });
92
102
  await accessor.addPageNode({ id: 'file:F1', nodeType: 'file', label: 'File 1' });
93
103
 
@@ -95,14 +105,47 @@ describe('BrainDataAccessor PageIndex', () => {
95
105
  expect(tasks).toHaveLength(2);
96
106
  expect(tasks.every((n) => n.nodeType === 'task')).toBe(true);
97
107
 
98
- const docs = await accessor.findPageNodes({ nodeType: 'doc' });
99
- expect(docs).toHaveLength(1);
100
- expect(docs[0]!.id).toBe('doc:D1');
108
+ const concepts = await accessor.findPageNodes({ nodeType: 'concept' });
109
+ expect(concepts).toHaveLength(1);
110
+ expect(concepts[0]!.id).toBe('concept:D1');
101
111
 
102
112
  const all = await accessor.findPageNodes();
103
113
  expect(all).toHaveLength(4);
104
114
  });
105
115
 
116
+ it('findPageNodes filters by minQualityScore', async () => {
117
+ const { getBrainAccessor } = await import('../brain-accessor.js');
118
+ const { closeBrainDb } = await import('../brain-sqlite.js');
119
+ closeBrainDb();
120
+
121
+ const accessor = await getBrainAccessor();
122
+ await accessor.addPageNode({
123
+ id: 'decision:D1',
124
+ nodeType: 'decision',
125
+ label: 'High quality',
126
+ qualityScore: 0.9,
127
+ });
128
+ await accessor.addPageNode({
129
+ id: 'decision:D2',
130
+ nodeType: 'decision',
131
+ label: 'Low quality',
132
+ qualityScore: 0.1,
133
+ });
134
+ await accessor.addPageNode({
135
+ id: 'decision:D3',
136
+ nodeType: 'decision',
137
+ label: 'Medium quality',
138
+ qualityScore: 0.5,
139
+ });
140
+
141
+ const highQuality = await accessor.findPageNodes({ minQualityScore: 0.8 });
142
+ expect(highQuality).toHaveLength(1);
143
+ expect(highQuality[0]!.id).toBe('decision:D1');
144
+
145
+ const mediumAndAbove = await accessor.findPageNodes({ minQualityScore: 0.5 });
146
+ expect(mediumAndAbove).toHaveLength(2);
147
+ });
148
+
106
149
  it('addPageEdge creates edge and returns it', async () => {
107
150
  const { getBrainAccessor } = await import('../brain-accessor.js');
108
151
  const { closeBrainDb } = await import('../brain-sqlite.js');
@@ -112,20 +155,41 @@ describe('BrainDataAccessor PageIndex', () => {
112
155
  await accessor.addPageNode({ id: 'task:T1', nodeType: 'task', label: 'Task 1' });
113
156
  await accessor.addPageNode({ id: 'task:T2', nodeType: 'task', label: 'Task 2' });
114
157
 
158
+ // 'depends_on' removed in T528; use 'derived_from'
115
159
  const edge = await accessor.addPageEdge({
116
160
  fromId: 'task:T1',
117
161
  toId: 'task:T2',
118
- edgeType: 'depends_on',
162
+ edgeType: 'derived_from',
119
163
  weight: 0.8,
120
164
  });
121
165
 
122
166
  expect(edge.fromId).toBe('task:T1');
123
167
  expect(edge.toId).toBe('task:T2');
124
- expect(edge.edgeType).toBe('depends_on');
168
+ expect(edge.edgeType).toBe('derived_from');
125
169
  expect(edge.weight).toBe(0.8);
126
170
  expect(edge.createdAt).toBeTruthy();
127
171
  });
128
172
 
173
+ it('addPageEdge supports provenance field', async () => {
174
+ const { getBrainAccessor } = await import('../brain-accessor.js');
175
+ const { closeBrainDb } = await import('../brain-sqlite.js');
176
+ closeBrainDb();
177
+
178
+ const accessor = await getBrainAccessor();
179
+ await accessor.addPageNode({ id: 'observation:O1', nodeType: 'observation', label: 'Obs 1' });
180
+ await accessor.addPageNode({ id: 'decision:D1', nodeType: 'decision', label: 'Decision 1' });
181
+
182
+ const edge = await accessor.addPageEdge({
183
+ fromId: 'observation:O1',
184
+ toId: 'decision:D1',
185
+ edgeType: 'supports',
186
+ provenance: 'auto:task-complete',
187
+ });
188
+
189
+ expect(edge.provenance).toBe('auto:task-complete');
190
+ expect(edge.weight).toBe(1.0);
191
+ });
192
+
129
193
  it('getPageEdges returns in/out/both edges', async () => {
130
194
  const { getBrainAccessor } = await import('../brain-accessor.js');
131
195
  const { closeBrainDb } = await import('../brain-sqlite.js');
@@ -134,9 +198,11 @@ describe('BrainDataAccessor PageIndex', () => {
134
198
  const accessor = await getBrainAccessor();
135
199
  await accessor.addPageNode({ id: 'A', nodeType: 'task', label: 'A' });
136
200
  await accessor.addPageNode({ id: 'B', nodeType: 'task', label: 'B' });
137
- await accessor.addPageNode({ id: 'C', nodeType: 'doc', label: 'C' });
201
+ // 'concept' replaces 'doc'
202
+ await accessor.addPageNode({ id: 'C', nodeType: 'concept', label: 'C' });
138
203
 
139
- await accessor.addPageEdge({ fromId: 'A', toId: 'B', edgeType: 'depends_on' });
204
+ // 'depends_on' 'derived_from'; 'documents' is still valid
205
+ await accessor.addPageEdge({ fromId: 'A', toId: 'B', edgeType: 'derived_from' });
140
206
  await accessor.addPageEdge({ fromId: 'C', toId: 'A', edgeType: 'documents' });
141
207
 
142
208
  const outEdges = await accessor.getPageEdges('A', 'out');
@@ -159,15 +225,17 @@ describe('BrainDataAccessor PageIndex', () => {
159
225
  const accessor = await getBrainAccessor();
160
226
  await accessor.addPageNode({ id: 'A', nodeType: 'task', label: 'A' });
161
227
  await accessor.addPageNode({ id: 'B', nodeType: 'task', label: 'B' });
162
- await accessor.addPageNode({ id: 'C', nodeType: 'doc', label: 'C' });
228
+ // 'concept' replaces 'doc'
229
+ await accessor.addPageNode({ id: 'C', nodeType: 'concept', label: 'C' });
163
230
 
164
- await accessor.addPageEdge({ fromId: 'A', toId: 'B', edgeType: 'depends_on' });
231
+ // 'depends_on' 'derived_from'; 'documents' is still valid
232
+ await accessor.addPageEdge({ fromId: 'A', toId: 'B', edgeType: 'derived_from' });
165
233
  await accessor.addPageEdge({ fromId: 'A', toId: 'C', edgeType: 'documents' });
166
234
 
167
235
  const allNeighbors = await accessor.getNeighbors('A');
168
236
  expect(allNeighbors).toHaveLength(2);
169
237
 
170
- const depNeighbors = await accessor.getNeighbors('A', 'depends_on');
238
+ const depNeighbors = await accessor.getNeighbors('A', 'derived_from');
171
239
  expect(depNeighbors).toHaveLength(1);
172
240
  expect(depNeighbors[0]!.id).toBe('B');
173
241
  });
@@ -180,9 +248,11 @@ describe('BrainDataAccessor PageIndex', () => {
180
248
  const accessor = await getBrainAccessor();
181
249
  await accessor.addPageNode({ id: 'A', nodeType: 'task', label: 'A' });
182
250
  await accessor.addPageNode({ id: 'B', nodeType: 'task', label: 'B' });
183
- await accessor.addPageNode({ id: 'C', nodeType: 'doc', label: 'C' });
251
+ // 'concept' replaces 'doc'
252
+ await accessor.addPageNode({ id: 'C', nodeType: 'concept', label: 'C' });
184
253
 
185
- await accessor.addPageEdge({ fromId: 'A', toId: 'B', edgeType: 'depends_on' });
254
+ // 'depends_on' 'derived_from'; 'documents' is still valid
255
+ await accessor.addPageEdge({ fromId: 'A', toId: 'B', edgeType: 'derived_from' });
186
256
  await accessor.addPageEdge({ fromId: 'C', toId: 'A', edgeType: 'documents' });
187
257
 
188
258
  await accessor.removePageNode('A');
@@ -209,16 +279,17 @@ describe('BrainDataAccessor PageIndex', () => {
209
279
  await accessor.addPageNode({ id: 'A', nodeType: 'task', label: 'A' });
210
280
  await accessor.addPageNode({ id: 'B', nodeType: 'task', label: 'B' });
211
281
 
212
- await accessor.addPageEdge({ fromId: 'A', toId: 'B', edgeType: 'depends_on' });
213
- await accessor.addPageEdge({ fromId: 'A', toId: 'B', edgeType: 'relates_to' });
282
+ // 'depends_on' 'derived_from'; 'relates_to' 'informed_by'
283
+ await accessor.addPageEdge({ fromId: 'A', toId: 'B', edgeType: 'derived_from' });
284
+ await accessor.addPageEdge({ fromId: 'A', toId: 'B', edgeType: 'informed_by' });
214
285
 
215
286
  let edges = await accessor.getPageEdges('A', 'out');
216
287
  expect(edges).toHaveLength(2);
217
288
 
218
- await accessor.removePageEdge('A', 'B', 'depends_on');
289
+ await accessor.removePageEdge('A', 'B', 'derived_from');
219
290
 
220
291
  edges = await accessor.getPageEdges('A', 'out');
221
292
  expect(edges).toHaveLength(1);
222
- expect(edges[0]!.edgeType).toBe('relates_to');
293
+ expect(edges[0]!.edgeType).toBe('informed_by');
223
294
  });
224
295
  });
@@ -439,18 +439,25 @@ export class BrainDataAccessor {
439
439
  }
440
440
 
441
441
  async findPageNodes(
442
- params: { nodeType?: (typeof brainSchema.BRAIN_NODE_TYPES)[number]; limit?: number } = {},
442
+ params: {
443
+ nodeType?: (typeof brainSchema.BRAIN_NODE_TYPES)[number];
444
+ minQualityScore?: number;
445
+ limit?: number;
446
+ } = {},
443
447
  ): Promise<BrainPageNodeRow[]> {
444
448
  const conditions: SQL[] = [];
445
449
 
446
450
  if (params.nodeType) {
447
451
  conditions.push(eq(brainSchema.brainPageNodes.nodeType, params.nodeType));
448
452
  }
453
+ if (params.minQualityScore !== undefined) {
454
+ conditions.push(gte(brainSchema.brainPageNodes.qualityScore, params.minQualityScore));
455
+ }
449
456
 
450
457
  let query = this.db
451
458
  .select()
452
459
  .from(brainSchema.brainPageNodes)
453
- .orderBy(desc(brainSchema.brainPageNodes.createdAt));
460
+ .orderBy(desc(brainSchema.brainPageNodes.lastActivityAt));
454
461
 
455
462
  if (conditions.length > 0) {
456
463
  query = query.where(and(...conditions)) as typeof query;
@@ -463,6 +470,13 @@ export class BrainDataAccessor {
463
470
  return query;
464
471
  }
465
472
 
473
+ async updatePageNode(id: string, updates: Partial<NewBrainPageNodeRow>): Promise<void> {
474
+ await this.db
475
+ .update(brainSchema.brainPageNodes)
476
+ .set({ ...updates, updatedAt: new Date().toISOString().replace('T', ' ').slice(0, 19) })
477
+ .where(eq(brainSchema.brainPageNodes.id, id));
478
+ }
479
+
466
480
  async removePageNode(id: string): Promise<void> {
467
481
  // Remove associated edges first (both directions)
468
482
  await this.db
@@ -493,6 +507,38 @@ export class BrainDataAccessor {
493
507
  return result[0]!;
494
508
  }
495
509
 
510
+ async findPageEdges(
511
+ params: {
512
+ edgeType?: (typeof brainSchema.BRAIN_EDGE_TYPES)[number];
513
+ provenance?: string;
514
+ limit?: number;
515
+ } = {},
516
+ ): Promise<BrainPageEdgeRow[]> {
517
+ const conditions: SQL[] = [];
518
+
519
+ if (params.edgeType) {
520
+ conditions.push(eq(brainSchema.brainPageEdges.edgeType, params.edgeType));
521
+ }
522
+ if (params.provenance) {
523
+ conditions.push(eq(brainSchema.brainPageEdges.provenance, params.provenance));
524
+ }
525
+
526
+ let query = this.db
527
+ .select()
528
+ .from(brainSchema.brainPageEdges)
529
+ .orderBy(desc(brainSchema.brainPageEdges.createdAt));
530
+
531
+ if (conditions.length > 0) {
532
+ query = query.where(and(...conditions)) as typeof query;
533
+ }
534
+
535
+ if (params.limit) {
536
+ query = query.limit(params.limit) as typeof query;
537
+ }
538
+
539
+ return query;
540
+ }
541
+
496
542
  async getPageEdges(
497
543
  nodeId: string,
498
544
  direction: 'in' | 'out' | 'both' = 'both',