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,169 +0,0 @@
|
|
|
1
|
-
// Tests for scripts/evolve/analyze.js
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const test = require('node:test');
|
|
5
|
-
const assert = require('node:assert/strict');
|
|
6
|
-
|
|
7
|
-
const A = require('../../scripts/evolve/analyze.js');
|
|
8
|
-
|
|
9
|
-
const FAKE_MEMORY = {
|
|
10
|
-
id: 'm1',
|
|
11
|
-
problem: 'Tailwind v4 migration breaks arbitrary values',
|
|
12
|
-
approach: 'Regenerate config with @config and update postcss-import order',
|
|
13
|
-
tags: ['css', 'tailwind', 'migration'],
|
|
14
|
-
language: 'typescript',
|
|
15
|
-
source: 'manual',
|
|
16
|
-
captured_at: '2026-04-01T00:00:00Z',
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// ── thresholds ──────────────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
test('exposes default thresholds', () => {
|
|
22
|
-
assert.ok(A.DEFAULT_REUSE_MIN_COUNT >= 1);
|
|
23
|
-
assert.ok(A.DEFAULT_REUSE_MIN_SESSIONS >= 1);
|
|
24
|
-
assert.ok(A.DEFAULT_MISS_MIN_COUNT >= 1);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// ── analyzeReuseForProposals ────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
test('analyzeReuseForProposals: skips when below threshold', () => {
|
|
30
|
-
const reuseStats = {
|
|
31
|
-
entries: [{ id: 'm1', count: 1, sessions: ['s1'], projects: [] }],
|
|
32
|
-
};
|
|
33
|
-
const proposals = A.analyzeReuseForProposals({ reuseStats, memories: [FAKE_MEMORY] });
|
|
34
|
-
assert.equal(proposals.length, 0);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('analyzeReuseForProposals: emits skill-upgrade when count + sessions clear thresholds', () => {
|
|
38
|
-
const reuseStats = {
|
|
39
|
-
entries: [{
|
|
40
|
-
id: 'm1', count: 4,
|
|
41
|
-
sessions: ['s1', 's2', 's3', 's4'],
|
|
42
|
-
projects: ['/p'],
|
|
43
|
-
firstSurfaced: '2026-04-01T...',
|
|
44
|
-
lastSurfaced: '2026-05-01T...',
|
|
45
|
-
}],
|
|
46
|
-
};
|
|
47
|
-
const proposals = A.analyzeReuseForProposals({ reuseStats, memories: [FAKE_MEMORY] });
|
|
48
|
-
assert.equal(proposals.length, 1);
|
|
49
|
-
const p = proposals[0];
|
|
50
|
-
assert.equal(p.type, 'skill-upgrade');
|
|
51
|
-
assert.equal(p.evidence.memoryId, 'm1');
|
|
52
|
-
assert.equal(p.evidence.reuseCount, 4);
|
|
53
|
-
assert.match(p.proposal.target_path, /^skills\/tailwind-v4-migration/);
|
|
54
|
-
assert.match(p.proposal.diff, /## Problem/);
|
|
55
|
-
assert.match(p.proposal.diff, /Tailwind v4 migration/);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('analyzeReuseForProposals: thresholds are configurable', () => {
|
|
59
|
-
const reuseStats = { entries: [{ id: 'm1', count: 2, sessions: ['s1'], projects: [] }] };
|
|
60
|
-
// Default: needs count>=3 AND sessions>=2 → skip
|
|
61
|
-
assert.equal(A.analyzeReuseForProposals({ reuseStats, memories: [FAKE_MEMORY] }).length, 0);
|
|
62
|
-
// Lower thresholds → emit
|
|
63
|
-
const proposals = A.analyzeReuseForProposals({
|
|
64
|
-
reuseStats, memories: [FAKE_MEMORY],
|
|
65
|
-
thresholds: { minCount: 1, minSessions: 1 },
|
|
66
|
-
});
|
|
67
|
-
assert.equal(proposals.length, 1);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('analyzeReuseForProposals: skips memories not present in store', () => {
|
|
71
|
-
const reuseStats = { entries: [{ id: 'orphan', count: 5, sessions: ['s1', 's2'], projects: [] }] };
|
|
72
|
-
const proposals = A.analyzeReuseForProposals({ reuseStats, memories: [FAKE_MEMORY] });
|
|
73
|
-
assert.equal(proposals.length, 0);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('analyzeReuseForProposals: deterministic ID across runs', () => {
|
|
77
|
-
const reuseStats = { entries: [{ id: 'm1', count: 4, sessions: ['s1', 's2', 's3', 's4'], projects: [] }] };
|
|
78
|
-
const a = A.analyzeReuseForProposals({ reuseStats, memories: [FAKE_MEMORY] })[0];
|
|
79
|
-
const b = A.analyzeReuseForProposals({ reuseStats, memories: [FAKE_MEMORY] })[0];
|
|
80
|
-
assert.equal(a.id, b.id);
|
|
81
|
-
assert.match(a.id, /^skill-[0-9a-f]{10}$/);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// ── clusterMisses ───────────────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
test('clusterMisses: groups by ≥2 token overlap', () => {
|
|
87
|
-
const entries = [
|
|
88
|
-
{ tokens: ['feature', 'flag', 'gradual', 'rollout'], count: 2, samples: [], hash: 'a' },
|
|
89
|
-
{ tokens: ['feature', 'flag', 'percentage', 'enabled'], count: 1, samples: [], hash: 'b' },
|
|
90
|
-
{ tokens: ['unrelated', 'topic', 'completely', 'different'], count: 1, samples: [], hash: 'c' },
|
|
91
|
-
];
|
|
92
|
-
const clusters = A.clusterMisses(entries);
|
|
93
|
-
assert.equal(clusters.length, 2, 'two distinct clusters');
|
|
94
|
-
assert.equal(clusters[0].count, 3, 'feature/flag cluster aggregated count');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('clusterMisses: single-overlap entries do NOT cluster', () => {
|
|
98
|
-
const entries = [
|
|
99
|
-
{ tokens: ['alpha', 'beta', 'gamma', 'delta'], count: 1, samples: [], hash: 'a' },
|
|
100
|
-
{ tokens: ['alpha', 'epsilon', 'zeta', 'eta'], count: 1, samples: [], hash: 'b' },
|
|
101
|
-
];
|
|
102
|
-
const clusters = A.clusterMisses(entries);
|
|
103
|
-
assert.equal(clusters.length, 2);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// ── analyzeMissesForProposals ───────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
test('analyzeMissesForProposals: emits routing-addition when cluster exceeds thresholds', () => {
|
|
109
|
-
const missStats = {
|
|
110
|
-
entries: [
|
|
111
|
-
{ tokens: ['feature', 'flag', 'gradual'], count: 2, samples: ['feature flag gradual rollout 1'], hash: 'a' },
|
|
112
|
-
{ tokens: ['feature', 'flag', 'percent'], count: 1, samples: ['feature flag percent enabled'], hash: 'b' },
|
|
113
|
-
{ tokens: ['feature', 'flag', 'kill'], count: 1, samples: ['feature flag kill switch'], hash: 'c' },
|
|
114
|
-
],
|
|
115
|
-
};
|
|
116
|
-
const proposals = A.analyzeMissesForProposals({ missStats });
|
|
117
|
-
assert.equal(proposals.length, 1);
|
|
118
|
-
const p = proposals[0];
|
|
119
|
-
assert.equal(p.type, 'routing-addition');
|
|
120
|
-
assert.equal(p.proposal.target_path, 'rules/common/agent-intent-routing.md');
|
|
121
|
-
assert.match(p.proposal.diff, /TODO-agent/);
|
|
122
|
-
assert.match(p.proposal.diff, /feature/);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('analyzeMissesForProposals: skips clusters below threshold', () => {
|
|
126
|
-
const missStats = { entries: [{ tokens: ['x', 'y', 'z'], count: 1, samples: [], hash: 'a' }] };
|
|
127
|
-
const proposals = A.analyzeMissesForProposals({ missStats });
|
|
128
|
-
assert.equal(proposals.length, 0);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// ── analyzeAll ──────────────────────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
test('analyzeAll: combines reuse + miss proposals', () => {
|
|
134
|
-
const out = A.analyzeAll({
|
|
135
|
-
reuseStats: { entries: [{ id: 'm1', count: 4, sessions: ['s1', 's2', 's3', 's4'], projects: [] }] },
|
|
136
|
-
missStats: { entries: [
|
|
137
|
-
{ tokens: ['database', 'vacuum', 'job'], count: 2, samples: ['database vacuum job 1'], hash: 'a' },
|
|
138
|
-
{ tokens: ['database', 'vacuum', 'big'], count: 1, samples: ['database vacuum big table'], hash: 'b' },
|
|
139
|
-
{ tokens: ['database', 'vacuum', 'fast'], count: 1, samples: ['database vacuum fast'], hash: 'c' },
|
|
140
|
-
] },
|
|
141
|
-
memories: [FAKE_MEMORY],
|
|
142
|
-
});
|
|
143
|
-
const types = out.map(p => p.type).sort();
|
|
144
|
-
assert.deepEqual(types, ['routing-addition', 'skill-upgrade']);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test('analyzeAll: empty inputs → empty output', () => {
|
|
148
|
-
assert.deepEqual(
|
|
149
|
-
A.analyzeAll({ reuseStats: { entries: [] }, missStats: { entries: [] }, memories: [] }),
|
|
150
|
-
[]
|
|
151
|
-
);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// ── slugify edge cases ─────────────────────────────────────────────────────
|
|
155
|
-
|
|
156
|
-
test('_internals.slugify: empty fallback', () => {
|
|
157
|
-
assert.equal(A._internals.slugify(''), 'untitled');
|
|
158
|
-
assert.equal(A._internals.slugify(null), 'untitled');
|
|
159
|
-
assert.equal(A._internals.slugify(undefined), 'untitled');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test('_internals.slugify: caps length', () => {
|
|
163
|
-
const s = A._internals.slugify('a'.repeat(200));
|
|
164
|
-
assert.ok(s.length <= 60);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('_internals.slugify: special chars normalized', () => {
|
|
168
|
-
assert.equal(A._internals.slugify('Hello, World! 2026'), 'hello-world-2026');
|
|
169
|
-
});
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
// Tests for scripts/evolve/proposals.js
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const test = require('node:test');
|
|
5
|
-
const assert = require('node:assert/strict');
|
|
6
|
-
const fs = require('node:fs');
|
|
7
|
-
const os = require('node:os');
|
|
8
|
-
const path = require('node:path');
|
|
9
|
-
|
|
10
|
-
const P = require('../../scripts/evolve/proposals.js');
|
|
11
|
-
|
|
12
|
-
function tmpDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-evolve-prop-')); }
|
|
13
|
-
function cleanup(dir) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ } }
|
|
14
|
-
|
|
15
|
-
const FAKE_PROPOSAL = {
|
|
16
|
-
id: 'skill-abc123',
|
|
17
|
-
type: 'skill-upgrade',
|
|
18
|
-
evidence: { memoryId: 'm1', reuseCount: 5 },
|
|
19
|
-
proposal: {
|
|
20
|
-
kind: 'create-skill',
|
|
21
|
-
target_path: 'skills/test-skill/SKILL.md',
|
|
22
|
-
diff: '# test skill\n\nbody\n',
|
|
23
|
-
rationale: 'because reuse',
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// ── appendProposal ──────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
test('appendProposal: rejects malformed input', () => {
|
|
30
|
-
const d = tmpDir();
|
|
31
|
-
try {
|
|
32
|
-
assert.throws(() => P.appendProposal({}, d), /must include id, type/);
|
|
33
|
-
assert.throws(() => P.appendProposal({ id: 'x' }, d), /must include id, type/);
|
|
34
|
-
} finally { cleanup(d); }
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('appendProposal: writes pending entry on first call', () => {
|
|
38
|
-
const d = tmpDir();
|
|
39
|
-
try {
|
|
40
|
-
const r = P.appendProposal(FAKE_PROPOSAL, d);
|
|
41
|
-
assert.equal(r.status, 'pending');
|
|
42
|
-
assert.ok(r.created_at);
|
|
43
|
-
const all = P.readLatest(d);
|
|
44
|
-
assert.equal(all.length, 1);
|
|
45
|
-
assert.equal(all[0].id, 'skill-abc123');
|
|
46
|
-
} finally { cleanup(d); }
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('appendProposal: idempotent on same id', () => {
|
|
50
|
-
const d = tmpDir();
|
|
51
|
-
try {
|
|
52
|
-
P.appendProposal(FAKE_PROPOSAL, d);
|
|
53
|
-
P.appendProposal(FAKE_PROPOSAL, d);
|
|
54
|
-
P.appendProposal(FAKE_PROPOSAL, d);
|
|
55
|
-
const all = P.readLatest(d);
|
|
56
|
-
assert.equal(all.length, 1, 'no duplicates');
|
|
57
|
-
const events = P.readAll(d);
|
|
58
|
-
assert.equal(events.length, 1, 'only one event written');
|
|
59
|
-
} finally { cleanup(d); }
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// ── setStatus ───────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
test('setStatus: rejects invalid status', () => {
|
|
65
|
-
const d = tmpDir();
|
|
66
|
-
try {
|
|
67
|
-
P.appendProposal(FAKE_PROPOSAL, d);
|
|
68
|
-
assert.throws(() => P.setStatus('skill-abc123', 'banana', d), /invalid status/);
|
|
69
|
-
} finally { cleanup(d); }
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('setStatus: returns null for unknown id', () => {
|
|
73
|
-
const d = tmpDir();
|
|
74
|
-
try {
|
|
75
|
-
assert.equal(P.setStatus('does-not-exist', 'accepted', d), null);
|
|
76
|
-
} finally { cleanup(d); }
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('setStatus: transitions pending → accepted, preserves diff/evidence', () => {
|
|
80
|
-
const d = tmpDir();
|
|
81
|
-
try {
|
|
82
|
-
P.appendProposal(FAKE_PROPOSAL, d);
|
|
83
|
-
const r = P.setStatus('skill-abc123', 'accepted', d, { applied_path: '/tmp/foo.md' });
|
|
84
|
-
assert.equal(r.status, 'accepted');
|
|
85
|
-
assert.equal(r.applied_path, '/tmp/foo.md');
|
|
86
|
-
|
|
87
|
-
const cur = P.findById('skill-abc123', d);
|
|
88
|
-
assert.equal(cur.status, 'accepted');
|
|
89
|
-
assert.equal(cur.applied_path, '/tmp/foo.md');
|
|
90
|
-
// Original proposal data preserved.
|
|
91
|
-
assert.equal(cur.proposal.target_path, 'skills/test-skill/SKILL.md');
|
|
92
|
-
assert.equal(cur.evidence.reuseCount, 5);
|
|
93
|
-
} finally { cleanup(d); }
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('setStatus: pending → rejected → accepted preserves audit trail', () => {
|
|
97
|
-
const d = tmpDir();
|
|
98
|
-
try {
|
|
99
|
-
P.appendProposal(FAKE_PROPOSAL, d);
|
|
100
|
-
P.setStatus('skill-abc123', 'rejected', d, { note: 'no thanks' });
|
|
101
|
-
P.setStatus('skill-abc123', 'accepted', d, { applied_path: '/x' });
|
|
102
|
-
const events = P.readAll(d);
|
|
103
|
-
assert.equal(events.length, 3, '3 events: append, reject, accept');
|
|
104
|
-
assert.equal(P.findById('skill-abc123', d).status, 'accepted');
|
|
105
|
-
} finally { cleanup(d); }
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// ── listByStatus / findById ─────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
test('listByStatus: filters latest state', () => {
|
|
111
|
-
const d = tmpDir();
|
|
112
|
-
try {
|
|
113
|
-
P.appendProposal(FAKE_PROPOSAL, d);
|
|
114
|
-
P.appendProposal({ ...FAKE_PROPOSAL, id: 'skill-second' }, d);
|
|
115
|
-
P.setStatus('skill-second', 'accepted', d);
|
|
116
|
-
assert.equal(P.listByStatus('pending', d).length, 1);
|
|
117
|
-
assert.equal(P.listByStatus('accepted', d).length, 1);
|
|
118
|
-
assert.equal(P.listByStatus(null, d).length, 2);
|
|
119
|
-
} finally { cleanup(d); }
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('findById: returns null when absent', () => {
|
|
123
|
-
const d = tmpDir();
|
|
124
|
-
try {
|
|
125
|
-
assert.equal(P.findById('nope', d), null);
|
|
126
|
-
} finally { cleanup(d); }
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// ── applyProposalToDisk ─────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
test('applyProposalToDisk: writes diff to target_path under repoRoot', () => {
|
|
132
|
-
const d = tmpDir();
|
|
133
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-apply-'));
|
|
134
|
-
try {
|
|
135
|
-
P.appendProposal(FAKE_PROPOSAL, d);
|
|
136
|
-
const proposal = P.findById('skill-abc123', d);
|
|
137
|
-
const r = P.applyProposalToDisk(proposal, { repoRoot: root });
|
|
138
|
-
assert.equal(r.written, true);
|
|
139
|
-
assert.ok(fs.existsSync(r.path));
|
|
140
|
-
assert.equal(fs.readFileSync(r.path, 'utf8'), '# test skill\n\nbody\n');
|
|
141
|
-
} finally { cleanup(d); cleanup(root); }
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test('applyProposalToDisk: refuses to overwrite without --overwrite', () => {
|
|
145
|
-
const d = tmpDir();
|
|
146
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-apply-'));
|
|
147
|
-
try {
|
|
148
|
-
P.appendProposal(FAKE_PROPOSAL, d);
|
|
149
|
-
const proposal = P.findById('skill-abc123', d);
|
|
150
|
-
P.applyProposalToDisk(proposal, { repoRoot: root });
|
|
151
|
-
assert.throws(
|
|
152
|
-
() => P.applyProposalToDisk(proposal, { repoRoot: root }),
|
|
153
|
-
/refusing to overwrite/
|
|
154
|
-
);
|
|
155
|
-
// overwrite=true succeeds
|
|
156
|
-
const r = P.applyProposalToDisk({
|
|
157
|
-
...proposal,
|
|
158
|
-
proposal: { ...proposal.proposal, diff: 'new content' },
|
|
159
|
-
}, { repoRoot: root, overwrite: true });
|
|
160
|
-
assert.equal(fs.readFileSync(r.path, 'utf8'), 'new content');
|
|
161
|
-
} finally { cleanup(d); cleanup(root); }
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test('applyProposalToDisk: requires repoRoot', () => {
|
|
165
|
-
assert.throws(() => P.applyProposalToDisk(FAKE_PROPOSAL, {}), /repoRoot is required/);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('applyProposalToDisk: rejects proposal without diff/target_path', () => {
|
|
169
|
-
assert.throws(
|
|
170
|
-
() => P.applyProposalToDisk({ id: 'x', proposal: {} }, { repoRoot: '/tmp' }),
|
|
171
|
-
/lacks diff or target_path/
|
|
172
|
-
);
|
|
173
|
-
});
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
// Tests for scripts/evolve/stats.js
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const test = require('node:test');
|
|
5
|
-
const assert = require('node:assert/strict');
|
|
6
|
-
const fs = require('node:fs');
|
|
7
|
-
const os = require('node:os');
|
|
8
|
-
const path = require('node:path');
|
|
9
|
-
|
|
10
|
-
const stats = require('../../scripts/evolve/stats.js');
|
|
11
|
-
|
|
12
|
-
function tmpDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-evolve-stats-')); }
|
|
13
|
-
function cleanup(dir) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ } }
|
|
14
|
-
|
|
15
|
-
// ── recordSurface ────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
test('recordSurface: rejects missing memoryId', () => {
|
|
18
|
-
const d = tmpDir();
|
|
19
|
-
try {
|
|
20
|
-
assert.equal(stats.recordSurface({ sessionId: 's1' }, d), false);
|
|
21
|
-
} finally { cleanup(d); }
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('recordSurface: bumps count on first session, idempotent within same session', () => {
|
|
25
|
-
const d = tmpDir();
|
|
26
|
-
try {
|
|
27
|
-
assert.equal(stats.recordSurface({ memoryId: 'm1', sessionId: 's1', projectRoot: '/proj' }, d), true);
|
|
28
|
-
assert.equal(stats.recordSurface({ memoryId: 'm1', sessionId: 's1', projectRoot: '/proj' }, d), true);
|
|
29
|
-
assert.equal(stats.recordSurface({ memoryId: 'm1', sessionId: 's1', projectRoot: '/proj' }, d), true);
|
|
30
|
-
|
|
31
|
-
const r = stats.getReuseStats(d);
|
|
32
|
-
assert.equal(r.total_memories_tracked, 1);
|
|
33
|
-
assert.equal(r.total_surfaces, 1, 'same session should only count once');
|
|
34
|
-
assert.equal(r.entries[0].count, 1);
|
|
35
|
-
assert.deepEqual(r.entries[0].sessions, ['s1']);
|
|
36
|
-
assert.deepEqual(r.entries[0].projects, ['/proj']);
|
|
37
|
-
} finally { cleanup(d); }
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('recordSurface: bumps count on each new session', () => {
|
|
41
|
-
const d = tmpDir();
|
|
42
|
-
try {
|
|
43
|
-
for (let i = 0; i < 4; i++) {
|
|
44
|
-
stats.recordSurface({ memoryId: 'm1', sessionId: `s${i}` }, d);
|
|
45
|
-
}
|
|
46
|
-
const r = stats.getReuseStats(d);
|
|
47
|
-
assert.equal(r.entries[0].count, 4);
|
|
48
|
-
assert.equal(r.entries[0].sessions.length, 4);
|
|
49
|
-
} finally { cleanup(d); }
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('recordSurface: tracks distinct projects without duplicating', () => {
|
|
53
|
-
const d = tmpDir();
|
|
54
|
-
try {
|
|
55
|
-
stats.recordSurface({ memoryId: 'm1', sessionId: 's1', projectRoot: '/a' }, d);
|
|
56
|
-
stats.recordSurface({ memoryId: 'm1', sessionId: 's2', projectRoot: '/a' }, d);
|
|
57
|
-
stats.recordSurface({ memoryId: 'm1', sessionId: 's3', projectRoot: '/b' }, d);
|
|
58
|
-
const r = stats.getReuseStats(d);
|
|
59
|
-
assert.deepEqual(r.entries[0].projects, ['/a', '/b']);
|
|
60
|
-
} finally { cleanup(d); }
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('recordSurface: never throws on bad dir; returns false', () => {
|
|
64
|
-
// Create a file then pass a subpath of it as the dir — fails on all platforms
|
|
65
|
-
// because mkdirSync can't create a directory where a file already exists.
|
|
66
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-stats-bad-'));
|
|
67
|
-
const fileAsDir = path.join(tmp, 'not-a-dir');
|
|
68
|
-
fs.writeFileSync(fileAsDir, 'x');
|
|
69
|
-
try {
|
|
70
|
-
assert.equal(stats.recordSurface({ memoryId: 'm1', sessionId: 's1' }, path.join(fileAsDir, 'sub')), false);
|
|
71
|
-
} finally {
|
|
72
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// ── recordRoutingMiss ────────────────────────────────────────────────────────
|
|
77
|
-
|
|
78
|
-
test('recordRoutingMiss: rejects empty prompt', () => {
|
|
79
|
-
const d = tmpDir();
|
|
80
|
-
try {
|
|
81
|
-
assert.equal(stats.recordRoutingMiss({ prompt: '', sessionId: 's1' }, d), false);
|
|
82
|
-
assert.equal(stats.recordRoutingMiss({ sessionId: 's1' }, d), false);
|
|
83
|
-
} finally { cleanup(d); }
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('recordRoutingMiss: appends one line per call', () => {
|
|
87
|
-
const d = tmpDir();
|
|
88
|
-
try {
|
|
89
|
-
stats.recordRoutingMiss({ prompt: 'how do I reset postgres replication', sessionId: 's1' }, d);
|
|
90
|
-
stats.recordRoutingMiss({ prompt: 'postgres logical replication is stuck', sessionId: 's2' }, d);
|
|
91
|
-
const m = stats.getRoutingMissStats(d);
|
|
92
|
-
assert.equal(m.total_misses, 2);
|
|
93
|
-
assert.equal(m.unique_prompts, 2);
|
|
94
|
-
assert.ok(m.entries.every(e => Array.isArray(e.tokens) && e.tokens.length > 0));
|
|
95
|
-
} finally { cleanup(d); }
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('recordRoutingMiss: same prompt aggregates by hash', () => {
|
|
99
|
-
const d = tmpDir();
|
|
100
|
-
try {
|
|
101
|
-
stats.recordRoutingMiss({ prompt: 'identical prompt body', sessionId: 's1' }, d);
|
|
102
|
-
stats.recordRoutingMiss({ prompt: 'identical prompt body', sessionId: 's2' }, d);
|
|
103
|
-
stats.recordRoutingMiss({ prompt: 'identical prompt body', sessionId: 's3' }, d);
|
|
104
|
-
const m = stats.getRoutingMissStats(d);
|
|
105
|
-
assert.equal(m.total_misses, 3);
|
|
106
|
-
assert.equal(m.unique_prompts, 1);
|
|
107
|
-
assert.equal(m.entries[0].count, 3);
|
|
108
|
-
} finally { cleanup(d); }
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('recordRoutingMiss: caps stored prompt to 1000 chars', () => {
|
|
112
|
-
const d = tmpDir();
|
|
113
|
-
try {
|
|
114
|
-
const huge = 'x'.repeat(5000);
|
|
115
|
-
stats.recordRoutingMiss({ prompt: huge, sessionId: 's1' }, d);
|
|
116
|
-
const m = stats.getRoutingMissStats(d);
|
|
117
|
-
assert.equal(m.entries[0].samples[0].length, 1000);
|
|
118
|
-
} finally { cleanup(d); }
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// ── empty / readback ────────────────────────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
test('readReuse on empty dir returns sane defaults', () => {
|
|
124
|
-
const d = tmpDir();
|
|
125
|
-
try {
|
|
126
|
-
const r = stats.readReuse(d);
|
|
127
|
-
assert.deepEqual(r.byMemory, {});
|
|
128
|
-
assert.equal(r.lastUpdated, null);
|
|
129
|
-
} finally { cleanup(d); }
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('getRoutingMissStats on empty dir returns empty arrays', () => {
|
|
133
|
-
const d = tmpDir();
|
|
134
|
-
try {
|
|
135
|
-
const m = stats.getRoutingMissStats(d);
|
|
136
|
-
assert.equal(m.total_misses, 0);
|
|
137
|
-
assert.equal(m.unique_prompts, 0);
|
|
138
|
-
assert.deepEqual(m.entries, []);
|
|
139
|
-
} finally { cleanup(d); }
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('resetAll clears both stores', () => {
|
|
143
|
-
const d = tmpDir();
|
|
144
|
-
try {
|
|
145
|
-
stats.recordSurface({ memoryId: 'm', sessionId: 's' }, d);
|
|
146
|
-
stats.recordRoutingMiss({ prompt: 'something here', sessionId: 's' }, d);
|
|
147
|
-
assert.equal(stats.resetAll(d), true);
|
|
148
|
-
assert.equal(stats.getReuseStats(d).total_memories_tracked, 0);
|
|
149
|
-
assert.equal(stats.getRoutingMissStats(d).total_misses, 0);
|
|
150
|
-
} finally { cleanup(d); }
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// ── tokenize / hash ──────────────────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
test('_internals.tokenize lowercases and splits', () => {
|
|
156
|
-
const out = stats._internals.tokenize('Hello, World! foo-bar/baz');
|
|
157
|
-
assert.ok(out.includes('hello'));
|
|
158
|
-
assert.ok(out.includes('world'));
|
|
159
|
-
assert.ok(out.includes('foo-bar/baz') || out.includes('foo-bar') || out.includes('baz'));
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test('_internals.hashPrompt is stable + 12 hex chars', () => {
|
|
163
|
-
const a = stats._internals.hashPrompt('hello');
|
|
164
|
-
const b = stats._internals.hashPrompt('hello');
|
|
165
|
-
assert.equal(a, b);
|
|
166
|
-
assert.match(a, /^[0-9a-f]{12}$/);
|
|
167
|
-
});
|