kodelyth-ecc 1.5.10 → 1.7.1

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.
Files changed (273) hide show
  1. package/.github/workflows/publish.yml +19 -0
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +475 -0
  4. package/CLAUDE.md +48 -16
  5. package/README.md +285 -81
  6. package/VERSION +1 -1
  7. package/actions/ecc-review/README.md +243 -0
  8. package/actions/ecc-review/action.yml +158 -0
  9. package/actions/ecc-review/post-comment.js +171 -0
  10. package/actions/ecc-review/run-review.js +285 -0
  11. package/agents/backdoor-hunter.md +260 -0
  12. package/agents/chaos-engineer.md +251 -0
  13. package/agents/code-stealer-detector.md +228 -0
  14. package/agents/jailbreak-tester.md +222 -0
  15. package/agents/license-violation-finder.md +212 -0
  16. package/agents/prompt-injection-hunter.md +126 -0
  17. package/agents/secret-hunter.md +209 -0
  18. package/agents/supply-chain-auditor.md +195 -0
  19. package/bin/kodelyth-ecc.js +875 -1
  20. package/bundles/enterprise.md +172 -0
  21. package/bundles/indie-hacker.md +106 -0
  22. package/bundles/red-team.md +138 -0
  23. package/cat +0 -0
  24. package/commands/dashboard.md +67 -0
  25. package/commands/devil-mode.md +121 -0
  26. package/commands/memory-evolve.md +71 -0
  27. package/commands/replay.md +61 -0
  28. package/commands/route-model.md +48 -0
  29. package/commands/swarm.md +68 -0
  30. package/commands/verify-supply-chain.md +59 -0
  31. package/docs/dashboard.md +211 -0
  32. package/docs/evolve.md +303 -0
  33. package/docs/mcp-clients.md +167 -0
  34. package/docs/mcp.md +178 -0
  35. package/docs/replay.md +244 -0
  36. package/docs/supply-chain.md +207 -0
  37. package/docs/swarm.md +243 -0
  38. package/hooks/hooks.json +52 -0
  39. package/hooks/memory/auto-recall.js +29 -1
  40. package/hooks/safety/README.md +124 -0
  41. package/hooks/safety/lib/patterns.js +179 -0
  42. package/hooks/safety/prompt-injection-guard.js +179 -0
  43. package/hooks/safety/token-budget.js +229 -0
  44. package/install.ps1 +199 -2
  45. package/install.sh +285 -3
  46. package/package.json +30 -4
  47. package/rules/common/agent-intent-routing.md +182 -0
  48. package/rules/common/cost-aware-model-routing.md +152 -0
  49. package/scripts/dashboard/data.js +353 -0
  50. package/scripts/dashboard/server.js +324 -0
  51. package/scripts/dashboard/static/index.html +645 -0
  52. package/scripts/evolve/analyze.js +303 -0
  53. package/scripts/evolve/proposals.js +162 -0
  54. package/scripts/evolve/stats.js +219 -0
  55. package/scripts/mcp/catalog.js +244 -0
  56. package/scripts/mcp/client.js +181 -0
  57. package/scripts/mcp/prompts.js +133 -0
  58. package/scripts/mcp/resources.js +94 -0
  59. package/scripts/mcp/server.js +153 -0
  60. package/scripts/mcp/tools.js +465 -0
  61. package/scripts/replay/bundle.js +191 -0
  62. package/scripts/replay/replay.js +107 -0
  63. package/scripts/router/classify.js +232 -0
  64. package/scripts/supply-chain/manifest.js +155 -0
  65. package/scripts/supply-chain/sbom.js +202 -0
  66. package/scripts/supply-chain/verify.js +102 -0
  67. package/scripts/swarm/build-plan.js +193 -0
  68. package/skills/cost-aware-model-routing/SKILL.md +153 -0
  69. package/skills/kodelyth-quickstart/SKILL.md +7 -0
  70. package/skills/observability-dashboard/SKILL.md +119 -0
  71. package/skills/self-evolving-memory/SKILL.md +175 -0
  72. package/skills/session-replay/SKILL.md +199 -0
  73. package/skills/supply-chain-verification/SKILL.md +201 -0
  74. package/skills/swarm-orchestrator/SKILL.md +177 -0
  75. package/social/card-install.svg +1 -1
  76. package/social/facebook-group/POST.md +121 -0
  77. package/social/facebook-group/fb-1-3am-debug.png +0 -0
  78. package/social/facebook-group/fb-1-3am-debug.svg +97 -0
  79. package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
  80. package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
  81. package/social/facebook-group/fb-3-before-after.png +0 -0
  82. package/social/facebook-group/fb-3-before-after.svg +94 -0
  83. package/social/facebook-v150.svg +6 -6
  84. package/social/github-social-preview.svg +119 -100
  85. package/social/readme-hero.svg +12 -12
  86. package/social/section-agents.svg +57 -0
  87. package/social/section-author.svg +54 -0
  88. package/social/section-dashboard.svg +59 -0
  89. package/social/section-devil.svg +54 -0
  90. package/social/section-hooks.svg +51 -0
  91. package/social/section-install.svg +42 -0
  92. package/social/section-learning.svg +52 -0
  93. package/social/section-mcp.svg +46 -0
  94. package/social/section-memory.svg +57 -0
  95. package/social/section-parallel.svg +72 -0
  96. package/social/section-routing.svg +51 -0
  97. package/social/x-card-agents-grid.svg +6 -6
  98. package/tests/dashboard/data.test.js +235 -0
  99. package/tests/dashboard/server.test.js +240 -0
  100. package/tests/evolve/analyze.test.js +169 -0
  101. package/tests/evolve/proposals.test.js +173 -0
  102. package/tests/evolve/stats.test.js +167 -0
  103. package/tests/mcp/catalog.test.js +98 -0
  104. package/tests/mcp/client.test.js +113 -0
  105. package/tests/mcp/resources-prompts.test.js +70 -0
  106. package/tests/mcp/tools.test.js +159 -0
  107. package/tests/replay/bundle.test.js +181 -0
  108. package/tests/replay/replay.test.js +131 -0
  109. package/tests/router/classify.test.js +164 -0
  110. package/tests/safety/patterns.test.js +94 -0
  111. package/tests/safety/prompt-injection-guard.test.js +111 -0
  112. package/tests/safety/token-budget.test.js +119 -0
  113. package/tests/supply-chain/manifest.test.js +147 -0
  114. package/tests/supply-chain/sbom.test.js +170 -0
  115. package/tests/supply-chain/verify.test.js +146 -0
  116. package/tests/swarm/build-plan.test.js +188 -0
  117. package/wiki/Agent-Reference.md +58 -7
  118. package/wiki/FAQ.md +204 -7
  119. package/wiki/Home.md +104 -29
  120. package/wiki/Hook-Reference.md +1 -1
  121. package/wiki/Installation-Guide.md +109 -6
  122. package/wiki/Platform-Support.md +189 -72
  123. package/wiki/Skill-Reference.md +101 -6
  124. package/.agent/rules/README.md +0 -108
  125. package/.agent/rules/agents.md +0 -94
  126. package/.agent/rules/code-review.md +0 -124
  127. package/.agent/rules/coding-style.md +0 -122
  128. package/.agent/rules/design-quality.md +0 -63
  129. package/.agent/rules/development-workflow.md +0 -44
  130. package/.agent/rules/git-mastery.md +0 -366
  131. package/.agent/rules/git-workflow.md +0 -24
  132. package/.agent/rules/hooks.md +0 -25
  133. package/.agent/rules/kodelyth-always-on.md +0 -63
  134. package/.agent/rules/kodelyth-quickstart.md +0 -207
  135. package/.agent/rules/observability.md +0 -390
  136. package/.agent/rules/patterns.md +0 -50
  137. package/.agent/rules/performance.md +0 -178
  138. package/.agent/rules/security.md +0 -58
  139. package/.agent/rules/smart-debug.md +0 -241
  140. package/.agent/rules/soul.md +0 -32
  141. package/.agent/rules/testing.md +0 -46
  142. package/.agent/skills/api-guardian.md +0 -279
  143. package/.agent/skills/architect.md +0 -211
  144. package/.agent/skills/build-error-resolver.md +0 -114
  145. package/.agent/skills/chief-of-staff.md +0 -151
  146. package/.agent/skills/code-architect.md +0 -71
  147. package/.agent/skills/code-explorer.md +0 -69
  148. package/.agent/skills/code-reviewer.md +0 -237
  149. package/.agent/skills/code-simplifier.md +0 -47
  150. package/.agent/skills/comment-analyzer.md +0 -45
  151. package/.agent/skills/conversation-analyzer.md +0 -52
  152. package/.agent/skills/cpp-build-resolver.md +0 -90
  153. package/.agent/skills/cpp-reviewer.md +0 -72
  154. package/.agent/skills/csharp-reviewer.md +0 -101
  155. package/.agent/skills/dart-build-resolver.md +0 -201
  156. package/.agent/skills/database-reviewer.md +0 -91
  157. package/.agent/skills/debug-detective.md +0 -409
  158. package/.agent/skills/doc-updater.md +0 -107
  159. package/.agent/skills/docs-lookup.md +0 -68
  160. package/.agent/skills/e2e-runner.md +0 -107
  161. package/.agent/skills/flutter-reviewer.md +0 -243
  162. package/.agent/skills/gan-evaluator.md +0 -209
  163. package/.agent/skills/gan-generator.md +0 -131
  164. package/.agent/skills/gan-planner.md +0 -99
  165. package/.agent/skills/go-build-resolver.md +0 -94
  166. package/.agent/skills/go-reviewer.md +0 -76
  167. package/.agent/skills/harness-optimizer.md +0 -35
  168. package/.agent/skills/healthcare-reviewer.md +0 -83
  169. package/.agent/skills/java-build-resolver.md +0 -153
  170. package/.agent/skills/java-reviewer.md +0 -92
  171. package/.agent/skills/kodelyth-advisor.md +0 -172
  172. package/.agent/skills/kotlin-build-resolver.md +0 -118
  173. package/.agent/skills/kotlin-reviewer.md +0 -159
  174. package/.agent/skills/loop-operator.md +0 -36
  175. package/.agent/skills/migration-guide.md +0 -368
  176. package/.agent/skills/opensource-forker.md +0 -198
  177. package/.agent/skills/opensource-packager.md +0 -249
  178. package/.agent/skills/opensource-sanitizer.md +0 -188
  179. package/.agent/skills/pair-programmer.md +0 -260
  180. package/.agent/skills/performance-optimizer.md +0 -446
  181. package/.agent/skills/planner.md +0 -212
  182. package/.agent/skills/pr-test-analyzer.md +0 -45
  183. package/.agent/skills/python-reviewer.md +0 -98
  184. package/.agent/skills/pytorch-build-resolver.md +0 -120
  185. package/.agent/skills/refactor-cleaner.md +0 -85
  186. package/.agent/skills/rust-build-resolver.md +0 -148
  187. package/.agent/skills/rust-reviewer.md +0 -94
  188. package/.agent/skills/security-reviewer.md +0 -108
  189. package/.agent/skills/seo-specialist.md +0 -62
  190. package/.agent/skills/silent-failure-hunter.md +0 -50
  191. package/.agent/skills/tdd-guide.md +0 -91
  192. package/.agent/skills/type-design-analyzer.md +0 -41
  193. package/.agent/skills/typescript-reviewer.md +0 -112
  194. package/.agent/skills/ux-reviewer.md +0 -483
  195. package/.agent/workflows/agent-sort.md +0 -23
  196. package/.agent/workflows/aside.md +0 -164
  197. package/.agent/workflows/build-fix.md +0 -62
  198. package/.agent/workflows/checkpoint.md +0 -74
  199. package/.agent/workflows/claw.md +0 -23
  200. package/.agent/workflows/code-review.md +0 -289
  201. package/.agent/workflows/context-budget.md +0 -23
  202. package/.agent/workflows/cpp-build.md +0 -173
  203. package/.agent/workflows/cpp-review.md +0 -132
  204. package/.agent/workflows/cpp-test.md +0 -251
  205. package/.agent/workflows/devfleet.md +0 -23
  206. package/.agent/workflows/docs.md +0 -23
  207. package/.agent/workflows/e2e.md +0 -268
  208. package/.agent/workflows/eval.md +0 -23
  209. package/.agent/workflows/evolve.md +0 -178
  210. package/.agent/workflows/feature-dev.md +0 -49
  211. package/.agent/workflows/flutter-build.md +0 -164
  212. package/.agent/workflows/flutter-review.md +0 -116
  213. package/.agent/workflows/flutter-test.md +0 -144
  214. package/.agent/workflows/gan-build.md +0 -99
  215. package/.agent/workflows/gan-design.md +0 -35
  216. package/.agent/workflows/go-build.md +0 -183
  217. package/.agent/workflows/go-review.md +0 -148
  218. package/.agent/workflows/go-test.md +0 -268
  219. package/.agent/workflows/gradle-build.md +0 -70
  220. package/.agent/workflows/harness-audit.md +0 -73
  221. package/.agent/workflows/hookify-configure.md +0 -14
  222. package/.agent/workflows/hookify-help.md +0 -46
  223. package/.agent/workflows/hookify-list.md +0 -21
  224. package/.agent/workflows/hookify.md +0 -50
  225. package/.agent/workflows/instinct-export.md +0 -66
  226. package/.agent/workflows/instinct-import.md +0 -114
  227. package/.agent/workflows/instinct-status.md +0 -59
  228. package/.agent/workflows/jira.md +0 -106
  229. package/.agent/workflows/kotlin-build.md +0 -174
  230. package/.agent/workflows/kotlin-review.md +0 -140
  231. package/.agent/workflows/kotlin-test.md +0 -312
  232. package/.agent/workflows/learn-eval.md +0 -116
  233. package/.agent/workflows/learn.md +0 -70
  234. package/.agent/workflows/loop-start.md +0 -32
  235. package/.agent/workflows/loop-status.md +0 -24
  236. package/.agent/workflows/model-route.md +0 -26
  237. package/.agent/workflows/multi-backend.md +0 -158
  238. package/.agent/workflows/multi-execute.md +0 -315
  239. package/.agent/workflows/multi-frontend.md +0 -158
  240. package/.agent/workflows/multi-plan.md +0 -268
  241. package/.agent/workflows/multi-workflow.md +0 -191
  242. package/.agent/workflows/orchestrate.md +0 -135
  243. package/.agent/workflows/plan.md +0 -117
  244. package/.agent/workflows/pm2.md +0 -272
  245. package/.agent/workflows/projects.md +0 -39
  246. package/.agent/workflows/promote.md +0 -41
  247. package/.agent/workflows/prompt-optimize.md +0 -23
  248. package/.agent/workflows/prp-commit.md +0 -112
  249. package/.agent/workflows/prp-implement.md +0 -385
  250. package/.agent/workflows/prp-plan.md +0 -502
  251. package/.agent/workflows/prp-pr.md +0 -184
  252. package/.agent/workflows/prp-prd.md +0 -447
  253. package/.agent/workflows/prune.md +0 -31
  254. package/.agent/workflows/python-review.md +0 -297
  255. package/.agent/workflows/quality-gate.md +0 -29
  256. package/.agent/workflows/refactor-clean.md +0 -80
  257. package/.agent/workflows/resume-session.md +0 -156
  258. package/.agent/workflows/review-pr.md +0 -37
  259. package/.agent/workflows/rules-distill.md +0 -20
  260. package/.agent/workflows/rust-build.md +0 -187
  261. package/.agent/workflows/rust-review.md +0 -142
  262. package/.agent/workflows/rust-test.md +0 -308
  263. package/.agent/workflows/santa-loop.md +0 -175
  264. package/.agent/workflows/save-session.md +0 -275
  265. package/.agent/workflows/sessions.md +0 -333
  266. package/.agent/workflows/setup-pm.md +0 -80
  267. package/.agent/workflows/skill-create.md +0 -174
  268. package/.agent/workflows/skill-health.md +0 -54
  269. package/.agent/workflows/tdd.md +0 -231
  270. package/.agent/workflows/test-coverage.md +0 -69
  271. package/.agent/workflows/update-codemaps.md +0 -72
  272. package/.agent/workflows/update-docs.md +0 -84
  273. package/.agent/workflows/verify.md +0 -23
@@ -0,0 +1,169 @@
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
+ });
@@ -0,0 +1,173 @@
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
+ });
@@ -0,0 +1,167 @@
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
+ });
@@ -0,0 +1,98 @@
1
+ // Tests for scripts/mcp/catalog.js — read-only loader for ECC catalog files.
2
+ 'use strict';
3
+
4
+ const test = require('node:test');
5
+ const assert = require('node:assert/strict');
6
+
7
+ const catalog = require('../../scripts/mcp/catalog');
8
+
9
+ test('parseFrontmatter parses simple key/value', () => {
10
+ const { meta, body } = catalog.parseFrontmatter('---\nname: foo\ndescription: bar\n---\nhello');
11
+ assert.equal(meta.name, 'foo');
12
+ assert.equal(meta.description, 'bar');
13
+ assert.equal(body, 'hello');
14
+ });
15
+
16
+ test('parseFrontmatter handles folded scalar (>)', () => {
17
+ const raw = '---\nname: x\ndescription: >\n line one\n line two\n---\n# body';
18
+ const { meta, body } = catalog.parseFrontmatter(raw);
19
+ assert.equal(meta.name, 'x');
20
+ assert.match(meta.description, /line one/);
21
+ assert.match(meta.description, /line two/);
22
+ assert.equal(body, '# body');
23
+ });
24
+
25
+ test('parseFrontmatter handles missing frontmatter', () => {
26
+ const { meta, body } = catalog.parseFrontmatter('# just markdown');
27
+ assert.deepEqual(meta, {});
28
+ assert.equal(body, '# just markdown');
29
+ });
30
+
31
+ test('loadAgents finds at least one known agent', () => {
32
+ const agents = catalog.loadAgents();
33
+ assert.ok(Array.isArray(agents));
34
+ assert.ok(agents.length >= 60, `expected >=60 agents, got ${agents.length}`);
35
+ const dd = agents.find(a => a.name === 'debug-detective');
36
+ assert.ok(dd, 'debug-detective agent must be present');
37
+ assert.ok(dd.description.length > 0);
38
+ assert.ok(dd.body.length > 100);
39
+ });
40
+
41
+ test('loadAgents includes the new devil-mode crew', () => {
42
+ const agents = catalog.loadAgents();
43
+ const names = new Set(agents.map(a => a.name));
44
+ for (const expected of [
45
+ 'prompt-injection-hunter',
46
+ 'supply-chain-auditor',
47
+ 'jailbreak-tester',
48
+ 'backdoor-hunter',
49
+ 'chaos-engineer',
50
+ ]) {
51
+ assert.ok(names.has(expected), `missing devil-mode agent: ${expected}`);
52
+ }
53
+ });
54
+
55
+ test('loadSkills returns a populated list with descriptions', () => {
56
+ const skills = catalog.loadSkills();
57
+ assert.ok(skills.length >= 100, `expected >=100 skills, got ${skills.length}`);
58
+ const ae = skills.find(s => s.name === 'agentic-engineering');
59
+ assert.ok(ae, 'agentic-engineering skill must be present');
60
+ assert.ok(ae.description.length > 0);
61
+ });
62
+
63
+ test('loadCommands returns commands and supports findCommand with leading slash', () => {
64
+ const cmds = catalog.loadCommands();
65
+ assert.ok(cmds.length >= 50);
66
+ const found1 = catalog.findCommand('code-review');
67
+ const found2 = catalog.findCommand('/code-review');
68
+ assert.ok(found1 && found2 && found1.name === found2.name);
69
+ });
70
+
71
+ test('loadAllRules returns the intent routing rule', () => {
72
+ const rules = catalog.loadAllRules();
73
+ const r = rules.find(x => x.name === 'agent-intent-routing');
74
+ assert.ok(r, 'agent-intent-routing rule must be present');
75
+ assert.ok(r.body.length > 500);
76
+ });
77
+
78
+ test('loadBundles returns the three power bundles', () => {
79
+ const bundles = catalog.loadBundles();
80
+ const names = new Set(bundles.map(b => b.name));
81
+ assert.ok(names.has('indie-hacker'));
82
+ assert.ok(names.has('red-team'));
83
+ assert.ok(names.has('enterprise'));
84
+ });
85
+
86
+ test('stats returns expected fields', () => {
87
+ const s = catalog.stats();
88
+ assert.ok(typeof s.agents === 'number' && s.agents > 0);
89
+ assert.ok(typeof s.skills === 'number' && s.skills > 0);
90
+ assert.ok(typeof s.commands === 'number' && s.commands > 0);
91
+ assert.ok(typeof s.rules === 'number' && s.rules > 0);
92
+ assert.ok(typeof s.bundles === 'number');
93
+ assert.ok(typeof s.root === 'string');
94
+ });
95
+
96
+ test('findAgent returns null for unknown name', () => {
97
+ assert.equal(catalog.findAgent('this-agent-does-not-exist'), null);
98
+ });