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/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
+ }
@@ -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
+ }