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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 IISweetHeartII
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,201 @@
1
+ <p align="center">
2
+ <img src="https://img.shields.io/badge/Node.js-22+-339933?style=flat-square&logo=node.js" />
3
+ <img src="https://img.shields.io/badge/Dependencies-0-brightgreen?style=flat-square" />
4
+ <img src="https://img.shields.io/badge/Tests-120-blue?style=flat-square" />
5
+ <img src="https://img.shields.io/badge/MIT-green?style=flat-square" />
6
+ </p>
7
+
8
+ <p align="center"><a href="README.ko.md">한국어</a> · <a href="CHANGELOG.md">Changelog</a></p>
9
+
10
+ # hebbian
11
+
12
+ ### *Folder-as-neuron brain for any AI agent.*
13
+
14
+ > "Neurons that fire together, wire together." — Donald Hebb (1949)
15
+
16
+ ---
17
+
18
+ ## TL;DR
19
+
20
+ **`mkdir` replaces system prompts.** Folders are neurons. Paths are sentences. Counter files are synaptic weights.
21
+
22
+ ```bash
23
+ npx hebbian init ./brain
24
+ npx hebbian grow brainstem/禁fallback --brain ./brain
25
+ npx hebbian fire brainstem/禁fallback --brain ./brain
26
+ npx hebbian emit claude --brain ./brain # → CLAUDE.md
27
+ npx hebbian emit all --brain ./brain # → All AI formats at once
28
+ ```
29
+
30
+ | Before | hebbian |
31
+ |--------|------|
32
+ | 1000-line prompts, manually edited | `mkdir` one folder |
33
+ | Vector DB $70/mo | **$0** (folders = DB) |
34
+ | Switch AI → full migration | `cp -r brain/` — 1 second |
35
+ | Rule violation → wishful thinking | `bomb.neuron` → **cascade halt** |
36
+ | Rules managed by humans | Correction → auto neuron growth |
37
+
38
+ ---
39
+
40
+ ## Why "hebbian"?
41
+
42
+ Donald Hebb's 1949 principle: **neurons that fire together, wire together.** Repeated corrections strengthen synaptic pathways. That's exactly what this tool does — every `hebbian fire` increments a counter, and only frequently-fired neurons survive. Natural selection on your filesystem.
43
+
44
+ ---
45
+
46
+ ## How It Works
47
+
48
+ ### Folder = Neuron. Path = Sentence.
49
+
50
+ ```
51
+ brain/cortex/frontend/禁console_log/40.neuron
52
+ ```
53
+
54
+ Read it: "Cortex > Frontend > Never use console.log. Reinforced 40 times."
55
+
56
+ ### 7-Region Subsumption Architecture
57
+
58
+ ```
59
+ P0: brainstem → Absolute laws (immutable, read-only)
60
+ P1: limbic → Emotion filters
61
+ P2: hippocampus → Memory, session restore
62
+ P3: sensors → Environment constraints
63
+ P4: cortex → Knowledge, skills (largest region)
64
+ P5: ego → Tone, personality
65
+ P6: prefrontal → Goals, projects
66
+
67
+ Rule: Lower P always suppresses higher P.
68
+ bomb.neuron in any region → cascade halt.
69
+ ```
70
+
71
+ ### File Types
72
+
73
+ | File | Meaning | Biology |
74
+ |------|---------|---------|
75
+ | `N.neuron` | Excitatory counter (strength) | Synaptic weight |
76
+ | `N.contra` | Inhibitory counter | Inhibitory synapse |
77
+ | `dopamineN.neuron` | Reward signal | Dopamine |
78
+ | `bomb.neuron` | Circuit breaker | Pain response |
79
+ | `memoryN.neuron` | Episode recording | Long-term memory |
80
+ | `*.dormant` | Inactive marker | Sleep pruning |
81
+ | `.axon` | Cross-region link | Axon connection |
82
+
83
+ ---
84
+
85
+ ## CLI Reference
86
+
87
+ ```bash
88
+ hebbian init <path> # Create brain with 7 regions
89
+ hebbian emit <target> [--brain <path>] # Compile rules
90
+ hebbian fire <neuron-path> # Increment counter (+1)
91
+ hebbian grow <neuron-path> # Create neuron (with merge detection)
92
+ hebbian rollback <neuron-path> # Decrement counter (min=1)
93
+ hebbian signal <type> <neuron-path> # Add signal (dopamine/bomb/memory)
94
+ hebbian decay [--days N] # Mark inactive neurons dormant
95
+ hebbian watch # Auto-recompile on changes
96
+ hebbian diag # Brain diagnostics
97
+ hebbian stats # Brain statistics
98
+ ```
99
+
100
+ ### Emit Targets
101
+
102
+ | Target | Output File |
103
+ |--------|-------------|
104
+ | `claude` | `CLAUDE.md` |
105
+ | `cursor` | `.cursorrules` |
106
+ | `gemini` | `.gemini/GEMINI.md` |
107
+ | `copilot` | `.github/copilot-instructions.md` |
108
+ | `generic` | `.neuronrc` |
109
+ | `all` | All of the above |
110
+
111
+ ### Environment Variables
112
+
113
+ | Variable | Purpose | Default |
114
+ |----------|---------|---------|
115
+ | `HEBBIAN_BRAIN` | Brain directory path | `./brain` |
116
+
117
+ ---
118
+
119
+ ## 3-Tier Emission
120
+
121
+ hebbian compiles your brain into 3 tiers:
122
+
123
+ | Tier | File | Tokens | When |
124
+ |------|------|--------|------|
125
+ | 1 | Target file (CLAUDE.md, etc.) | ~500 | Auto-loaded by AI |
126
+ | 2 | `brain/_index.md` | ~1000 | AI reads at session start |
127
+ | 3 | `brain/{region}/_rules.md` | Variable | AI reads on demand |
128
+
129
+ This keeps the token budget lean. 293 neurons → ~500 tokens at startup.
130
+
131
+ ---
132
+
133
+ ## Synaptic Consolidation
134
+
135
+ When you `hebbian grow`, the system checks for similar existing neurons using Jaccard similarity:
136
+
137
+ ```bash
138
+ hebbian grow cortex/frontend/禁console_logging --brain ./brain
139
+ # → "禁console_logging" ≈ "禁console_log" (Jaccard ≥ 0.6)
140
+ # → Fires existing neuron instead of creating duplicate
141
+ ```
142
+
143
+ **Consolidation over duplication.** Hebbian principle in action.
144
+
145
+ ---
146
+
147
+ ## Compared to
148
+
149
+ | Feature | .cursorrules | Mem0/Letta | hebbian |
150
+ |---------|-------------|------------|------|
151
+ | 1000+ rules | Token overflow | Vector DB | Folder tree |
152
+ | Infrastructure | $0 | $$$$ | **$0** |
153
+ | Switch AI | Manual migration | Full re-setup | **`cp -r brain/`** |
154
+ | Self-growth | Manual | Bot-based | **Counter-based** |
155
+ | Immutable guardrails | None | None | **brainstem + bomb** |
156
+ | Audit trail | Hidden | DB logs | **`ls -R` = full history** |
157
+ | Runtime deps | N/A | Many | **0** |
158
+
159
+ ---
160
+
161
+ ## Zero Dependencies
162
+
163
+ hebbian uses only Node.js built-in modules:
164
+
165
+ - `node:fs` — filesystem operations
166
+ - `node:path` — path handling
167
+ - `node:util` — CLI argument parsing
168
+ - `node:http` — REST API (planned)
169
+
170
+ **Runtime dependencies: 0.** Matches the Go version's zero-dep philosophy, but with `npx` instant execution.
171
+
172
+ ---
173
+
174
+ ## Governance
175
+
176
+ 120 tests pass in ~850ms:
177
+
178
+ - **SCC** (Subsumption Cascade Correctness): 17/17 = **100%**
179
+ - **MLA** (Memory Lifecycle Accuracy): 15/15 = **100%**
180
+ - Scanner: 15 tests
181
+ - Lifecycle: 18 tests
182
+ - Emit: 16 tests
183
+ - Similarity: 12 tests
184
+ - CLI E2E: 9 tests
185
+
186
+ ```bash
187
+ node --test # Run all tests
188
+ node --test test/governance.test.js # Governance only
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Inspired By
194
+
195
+ [NeuronFS](https://github.com/rhino-acoustic/NeuronFS) — the original Go implementation that proved folders can be neurons. hebbian is a JavaScript reimagination, designed for the npm ecosystem and zero-dependency operation.
196
+
197
+ ---
198
+
199
+ ## License
200
+
201
+ MIT
package/bin/hebbian.js ADDED
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+
3
+ // hebbian — Folder-as-Neuron Brain for Any AI Agent
4
+ //
5
+ // "Neurons that fire together, wire together." — Donald Hebb (1949)
6
+ //
7
+ // USAGE:
8
+ // hebbian init <path> Create brain with 7 regions
9
+ // hebbian emit <target> [--brain <path>] Compile rules (claude/cursor/gemini/copilot/generic/all)
10
+ // hebbian fire <neuron-path> Increment neuron counter
11
+ // hebbian grow <neuron-path> Create neuron (with merge detection)
12
+ // hebbian rollback <neuron-path> Decrement counter (min=1)
13
+ // hebbian signal <type> <neuron-path> Add dopamine/bomb/memory signal
14
+ // hebbian decay [--days N] Mark inactive neurons dormant (default 30 days)
15
+ // hebbian watch [--brain <path>] Watch for changes + auto-recompile
16
+ // hebbian diag Print brain diagnostics
17
+ // hebbian stats Print brain statistics
18
+
19
+ import { parseArgs } from 'node:util';
20
+ import { resolve } from 'node:path';
21
+ import { existsSync } from 'node:fs';
22
+
23
+ const VERSION = '0.1.0';
24
+
25
+ const HELP = `
26
+ hebbian v${VERSION} — Folder-as-neuron brain for any AI agent.
27
+
28
+ "Neurons that fire together, wire together." — Donald Hebb (1949)
29
+
30
+ USAGE:
31
+ hebbian <command> [options]
32
+
33
+ COMMANDS:
34
+ init <path> Create brain with 7 regions
35
+ emit <target> [--brain <path>] Compile rules (claude/cursor/gemini/copilot/generic/all)
36
+ fire <neuron-path> Increment neuron counter (+1)
37
+ grow <neuron-path> Create neuron (with merge detection)
38
+ rollback <neuron-path> Decrement neuron counter (min=1)
39
+ signal <type> <neuron-path> Add signal (dopamine/bomb/memory)
40
+ decay [--days N] Mark inactive neurons dormant (default 30)
41
+ dedup Batch merge similar neurons (Jaccard >= 0.6)
42
+ snapshot Git commit current brain state
43
+ watch Watch for changes + auto-recompile
44
+ diag Print brain diagnostics
45
+ stats Print brain statistics
46
+
47
+ OPTIONS:
48
+ --brain <path> Brain directory (default: $HEBBIAN_BRAIN or ./brain)
49
+ --help, -h Show this help
50
+ --version, -v Show version
51
+
52
+ EXAMPLES:
53
+ hebbian init ./my-brain
54
+ hebbian grow cortex/frontend/禁console_log --brain ./my-brain
55
+ hebbian fire cortex/frontend/禁console_log --brain ./my-brain
56
+ hebbian emit claude --brain ./my-brain
57
+ hebbian emit all
58
+ `.trim();
59
+
60
+ /** Resolve brain root path from --brain flag, env var, or defaults */
61
+ function resolveBrainRoot(brainFlag) {
62
+ if (brainFlag) return resolve(brainFlag);
63
+ if (process.env.HEBBIAN_BRAIN) return resolve(process.env.HEBBIAN_BRAIN);
64
+ if (existsSync(resolve('./brain'))) return resolve('./brain');
65
+ return resolve(process.env.HOME || '~', 'hebbian', 'brain');
66
+ }
67
+
68
+ async function main(argv) {
69
+ const { values, positionals } = parseArgs({
70
+ args: argv,
71
+ options: {
72
+ brain: { type: 'string', short: 'b' },
73
+ days: { type: 'string', short: 'd' },
74
+ port: { type: 'string', short: 'p' },
75
+ help: { type: 'boolean', short: 'h' },
76
+ version: { type: 'boolean', short: 'v' },
77
+ },
78
+ allowPositionals: true,
79
+ strict: false,
80
+ });
81
+
82
+ if (values.version) {
83
+ console.log(`hebbian v${VERSION}`);
84
+ return;
85
+ }
86
+
87
+ const command = positionals[0];
88
+
89
+ if (values.help || !command) {
90
+ console.log(HELP);
91
+ return;
92
+ }
93
+
94
+ const brainRoot = resolveBrainRoot(values.brain);
95
+
96
+ switch (command) {
97
+ case 'init': {
98
+ const target = positionals[1];
99
+ if (!target) {
100
+ console.error('Usage: hebbian init <path>');
101
+ process.exit(1);
102
+ }
103
+ const { initBrain } = await import('../lib/init.js');
104
+ await initBrain(resolve(target));
105
+ break;
106
+ }
107
+ case 'emit': {
108
+ const target = positionals[1];
109
+ if (!target) {
110
+ console.error('Usage: hebbian emit <target> (claude/cursor/gemini/copilot/generic/all)');
111
+ process.exit(1);
112
+ }
113
+ const { emitToTarget } = await import('../lib/emit.js');
114
+ await emitToTarget(brainRoot, target);
115
+ break;
116
+ }
117
+ case 'fire': {
118
+ const neuronPath = positionals[1];
119
+ if (!neuronPath) {
120
+ console.error('Usage: hebbian fire <neuron-path>');
121
+ process.exit(1);
122
+ }
123
+ const { fireNeuron } = await import('../lib/fire.js');
124
+ await fireNeuron(brainRoot, neuronPath);
125
+ break;
126
+ }
127
+ case 'grow': {
128
+ const neuronPath = positionals[1];
129
+ if (!neuronPath) {
130
+ console.error('Usage: hebbian grow <neuron-path>');
131
+ process.exit(1);
132
+ }
133
+ const { growNeuron } = await import('../lib/grow.js');
134
+ await growNeuron(brainRoot, neuronPath);
135
+ break;
136
+ }
137
+ case 'rollback': {
138
+ const neuronPath = positionals[1];
139
+ if (!neuronPath) {
140
+ console.error('Usage: hebbian rollback <neuron-path>');
141
+ process.exit(1);
142
+ }
143
+ const { rollbackNeuron } = await import('../lib/rollback.js');
144
+ await rollbackNeuron(brainRoot, neuronPath);
145
+ break;
146
+ }
147
+ case 'signal': {
148
+ const signalType = positionals[1];
149
+ const neuronPath = positionals[2];
150
+ if (!signalType || !neuronPath) {
151
+ console.error('Usage: hebbian signal <type> <neuron-path> (type: dopamine/bomb/memory)');
152
+ process.exit(1);
153
+ }
154
+ const { signalNeuron } = await import('../lib/signal.js');
155
+ await signalNeuron(brainRoot, neuronPath, signalType);
156
+ break;
157
+ }
158
+ case 'decay': {
159
+ const days = values.days ? parseInt(values.days, 10) : 30;
160
+ const { runDecay } = await import('../lib/decay.js');
161
+ await runDecay(brainRoot, days);
162
+ break;
163
+ }
164
+ case 'dedup': {
165
+ const { runDedup } = await import('../lib/dedup.js');
166
+ runDedup(brainRoot);
167
+ break;
168
+ }
169
+ case 'snapshot': {
170
+ const { gitSnapshot } = await import('../lib/snapshot.js');
171
+ gitSnapshot(brainRoot);
172
+ break;
173
+ }
174
+ case 'watch': {
175
+ const { startWatch } = await import('../lib/watch.js');
176
+ await startWatch(brainRoot);
177
+ break;
178
+ }
179
+ case 'diag':
180
+ case 'stats': {
181
+ const { scanBrain } = await import('../lib/scanner.js');
182
+ const { runSubsumption } = await import('../lib/subsumption.js');
183
+ const brain = scanBrain(brainRoot);
184
+ const result = runSubsumption(brain);
185
+ const { printDiag } = await import('../lib/emit.js');
186
+ printDiag(brain, result);
187
+ break;
188
+ }
189
+ default:
190
+ console.error(`Unknown command: ${command}`);
191
+ console.log(HELP);
192
+ process.exit(1);
193
+ }
194
+ }
195
+
196
+ main(process.argv.slice(2)).catch((err) => {
197
+ console.error(err.message);
198
+ process.exit(1);
199
+ });
@@ -0,0 +1,83 @@
1
+ // hebbian — Constants & Configuration
2
+ //
3
+ // AXIOMS:
4
+ // 1. Folder = Neuron (name is meaning, depth is specificity)
5
+ // 2. File = Firing Trace (N.neuron = counter, dopamineN = reward, bomb = pain)
6
+ // 3. Path = Sentence (brain/cortex/quality/no_hardcoded → "cortex > quality > no_hardcoded")
7
+ // 4. Counter = Activation (higher = stronger/myelinated path)
8
+ // 5. AI writes back (counter increment = experience growth)
9
+
10
+ /** Ordered list of brain regions by subsumption priority (P0 → P6) */
11
+ export const REGIONS = [
12
+ 'brainstem',
13
+ 'limbic',
14
+ 'hippocampus',
15
+ 'sensors',
16
+ 'cortex',
17
+ 'ego',
18
+ 'prefrontal',
19
+ ];
20
+
21
+ /** Region → priority index (lower = higher priority) */
22
+ export const REGION_PRIORITY = /** @type {Record<string, number>} */ ({
23
+ brainstem: 0,
24
+ limbic: 1,
25
+ hippocampus: 2,
26
+ sensors: 3,
27
+ cortex: 4,
28
+ ego: 5,
29
+ prefrontal: 6,
30
+ });
31
+
32
+ /** Region display icons */
33
+ export const REGION_ICONS = /** @type {Record<string, string>} */ ({
34
+ brainstem: '\u{1F6E1}\uFE0F', // 🛡️
35
+ limbic: '\u{1F493}', // 💓
36
+ hippocampus: '\u{1F4DD}', // 📝
37
+ sensors: '\u{1F441}\uFE0F', // 👁️
38
+ cortex: '\u{1F9E0}', // 🧠
39
+ ego: '\u{1F3AD}', // 🎭
40
+ prefrontal: '\u{1F3AF}', // 🎯
41
+ });
42
+
43
+ /** Region Korean descriptions */
44
+ export const REGION_KO = /** @type {Record<string, string>} */ ({
45
+ brainstem: '양심/본능',
46
+ limbic: '감정 필터',
47
+ hippocampus: '기록/기억',
48
+ sensors: '환경 제약',
49
+ cortex: '지식/기술',
50
+ ego: '성향/톤',
51
+ prefrontal: '목표/계획',
52
+ });
53
+
54
+ /** Minimum counter value for a neuron to appear in emitted output */
55
+ export const EMIT_THRESHOLD = 5;
56
+
57
+ /** Days a new neuron gets spotlight coverage regardless of counter */
58
+ export const SPOTLIGHT_DAYS = 7;
59
+
60
+ /** Jaccard similarity threshold for neuron merge detection */
61
+ export const JACCARD_THRESHOLD = 0.6;
62
+
63
+ /** Default number of days before marking a neuron dormant */
64
+ export const DECAY_DAYS = 30;
65
+
66
+ /** Maximum recursion depth when walking brain directories */
67
+ export const MAX_DEPTH = 6;
68
+
69
+ /** Emit target → file path mapping */
70
+ export const EMIT_TARGETS = /** @type {Record<string, string>} */ ({
71
+ gemini: '.gemini/GEMINI.md',
72
+ cursor: '.cursorrules',
73
+ claude: 'CLAUDE.md',
74
+ copilot: '.github/copilot-instructions.md',
75
+ generic: '.neuronrc',
76
+ });
77
+
78
+ /** Valid signal types */
79
+ export const SIGNAL_TYPES = ['dopamine', 'bomb', 'memory'];
80
+
81
+ /** Emit markers for injection into existing files */
82
+ export const MARKER_START = '<!-- HEBBIAN:START -->';
83
+ export const MARKER_END = '<!-- HEBBIAN:END -->';
package/lib/decay.js ADDED
@@ -0,0 +1,100 @@
1
+ // hebbian — Decay (dormancy sweep)
2
+ //
3
+ // Neurons that haven't been touched in N days are marked dormant.
4
+ // Dormancy = *.dormant file in the neuron directory.
5
+ // Dormant neurons are excluded from emission and activation counts.
6
+ //
7
+ // "Most neurons die. Only the repeatedly fired ones survive." — Natural selection on OS.
8
+
9
+ import { readdirSync, statSync, writeFileSync, existsSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { REGIONS, MAX_DEPTH } from './constants.js';
12
+
13
+ /**
14
+ * Sweep the brain and mark inactive neurons as dormant.
15
+ *
16
+ * @param {string} brainRoot - Absolute path to brain root
17
+ * @param {number} days - Number of days of inactivity before dormancy
18
+ * @returns {{ scanned: number, decayed: number }}
19
+ */
20
+ export function runDecay(brainRoot, days) {
21
+ const threshold = Date.now() - days * 24 * 60 * 60 * 1000;
22
+ let scanned = 0;
23
+ let decayed = 0;
24
+
25
+ for (const regionName of REGIONS) {
26
+ const regionPath = join(brainRoot, regionName);
27
+ if (!existsSync(regionPath)) continue;
28
+ const result = decayWalk(regionPath, threshold, 0);
29
+ scanned += result.scanned;
30
+ decayed += result.decayed;
31
+ }
32
+
33
+ console.log(`\u{1F4A4} decay: scanned ${scanned} neurons, decayed ${decayed} (>${days} days inactive)`);
34
+ return { scanned, decayed };
35
+ }
36
+
37
+ /**
38
+ * @param {string} dir
39
+ * @param {number} threshold - Timestamp threshold
40
+ * @param {number} depth
41
+ * @returns {{ scanned: number, decayed: number }}
42
+ */
43
+ function decayWalk(dir, threshold, depth) {
44
+ if (depth > MAX_DEPTH) return { scanned: 0, decayed: 0 };
45
+
46
+ let scanned = 0;
47
+ let decayed = 0;
48
+ let entries;
49
+
50
+ try {
51
+ entries = readdirSync(dir, { withFileTypes: true });
52
+ } catch {
53
+ return { scanned: 0, decayed: 0 };
54
+ }
55
+
56
+ // Check if this directory is a neuron (has .neuron files)
57
+ let hasNeuronFile = false;
58
+ let isDormant = false;
59
+ let latestMod = 0;
60
+
61
+ for (const entry of entries) {
62
+ if (entry.isFile()) {
63
+ if (entry.name.endsWith('.neuron')) {
64
+ hasNeuronFile = true;
65
+ try {
66
+ const st = statSync(join(dir, entry.name));
67
+ if (st.mtimeMs > latestMod) latestMod = st.mtimeMs;
68
+ } catch {}
69
+ }
70
+ if (entry.name.endsWith('.dormant')) {
71
+ isDormant = true;
72
+ }
73
+ }
74
+ }
75
+
76
+ if (hasNeuronFile) {
77
+ scanned++;
78
+ if (!isDormant && latestMod < threshold) {
79
+ const age = Math.floor((Date.now() - latestMod) / (24 * 60 * 60 * 1000));
80
+ writeFileSync(
81
+ join(dir, 'decay.dormant'),
82
+ `Dormant since ${new Date().toISOString()} (${age} days inactive)`,
83
+ 'utf8',
84
+ );
85
+ decayed++;
86
+ }
87
+ }
88
+
89
+ // Recurse
90
+ for (const entry of entries) {
91
+ if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
92
+ if (entry.isDirectory()) {
93
+ const sub = decayWalk(join(dir, entry.name), threshold, depth + 1);
94
+ scanned += sub.scanned;
95
+ decayed += sub.decayed;
96
+ }
97
+ }
98
+
99
+ return { scanned, decayed };
100
+ }
package/lib/dedup.js ADDED
@@ -0,0 +1,66 @@
1
+ // hebbian — Batch Deduplication
2
+ //
3
+ // Scans all neurons in a region and merges duplicates via Jaccard similarity.
4
+ // When a match is found (>= threshold), fires the higher-counter neuron
5
+ // and marks the lower-counter one dormant.
6
+
7
+ import { writeFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { JACCARD_THRESHOLD } from './constants.js';
10
+ import { tokenize, jaccardSimilarity } from './similarity.js';
11
+ import { fireNeuron } from './fire.js';
12
+ import { scanBrain } from './scanner.js';
13
+
14
+ /**
15
+ * Run batch deduplication across all regions.
16
+ * @param {string} brainRoot
17
+ * @returns {{ scanned: number, merged: number }}
18
+ */
19
+ export function runDedup(brainRoot) {
20
+ const brain = scanBrain(brainRoot);
21
+ let scanned = 0;
22
+ let merged = 0;
23
+
24
+ for (const region of brain.regions) {
25
+ const neurons = region.neurons.filter((n) => !n.isDormant);
26
+ scanned += neurons.length;
27
+
28
+ // O(n^2) pairwise comparison within each region
29
+ const consumed = new Set();
30
+ for (let i = 0; i < neurons.length; i++) {
31
+ if (consumed.has(i)) continue;
32
+ const tokensI = tokenize(neurons[i].name);
33
+
34
+ for (let j = i + 1; j < neurons.length; j++) {
35
+ if (consumed.has(j)) continue;
36
+ const tokensJ = tokenize(neurons[j].name);
37
+ const sim = jaccardSimilarity(tokensI, tokensJ);
38
+
39
+ if (sim >= JACCARD_THRESHOLD) {
40
+ // Keep the one with higher counter, mark other dormant
41
+ const [keep, drop] = neurons[i].counter >= neurons[j].counter
42
+ ? [neurons[i], neurons[j]]
43
+ : [neurons[j], neurons[i]];
44
+
45
+ // Fire the keeper to absorb the dropped counter
46
+ const relKeep = `${region.name}/${keep.path}`;
47
+ fireNeuron(brainRoot, relKeep);
48
+
49
+ // Mark the dropped neuron dormant
50
+ writeFileSync(
51
+ join(drop.fullPath, 'dedup.dormant'),
52
+ `Merged into ${keep.path} on ${new Date().toISOString()}`,
53
+ 'utf8',
54
+ );
55
+
56
+ consumed.add(neurons[i] === drop ? i : j);
57
+ merged++;
58
+ console.log(`\u{1F500} merged: "${drop.path}" → "${keep.path}" (sim=${sim.toFixed(2)})`);
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ console.log(`\u{1F9F9} dedup: scanned ${scanned} neurons, merged ${merged}`);
65
+ return { scanned, merged };
66
+ }