@cleocode/core 2026.4.50 → 2026.4.52
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/index.js +511 -41
- package/dist/index.js.map +4 -4
- package/dist/internal.d.ts +4 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +669 -45
- package/dist/internal.js.map +4 -4
- package/dist/memory/brain-export.d.ts +70 -0
- package/dist/memory/brain-export.d.ts.map +1 -0
- package/dist/memory/brain-lifecycle.d.ts +7 -0
- package/dist/memory/brain-lifecycle.d.ts.map +1 -1
- package/dist/memory/brain-retrieval.d.ts.map +1 -1
- package/dist/memory/brain-stdp.d.ts +122 -0
- package/dist/memory/brain-stdp.d.ts.map +1 -0
- package/dist/memory/decision-cross-link.d.ts +70 -0
- package/dist/memory/decision-cross-link.d.ts.map +1 -0
- package/dist/memory/decisions.d.ts.map +1 -1
- package/dist/memory/edge-types.d.ts +24 -0
- package/dist/memory/edge-types.d.ts.map +1 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/store/brain-schema.d.ts +150 -3
- package/dist/store/brain-schema.d.ts.map +1 -1
- package/dist/store/brain-sqlite.d.ts.map +1 -1
- package/dist/store/validation-schemas.d.ts +1 -0
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/dist/validation/verification.d.ts.map +1 -1
- package/migrations/drizzle-brain/20260415000001_t626-normalize-co-retrieved-edge-type/migration.sql +14 -0
- package/package.json +8 -8
- package/src/internal.ts +14 -0
- package/src/memory/__tests__/brain-stdp.test.ts +452 -0
- package/src/memory/__tests__/decision-cross-link.test.ts +240 -0
- package/src/memory/brain-embedding.ts +1 -1
- package/src/memory/brain-export.ts +286 -0
- package/src/memory/brain-lifecycle.ts +23 -4
- package/src/memory/brain-retrieval.ts +80 -14
- package/src/memory/brain-similarity.ts +1 -1
- package/src/memory/brain-stdp.ts +448 -0
- package/src/memory/claude-mem-migration.ts +1 -1
- package/src/memory/decision-cross-link.ts +276 -0
- package/src/memory/decisions.ts +7 -0
- package/src/memory/edge-types.ts +31 -0
- package/src/memory/index.ts +2 -0
- package/src/sessions/briefing.ts +1 -1
- package/src/skills/dispatch.ts +1 -1
- package/src/skills/injection/subagent.ts +1 -1
- package/src/skills/orchestrator/spawn.ts +1 -1
- package/src/store/brain-schema.ts +54 -0
- package/src/store/brain-sqlite.ts +17 -0
- package/src/store/json.ts +2 -2
- package/src/system/archive-analytics.ts +1 -1
- package/src/tasks/task-ops.ts +2 -2
- package/src/validation/verification.ts +2 -6
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision cross-link module for CLEO BRAIN.
|
|
3
|
+
*
|
|
4
|
+
* Extracts file paths and symbol names referenced in a decision's text and
|
|
5
|
+
* rationale, then creates `affects` edges from the decision graph node to
|
|
6
|
+
* matching `file` / `symbol` nodes in brain_page_nodes. This implements
|
|
7
|
+
* the cross-substrate edge described in
|
|
8
|
+
* docs/plans/brain-synaptic-visualization-research.md §3.2.
|
|
9
|
+
*
|
|
10
|
+
* All database operations are best-effort — they never throw or block the
|
|
11
|
+
* caller. Nodes for referenced files / symbols are upserted on demand so
|
|
12
|
+
* the graph remains consistent even when the target has not yet been
|
|
13
|
+
* independently indexed.
|
|
14
|
+
*
|
|
15
|
+
* @task T626
|
|
16
|
+
* @epic T626
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { addGraphEdge, upsertGraphNode } from './graph-auto-populate.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Extraction
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** A reference extracted from a decision or rationale string. */
|
|
26
|
+
export interface ExtractedRef {
|
|
27
|
+
/** Raw text that matched. */
|
|
28
|
+
raw: string;
|
|
29
|
+
/** Resolved graph node ID: 'file:<path>' or 'symbol:<name>'. */
|
|
30
|
+
nodeId: string;
|
|
31
|
+
/** Discriminated node type. */
|
|
32
|
+
nodeType: 'file' | 'symbol';
|
|
33
|
+
/** Human-readable label for the graph node. */
|
|
34
|
+
label: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Regex patterns used to locate file paths and symbol names inside text.
|
|
39
|
+
*
|
|
40
|
+
* File-path pattern — matches:
|
|
41
|
+
* - Relative paths: `src/store/brain-schema.ts`
|
|
42
|
+
* - Absolute paths: `/mnt/projects/cleocode/packages/core/src/…`
|
|
43
|
+
* - Extension-gated: only `.ts`, `.tsx`, `.js`, `.jsx`, `.rs`, `.json`
|
|
44
|
+
*
|
|
45
|
+
* Symbol pattern — matches:
|
|
46
|
+
* - PascalCase class/interface names: `BrainPageNodes`
|
|
47
|
+
* - camelCase function names at word boundaries: `upsertGraphNode`
|
|
48
|
+
* - snake_case identifiers: `brain_page_edges`
|
|
49
|
+
*
|
|
50
|
+
* Overlapping matches are deduplicated by nodeId before edge creation.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
const FILE_PATH_RE =
|
|
54
|
+
/(?:^|[\s`"'([\]{,])((\/[\w.\-/]+|[\w.-]+(?:\/[\w.-]+)+)\.(ts|tsx|js|jsx|rs|json))(?=$|[\s`"')[\]{,])/gm;
|
|
55
|
+
|
|
56
|
+
const SYMBOL_RE =
|
|
57
|
+
/(?<![`"'/\w.])(?:[A-Z][a-zA-Z0-9]{2,}|[a-z][a-zA-Z0-9]*(?:[A-Z][a-zA-Z0-9]*)+|[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*){2,})(?![`"'/\w])/g;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract file-path and symbol references from free-form text.
|
|
61
|
+
*
|
|
62
|
+
* Symbols shorter than 4 characters or matching common English stop-words
|
|
63
|
+
* are filtered to reduce noise.
|
|
64
|
+
*
|
|
65
|
+
* @param text - Decision text and/or rationale to scan.
|
|
66
|
+
* @returns Deduplicated array of extracted references.
|
|
67
|
+
*/
|
|
68
|
+
export function extractReferencedSymbols(text: string): ExtractedRef[] {
|
|
69
|
+
const seen = new Set<string>();
|
|
70
|
+
const refs: ExtractedRef[] = [];
|
|
71
|
+
|
|
72
|
+
// --- File paths ---
|
|
73
|
+
for (const match of text.matchAll(FILE_PATH_RE)) {
|
|
74
|
+
const raw = match[1];
|
|
75
|
+
if (!raw) continue;
|
|
76
|
+
const nodeId = `file:${raw}`;
|
|
77
|
+
if (seen.has(nodeId)) continue;
|
|
78
|
+
seen.add(nodeId);
|
|
79
|
+
refs.push({ raw, nodeId, nodeType: 'file', label: raw });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Symbol names ---
|
|
83
|
+
for (const match of text.matchAll(SYMBOL_RE)) {
|
|
84
|
+
const raw = match[0];
|
|
85
|
+
if (!raw || raw.length < 4) continue;
|
|
86
|
+
if (SYMBOL_STOP_WORDS.has(raw.toLowerCase())) continue;
|
|
87
|
+
const nodeId = `symbol:${raw}`;
|
|
88
|
+
if (seen.has(nodeId)) continue;
|
|
89
|
+
seen.add(nodeId);
|
|
90
|
+
refs.push({ raw, nodeId, nodeType: 'symbol', label: raw });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return refs;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Common English / technical words that look like camelCase or PascalCase
|
|
98
|
+
* symbols but carry no meaningful code reference. Filtered out to keep the
|
|
99
|
+
* extracted reference set signal-rich.
|
|
100
|
+
*/
|
|
101
|
+
const SYMBOL_STOP_WORDS = new Set([
|
|
102
|
+
'this',
|
|
103
|
+
'that',
|
|
104
|
+
'with',
|
|
105
|
+
'from',
|
|
106
|
+
'into',
|
|
107
|
+
'when',
|
|
108
|
+
'then',
|
|
109
|
+
'also',
|
|
110
|
+
'both',
|
|
111
|
+
'each',
|
|
112
|
+
'such',
|
|
113
|
+
'over',
|
|
114
|
+
'after',
|
|
115
|
+
'before',
|
|
116
|
+
'always',
|
|
117
|
+
'never',
|
|
118
|
+
'should',
|
|
119
|
+
'must',
|
|
120
|
+
'will',
|
|
121
|
+
'would',
|
|
122
|
+
'could',
|
|
123
|
+
'have',
|
|
124
|
+
'been',
|
|
125
|
+
'there',
|
|
126
|
+
'their',
|
|
127
|
+
'they',
|
|
128
|
+
'them',
|
|
129
|
+
'these',
|
|
130
|
+
'those',
|
|
131
|
+
'some',
|
|
132
|
+
'only',
|
|
133
|
+
'just',
|
|
134
|
+
'more',
|
|
135
|
+
'most',
|
|
136
|
+
'many',
|
|
137
|
+
'much',
|
|
138
|
+
'well',
|
|
139
|
+
'very',
|
|
140
|
+
'here',
|
|
141
|
+
'where',
|
|
142
|
+
'which',
|
|
143
|
+
'what',
|
|
144
|
+
'why',
|
|
145
|
+
'how',
|
|
146
|
+
'the',
|
|
147
|
+
'and',
|
|
148
|
+
'but',
|
|
149
|
+
'for',
|
|
150
|
+
'not',
|
|
151
|
+
'are',
|
|
152
|
+
'was',
|
|
153
|
+
'were',
|
|
154
|
+
'has',
|
|
155
|
+
'had',
|
|
156
|
+
'its',
|
|
157
|
+
'the',
|
|
158
|
+
'data',
|
|
159
|
+
'true',
|
|
160
|
+
'false',
|
|
161
|
+
'null',
|
|
162
|
+
'none',
|
|
163
|
+
'type',
|
|
164
|
+
'test',
|
|
165
|
+
'spec',
|
|
166
|
+
'todo',
|
|
167
|
+
'fixme',
|
|
168
|
+
'note',
|
|
169
|
+
'example',
|
|
170
|
+
'index',
|
|
171
|
+
'config',
|
|
172
|
+
'error',
|
|
173
|
+
'value',
|
|
174
|
+
'input',
|
|
175
|
+
'output',
|
|
176
|
+
'result',
|
|
177
|
+
'return',
|
|
178
|
+
'default',
|
|
179
|
+
'source',
|
|
180
|
+
'target',
|
|
181
|
+
'import',
|
|
182
|
+
'export',
|
|
183
|
+
'class',
|
|
184
|
+
'interface',
|
|
185
|
+
'function',
|
|
186
|
+
'const',
|
|
187
|
+
'async',
|
|
188
|
+
'await',
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Edge creation
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create `affects` edges from a decision graph node to every referenced
|
|
197
|
+
* file / symbol node.
|
|
198
|
+
*
|
|
199
|
+
* For each reference:
|
|
200
|
+
* 1. Upsert the target node (file or symbol) so the graph stays consistent.
|
|
201
|
+
* 2. Insert an `applies_to` edge from `decision:<id>` to the target node.
|
|
202
|
+
*
|
|
203
|
+
* All writes are best-effort via {@link upsertGraphNode} and
|
|
204
|
+
* {@link addGraphEdge} — failures are swallowed internally.
|
|
205
|
+
*
|
|
206
|
+
* @param projectRoot - Absolute path to the project root directory.
|
|
207
|
+
* @param decisionId - The decision ID (e.g. `D001`).
|
|
208
|
+
* @param refs - Extracted references returned by {@link extractReferencedSymbols}.
|
|
209
|
+
*/
|
|
210
|
+
export async function linkDecisionToTargets(
|
|
211
|
+
projectRoot: string,
|
|
212
|
+
decisionId: string,
|
|
213
|
+
refs: ExtractedRef[],
|
|
214
|
+
): Promise<void> {
|
|
215
|
+
const fromId = `decision:${decisionId}`;
|
|
216
|
+
|
|
217
|
+
const writes = refs.map(async (ref) => {
|
|
218
|
+
// Upsert the target node so the edge has a valid destination even if the
|
|
219
|
+
// file / symbol has not been independently indexed yet.
|
|
220
|
+
await upsertGraphNode(
|
|
221
|
+
projectRoot,
|
|
222
|
+
ref.nodeId,
|
|
223
|
+
ref.nodeType,
|
|
224
|
+
ref.label,
|
|
225
|
+
0.5, // placeholder quality until nexus indexes it
|
|
226
|
+
ref.raw,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await addGraphEdge(
|
|
230
|
+
projectRoot,
|
|
231
|
+
fromId,
|
|
232
|
+
ref.nodeId,
|
|
233
|
+
'applies_to',
|
|
234
|
+
1.0,
|
|
235
|
+
'auto:decision-cross-link',
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Fire all writes concurrently — individual failures are swallowed inside
|
|
240
|
+
// upsertGraphNode / addGraphEdge.
|
|
241
|
+
await Promise.allSettled(writes);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Convenience facade
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Extract file/symbol references from a decision and create `applies_to`
|
|
250
|
+
* edges in the brain graph. Combines {@link extractReferencedSymbols} and
|
|
251
|
+
* {@link linkDecisionToTargets} in one call.
|
|
252
|
+
*
|
|
253
|
+
* This is the function wired into {@link storeDecision} after a new decision
|
|
254
|
+
* is saved. It is always fire-and-forget: the caller should NOT await it
|
|
255
|
+
* when used inside the decision write path.
|
|
256
|
+
*
|
|
257
|
+
* @param projectRoot - Absolute path to the project root directory.
|
|
258
|
+
* @param decisionId - The saved decision ID (e.g. `D001`).
|
|
259
|
+
* @param decisionText - Full decision text.
|
|
260
|
+
* @param rationale - Full rationale text.
|
|
261
|
+
*/
|
|
262
|
+
export async function autoCrossLinkDecision(
|
|
263
|
+
projectRoot: string,
|
|
264
|
+
decisionId: string,
|
|
265
|
+
decisionText: string,
|
|
266
|
+
rationale: string,
|
|
267
|
+
): Promise<void> {
|
|
268
|
+
try {
|
|
269
|
+
const combined = `${decisionText} ${rationale}`;
|
|
270
|
+
const refs = extractReferencedSymbols(combined);
|
|
271
|
+
if (refs.length === 0) return;
|
|
272
|
+
await linkDecisionToTargets(projectRoot, decisionId, refs);
|
|
273
|
+
} catch {
|
|
274
|
+
/* best-effort — never surface errors to caller */
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/memory/decisions.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { getBrainAccessor } from '../store/brain-accessor.js';
|
|
|
13
13
|
import type { BrainDecisionRow, NewBrainDecisionRow } from '../store/brain-schema.js';
|
|
14
14
|
import { taskExistsInTasksDb } from '../store/cross-db-cleanup.js';
|
|
15
15
|
import { getDb } from '../store/sqlite.js';
|
|
16
|
+
import { autoCrossLinkDecision } from './decision-cross-link.js';
|
|
16
17
|
import { addGraphEdge, upsertGraphNode } from './graph-auto-populate.js';
|
|
17
18
|
import { computeDecisionQuality } from './quality-scoring.js';
|
|
18
19
|
import { detectSupersession, supersedeMemory } from './temporal-supersession.js';
|
|
@@ -229,6 +230,12 @@ export async function storeDecision(
|
|
|
229
230
|
'auto:store-decision',
|
|
230
231
|
);
|
|
231
232
|
}
|
|
233
|
+
|
|
234
|
+
// Cross-link decision → referenced file/symbol nodes (T626 phase 1).
|
|
235
|
+
// Fire-and-forget — autoCrossLinkDecision swallows its own errors.
|
|
236
|
+
autoCrossLinkDecision(projectRoot, saved.id, saved.decision, saved.rationale).catch(() => {
|
|
237
|
+
/* best-effort */
|
|
238
|
+
});
|
|
232
239
|
} catch {
|
|
233
240
|
/* Graph population is best-effort — never block the primary return */
|
|
234
241
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical edge-type constants for `brain_page_edges`.
|
|
3
|
+
*
|
|
4
|
+
* All code that writes or queries `brain_page_edges.edge_type` MUST use
|
|
5
|
+
* these constants instead of raw string literals to prevent enum drift.
|
|
6
|
+
*
|
|
7
|
+
* The values here are a subset of `BRAIN_EDGE_TYPES` (brain-schema.ts).
|
|
8
|
+
* They are duplicated as constants so callers do not need to import the
|
|
9
|
+
* schema module (which carries Drizzle + SQLite dependencies).
|
|
10
|
+
*
|
|
11
|
+
* @epic T626
|
|
12
|
+
*/
|
|
13
|
+
export const EDGE_TYPES = {
|
|
14
|
+
// Plasticity (Hebbian / STDP co-retrieval)
|
|
15
|
+
CO_RETRIEVED: 'co_retrieved',
|
|
16
|
+
// Temporal supersession
|
|
17
|
+
SUPERSEDES: 'supersedes',
|
|
18
|
+
// Task / decision / pattern → target context
|
|
19
|
+
APPLIES_TO: 'applies_to',
|
|
20
|
+
// Provenance
|
|
21
|
+
DERIVED_FROM: 'derived_from',
|
|
22
|
+
// Observation → symbol/file impact
|
|
23
|
+
AFFECTS: 'affects',
|
|
24
|
+
// Observation → symbol name mention
|
|
25
|
+
MENTIONS: 'mentions',
|
|
26
|
+
// Observation → symbol/file structural link
|
|
27
|
+
DOCUMENTS: 'documents',
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
/** Discriminated union of the canonical edge type constant values. */
|
|
31
|
+
export type EdgeType = (typeof EDGE_TYPES)[keyof typeof EDGE_TYPES];
|
package/src/memory/index.ts
CHANGED
|
@@ -1501,6 +1501,8 @@ export * from './brain-retrieval.js';
|
|
|
1501
1501
|
export * from './brain-search.js';
|
|
1502
1502
|
// === BRAIN Memory modules (brain.db backed) ===
|
|
1503
1503
|
export * from './decisions.js';
|
|
1504
|
+
// === T626-M1: Canonical edge-type constants ===
|
|
1505
|
+
export * from './edge-types.js';
|
|
1504
1506
|
// === T549 Wave 2: Extraction Gate ===
|
|
1505
1507
|
export * from './extraction-gate.js';
|
|
1506
1508
|
export * from './learnings.js';
|
package/src/sessions/briefing.ts
CHANGED
|
@@ -292,7 +292,7 @@ async function computeLastSession(
|
|
|
292
292
|
const allSessions = await accessor.loadSessions();
|
|
293
293
|
|
|
294
294
|
const session = allSessions.find((s) => s.id === sessionId);
|
|
295
|
-
if (!session
|
|
295
|
+
if (!session?.endedAt) return null;
|
|
296
296
|
|
|
297
297
|
// Calculate duration if startedAt is available
|
|
298
298
|
let duration = 0;
|
package/src/skills/dispatch.ts
CHANGED
|
@@ -205,7 +205,7 @@ export async function orchestratorSpawnSkill(
|
|
|
205
205
|
): Promise<string> {
|
|
206
206
|
// Find the skill
|
|
207
207
|
const skill = findSkill(skillName, cwd);
|
|
208
|
-
if (!skill
|
|
208
|
+
if (!skill?.content) {
|
|
209
209
|
throw new CleoError(ExitCode.NOT_FOUND, `Skill not found: ${skillName}`, {
|
|
210
210
|
fix: `Check skills directory for ${skillName}/SKILL.md`,
|
|
211
211
|
});
|
|
@@ -45,7 +45,7 @@ export async function buildPrompt(
|
|
|
45
45
|
|
|
46
46
|
// Find skill template
|
|
47
47
|
const skill = findSkill(templateName, cwd);
|
|
48
|
-
if (!skill
|
|
48
|
+
if (!skill?.content) {
|
|
49
49
|
const { canonical } = mapSkillName(templateName);
|
|
50
50
|
throw new CleoError(ExitCode.NOT_FOUND, `Skill template ${templateName} not found`, {
|
|
51
51
|
fix: `Expected at skills/${canonical}/SKILL.md`,
|
|
@@ -563,6 +563,8 @@ export const BRAIN_EDGE_TYPES = [
|
|
|
563
563
|
// Graph bridging (memory ↔ code)
|
|
564
564
|
'references', // observation → references → symbol
|
|
565
565
|
'modified_by', // file → modified_by → session
|
|
566
|
+
// Plasticity (Hebbian + STDP co-retrieval)
|
|
567
|
+
'co_retrieved', // A → co_retrieved → B (Hebbian: frequently retrieved together)
|
|
566
568
|
] as const;
|
|
567
569
|
|
|
568
570
|
/** Discriminated union of all supported brain graph edge types. */
|
|
@@ -705,11 +707,61 @@ export const brainRetrievalLog = sqliteTable(
|
|
|
705
707
|
/** Estimated tokens consumed by this retrieval. */
|
|
706
708
|
tokensUsed: integer('tokens_used'),
|
|
707
709
|
|
|
710
|
+
/** Session ID (soft FK to tasks.db sessions). Enables grouping retrievals by session for STDP analysis. */
|
|
711
|
+
sessionId: text('session_id'),
|
|
712
|
+
|
|
708
713
|
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
|
|
709
714
|
},
|
|
710
715
|
(table) => [
|
|
711
716
|
index('idx_retrieval_log_created').on(table.createdAt),
|
|
712
717
|
index('idx_retrieval_log_source').on(table.source),
|
|
718
|
+
index('idx_retrieval_log_session').on(table.sessionId),
|
|
719
|
+
],
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
// ============================================================================
|
|
723
|
+
// PLASTICITY EVENTS — STDP weight-change audit log (T626 phase 5)
|
|
724
|
+
// ============================================================================
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Records every STDP weight-change event applied to a brain_page_edges row.
|
|
728
|
+
*
|
|
729
|
+
* Each row captures the causal pair (source_node, target_node), the signed
|
|
730
|
+
* delta applied to the edge weight, whether it was a potentiation or
|
|
731
|
+
* depression event, and which session and timestamp triggered it.
|
|
732
|
+
*
|
|
733
|
+
* @task T626
|
|
734
|
+
* @epic T626
|
|
735
|
+
*/
|
|
736
|
+
export const brainPlasticityEvents = sqliteTable(
|
|
737
|
+
'brain_plasticity_events',
|
|
738
|
+
{
|
|
739
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
740
|
+
/** from_id of the affected brain_page_edges row. */
|
|
741
|
+
sourceNode: text('source_node').notNull(),
|
|
742
|
+
/** to_id of the affected brain_page_edges row. */
|
|
743
|
+
targetNode: text('target_node').notNull(),
|
|
744
|
+
/**
|
|
745
|
+
* Signed weight delta applied to the edge.
|
|
746
|
+
* Positive = potentiation (LTP), negative = depression (LTD).
|
|
747
|
+
*/
|
|
748
|
+
deltaW: real('delta_w').notNull(),
|
|
749
|
+
/**
|
|
750
|
+
* STDP event kind: `ltp` (Long-Term Potentiation) or `ltd` (Long-Term
|
|
751
|
+
* Depression).
|
|
752
|
+
*/
|
|
753
|
+
kind: text('kind', { enum: ['ltp', 'ltd'] }).notNull(),
|
|
754
|
+
/** ISO 8601 timestamp when this event was applied. */
|
|
755
|
+
timestamp: text('timestamp').notNull().default(sql`(datetime('now'))`),
|
|
756
|
+
/** Session ID that triggered the STDP pass, if available. */
|
|
757
|
+
sessionId: text('session_id'),
|
|
758
|
+
},
|
|
759
|
+
(table) => [
|
|
760
|
+
index('idx_plasticity_source').on(table.sourceNode),
|
|
761
|
+
index('idx_plasticity_target').on(table.targetNode),
|
|
762
|
+
index('idx_plasticity_timestamp').on(table.timestamp),
|
|
763
|
+
index('idx_plasticity_session').on(table.sessionId),
|
|
764
|
+
index('idx_plasticity_kind').on(table.kind),
|
|
713
765
|
],
|
|
714
766
|
);
|
|
715
767
|
|
|
@@ -733,4 +785,6 @@ export type BrainPageEdgeRow = typeof brainPageEdges.$inferSelect;
|
|
|
733
785
|
export type NewBrainPageEdgeRow = typeof brainPageEdges.$inferInsert;
|
|
734
786
|
export type BrainStickyNoteRow = typeof brainStickyNotes.$inferSelect;
|
|
735
787
|
export type NewBrainStickyNoteRow = typeof brainStickyNotes.$inferInsert;
|
|
788
|
+
export type BrainPlasticityEventRow = typeof brainPlasticityEvents.$inferSelect;
|
|
789
|
+
export type NewBrainPlasticityEventRow = typeof brainPlasticityEvents.$inferInsert;
|
|
736
790
|
// BrainNodeType and BrainEdgeType are declared alongside their enum arrays above.
|
|
@@ -158,6 +158,23 @@ function runBrainMigrations(
|
|
|
158
158
|
// all migrations as applied without actually running them (Scenario 2 race).
|
|
159
159
|
// ensureColumns is idempotent — no-op if the column already exists.
|
|
160
160
|
ensureColumns(nativeDb, 'brain_observations', [{ name: 'agent', ddl: 'text' }], 'brain');
|
|
161
|
+
|
|
162
|
+
// T626-M1: Normalize co_retrieved edge type — idempotent safety-net UPDATE.
|
|
163
|
+
// The shipped Hebbian strengthener emitted edge_type = 'relates_to' instead of
|
|
164
|
+
// 'co_retrieved'. Relabel only rows from the consolidation provenance so no
|
|
165
|
+
// semantic edges are affected. The Drizzle migration file does the same UPDATE;
|
|
166
|
+
// this guard handles installs where the journal reconciler already marked
|
|
167
|
+
// the migration applied before the SQL ran.
|
|
168
|
+
if (tableExists(nativeDb, 'brain_page_edges')) {
|
|
169
|
+
nativeDb
|
|
170
|
+
.prepare(
|
|
171
|
+
`UPDATE brain_page_edges
|
|
172
|
+
SET edge_type = 'co_retrieved'
|
|
173
|
+
WHERE edge_type = 'relates_to'
|
|
174
|
+
AND provenance LIKE 'consolidation:%'`,
|
|
175
|
+
)
|
|
176
|
+
.run();
|
|
177
|
+
}
|
|
161
178
|
}
|
|
162
179
|
|
|
163
180
|
/**
|
package/src/store/json.ts
CHANGED
|
@@ -206,7 +206,7 @@ export async function readLogEntries(filePath: string): Promise<Record<string, u
|
|
|
206
206
|
if (remainder) {
|
|
207
207
|
for (const line of remainder.split('\n')) {
|
|
208
208
|
const l = line.trim();
|
|
209
|
-
if (!l
|
|
209
|
+
if (!l?.startsWith('{')) continue;
|
|
210
210
|
try {
|
|
211
211
|
entries.push(JSON.parse(l) as Record<string, unknown>);
|
|
212
212
|
} catch {
|
|
@@ -219,7 +219,7 @@ export async function readLogEntries(filePath: string): Promise<Record<string, u
|
|
|
219
219
|
// Pure JSONL (no initial JSON object)
|
|
220
220
|
for (const line of trimmed.split('\n')) {
|
|
221
221
|
const l = line.trim();
|
|
222
|
-
if (!l
|
|
222
|
+
if (!l?.startsWith('{')) continue;
|
|
223
223
|
try {
|
|
224
224
|
entries.push(JSON.parse(l) as Record<string, unknown>);
|
|
225
225
|
} catch {
|
package/src/tasks/task-ops.ts
CHANGED
|
@@ -144,7 +144,7 @@ function measureDependencyDepth(
|
|
|
144
144
|
visited.add(taskId);
|
|
145
145
|
|
|
146
146
|
const task = taskMap.get(taskId);
|
|
147
|
-
if (!task
|
|
147
|
+
if (!task?.depends || task.depends.length === 0) return 0;
|
|
148
148
|
|
|
149
149
|
let maxDepth = 0;
|
|
150
150
|
for (const depId of task.depends) {
|
|
@@ -921,7 +921,7 @@ export async function coreTaskUnarchive(
|
|
|
921
921
|
}
|
|
922
922
|
|
|
923
923
|
const archive = await accessor.loadArchive();
|
|
924
|
-
if (!archive
|
|
924
|
+
if (!archive?.archivedTasks) {
|
|
925
925
|
throw new Error('No archive file found');
|
|
926
926
|
}
|
|
927
927
|
|
|
@@ -438,9 +438,7 @@ export function allEpicChildrenVerified(epicId: string, tasks: TaskForVerificati
|
|
|
438
438
|
const incomplete = children.filter((t) => t.status !== 'done');
|
|
439
439
|
if (incomplete.length > 0) return false;
|
|
440
440
|
|
|
441
|
-
const unverified = children.filter(
|
|
442
|
-
(t) => t.status === 'done' && (!t.verification || !t.verification.passed),
|
|
443
|
-
);
|
|
441
|
+
const unverified = children.filter((t) => t.status === 'done' && !t.verification?.passed);
|
|
444
442
|
return unverified.length === 0;
|
|
445
443
|
}
|
|
446
444
|
|
|
@@ -451,9 +449,7 @@ export function allEpicChildrenVerified(epicId: string, tasks: TaskForVerificati
|
|
|
451
449
|
export function allSiblingsVerified(parentId: string, tasks: TaskForVerification[]): boolean {
|
|
452
450
|
const siblings = tasks.filter((t) => t.parentId === parentId);
|
|
453
451
|
|
|
454
|
-
const unverifiedDone = siblings.filter(
|
|
455
|
-
(t) => t.status === 'done' && (!t.verification || !t.verification.passed),
|
|
456
|
-
);
|
|
452
|
+
const unverifiedDone = siblings.filter((t) => t.status === 'done' && !t.verification?.passed);
|
|
457
453
|
|
|
458
454
|
const incomplete = siblings.filter(
|
|
459
455
|
(t) => t.status === 'pending' || t.status === 'active' || t.status === 'blocked',
|