@cleocode/core 2026.4.30 → 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.
- package/dist/bootstrap.d.ts +35 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/code/index.d.ts +8 -4
- package/dist/code/index.d.ts.map +1 -1
- package/dist/code/parser.d.ts +22 -9
- package/dist/code/parser.d.ts.map +1 -1
- package/dist/hooks/handlers/session-hooks.d.ts +11 -4
- package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
- package/dist/hooks/payload-schemas.d.ts +6 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3859 -3008
- package/dist/index.js.map +4 -4
- package/dist/internal.d.ts +10 -7
- package/dist/internal.d.ts.map +1 -1
- package/dist/lib/tree-sitter-languages.d.ts +11 -7
- package/dist/lib/tree-sitter-languages.d.ts.map +1 -1
- package/dist/memory/auto-extract.d.ts +27 -15
- package/dist/memory/auto-extract.d.ts.map +1 -1
- package/dist/memory/brain-backfill.d.ts +59 -0
- package/dist/memory/brain-backfill.d.ts.map +1 -0
- package/dist/memory/brain-purge.d.ts +51 -0
- package/dist/memory/brain-purge.d.ts.map +1 -0
- package/dist/memory/brain-retrieval.d.ts.map +1 -1
- package/dist/memory/brain-search.d.ts.map +1 -1
- package/dist/memory/decisions.d.ts.map +1 -1
- package/dist/memory/engine-compat.d.ts +71 -0
- package/dist/memory/engine-compat.d.ts.map +1 -1
- package/dist/memory/graph-auto-populate.d.ts +65 -0
- package/dist/memory/graph-auto-populate.d.ts.map +1 -0
- package/dist/memory/graph-queries.d.ts +127 -0
- package/dist/memory/graph-queries.d.ts.map +1 -0
- package/dist/memory/learnings.d.ts +2 -0
- package/dist/memory/learnings.d.ts.map +1 -1
- package/dist/memory/patterns.d.ts +2 -0
- package/dist/memory/patterns.d.ts.map +1 -1
- package/dist/memory/quality-scoring.d.ts +90 -0
- package/dist/memory/quality-scoring.d.ts.map +1 -0
- package/dist/sessions/session-memory-bridge.d.ts +16 -10
- package/dist/sessions/session-memory-bridge.d.ts.map +1 -1
- package/dist/store/brain-accessor.d.ts +7 -0
- package/dist/store/brain-accessor.d.ts.map +1 -1
- package/dist/store/brain-schema.d.ts +185 -11
- package/dist/store/brain-schema.d.ts.map +1 -1
- package/dist/store/brain-sqlite.d.ts.map +1 -1
- package/dist/store/nexus-schema.d.ts +480 -2
- package/dist/store/nexus-schema.d.ts.map +1 -1
- package/dist/store/tasks-schema.d.ts +9 -9
- package/dist/store/validation-schemas.d.ts +44 -28
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/dist/system/dependencies.d.ts +43 -0
- package/dist/system/dependencies.d.ts.map +1 -0
- package/dist/system/health.d.ts +3 -0
- package/dist/system/health.d.ts.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/package.json +19 -19
- package/src/bootstrap.ts +124 -0
- package/src/code/index.ts +20 -4
- package/src/code/parser.ts +310 -110
- package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +19 -45
- package/src/hooks/handlers/__tests__/session-hooks.test.ts +42 -54
- package/src/hooks/handlers/session-hooks.ts +11 -33
- package/src/index.ts +14 -0
- package/src/internal.ts +37 -7
- package/src/lib/tree-sitter-languages.ts +11 -7
- package/src/memory/__tests__/auto-extract.test.ts +20 -82
- package/src/memory/__tests__/embedding-pipeline.test.ts +389 -0
- package/src/memory/auto-extract.ts +34 -120
- package/src/memory/brain-backfill.ts +471 -0
- package/src/memory/brain-purge.ts +315 -0
- package/src/memory/brain-retrieval.ts +43 -2
- package/src/memory/brain-search.ts +23 -6
- package/src/memory/decisions.ts +76 -3
- package/src/memory/engine-compat.ts +168 -0
- package/src/memory/graph-auto-populate.ts +173 -0
- package/src/memory/graph-queries.ts +424 -0
- package/src/memory/learnings.ts +55 -7
- package/src/memory/patterns.ts +66 -13
- package/src/memory/quality-scoring.ts +173 -0
- package/src/sessions/__tests__/session-memory-bridge.test.ts +27 -49
- package/src/sessions/session-memory-bridge.ts +19 -47
- package/src/store/__tests__/brain-accessor-pageindex.test.ts +93 -22
- package/src/store/brain-accessor.ts +48 -2
- package/src/store/brain-schema.ts +165 -13
- package/src/store/brain-sqlite.ts +35 -0
- package/src/store/nexus-schema.ts +257 -3
- package/src/system/dependencies.ts +534 -0
- package/src/system/health.ts +126 -22
- 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 {
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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('
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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('
|
|
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 —
|
|
2
|
+
* Memory-session bridge — no-op placeholder retained for call-site compatibility.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
26
|
+
* Bridge session end data — currently a no-op.
|
|
24
27
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
29
|
-
* @param
|
|
32
|
+
* @param _projectRoot - Project root directory (unused)
|
|
33
|
+
* @param _sessionData - Session metadata (unused)
|
|
30
34
|
*/
|
|
31
35
|
export async function bridgeSessionToMemory(
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
_projectRoot: string,
|
|
37
|
+
_sessionData: SessionBridgeData,
|
|
34
38
|
): Promise<void> {
|
|
35
|
-
|
|
36
|
-
|
|
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: '
|
|
62
|
-
nodeType: '
|
|
70
|
+
id: 'concept:BRAIN-SPEC',
|
|
71
|
+
nodeType: 'concept',
|
|
63
72
|
label: 'CLEO BRAIN Specification',
|
|
64
73
|
});
|
|
65
74
|
|
|
66
|
-
const fetched = await accessor.getPageNode('
|
|
75
|
+
const fetched = await accessor.getPageNode('concept:BRAIN-SPEC');
|
|
67
76
|
expect(fetched).not.toBeNull();
|
|
68
|
-
expect(fetched!.id).toBe('
|
|
69
|
-
expect(fetched!.nodeType).toBe('
|
|
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
|
-
|
|
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
|
|
99
|
-
expect(
|
|
100
|
-
expect(
|
|
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: '
|
|
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('
|
|
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
|
-
|
|
201
|
+
// 'concept' replaces 'doc'
|
|
202
|
+
await accessor.addPageNode({ id: 'C', nodeType: 'concept', label: 'C' });
|
|
138
203
|
|
|
139
|
-
|
|
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
|
-
|
|
228
|
+
// 'concept' replaces 'doc'
|
|
229
|
+
await accessor.addPageNode({ id: 'C', nodeType: 'concept', label: 'C' });
|
|
163
230
|
|
|
164
|
-
|
|
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', '
|
|
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
|
-
|
|
251
|
+
// 'concept' replaces 'doc'
|
|
252
|
+
await accessor.addPageNode({ id: 'C', nodeType: 'concept', label: 'C' });
|
|
184
253
|
|
|
185
|
-
|
|
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
|
-
|
|
213
|
-
await accessor.addPageEdge({ fromId: 'A', toId: 'B', edgeType: '
|
|
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', '
|
|
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('
|
|
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: {
|
|
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.
|
|
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',
|