hebbian 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/bin/hebbian.js +199 -0
- package/lib/constants.js +83 -0
- package/lib/decay.js +100 -0
- package/lib/dedup.js +66 -0
- package/lib/emit.js +376 -0
- package/lib/fire.js +59 -0
- package/lib/grow.js +98 -0
- package/lib/init.js +89 -0
- package/lib/rollback.js +34 -0
- package/lib/scanner.js +227 -0
- package/lib/signal.js +68 -0
- package/lib/similarity.js +62 -0
- package/lib/snapshot.js +46 -0
- package/lib/subsumption.js +77 -0
- package/lib/watch.js +79 -0
- package/package.json +43 -0
package/lib/emit.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// hebbian — 3-Tier Emit System + Multi-Target Output
|
|
2
|
+
//
|
|
3
|
+
// Tier 1: Bootstrap (~500 tokens) — auto-loaded by AI (CLAUDE.md, .cursorrules, etc.)
|
|
4
|
+
// Tier 2: _index.md — brain overview (AI reads at conversation start)
|
|
5
|
+
// Tier 3: {region}/_rules.md — per-region detail (AI reads on demand)
|
|
6
|
+
//
|
|
7
|
+
// Multi-target: claude, cursor, gemini, copilot, generic, all
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
10
|
+
import { join, dirname } from 'node:path';
|
|
11
|
+
import { scanBrain } from './scanner.js';
|
|
12
|
+
import { runSubsumption } from './subsumption.js';
|
|
13
|
+
import {
|
|
14
|
+
REGIONS, REGION_ICONS, REGION_KO, EMIT_TARGETS,
|
|
15
|
+
EMIT_THRESHOLD, SPOTLIGHT_DAYS, MARKER_START, MARKER_END,
|
|
16
|
+
} from './constants.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {import('./scanner.js').Neuron} Neuron
|
|
20
|
+
* @typedef {import('./scanner.js').Region} Region
|
|
21
|
+
* @typedef {import('./subsumption.js').SubsumptionResult} SubsumptionResult
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
25
|
+
// TIER 1: Bootstrap (~500 tokens)
|
|
26
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate Tier 1 bootstrap content.
|
|
30
|
+
* @param {SubsumptionResult} result
|
|
31
|
+
* @param {import('./scanner.js').Brain} brain
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function emitBootstrap(result, brain) {
|
|
35
|
+
const lines = [];
|
|
36
|
+
const now = new Date().toISOString().replace(/\.\d+Z$/, '');
|
|
37
|
+
|
|
38
|
+
lines.push(MARKER_START);
|
|
39
|
+
lines.push(`<!-- Generated: ${now} -->`);
|
|
40
|
+
lines.push('<!-- Axiom: Folder=Neuron | File=Trace | Path=Sentence -->');
|
|
41
|
+
lines.push(`<!-- Active: ${result.firedNeurons}/${result.totalNeurons} neurons | Total activation: ${result.totalCounter} -->`);
|
|
42
|
+
lines.push('');
|
|
43
|
+
|
|
44
|
+
// Circuit breaker
|
|
45
|
+
if (result.bombSource) {
|
|
46
|
+
lines.push(`## \u{1F6A8} CIRCUIT BREAKER: ${result.bombSource}`);
|
|
47
|
+
lines.push('**ALL OPERATIONS HALTED. REPAIR REQUIRED.**');
|
|
48
|
+
lines.push('');
|
|
49
|
+
lines.push(MARKER_END);
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lines.push('## hebbian Active Rules');
|
|
54
|
+
lines.push('');
|
|
55
|
+
|
|
56
|
+
// Persona (ego region)
|
|
57
|
+
lines.push(`### ${REGION_ICONS.ego} Persona`);
|
|
58
|
+
for (const region of result.activeRegions) {
|
|
59
|
+
if (region.name === 'ego') {
|
|
60
|
+
const top = sortedActive(region.neurons, 10);
|
|
61
|
+
for (const n of top) {
|
|
62
|
+
lines.push(`- ${pathToSentence(n.path)}`);
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
lines.push('');
|
|
68
|
+
|
|
69
|
+
// Subsumption cascade
|
|
70
|
+
lines.push('### \u{1F517} Subsumption Cascade');
|
|
71
|
+
lines.push('```');
|
|
72
|
+
lines.push('brainstem \u2190\u2192 limbic \u2190\u2192 hippocampus \u2190\u2192 sensors \u2190\u2192 cortex \u2190\u2192 ego \u2190\u2192 prefrontal');
|
|
73
|
+
lines.push(' (P0) (P1) (P2) (P3) (P4) (P5) (P6)');
|
|
74
|
+
lines.push('```');
|
|
75
|
+
lines.push('Lower P always overrides higher P. bomb = full stop.');
|
|
76
|
+
lines.push('');
|
|
77
|
+
|
|
78
|
+
// TOP 5 brainstem rules
|
|
79
|
+
lines.push(`### ${REGION_ICONS.brainstem} Core Directives TOP 5`);
|
|
80
|
+
for (const region of result.activeRegions) {
|
|
81
|
+
if (region.name === 'brainstem') {
|
|
82
|
+
const top = sortedActive(region.neurons, 5);
|
|
83
|
+
top.forEach((n, i) => {
|
|
84
|
+
lines.push(`${i + 1}. **${pathToSentence(n.path)}**`);
|
|
85
|
+
});
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
lines.push('');
|
|
90
|
+
|
|
91
|
+
// Active regions summary
|
|
92
|
+
lines.push('### Active Regions');
|
|
93
|
+
lines.push('| Region | Neurons | Activation |');
|
|
94
|
+
lines.push('|--------|---------|------------|');
|
|
95
|
+
for (const region of result.activeRegions) {
|
|
96
|
+
const active = region.neurons.filter((n) => !n.isDormant);
|
|
97
|
+
const activation = active.reduce((sum, n) => sum + n.counter, 0);
|
|
98
|
+
const icon = REGION_ICONS[region.name] || '';
|
|
99
|
+
lines.push(`| ${icon} ${region.name} | ${active.length} | ${activation} |`);
|
|
100
|
+
}
|
|
101
|
+
lines.push('');
|
|
102
|
+
lines.push(MARKER_END);
|
|
103
|
+
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
108
|
+
// TIER 2: Index (_index.md)
|
|
109
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate Tier 2 brain index content.
|
|
113
|
+
* @param {SubsumptionResult} result
|
|
114
|
+
* @param {import('./scanner.js').Brain} brain
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function emitIndex(result, brain) {
|
|
118
|
+
const lines = [];
|
|
119
|
+
lines.push('# hebbian Brain Index');
|
|
120
|
+
lines.push('');
|
|
121
|
+
lines.push(`> ${result.firedNeurons} active / ${result.totalNeurons} total neurons | activation: ${result.totalCounter}`);
|
|
122
|
+
lines.push('');
|
|
123
|
+
|
|
124
|
+
if (result.bombSource) {
|
|
125
|
+
lines.push(`## \u{1F6A8} CIRCUIT BREAKER: ${result.bombSource}`);
|
|
126
|
+
lines.push('');
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Top 10 neurons by counter
|
|
131
|
+
const allNeurons = result.activeRegions.flatMap((r) =>
|
|
132
|
+
r.neurons.filter((n) => !n.isDormant && n.counter >= EMIT_THRESHOLD),
|
|
133
|
+
);
|
|
134
|
+
allNeurons.sort((a, b) => b.counter - a.counter);
|
|
135
|
+
|
|
136
|
+
lines.push('## Top 10 Active Neurons');
|
|
137
|
+
lines.push('| # | Path | Counter | Strength |');
|
|
138
|
+
lines.push('|---|------|---------|----------|');
|
|
139
|
+
for (const n of allNeurons.slice(0, 10)) {
|
|
140
|
+
const strength = n.counter >= 10 ? '\u{1F534}' : n.counter >= 5 ? '\u{1F7E1}' : '\u26AA';
|
|
141
|
+
lines.push(`| ${allNeurons.indexOf(n) + 1} | ${n.path} | ${n.counter} | ${strength} |`);
|
|
142
|
+
}
|
|
143
|
+
lines.push('');
|
|
144
|
+
|
|
145
|
+
// Spotlight: new neurons (< SPOTLIGHT_DAYS old, counter < EMIT_THRESHOLD)
|
|
146
|
+
const cutoff = new Date(Date.now() - SPOTLIGHT_DAYS * 24 * 60 * 60 * 1000);
|
|
147
|
+
const spotlightNeurons = result.activeRegions.flatMap((r) =>
|
|
148
|
+
r.neurons.filter((n) => !n.isDormant && n.counter < EMIT_THRESHOLD && n.modTime > cutoff),
|
|
149
|
+
);
|
|
150
|
+
if (spotlightNeurons.length > 0) {
|
|
151
|
+
lines.push('## Spotlight (Probation)');
|
|
152
|
+
for (const n of spotlightNeurons) {
|
|
153
|
+
lines.push(`- ${n.path} (${n.counter}) — new`);
|
|
154
|
+
}
|
|
155
|
+
lines.push('');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Per-region summary
|
|
159
|
+
lines.push('## Regions');
|
|
160
|
+
lines.push('| Region | Active | Dormant | Activation | Rules |');
|
|
161
|
+
lines.push('|--------|--------|---------|------------|-------|');
|
|
162
|
+
for (const region of result.activeRegions) {
|
|
163
|
+
const active = region.neurons.filter((n) => !n.isDormant);
|
|
164
|
+
const dormant = region.neurons.filter((n) => n.isDormant);
|
|
165
|
+
const activation = active.reduce((sum, n) => sum + n.counter, 0);
|
|
166
|
+
const icon = REGION_ICONS[region.name] || '';
|
|
167
|
+
lines.push(`| ${icon} ${region.name} | ${active.length} | ${dormant.length} | ${activation} | [_rules.md](${region.name}/_rules.md) |`);
|
|
168
|
+
}
|
|
169
|
+
lines.push('');
|
|
170
|
+
|
|
171
|
+
return lines.join('\n');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
175
|
+
// TIER 3: Per-region rules ({region}/_rules.md)
|
|
176
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Generate Tier 3 per-region rules content.
|
|
180
|
+
* @param {Region} region
|
|
181
|
+
* @returns {string}
|
|
182
|
+
*/
|
|
183
|
+
export function emitRegionRules(region) {
|
|
184
|
+
const icon = REGION_ICONS[region.name] || '';
|
|
185
|
+
const ko = REGION_KO[region.name] || '';
|
|
186
|
+
const active = region.neurons.filter((n) => !n.isDormant);
|
|
187
|
+
const dormant = region.neurons.filter((n) => n.isDormant);
|
|
188
|
+
const activation = active.reduce((sum, n) => sum + n.counter, 0);
|
|
189
|
+
|
|
190
|
+
const lines = [];
|
|
191
|
+
lines.push(`# ${icon} ${region.name} (${ko})`);
|
|
192
|
+
lines.push(`> Active: ${active.length} | Dormant: ${dormant.length} | Activation: ${activation}`);
|
|
193
|
+
lines.push('');
|
|
194
|
+
|
|
195
|
+
// Axons
|
|
196
|
+
if (region.axons.length > 0) {
|
|
197
|
+
lines.push('## Connections');
|
|
198
|
+
for (const axon of region.axons) {
|
|
199
|
+
lines.push(`- \u2194 ${axon}`);
|
|
200
|
+
}
|
|
201
|
+
lines.push('');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Neuron tree
|
|
205
|
+
if (active.length > 0) {
|
|
206
|
+
lines.push('## Rules');
|
|
207
|
+
const sorted = [...active].sort((a, b) => b.counter - a.counter);
|
|
208
|
+
for (const n of sorted) {
|
|
209
|
+
const indent = ' '.repeat(Math.min(n.depth, 4));
|
|
210
|
+
const prefix = strengthPrefix(n.counter);
|
|
211
|
+
const signals = [];
|
|
212
|
+
if (n.dopamine > 0) signals.push(`\u{1F7E2}+${n.dopamine}`);
|
|
213
|
+
if (n.hasBomb) signals.push('\u{1F4A3}');
|
|
214
|
+
if (n.hasMemory) signals.push('\u{1F4BE}');
|
|
215
|
+
const signalStr = signals.length > 0 ? ` ${signals.join(' ')}` : '';
|
|
216
|
+
lines.push(`${indent}- ${prefix}${pathToSentence(n.path)} (${n.counter})${signalStr}`);
|
|
217
|
+
}
|
|
218
|
+
lines.push('');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Dormant section
|
|
222
|
+
if (dormant.length > 0) {
|
|
223
|
+
lines.push('## Dormant');
|
|
224
|
+
for (const n of dormant) {
|
|
225
|
+
lines.push(`- ~~${pathToSentence(n.path)} (${n.counter})~~`);
|
|
226
|
+
}
|
|
227
|
+
lines.push('');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
234
|
+
// MULTI-TARGET OUTPUT
|
|
235
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Emit rules to a specific target or all targets.
|
|
239
|
+
* @param {string} brainRoot
|
|
240
|
+
* @param {string} target - Target name or "all"
|
|
241
|
+
*/
|
|
242
|
+
export function emitToTarget(brainRoot, target) {
|
|
243
|
+
const brain = scanBrain(brainRoot);
|
|
244
|
+
const result = runSubsumption(brain);
|
|
245
|
+
const content = emitBootstrap(result, brain);
|
|
246
|
+
|
|
247
|
+
if (target === 'all') {
|
|
248
|
+
for (const [name, filePath] of Object.entries(EMIT_TARGETS)) {
|
|
249
|
+
writeTarget(filePath, content);
|
|
250
|
+
console.log(`\u{1F4E4} emitted: ${name} → ${filePath}`);
|
|
251
|
+
}
|
|
252
|
+
} else if (EMIT_TARGETS[target]) {
|
|
253
|
+
writeTarget(EMIT_TARGETS[target], content);
|
|
254
|
+
console.log(`\u{1F4E4} emitted: ${target} → ${EMIT_TARGETS[target]}`);
|
|
255
|
+
} else {
|
|
256
|
+
throw new Error(`Unknown target: ${target}. Valid: ${Object.keys(EMIT_TARGETS).join(', ')}, all`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Always write tier 2 + tier 3 into the brain
|
|
260
|
+
writeAllTiers(brainRoot, result, brain);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Write all 3 tiers into the brain directory.
|
|
265
|
+
* @param {string} brainRoot
|
|
266
|
+
* @param {SubsumptionResult} result
|
|
267
|
+
* @param {import('./scanner.js').Brain} brain
|
|
268
|
+
*/
|
|
269
|
+
export function writeAllTiers(brainRoot, result, brain) {
|
|
270
|
+
// Tier 2: _index.md
|
|
271
|
+
const indexContent = emitIndex(result, brain);
|
|
272
|
+
writeFileSync(join(brainRoot, '_index.md'), indexContent, 'utf8');
|
|
273
|
+
|
|
274
|
+
// Tier 3: per-region _rules.md
|
|
275
|
+
for (const region of result.activeRegions) {
|
|
276
|
+
if (existsSync(region.path)) {
|
|
277
|
+
const rulesContent = emitRegionRules(region);
|
|
278
|
+
writeFileSync(join(region.path, '_rules.md'), rulesContent, 'utf8');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Write content to a target file, using marker-based injection if file already exists.
|
|
285
|
+
* @param {string} filePath
|
|
286
|
+
* @param {string} content
|
|
287
|
+
*/
|
|
288
|
+
function writeTarget(filePath, content) {
|
|
289
|
+
const dir = dirname(filePath);
|
|
290
|
+
if (dir !== '.' && !existsSync(dir)) {
|
|
291
|
+
mkdirSync(dir, { recursive: true });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (existsSync(filePath)) {
|
|
295
|
+
// Marker-based injection: replace between markers, preserve surrounding content
|
|
296
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
297
|
+
const startIdx = existing.indexOf(MARKER_START);
|
|
298
|
+
const endIdx = existing.indexOf(MARKER_END);
|
|
299
|
+
|
|
300
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
301
|
+
const before = existing.slice(0, startIdx);
|
|
302
|
+
const after = existing.slice(endIdx + MARKER_END.length);
|
|
303
|
+
writeFileSync(filePath, before + content + after, 'utf8');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
writeFileSync(filePath, content, 'utf8');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
312
|
+
// DIAGNOSTICS (for CLI `hebbian diag` / `hebbian stats`)
|
|
313
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Print brain diagnostics to stdout.
|
|
317
|
+
* @param {import('./scanner.js').Brain} brain
|
|
318
|
+
* @param {SubsumptionResult} result
|
|
319
|
+
*/
|
|
320
|
+
export function printDiag(brain, result) {
|
|
321
|
+
console.log('');
|
|
322
|
+
console.log(`\u{1F9E0} hebbian Brain Diagnostics`);
|
|
323
|
+
console.log(` Root: ${brain.root}`);
|
|
324
|
+
console.log(` Neurons: ${result.firedNeurons} active / ${result.totalNeurons} total`);
|
|
325
|
+
console.log(` Activation: ${result.totalCounter}`);
|
|
326
|
+
if (result.bombSource) {
|
|
327
|
+
console.log(` \u{1F4A3} BOMB: ${result.bombSource} — cascade halted!`);
|
|
328
|
+
}
|
|
329
|
+
console.log('');
|
|
330
|
+
|
|
331
|
+
for (const region of brain.regions) {
|
|
332
|
+
const icon = REGION_ICONS[region.name] || '';
|
|
333
|
+
const active = region.neurons.filter((n) => !n.isDormant);
|
|
334
|
+
const dormant = region.neurons.filter((n) => n.isDormant);
|
|
335
|
+
const activation = active.reduce((sum, n) => sum + n.counter, 0);
|
|
336
|
+
const isBlocked = result.blockedRegions.some((r) => r.name === region.name);
|
|
337
|
+
const status = region.hasBomb ? '\u{1F4A3} BOMB' : isBlocked ? '\u{1F6AB} BLOCKED' : '\u2705 ACTIVE';
|
|
338
|
+
|
|
339
|
+
console.log(` ${icon} ${region.name} [${status}]`);
|
|
340
|
+
console.log(` neurons: ${active.length} active, ${dormant.length} dormant | activation: ${activation}`);
|
|
341
|
+
|
|
342
|
+
if (region.axons.length > 0) {
|
|
343
|
+
console.log(` axons: ${region.axons.join(', ')}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const top3 = sortedActive(region.neurons, 3);
|
|
347
|
+
for (const n of top3) {
|
|
348
|
+
console.log(` \u251C ${n.path} (${n.counter})`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
console.log('');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
355
|
+
// HELPERS
|
|
356
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
357
|
+
|
|
358
|
+
/** Convert a neuron relative path to a human-readable sentence. */
|
|
359
|
+
function pathToSentence(path) {
|
|
360
|
+
return path.replace(/\//g, ' > ').replace(/_/g, ' ');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Sort non-dormant neurons by counter (descending), take first N. */
|
|
364
|
+
function sortedActive(neurons, n) {
|
|
365
|
+
return [...neurons]
|
|
366
|
+
.filter((neuron) => !neuron.isDormant)
|
|
367
|
+
.sort((a, b) => b.counter - a.counter)
|
|
368
|
+
.slice(0, n);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Strength prefix based on counter value. */
|
|
372
|
+
function strengthPrefix(counter) {
|
|
373
|
+
if (counter >= 10) return '**[절대]** ';
|
|
374
|
+
if (counter >= 5) return '**[반드시]** ';
|
|
375
|
+
return '';
|
|
376
|
+
}
|
package/lib/fire.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// hebbian — Fire Neuron (increment counter)
|
|
2
|
+
//
|
|
3
|
+
// Firing = reinforcing a synaptic pathway.
|
|
4
|
+
// The counter file is renamed: N.neuron → (N+1).neuron
|
|
5
|
+
|
|
6
|
+
import { readdirSync, renameSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Increment a neuron's counter by 1.
|
|
11
|
+
* If the neuron doesn't exist, auto-grows it with counter=1.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} brainRoot - Absolute path to brain root
|
|
14
|
+
* @param {string} neuronPath - Relative path (e.g. "cortex/frontend/禁console_log")
|
|
15
|
+
* @returns {number} New counter value
|
|
16
|
+
*/
|
|
17
|
+
export function fireNeuron(brainRoot, neuronPath) {
|
|
18
|
+
const fullPath = join(brainRoot, neuronPath);
|
|
19
|
+
|
|
20
|
+
// Auto-grow if neuron doesn't exist
|
|
21
|
+
if (!existsSync(fullPath)) {
|
|
22
|
+
mkdirSync(fullPath, { recursive: true });
|
|
23
|
+
writeFileSync(join(fullPath, '1.neuron'), '', 'utf8');
|
|
24
|
+
console.log(`\u{1F331} grew + fired: ${neuronPath} (1)`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Find current counter
|
|
29
|
+
const current = getCurrentCounter(fullPath);
|
|
30
|
+
const newCounter = current + 1;
|
|
31
|
+
|
|
32
|
+
// Rename: N.neuron → (N+1).neuron
|
|
33
|
+
if (current > 0) {
|
|
34
|
+
renameSync(join(fullPath, `${current}.neuron`), join(fullPath, `${newCounter}.neuron`));
|
|
35
|
+
} else {
|
|
36
|
+
writeFileSync(join(fullPath, `${newCounter}.neuron`), '', 'utf8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(`\u{1F525} fired: ${neuronPath} (${current} → ${newCounter})`);
|
|
40
|
+
return newCounter;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get current counter value from the highest N.neuron file.
|
|
45
|
+
* @param {string} dir
|
|
46
|
+
* @returns {number}
|
|
47
|
+
*/
|
|
48
|
+
export function getCurrentCounter(dir) {
|
|
49
|
+
let max = 0;
|
|
50
|
+
try {
|
|
51
|
+
for (const entry of readdirSync(dir)) {
|
|
52
|
+
if (entry.endsWith('.neuron') && !entry.startsWith('dopamine') && !entry.startsWith('memory') && entry !== 'bomb.neuron') {
|
|
53
|
+
const n = parseInt(entry, 10);
|
|
54
|
+
if (!isNaN(n) && n > max) max = n;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {}
|
|
58
|
+
return max;
|
|
59
|
+
}
|
package/lib/grow.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// hebbian — Grow Neuron (with synaptic consolidation)
|
|
2
|
+
//
|
|
3
|
+
// Creates a new neuron (folder + 1.neuron).
|
|
4
|
+
// Before creating, checks for similar existing neurons via Jaccard similarity.
|
|
5
|
+
// If a match is found (>= 0.6), fires the existing neuron instead.
|
|
6
|
+
// This prevents duplication and strengthens existing pathways.
|
|
7
|
+
//
|
|
8
|
+
// "Consolidation over duplication." — Hebb's principle.
|
|
9
|
+
|
|
10
|
+
import { mkdirSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
|
|
11
|
+
import { join, relative } from 'node:path';
|
|
12
|
+
import { REGIONS, JACCARD_THRESHOLD } from './constants.js';
|
|
13
|
+
import { tokenize, jaccardSimilarity } from './similarity.js';
|
|
14
|
+
import { fireNeuron } from './fire.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Grow a new neuron at the given path.
|
|
18
|
+
* If a similar neuron already exists in the same region, fires it instead.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} brainRoot - Absolute path to brain root
|
|
21
|
+
* @param {string} neuronPath - Relative path (e.g. "cortex/frontend/禁console_log")
|
|
22
|
+
* @returns {{ action: 'grew' | 'fired', path: string, counter: number }}
|
|
23
|
+
*/
|
|
24
|
+
export function growNeuron(brainRoot, neuronPath) {
|
|
25
|
+
const fullPath = join(brainRoot, neuronPath);
|
|
26
|
+
|
|
27
|
+
// If neuron already exists, just fire it
|
|
28
|
+
if (existsSync(fullPath)) {
|
|
29
|
+
const counter = fireNeuron(brainRoot, neuronPath);
|
|
30
|
+
return { action: 'fired', path: neuronPath, counter };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Extract region name and leaf name
|
|
34
|
+
const parts = neuronPath.split('/');
|
|
35
|
+
const regionName = parts[0];
|
|
36
|
+
if (!REGIONS.includes(regionName)) {
|
|
37
|
+
throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const leafName = parts[parts.length - 1];
|
|
41
|
+
const newTokens = tokenize(leafName);
|
|
42
|
+
|
|
43
|
+
// Search for similar neurons in the same region
|
|
44
|
+
const regionPath = join(brainRoot, regionName);
|
|
45
|
+
if (existsSync(regionPath)) {
|
|
46
|
+
const match = findSimilar(regionPath, regionPath, newTokens);
|
|
47
|
+
if (match) {
|
|
48
|
+
const matchRelPath = regionName + '/' + relative(regionPath, match);
|
|
49
|
+
console.log(`\u{1F504} consolidation: "${neuronPath}" ≈ "${matchRelPath}" (firing existing)`);
|
|
50
|
+
const counter = fireNeuron(brainRoot, matchRelPath);
|
|
51
|
+
return { action: 'fired', path: matchRelPath, counter };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// No match — create new neuron
|
|
56
|
+
mkdirSync(fullPath, { recursive: true });
|
|
57
|
+
writeFileSync(join(fullPath, '1.neuron'), '', 'utf8');
|
|
58
|
+
console.log(`\u{1F331} grew: ${neuronPath} (1)`);
|
|
59
|
+
return { action: 'grew', path: neuronPath, counter: 1 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Walk a region and find a neuron whose name is similar to the given tokens.
|
|
64
|
+
* @param {string} dir - Current directory
|
|
65
|
+
* @param {string} regionRoot - Region root for relative paths
|
|
66
|
+
* @param {string[]} targetTokens - Tokens to compare against
|
|
67
|
+
* @returns {string|null} Absolute path of matching neuron, or null
|
|
68
|
+
*/
|
|
69
|
+
function findSimilar(dir, regionRoot, targetTokens) {
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check if this directory has .neuron files (is a neuron)
|
|
78
|
+
const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith('.neuron'));
|
|
79
|
+
if (hasNeuron) {
|
|
80
|
+
const folderName = dir.split('/').pop() || '';
|
|
81
|
+
const existingTokens = tokenize(folderName);
|
|
82
|
+
const similarity = jaccardSimilarity(targetTokens, existingTokens);
|
|
83
|
+
if (similarity >= JACCARD_THRESHOLD) {
|
|
84
|
+
return dir;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Recurse into subdirectories
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
const match = findSimilar(join(dir, entry.name), regionRoot, targetTokens);
|
|
93
|
+
if (match) return match;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// hebbian — Brain Initialization
|
|
2
|
+
//
|
|
3
|
+
// Creates a brain directory with 7 canonical regions and starter neurons.
|
|
4
|
+
// Each region gets a _rules.md explaining its purpose.
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { REGIONS, REGION_ICONS, REGION_KO } from './constants.js';
|
|
9
|
+
|
|
10
|
+
/** @type {Record<string, { description: string, starters: string[] }>} */
|
|
11
|
+
const REGION_TEMPLATES = {
|
|
12
|
+
brainstem: {
|
|
13
|
+
description: 'Absolute principles. Immutable. Read-only conscience.\nP0 — highest priority. bomb here halts EVERYTHING.',
|
|
14
|
+
starters: ['禁fallback', '推execute_not_debate'],
|
|
15
|
+
},
|
|
16
|
+
limbic: {
|
|
17
|
+
description: 'Emotional filters and somatic markers.\nP1 — automatic, influences downstream regions.',
|
|
18
|
+
starters: [],
|
|
19
|
+
},
|
|
20
|
+
hippocampus: {
|
|
21
|
+
description: 'Memory and episode recording.\nP2 — accumulated experience, session logs.',
|
|
22
|
+
starters: [],
|
|
23
|
+
},
|
|
24
|
+
sensors: {
|
|
25
|
+
description: 'Environment constraints and input validation.\nP3 — read-only, set by environment.',
|
|
26
|
+
starters: [],
|
|
27
|
+
},
|
|
28
|
+
cortex: {
|
|
29
|
+
description: 'Knowledge and skills. The largest region.\nP4 — learnable, grows with corrections.',
|
|
30
|
+
starters: [],
|
|
31
|
+
},
|
|
32
|
+
ego: {
|
|
33
|
+
description: 'Personality, tone, and communication style.\nP5 — set by user preference.',
|
|
34
|
+
starters: [],
|
|
35
|
+
},
|
|
36
|
+
prefrontal: {
|
|
37
|
+
description: 'Goals, projects, and planning.\nP6 — lowest priority, longest time horizon.',
|
|
38
|
+
starters: [],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Initialize a new brain directory with 7 regions.
|
|
44
|
+
* @param {string} brainPath - Absolute path to create the brain at
|
|
45
|
+
*/
|
|
46
|
+
export function initBrain(brainPath) {
|
|
47
|
+
if (existsSync(brainPath)) {
|
|
48
|
+
const entries = readdirSync(brainPath);
|
|
49
|
+
if (entries.some((e) => REGIONS.includes(e))) {
|
|
50
|
+
console.log(`\u{26A0}\uFE0F Brain already exists at ${brainPath}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
mkdirSync(brainPath, { recursive: true });
|
|
56
|
+
|
|
57
|
+
for (const regionName of REGIONS) {
|
|
58
|
+
const regionDir = join(brainPath, regionName);
|
|
59
|
+
mkdirSync(regionDir, { recursive: true });
|
|
60
|
+
|
|
61
|
+
const template = REGION_TEMPLATES[regionName];
|
|
62
|
+
const icon = REGION_ICONS[regionName];
|
|
63
|
+
const ko = REGION_KO[regionName];
|
|
64
|
+
|
|
65
|
+
// Write _rules.md template
|
|
66
|
+
writeFileSync(
|
|
67
|
+
join(regionDir, '_rules.md'),
|
|
68
|
+
`# ${icon} ${regionName} (${ko})\n\n${template.description}\n`,
|
|
69
|
+
'utf8',
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Create starter neurons
|
|
73
|
+
for (const starter of template.starters) {
|
|
74
|
+
const neuronDir = join(regionDir, starter);
|
|
75
|
+
mkdirSync(neuronDir, { recursive: true });
|
|
76
|
+
writeFileSync(join(neuronDir, '1.neuron'), '', 'utf8');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Create _agents inbox
|
|
81
|
+
mkdirSync(join(brainPath, '_agents', 'global_inbox'), { recursive: true });
|
|
82
|
+
|
|
83
|
+
console.log(`\u{1F9E0} Brain initialized at ${brainPath}`);
|
|
84
|
+
console.log(` 7 regions created: ${REGIONS.join(', ')}`);
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(' Next steps:');
|
|
87
|
+
console.log(` hebbian grow brainstem/禁your_rule --brain ${brainPath}`);
|
|
88
|
+
console.log(` hebbian emit claude --brain ${brainPath}`);
|
|
89
|
+
}
|
package/lib/rollback.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// hebbian — Rollback Neuron (decrement counter)
|
|
2
|
+
//
|
|
3
|
+
// Undo a firing. Counter cannot go below 1 (minimum activation).
|
|
4
|
+
|
|
5
|
+
import { renameSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { getCurrentCounter } from './fire.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Decrement a neuron's counter by 1. Minimum counter is 1.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} brainRoot - Absolute path to brain root
|
|
13
|
+
* @param {string} neuronPath - Relative path
|
|
14
|
+
* @returns {number} New counter value
|
|
15
|
+
* @throws {Error} If counter is already at minimum (1) or neuron doesn't exist
|
|
16
|
+
*/
|
|
17
|
+
export function rollbackNeuron(brainRoot, neuronPath) {
|
|
18
|
+
const fullPath = join(brainRoot, neuronPath);
|
|
19
|
+
const current = getCurrentCounter(fullPath);
|
|
20
|
+
|
|
21
|
+
if (current === 0) {
|
|
22
|
+
throw new Error(`Neuron not found: ${neuronPath}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (current <= 1) {
|
|
26
|
+
throw new Error(`Counter already at minimum (1): ${neuronPath}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const newCounter = current - 1;
|
|
30
|
+
renameSync(join(fullPath, `${current}.neuron`), join(fullPath, `${newCounter}.neuron`));
|
|
31
|
+
|
|
32
|
+
console.log(`\u{23EA} rollback: ${neuronPath} (${current} → ${newCounter})`);
|
|
33
|
+
return newCounter;
|
|
34
|
+
}
|