kodelyth-ecc 1.8.0 → 1.8.2
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/CLAUDE.md +1 -1
- package/VERSION +1 -1
- package/bin/kodelyth-ecc.js +54 -5
- package/install.ps1 +30 -13
- package/install.sh +11 -4
- package/package.json +1 -1
- package/rules/common/agent-intent-routing.md +96 -195
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -47
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -41
- package/.github/ISSUE_TEMPLATE/good_first_issue.md +0 -36
- package/.github/ISSUE_TEMPLATE/new_agent.md +0 -52
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -53
- package/.github/workflows/ci.yml +0 -45
- package/.github/workflows/publish.yml +0 -60
- package/brand/concepts.svg +0 -109
- package/brand/convert.js +0 -161
- package/brand/favicon.svg +0 -7
- package/brand/fb-cover.svg +0 -34
- package/brand/fb-profile.svg +0 -21
- package/brand/kodelyth-dark.svg +0 -24
- package/brand/kodelyth-light.svg +0 -24
- package/brand/kodelyth-mark.svg +0 -20
- package/brand/package.json +0 -12
- package/docs/dashboard.md +0 -211
- package/docs/evolve.md +0 -303
- package/docs/mcp-clients.md +0 -167
- package/docs/mcp.md +0 -178
- package/docs/replay.md +0 -244
- package/docs/supply-chain.md +0 -207
- package/docs/swarm.md +0 -243
- package/skills/skill-comply/tests/test_grader.py +0 -137
- package/skills/skill-comply/tests/test_parser.py +0 -90
- package/social/card-agents.svg +0 -104
- package/social/card-install.svg +0 -58
- package/social/card-main.svg +0 -54
- package/social/facebook-v150.svg +0 -110
- package/social/fb-ad-main.svg +0 -128
- package/social/fb-post-features.svg +0 -118
- package/social/fb-post-launch.svg +0 -144
- package/social/fb-post-platforms.svg +0 -135
- package/social/github-social-preview.svg +0 -151
- package/social/hype-compound-learning.svg +0 -129
- package/social/hype-devil-mode.svg +0 -240
- package/social/hype-mcp-server.svg +0 -148
- package/social/hype-parallel-agents.svg +0 -162
- package/social/hype-stats-hero.svg +0 -138
- package/social/og-image.svg +0 -151
- package/social/readme-agents.svg +0 -138
- package/social/readme-hero.svg +0 -102
- package/social/section-agents.svg +0 -69
- package/social/section-author.svg +0 -66
- package/social/section-dashboard.svg +0 -71
- package/social/section-devil.svg +0 -66
- package/social/section-hooks.svg +0 -63
- package/social/section-install.svg +0 -54
- package/social/section-learning.svg +0 -64
- package/social/section-mcp.svg +0 -58
- package/social/section-memory.svg +0 -69
- package/social/section-parallel.svg +0 -84
- package/social/section-routing.svg +0 -63
- package/social/twitter-threads.md +0 -621
- package/social/x-card-agents-grid.svg +0 -120
- package/social/x-card-free.svg +0 -71
- package/social/x-card-hook.svg +0 -94
- package/tests/dashboard/data.test.js +0 -391
- package/tests/dashboard/server.test.js +0 -257
- package/tests/evolve/analyze.test.js +0 -169
- package/tests/evolve/proposals.test.js +0 -173
- package/tests/evolve/stats.test.js +0 -167
- package/tests/hooks/branch-name-check.test.js +0 -184
- package/tests/hooks/smart-suggest.test.js +0 -149
- package/tests/hooks/test-reminder.test.js +0 -181
- package/tests/mcp/catalog.test.js +0 -98
- package/tests/mcp/client.test.js +0 -113
- package/tests/mcp/resources-prompts.test.js +0 -70
- package/tests/mcp/tools.test.js +0 -159
- package/tests/memory/auto-recall.test.js +0 -132
- package/tests/memory/instincts.test.js +0 -258
- package/tests/memory/store.test.js +0 -203
- package/tests/replay/bundle.test.js +0 -181
- package/tests/replay/replay.test.js +0 -131
- package/tests/router/classify.test.js +0 -164
- package/tests/run-all.js +0 -55
- package/tests/safety/patterns.test.js +0 -94
- package/tests/safety/prompt-injection-guard.test.js +0 -111
- package/tests/safety/token-budget.test.js +0 -119
- package/tests/supply-chain/manifest.test.js +0 -147
- package/tests/supply-chain/sbom.test.js +0 -170
- package/tests/supply-chain/verify.test.js +0 -146
- package/tests/swarm/build-plan.test.js +0 -188
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
// Tests for scripts/memory/instincts.js — runs against a temp directory.
|
|
2
|
-
// Covers Improvement B (structured instinct schema), C (outcome tracking),
|
|
3
|
-
// and D (pattern confidence decay).
|
|
4
|
-
'use strict';
|
|
5
|
-
|
|
6
|
-
const test = require('node:test');
|
|
7
|
-
const assert = require('node:assert/strict');
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const os = require('os');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
|
|
12
|
-
// ── Temp directory isolation ──────────────────────────────────────────────────
|
|
13
|
-
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-instincts-'));
|
|
14
|
-
process.env.KODELYTH_MEMORY_DIR = TMP;
|
|
15
|
-
|
|
16
|
-
// Require AFTER setting env
|
|
17
|
-
const instincts = require('../../scripts/memory/instincts');
|
|
18
|
-
|
|
19
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
20
|
-
const PROJECT_A = '/projects/test-a';
|
|
21
|
-
const PROJECT_B = '/projects/test-b';
|
|
22
|
-
|
|
23
|
-
function freshFile() {
|
|
24
|
-
// Wipe the instincts file before each group of tests that needs isolation
|
|
25
|
-
if (fs.existsSync(instincts.INSTINCTS_FILE)) {
|
|
26
|
-
fs.unlinkSync(instincts.INSTINCTS_FILE);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ── captureFromCorrection ─────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
test('captureFromCorrection stores a new instinct', () => {
|
|
33
|
-
freshFile();
|
|
34
|
-
const inst = instincts.captureFromCorrection('always use pnpm not npm', PROJECT_A, 'project');
|
|
35
|
-
assert.ok(inst.id, 'should have an id');
|
|
36
|
-
assert.ok(inst.rule.includes('pnpm'), 'rule should contain original text');
|
|
37
|
-
assert.equal(inst.scope, 'project');
|
|
38
|
-
assert.equal(inst.source, 'correction');
|
|
39
|
-
assert.ok(inst.confidence >= 0.5 && inst.confidence <= 1.0, 'confidence in range');
|
|
40
|
-
assert.equal(inst.use_count, 1);
|
|
41
|
-
assert.equal(inst.stale, false);
|
|
42
|
-
assert.equal(inst.outcome, null);
|
|
43
|
-
assert.equal(inst.project_path, PROJECT_A);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('captureFromCorrection is idempotent — re-capture increments use_count', () => {
|
|
47
|
-
freshFile();
|
|
48
|
-
const first = instincts.captureFromCorrection('never use var', PROJECT_A, 'project');
|
|
49
|
-
const second = instincts.captureFromCorrection('never use var', PROJECT_A, 'project');
|
|
50
|
-
assert.equal(first.id, second.id, 'same correction should produce same id');
|
|
51
|
-
assert.equal(second.use_count, 2);
|
|
52
|
-
assert.ok(second.confidence > first.confidence, 're-capture should boost confidence');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('captureFromCorrection creates distinct records for different projects', () => {
|
|
56
|
-
freshFile();
|
|
57
|
-
const a = instincts.captureFromCorrection('always use async/await', PROJECT_A, 'project');
|
|
58
|
-
const b = instincts.captureFromCorrection('always use async/await', PROJECT_B, 'project');
|
|
59
|
-
assert.notEqual(a.id, b.id, 'same rule, different project = different instinct');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('captureFromCorrection caps confidence at 1.0', () => {
|
|
63
|
-
freshFile();
|
|
64
|
-
// Capture 10 times — confidence should never exceed 1.0
|
|
65
|
-
for (let i = 0; i < 10; i++) {
|
|
66
|
-
instincts.captureFromCorrection('use const not let', PROJECT_A, 'project');
|
|
67
|
-
}
|
|
68
|
-
const all = instincts.list({ projectPath: PROJECT_A });
|
|
69
|
-
const inst = all.find(i => i.rule.includes('const'));
|
|
70
|
-
assert.ok(inst, 'instinct should exist');
|
|
71
|
-
assert.ok(inst.confidence <= 1.0, 'confidence should not exceed 1.0');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// ── recordOutcome (by id) ─────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
test('recordOutcome boosts confidence on success', () => {
|
|
77
|
-
freshFile();
|
|
78
|
-
const inst = instincts.captureFromCorrection('prefer interface over type for objects', PROJECT_A);
|
|
79
|
-
const before = inst.confidence;
|
|
80
|
-
const updated = instincts.recordOutcome(inst.id, 'success');
|
|
81
|
-
assert.ok(updated.confidence > before, 'success should boost confidence');
|
|
82
|
-
assert.equal(updated.outcome, 'success');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('recordOutcome reduces confidence on failure', () => {
|
|
86
|
-
freshFile();
|
|
87
|
-
const inst = instincts.captureFromCorrection('avoid class components', PROJECT_A);
|
|
88
|
-
const before = inst.confidence;
|
|
89
|
-
const updated = instincts.recordOutcome(inst.id, 'failure');
|
|
90
|
-
assert.ok(updated.confidence < before, 'failure should reduce confidence');
|
|
91
|
-
assert.equal(updated.outcome, 'failure');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test('recordOutcome floors confidence at 0.0', () => {
|
|
95
|
-
freshFile();
|
|
96
|
-
const inst = instincts.captureFromCorrection('do not use callbacks', PROJECT_A);
|
|
97
|
-
// Record failure 5 times — should never go below 0
|
|
98
|
-
for (let i = 0; i < 5; i++) {
|
|
99
|
-
instincts.recordOutcome(inst.id, 'failure');
|
|
100
|
-
}
|
|
101
|
-
const updated = instincts.recordOutcome(inst.id, 'failure');
|
|
102
|
-
assert.ok(updated.confidence >= 0.0, 'confidence should not go below 0');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('recordOutcome returns null for unknown id', () => {
|
|
106
|
-
freshFile();
|
|
107
|
-
const result = instincts.recordOutcome('nonexistent-id-xyz', 'success');
|
|
108
|
-
assert.equal(result, null);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// ── recordOutcomeByProject (Improvement C bridge) ─────────────────────────────
|
|
112
|
-
|
|
113
|
-
test('recordOutcomeByProject downgrades matching instincts by project + hint', () => {
|
|
114
|
-
freshFile();
|
|
115
|
-
instincts.captureFromCorrection('always use pnpm not npm', PROJECT_A);
|
|
116
|
-
instincts.captureFromCorrection('always run tests before commit', PROJECT_A);
|
|
117
|
-
instincts.captureFromCorrection('use tailwind not inline styles', PROJECT_B);
|
|
118
|
-
|
|
119
|
-
const before = instincts.list({ projectPath: PROJECT_A });
|
|
120
|
-
const pnpmBefore = before.find(i => i.rule.includes('pnpm')).confidence;
|
|
121
|
-
|
|
122
|
-
const changed = instincts.recordOutcomeByProject(PROJECT_A, 'use pnpm not npm', false);
|
|
123
|
-
|
|
124
|
-
assert.ok(changed.length >= 1, 'at least one instinct should be changed');
|
|
125
|
-
|
|
126
|
-
const after = instincts.list({ projectPath: PROJECT_A });
|
|
127
|
-
const pnpmAfter = after.find(i => i.rule.includes('pnpm')).confidence;
|
|
128
|
-
assert.ok(pnpmAfter < pnpmBefore, 'matched instinct confidence should decrease');
|
|
129
|
-
|
|
130
|
-
// PROJECT_B instinct should be untouched
|
|
131
|
-
const bInstincts = instincts.list({ projectPath: PROJECT_B });
|
|
132
|
-
assert.ok(bInstincts[0].confidence >= 0.7, 'unrelated project instinct should be unchanged');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test('recordOutcomeByProject skips instincts with < 20% token overlap', () => {
|
|
136
|
-
freshFile();
|
|
137
|
-
instincts.captureFromCorrection('always use pnpm', PROJECT_A);
|
|
138
|
-
const changed = instincts.recordOutcomeByProject(PROJECT_A, 'completely unrelated topic xyz', false);
|
|
139
|
-
// Should change 0 because token overlap < 20%
|
|
140
|
-
assert.equal(changed.length, 0, 'should not match on low overlap');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// ── runDecayCheck (Improvement D) ─────────────────────────────────────────────
|
|
144
|
-
|
|
145
|
-
test('runDecayCheck marks old instincts as stale', () => {
|
|
146
|
-
freshFile();
|
|
147
|
-
const inst = instincts.captureFromCorrection('prefer named exports', PROJECT_A);
|
|
148
|
-
|
|
149
|
-
// Manually backdate last_used to 35 days ago
|
|
150
|
-
const all = instincts.list();
|
|
151
|
-
const record = all.find(i => i.id === inst.id);
|
|
152
|
-
const old = new Date();
|
|
153
|
-
old.setDate(old.getDate() - 35);
|
|
154
|
-
record.last_used = old.toISOString();
|
|
155
|
-
// Write back manually via the file
|
|
156
|
-
const lines = fs.readFileSync(instincts.INSTINCTS_FILE, 'utf8').split('\n').filter(Boolean);
|
|
157
|
-
const rewritten = lines.map(l => {
|
|
158
|
-
try {
|
|
159
|
-
const r = JSON.parse(l);
|
|
160
|
-
return r.id === inst.id ? JSON.stringify(record) : l;
|
|
161
|
-
} catch { return l; }
|
|
162
|
-
});
|
|
163
|
-
fs.writeFileSync(instincts.INSTINCTS_FILE, rewritten.join('\n') + '\n', 'utf8');
|
|
164
|
-
|
|
165
|
-
const stale = instincts.runDecayCheck();
|
|
166
|
-
assert.ok(stale.length >= 1, 'should detect at least one stale instinct');
|
|
167
|
-
assert.equal(stale[0].id, inst.id);
|
|
168
|
-
assert.equal(stale[0].stale, true);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test('runDecayCheck does not mark recent instincts as stale', () => {
|
|
172
|
-
freshFile();
|
|
173
|
-
instincts.captureFromCorrection('use strict mode', PROJECT_A);
|
|
174
|
-
const stale = instincts.runDecayCheck();
|
|
175
|
-
assert.equal(stale.length, 0, 'fresh instinct should not be stale');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('runDecayCheck returns only newly-stale (not already-stale)', () => {
|
|
179
|
-
freshFile();
|
|
180
|
-
const inst = instincts.captureFromCorrection('prefer optional chaining', PROJECT_A);
|
|
181
|
-
|
|
182
|
-
// Backdate and mark already stale
|
|
183
|
-
const lines = fs.readFileSync(instincts.INSTINCTS_FILE, 'utf8').split('\n').filter(Boolean);
|
|
184
|
-
const old = new Date();
|
|
185
|
-
old.setDate(old.getDate() - 40);
|
|
186
|
-
const rewritten = lines.map(l => {
|
|
187
|
-
try {
|
|
188
|
-
const r = JSON.parse(l);
|
|
189
|
-
if (r.id === inst.id) {
|
|
190
|
-
return JSON.stringify({ ...r, stale: true, last_used: old.toISOString() });
|
|
191
|
-
}
|
|
192
|
-
return l;
|
|
193
|
-
} catch { return l; }
|
|
194
|
-
});
|
|
195
|
-
fs.writeFileSync(instincts.INSTINCTS_FILE, rewritten.join('\n') + '\n', 'utf8');
|
|
196
|
-
|
|
197
|
-
const stale = instincts.runDecayCheck();
|
|
198
|
-
assert.equal(stale.length, 0, 'already-stale instincts should not be re-reported');
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// ── pruneWeak ─────────────────────────────────────────────────────────────────
|
|
202
|
-
|
|
203
|
-
test('pruneWeak removes instincts below threshold', () => {
|
|
204
|
-
freshFile();
|
|
205
|
-
const inst = instincts.captureFromCorrection('do not shadow outer variables', PROJECT_A);
|
|
206
|
-
|
|
207
|
-
// Drive confidence below threshold via repeated failures
|
|
208
|
-
for (let i = 0; i < 5; i++) {
|
|
209
|
-
instincts.recordOutcome(inst.id, 'failure');
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const pruned = instincts.pruneWeak(0.2);
|
|
213
|
-
assert.ok(pruned >= 1, 'should prune at least one weak instinct');
|
|
214
|
-
|
|
215
|
-
const remaining = instincts.list();
|
|
216
|
-
const found = remaining.find(i => i.id === inst.id);
|
|
217
|
-
assert.equal(found, undefined, 'pruned instinct should be gone');
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test('pruneWeak keeps high-confidence instincts', () => {
|
|
221
|
-
freshFile();
|
|
222
|
-
instincts.captureFromCorrection('use es modules not commonjs', PROJECT_A);
|
|
223
|
-
const pruned = instincts.pruneWeak(0.2);
|
|
224
|
-
assert.equal(pruned, 0, 'strong instinct should not be pruned');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// ── list + filtering ──────────────────────────────────────────────────────────
|
|
228
|
-
|
|
229
|
-
test('list filters by scope', () => {
|
|
230
|
-
freshFile();
|
|
231
|
-
instincts.captureFromCorrection('global rule one', null, 'global');
|
|
232
|
-
instincts.captureFromCorrection('project rule one', PROJECT_A, 'project');
|
|
233
|
-
|
|
234
|
-
const globals = instincts.list({ scope: 'global' });
|
|
235
|
-
const projects = instincts.list({ scope: 'project' });
|
|
236
|
-
assert.ok(globals.every(i => i.scope === 'global'));
|
|
237
|
-
assert.ok(projects.every(i => i.scope === 'project'));
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test('list filters by minConfidence', () => {
|
|
241
|
-
freshFile();
|
|
242
|
-
const inst = instincts.captureFromCorrection('high confidence rule', PROJECT_A);
|
|
243
|
-
instincts.recordOutcome(inst.id, 'success'); // boosts to ~0.85
|
|
244
|
-
|
|
245
|
-
const high = instincts.list({ minConfidence: 0.8 });
|
|
246
|
-
assert.ok(high.length >= 1);
|
|
247
|
-
assert.ok(high.every(i => i.confidence >= 0.8));
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test('list filters by projectPath', () => {
|
|
251
|
-
freshFile();
|
|
252
|
-
instincts.captureFromCorrection('rule for project a', PROJECT_A, 'project');
|
|
253
|
-
instincts.captureFromCorrection('rule for project b', PROJECT_B, 'project');
|
|
254
|
-
|
|
255
|
-
const aOnly = instincts.list({ projectPath: PROJECT_A });
|
|
256
|
-
assert.ok(aOnly.every(i => i.project_path === PROJECT_A));
|
|
257
|
-
assert.ok(aOnly.length >= 1);
|
|
258
|
-
});
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
// Tests for scripts/memory/store.js — runs against a temp directory
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const test = require('node:test');
|
|
5
|
-
const assert = require('node:assert/strict');
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
|
|
10
|
-
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-mem-'));
|
|
11
|
-
process.env.KODELYTH_MEMORY_DIR = TMP;
|
|
12
|
-
|
|
13
|
-
// Require AFTER setting env so the store picks up the temp dir
|
|
14
|
-
const store = require('../../scripts/memory/store');
|
|
15
|
-
const { buildContextBlock } = require('../../scripts/memory/inject');
|
|
16
|
-
|
|
17
|
-
test('tokenise removes stopwords and short tokens', () => {
|
|
18
|
-
const tokens = store.tokenise('The Stripe webhook signature failed in production');
|
|
19
|
-
assert.ok(tokens.includes('stripe'));
|
|
20
|
-
assert.ok(tokens.includes('webhook'));
|
|
21
|
-
assert.ok(tokens.includes('signature'));
|
|
22
|
-
assert.ok(tokens.includes('failed'));
|
|
23
|
-
assert.ok(!tokens.includes('the'));
|
|
24
|
-
assert.ok(!tokens.includes('in'));
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('capture stores a memory and returns it with id', () => {
|
|
28
|
-
const m = store.capture({
|
|
29
|
-
problem: 'Stripe webhook signature failed in production',
|
|
30
|
-
approach: 'Switched body parser from json to raw, validated with constructEvent',
|
|
31
|
-
tags: ['payments', 'stripe', 'webhooks'],
|
|
32
|
-
project: '/test/project-a',
|
|
33
|
-
language: 'typescript',
|
|
34
|
-
});
|
|
35
|
-
assert.ok(m.id);
|
|
36
|
-
assert.equal(m.problem, 'Stripe webhook signature failed in production');
|
|
37
|
-
assert.equal(m.tags.length, 3);
|
|
38
|
-
assert.ok(m.captured_at);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('capture rejects missing problem or approach', () => {
|
|
42
|
-
assert.throws(() => store.capture({ problem: '', approach: 'x' }));
|
|
43
|
-
assert.throws(() => store.capture({ problem: 'x', approach: '' }));
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('recall finds memory by keyword', () => {
|
|
47
|
-
const results = store.recall('stripe webhook');
|
|
48
|
-
assert.ok(results.length >= 1);
|
|
49
|
-
assert.match(results[0].problem, /stripe/i);
|
|
50
|
-
assert.ok(results[0].score > 0);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('recall returns empty for irrelevant query', () => {
|
|
54
|
-
const results = store.recall('completely unrelated quantum mechanics');
|
|
55
|
-
assert.equal(results.length, 0);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('recallForProject prioritises project memories then falls back to global', () => {
|
|
59
|
-
store.capture({
|
|
60
|
-
problem: 'Database connection pool exhausted',
|
|
61
|
-
approach: 'Increased pool size and added connection timeout',
|
|
62
|
-
tags: ['database', 'postgres'],
|
|
63
|
-
project: '/test/project-b',
|
|
64
|
-
language: 'typescript',
|
|
65
|
-
});
|
|
66
|
-
const projectA = store.recallForProject('/test/project-a', 'webhook');
|
|
67
|
-
assert.ok(projectA.length >= 1);
|
|
68
|
-
assert.equal(projectA[0].project_path, '/test/project-a');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('listAll returns all non-deleted memories', () => {
|
|
72
|
-
const all = store.listAll();
|
|
73
|
-
assert.ok(all.length >= 2);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('forget marks memory deleted', () => {
|
|
77
|
-
const all = store.listAll();
|
|
78
|
-
const target = all[0];
|
|
79
|
-
const ok = store.forget(target.id);
|
|
80
|
-
assert.equal(ok, true);
|
|
81
|
-
const after = store.listAll();
|
|
82
|
-
assert.ok(after.length < all.length);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('rebuildIndex restores searchability after manual log edit', () => {
|
|
86
|
-
const r = store.rebuildIndex();
|
|
87
|
-
assert.ok(r.count >= 1);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('stats summarises store contents', () => {
|
|
91
|
-
const s = store.stats();
|
|
92
|
-
assert.ok(s.total >= 1);
|
|
93
|
-
assert.equal(s.storageDir, TMP);
|
|
94
|
-
assert.ok(s.byLanguage.typescript >= 1);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('buildContextBlock returns null when memory is empty', () => {
|
|
98
|
-
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-empty-'));
|
|
99
|
-
process.env.KODELYTH_MEMORY_DIR = emptyDir;
|
|
100
|
-
// Force fresh require by clearing cache
|
|
101
|
-
delete require.cache[require.resolve('../../scripts/memory/store')];
|
|
102
|
-
delete require.cache[require.resolve('../../scripts/memory/inject')];
|
|
103
|
-
const { buildContextBlock: bcb } = require('../../scripts/memory/inject');
|
|
104
|
-
const result = bcb({ projectRoot: '/test/project-x' });
|
|
105
|
-
assert.equal(result, null);
|
|
106
|
-
// Restore
|
|
107
|
-
process.env.KODELYTH_MEMORY_DIR = TMP;
|
|
108
|
-
delete require.cache[require.resolve('../../scripts/memory/store')];
|
|
109
|
-
delete require.cache[require.resolve('../../scripts/memory/inject')];
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('buildContextBlock returns structured block when memory exists', () => {
|
|
113
|
-
const fresh = require('../../scripts/memory/inject');
|
|
114
|
-
const result = fresh.buildContextBlock({
|
|
115
|
-
projectRoot: '/test/project-b',
|
|
116
|
-
query: 'database pool',
|
|
117
|
-
});
|
|
118
|
-
assert.ok(result);
|
|
119
|
-
assert.ok(result.text.includes('Kodelyth Memory'));
|
|
120
|
-
assert.ok(result.memoryCount >= 1);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// ── Improvement C: outcome tracking ──────────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
test('resolveMemory marks a memory with outcome', () => {
|
|
126
|
-
// Capture a memory with a file reference
|
|
127
|
-
const m = store.capture({
|
|
128
|
-
problem: 'Auth token refresh race condition',
|
|
129
|
-
approach: 'Added mutex lock around token refresh logic',
|
|
130
|
-
tags: ['auth', 'race-condition'],
|
|
131
|
-
project: '/test/project-c',
|
|
132
|
-
files: ['/test/project-c/src/auth/refresh.ts'],
|
|
133
|
-
});
|
|
134
|
-
const ok = store.resolveMemory(m.id, false);
|
|
135
|
-
assert.equal(ok, true, 'resolveMemory should return true when memory found');
|
|
136
|
-
|
|
137
|
-
const all = store.listAll();
|
|
138
|
-
const updated = all.find(mem => mem.id === m.id);
|
|
139
|
-
assert.ok(updated, 'memory should still exist');
|
|
140
|
-
assert.equal(updated.resolved, false);
|
|
141
|
-
assert.ok(updated.resolved_at, 'resolved_at should be set');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test('resolveMemory returns false for unknown id', () => {
|
|
145
|
-
const ok = store.resolveMemory('totally-unknown-id-123', true);
|
|
146
|
-
assert.equal(ok, false);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test('findMemoriesForFile finds memories by exact file path', () => {
|
|
150
|
-
store.capture({
|
|
151
|
-
problem: 'Database migration ran twice',
|
|
152
|
-
approach: 'Added idempotency check in migration runner',
|
|
153
|
-
tags: ['migrations', 'database'],
|
|
154
|
-
project: '/test/project-d',
|
|
155
|
-
files: ['/test/project-d/migrations/001_users.sql'],
|
|
156
|
-
});
|
|
157
|
-
const matches = store.findMemoriesForFile('/test/project-d/migrations/001_users.sql');
|
|
158
|
-
assert.ok(matches.length >= 1, 'should find memory with exact file path');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test('findMemoriesForFile returns empty when no file match', () => {
|
|
162
|
-
const matches = store.findMemoriesForFile('/completely/unrelated/path/file.ts');
|
|
163
|
-
assert.equal(matches.length, 0);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test('findMemoriesForFile excludes already-resolved memories', () => {
|
|
167
|
-
const m = store.capture({
|
|
168
|
-
problem: 'Cache invalidation bug in product listing',
|
|
169
|
-
approach: 'Switched to event-driven cache clearing',
|
|
170
|
-
tags: ['cache', 'redis'],
|
|
171
|
-
project: '/test/project-e',
|
|
172
|
-
files: ['/test/project-e/src/cache/products.ts'],
|
|
173
|
-
});
|
|
174
|
-
store.resolveMemory(m.id, true); // mark as resolved
|
|
175
|
-
|
|
176
|
-
const matches = store.findMemoriesForFile('/test/project-e/src/cache/products.ts');
|
|
177
|
-
const found = matches.find(mem => mem.id === m.id);
|
|
178
|
-
assert.equal(found, undefined, 'resolved memory should not be returned');
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test('autoResolveOnEdit marks matching memory resolved:false', () => {
|
|
182
|
-
const filePath = '/test/project-f/src/api/users.ts';
|
|
183
|
-
const m = store.capture({
|
|
184
|
-
problem: 'User endpoint returned 500 on empty body',
|
|
185
|
-
approach: 'Added body validation middleware before handler',
|
|
186
|
-
tags: ['api', 'validation'],
|
|
187
|
-
project: '/test/project-f',
|
|
188
|
-
files: [filePath],
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const resolved = store.autoResolveOnEdit(filePath, '/test/project-f');
|
|
192
|
-
assert.ok(resolved.length >= 1, 'should resolve at least one memory');
|
|
193
|
-
assert.equal(resolved[0].id, m.id);
|
|
194
|
-
|
|
195
|
-
const all = store.listAll();
|
|
196
|
-
const updated = all.find(mem => mem.id === m.id);
|
|
197
|
-
assert.equal(updated.resolved, false);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test('autoResolveOnEdit returns empty when no matching memory', () => {
|
|
201
|
-
const resolved = store.autoResolveOnEdit('/no/memory/for/this/file.ts');
|
|
202
|
-
assert.equal(resolved.length, 0);
|
|
203
|
-
});
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
// Tests for scripts/replay/bundle.js — pure read/write of session bundles.
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const test = require('node:test');
|
|
5
|
-
const assert = require('node:assert/strict');
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
|
|
10
|
-
const B = require('../../scripts/replay/bundle.js');
|
|
11
|
-
|
|
12
|
-
function tmp() {
|
|
13
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-bundle-'));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function seedSession(root, sessionName, workers) {
|
|
17
|
-
const sessionDir = path.join(root, sessionName);
|
|
18
|
-
fs.mkdirSync(sessionDir, { recursive: true });
|
|
19
|
-
for (const w of workers) {
|
|
20
|
-
const wdir = path.join(sessionDir, w.slug);
|
|
21
|
-
fs.mkdirSync(wdir);
|
|
22
|
-
fs.writeFileSync(path.join(wdir, 'task.md'), w.task || `# task for ${w.slug}`);
|
|
23
|
-
fs.writeFileSync(path.join(wdir, 'handoff.md'), w.handoff || `# handoff for ${w.slug}`);
|
|
24
|
-
fs.writeFileSync(path.join(wdir, 'status.md'), w.status || `# status for ${w.slug}\n- State: completed`);
|
|
25
|
-
}
|
|
26
|
-
return sessionDir;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ── exportBundle ─────────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
test('exportBundle: rejects missing session dir', () => {
|
|
32
|
-
assert.throws(() => B.exportBundle({ sessionDir: '/no/such/path' }), /not found/);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('exportBundle: gathers task/handoff/status per worker', () => {
|
|
36
|
-
const root = tmp();
|
|
37
|
-
const dir = seedSession(root, 'swarm-test', [
|
|
38
|
-
{ slug: 'security-reviewer', task: 'audit', handoff: 'found 3 issues', status: 'completed' },
|
|
39
|
-
{ slug: 'code-reviewer', task: 'review', handoff: 'all good', status: 'completed' },
|
|
40
|
-
]);
|
|
41
|
-
const bundle = B.exportBundle({ sessionDir: dir, sessionName: 'swarm-test', meta: { task: 'audit oauth', harness: 'claude' } });
|
|
42
|
-
assert.equal(bundle.schema, B.BUNDLE_SCHEMA);
|
|
43
|
-
assert.equal(bundle.session, 'swarm-test');
|
|
44
|
-
assert.equal(bundle.workers.length, 2);
|
|
45
|
-
assert.equal(bundle.meta.task, 'audit oauth');
|
|
46
|
-
// Sorted by slug:
|
|
47
|
-
assert.deepEqual(bundle.workers.map(w => w.slug), ['code-reviewer', 'security-reviewer']);
|
|
48
|
-
// Content preserved:
|
|
49
|
-
const sec = bundle.workers.find(w => w.slug === 'security-reviewer');
|
|
50
|
-
assert.equal(sec.task, 'audit');
|
|
51
|
-
assert.equal(sec.handoff, 'found 3 issues');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('exportBundle: handles empty session dir gracefully', () => {
|
|
55
|
-
const root = tmp();
|
|
56
|
-
const empty = path.join(root, 'empty');
|
|
57
|
-
fs.mkdirSync(empty);
|
|
58
|
-
const bundle = B.exportBundle({ sessionDir: empty });
|
|
59
|
-
assert.equal(bundle.workers.length, 0);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// ── writeBundle / readBundle round-trip ──────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
test('writeBundle + readBundle: round-trips losslessly', () => {
|
|
65
|
-
const root = tmp();
|
|
66
|
-
const dir = seedSession(root, 's1', [{ slug: 'a' }, { slug: 'b' }]);
|
|
67
|
-
const bundle = B.exportBundle({ sessionDir: dir, meta: { task: 'foo' } });
|
|
68
|
-
const out = path.join(root, 'out.json');
|
|
69
|
-
B.writeBundle(bundle, out);
|
|
70
|
-
|
|
71
|
-
const reread = B.readBundle(out);
|
|
72
|
-
assert.equal(reread.session, bundle.session);
|
|
73
|
-
assert.equal(reread.workers.length, 2);
|
|
74
|
-
assert.equal(reread.meta.task, 'foo');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('writeBundle: rejects bad schema', () => {
|
|
78
|
-
assert.throws(() => B.writeBundle({ schema: 'bogus' }, '/tmp/x.json'), /invalid schema/);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('readBundle: rejects unknown schema', () => {
|
|
82
|
-
const root = tmp();
|
|
83
|
-
const out = path.join(root, 'bad.json');
|
|
84
|
-
fs.writeFileSync(out, JSON.stringify({ schema: 'not.kodelyth/v0', session: 'x', workers: [] }));
|
|
85
|
-
assert.throws(() => B.readBundle(out), /unsupported schema/);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('readBundle: rejects empty workers', () => {
|
|
89
|
-
const root = tmp();
|
|
90
|
-
const out = path.join(root, 'empty.json');
|
|
91
|
-
fs.writeFileSync(out, JSON.stringify({ schema: B.BUNDLE_SCHEMA, session: 'x', workers: [] }));
|
|
92
|
-
assert.throws(() => B.readBundle(out), /non-empty array/);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// ── importBundle ─────────────────────────────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
test('importBundle: restores worker files into target dir', () => {
|
|
98
|
-
const root = tmp();
|
|
99
|
-
const dir = seedSession(root, 'src', [
|
|
100
|
-
{ slug: 'a', task: 'task-a', handoff: 'handoff-a', status: 'status-a' },
|
|
101
|
-
]);
|
|
102
|
-
const bundle = B.exportBundle({ sessionDir: dir });
|
|
103
|
-
|
|
104
|
-
const target = path.join(root, 'restored');
|
|
105
|
-
const out = B.importBundle(bundle, { targetDir: target });
|
|
106
|
-
assert.equal(out.targetDir, target);
|
|
107
|
-
assert.deepEqual(out.workers, ['a']);
|
|
108
|
-
|
|
109
|
-
assert.equal(fs.readFileSync(path.join(target, 'a', 'task.md'), 'utf8'), 'task-a');
|
|
110
|
-
assert.equal(fs.readFileSync(path.join(target, 'a', 'handoff.md'), 'utf8'), 'handoff-a');
|
|
111
|
-
assert.equal(fs.readFileSync(path.join(target, 'a', 'status.md'), 'utf8'), 'status-a');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('importBundle: refuses to overwrite without flag', () => {
|
|
115
|
-
const root = tmp();
|
|
116
|
-
const dir = seedSession(root, 'src', [{ slug: 'a' }]);
|
|
117
|
-
const bundle = B.exportBundle({ sessionDir: dir });
|
|
118
|
-
const target = path.join(root, 'exists');
|
|
119
|
-
fs.mkdirSync(target);
|
|
120
|
-
assert.throws(() => B.importBundle(bundle, { targetDir: target }), /already exists/);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('importBundle: overwrite=true replaces existing dir', () => {
|
|
124
|
-
const root = tmp();
|
|
125
|
-
const dir = seedSession(root, 'src', [{ slug: 'a', task: 'fresh' }]);
|
|
126
|
-
const bundle = B.exportBundle({ sessionDir: dir });
|
|
127
|
-
const target = path.join(root, 'exists');
|
|
128
|
-
fs.mkdirSync(target);
|
|
129
|
-
fs.writeFileSync(path.join(target, 'stale.txt'), 'leftover');
|
|
130
|
-
|
|
131
|
-
B.importBundle(bundle, { targetDir: target, overwrite: true });
|
|
132
|
-
assert.equal(fs.existsSync(path.join(target, 'stale.txt')), false);
|
|
133
|
-
assert.equal(fs.readFileSync(path.join(target, 'a', 'task.md'), 'utf8'), 'fresh');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// ── diffBundles ──────────────────────────────────────────────────────────────
|
|
137
|
-
|
|
138
|
-
test('diffBundles: detects handoff changes', () => {
|
|
139
|
-
const root = tmp();
|
|
140
|
-
const a = B.exportBundle({ sessionDir: seedSession(root, 'a', [
|
|
141
|
-
{ slug: 'x', handoff: 'aaa' }, { slug: 'y', handoff: 'shared' },
|
|
142
|
-
]) });
|
|
143
|
-
const b = B.exportBundle({ sessionDir: seedSession(root, 'b', [
|
|
144
|
-
{ slug: 'x', handoff: 'bbb' }, { slug: 'y', handoff: 'shared' },
|
|
145
|
-
]) });
|
|
146
|
-
const d = B.diffBundles(a, b);
|
|
147
|
-
const x = d.find(r => r.slug === 'x');
|
|
148
|
-
const y = d.find(r => r.slug === 'y');
|
|
149
|
-
assert.equal(x.handoff_changed, true);
|
|
150
|
-
assert.equal(y.handoff_changed, false);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test('diffBundles: surfaces workers that exist on only one side', () => {
|
|
154
|
-
const root = tmp();
|
|
155
|
-
const a = B.exportBundle({ sessionDir: seedSession(root, 'a', [
|
|
156
|
-
{ slug: 'only-a' }, { slug: 'shared' },
|
|
157
|
-
]) });
|
|
158
|
-
const b = B.exportBundle({ sessionDir: seedSession(root, 'b', [
|
|
159
|
-
{ slug: 'shared' }, { slug: 'only-b' },
|
|
160
|
-
]) });
|
|
161
|
-
const d = B.diffBundles(a, b);
|
|
162
|
-
assert.equal(d.find(r => r.slug === 'only-a').in_a, true);
|
|
163
|
-
assert.equal(d.find(r => r.slug === 'only-a').in_b, false);
|
|
164
|
-
assert.equal(d.find(r => r.slug === 'only-b').in_b, true);
|
|
165
|
-
assert.equal(d.find(r => r.slug === 'only-b').in_a, false);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// ── nextReplayName ───────────────────────────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
test('nextReplayName: generates -replay-1 for fresh origin', () => {
|
|
171
|
-
assert.equal(B.nextReplayName('swarm-2026-05-10-4a'), 'swarm-2026-05-10-4a-replay-1');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test('nextReplayName: skips taken numbers', () => {
|
|
175
|
-
const taken = new Set(['swarm-x-replay-1', 'swarm-x-replay-2']);
|
|
176
|
-
assert.equal(B.nextReplayName('swarm-x', taken), 'swarm-x-replay-3');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test('nextReplayName: strips existing -replay-N suffix to find canonical base', () => {
|
|
180
|
-
assert.equal(B.nextReplayName('swarm-x-replay-3'), 'swarm-x-replay-1');
|
|
181
|
-
});
|