chainlesschain 0.47.8 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/bin/chainlesschain.js +0 -0
  2. package/package.json +10 -8
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
  5. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  6. package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
  7. package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
  8. package/src/assets/web-panel/index.html +2 -2
  9. package/src/commands/activitypub.js +533 -0
  10. package/src/commands/codegen.js +303 -0
  11. package/src/commands/collab.js +482 -0
  12. package/src/commands/compliance.js +597 -6
  13. package/src/commands/crosschain.js +382 -0
  14. package/src/commands/dbevo.js +388 -0
  15. package/src/commands/dev.js +411 -0
  16. package/src/commands/federation.js +427 -0
  17. package/src/commands/fusion.js +332 -0
  18. package/src/commands/governance.js +505 -0
  19. package/src/commands/hardening.js +110 -0
  20. package/src/commands/incentive.js +373 -0
  21. package/src/commands/inference.js +304 -0
  22. package/src/commands/infra.js +361 -0
  23. package/src/commands/kg.js +371 -0
  24. package/src/commands/marketplace.js +326 -0
  25. package/src/commands/matrix.js +283 -0
  26. package/src/commands/mcp.js +441 -18
  27. package/src/commands/nlprog.js +329 -0
  28. package/src/commands/nostr.js +196 -7
  29. package/src/commands/ops.js +408 -0
  30. package/src/commands/perception.js +385 -0
  31. package/src/commands/pqc.js +34 -0
  32. package/src/commands/privacy.js +345 -0
  33. package/src/commands/quantization.js +280 -0
  34. package/src/commands/recommend.js +336 -0
  35. package/src/commands/reputation.js +349 -0
  36. package/src/commands/runtime.js +500 -0
  37. package/src/commands/sla.js +352 -0
  38. package/src/commands/social.js +265 -0
  39. package/src/commands/stress.js +252 -0
  40. package/src/commands/tech.js +268 -0
  41. package/src/commands/tenant.js +576 -0
  42. package/src/commands/trust.js +366 -0
  43. package/src/harness/mcp-client.js +330 -54
  44. package/src/index.js +114 -0
  45. package/src/lib/activitypub-bridge.js +623 -0
  46. package/src/lib/aiops.js +523 -0
  47. package/src/lib/autonomous-developer.js +524 -0
  48. package/src/lib/code-agent.js +442 -0
  49. package/src/lib/collaboration-governance.js +556 -0
  50. package/src/lib/community-governance.js +649 -0
  51. package/src/lib/compliance-framework-reporter.js +600 -0
  52. package/src/lib/content-recommendation.js +600 -0
  53. package/src/lib/cross-chain.js +669 -0
  54. package/src/lib/dbevo.js +669 -0
  55. package/src/lib/decentral-infra.js +445 -0
  56. package/src/lib/federation-hardening.js +587 -0
  57. package/src/lib/hardening-manager.js +409 -0
  58. package/src/lib/inference-network.js +407 -0
  59. package/src/lib/knowledge-graph.js +530 -0
  60. package/src/lib/matrix-bridge.js +252 -0
  61. package/src/lib/mcp-client.js +3 -0
  62. package/src/lib/mcp-registry.js +347 -0
  63. package/src/lib/mcp-scaffold.js +385 -0
  64. package/src/lib/multimodal.js +698 -0
  65. package/src/lib/nl-programming.js +595 -0
  66. package/src/lib/nostr-bridge.js +214 -38
  67. package/src/lib/perception.js +500 -0
  68. package/src/lib/pqc-manager.js +141 -9
  69. package/src/lib/privacy-computing.js +575 -0
  70. package/src/lib/protocol-fusion.js +535 -0
  71. package/src/lib/quantization.js +362 -0
  72. package/src/lib/reputation-optimizer.js +509 -0
  73. package/src/lib/skill-marketplace.js +397 -0
  74. package/src/lib/sla-manager.js +484 -0
  75. package/src/lib/social-graph.js +408 -0
  76. package/src/lib/stix-parser.js +167 -0
  77. package/src/lib/stress-tester.js +383 -0
  78. package/src/lib/tech-learning-engine.js +651 -0
  79. package/src/lib/tenant-saas.js +831 -0
  80. package/src/lib/threat-intel.js +268 -0
  81. package/src/lib/token-incentive.js +513 -0
  82. package/src/lib/topic-classifier.js +400 -0
  83. package/src/lib/trust-security.js +473 -0
  84. package/src/lib/ueba.js +403 -0
  85. package/src/lib/universal-runtime.js +771 -0
  86. package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
@@ -0,0 +1,651 @@
1
+ /**
2
+ * Tech Learning Engine — CLI port of Phase 62 tech-learning-engine
3
+ * (docs/design/modules/34_技术学习引擎系统.md).
4
+ *
5
+ * The Desktop build runs auto-scans, extracts patterns from open-source
6
+ * corpora and builds a knowledge graph. Those parts depend on long-running
7
+ * crawlers and graph infrastructure the CLI doesn't have. The CLI port
8
+ * ships the tractable pieces:
9
+ *
10
+ * - analyzeTechStack(path) — static parse of package.json /
11
+ * requirements.txt / pyproject.toml /
12
+ * Cargo.toml / go.mod with a built-in
13
+ * classifier (framework/library/db/tool).
14
+ * - detectAntiPatterns(file) — heuristic file scan (god_object /
15
+ * long_method / magic_numbers /
16
+ * tight_coupling / spaghetti_code).
17
+ * - recordPractice / listPractices — hand-curated practice store.
18
+ * - getRecommendations(stack) — pairs analyzed stack with practices
19
+ * table to surface relevant items.
20
+ */
21
+
22
+ import crypto from "crypto";
23
+ import fs from "fs";
24
+ import path from "path";
25
+
26
+ /* ── Constants ─────────────────────────────────────────────── */
27
+
28
+ export const TECH_TYPES = Object.freeze({
29
+ LANGUAGE: "language",
30
+ FRAMEWORK: "framework",
31
+ LIBRARY: "library",
32
+ DATABASE: "database",
33
+ TOOL: "tool",
34
+ PATTERN: "pattern",
35
+ });
36
+
37
+ export const PRACTICE_LEVELS = Object.freeze({
38
+ BEGINNER: "beginner",
39
+ INTERMEDIATE: "intermediate",
40
+ ADVANCED: "advanced",
41
+ EXPERT: "expert",
42
+ });
43
+
44
+ export const ANTI_PATTERNS = Object.freeze({
45
+ GOD_OBJECT: "god_object",
46
+ LONG_METHOD: "long_method",
47
+ MAGIC_NUMBERS: "magic_numbers",
48
+ TIGHT_COUPLING: "tight_coupling",
49
+ SPAGHETTI_CODE: "spaghetti_code",
50
+ PREMATURE_OPTIMIZATION: "premature_optimization",
51
+ });
52
+
53
+ const VALID_TECH_TYPES = new Set(Object.values(TECH_TYPES));
54
+ const VALID_LEVELS = new Set(Object.values(PRACTICE_LEVELS));
55
+
56
+ /* ── Classifier catalog ────────────────────────────────────── */
57
+
58
+ // Lowercased lookup. Entries missing here default to LIBRARY.
59
+ const TECH_CLASSIFIER = Object.freeze({
60
+ // Languages
61
+ typescript: TECH_TYPES.LANGUAGE,
62
+ javascript: TECH_TYPES.LANGUAGE,
63
+ python: TECH_TYPES.LANGUAGE,
64
+ rust: TECH_TYPES.LANGUAGE,
65
+ go: TECH_TYPES.LANGUAGE,
66
+ java: TECH_TYPES.LANGUAGE,
67
+ kotlin: TECH_TYPES.LANGUAGE,
68
+
69
+ // Frameworks (Node / Python / JVM / Rust / Go)
70
+ react: TECH_TYPES.FRAMEWORK,
71
+ vue: TECH_TYPES.FRAMEWORK,
72
+ "@vue/core": TECH_TYPES.FRAMEWORK,
73
+ angular: TECH_TYPES.FRAMEWORK,
74
+ "@angular/core": TECH_TYPES.FRAMEWORK,
75
+ svelte: TECH_TYPES.FRAMEWORK,
76
+ next: TECH_TYPES.FRAMEWORK,
77
+ nuxt: TECH_TYPES.FRAMEWORK,
78
+ express: TECH_TYPES.FRAMEWORK,
79
+ fastify: TECH_TYPES.FRAMEWORK,
80
+ koa: TECH_TYPES.FRAMEWORK,
81
+ "@nestjs/core": TECH_TYPES.FRAMEWORK,
82
+ flask: TECH_TYPES.FRAMEWORK,
83
+ django: TECH_TYPES.FRAMEWORK,
84
+ fastapi: TECH_TYPES.FRAMEWORK,
85
+ rocket: TECH_TYPES.FRAMEWORK,
86
+ actix: TECH_TYPES.FRAMEWORK,
87
+ gin: TECH_TYPES.FRAMEWORK,
88
+ fiber: TECH_TYPES.FRAMEWORK,
89
+ electron: TECH_TYPES.FRAMEWORK,
90
+ "spring-boot": TECH_TYPES.FRAMEWORK,
91
+
92
+ // Databases / stores
93
+ "better-sqlite3": TECH_TYPES.DATABASE,
94
+ sqlite3: TECH_TYPES.DATABASE,
95
+ pg: TECH_TYPES.DATABASE,
96
+ mysql: TECH_TYPES.DATABASE,
97
+ mysql2: TECH_TYPES.DATABASE,
98
+ mongodb: TECH_TYPES.DATABASE,
99
+ mongoose: TECH_TYPES.DATABASE,
100
+ redis: TECH_TYPES.DATABASE,
101
+ ioredis: TECH_TYPES.DATABASE,
102
+ psycopg2: TECH_TYPES.DATABASE,
103
+ sqlalchemy: TECH_TYPES.DATABASE,
104
+ diesel: TECH_TYPES.DATABASE,
105
+ sqlx: TECH_TYPES.DATABASE,
106
+
107
+ // Tools / bundlers / test runners
108
+ webpack: TECH_TYPES.TOOL,
109
+ vite: TECH_TYPES.TOOL,
110
+ rollup: TECH_TYPES.TOOL,
111
+ esbuild: TECH_TYPES.TOOL,
112
+ typescript_tool: TECH_TYPES.TOOL,
113
+ eslint: TECH_TYPES.TOOL,
114
+ prettier: TECH_TYPES.TOOL,
115
+ vitest: TECH_TYPES.TOOL,
116
+ jest: TECH_TYPES.TOOL,
117
+ mocha: TECH_TYPES.TOOL,
118
+ pytest: TECH_TYPES.TOOL,
119
+ });
120
+
121
+ function _classify(name) {
122
+ const key = String(name).toLowerCase();
123
+ return TECH_CLASSIFIER[key] || TECH_TYPES.LIBRARY;
124
+ }
125
+
126
+ /* ── In-memory stores ─────────────────────────────────────── */
127
+
128
+ const _profiles = new Map(); // projectPath → profile
129
+ const _practices = new Map(); // practiceId → practice
130
+ let _seq = 0;
131
+
132
+ /* ── Schema ────────────────────────────────────────────────── */
133
+
134
+ export function ensureTechLearningTables(db) {
135
+ db.exec(`
136
+ CREATE TABLE IF NOT EXISTS tech_stack_profiles (
137
+ profile_id TEXT PRIMARY KEY,
138
+ project_path TEXT NOT NULL,
139
+ tech_stack TEXT NOT NULL,
140
+ dependencies TEXT,
141
+ languages TEXT,
142
+ frameworks TEXT,
143
+ analysis_timestamp INTEGER NOT NULL,
144
+ created_at INTEGER NOT NULL,
145
+ updated_at INTEGER NOT NULL
146
+ )
147
+ `);
148
+ db.exec(`
149
+ CREATE TABLE IF NOT EXISTS learned_practices (
150
+ practice_id TEXT PRIMARY KEY,
151
+ tech_type TEXT NOT NULL,
152
+ tech_name TEXT NOT NULL,
153
+ pattern_name TEXT NOT NULL,
154
+ level TEXT NOT NULL,
155
+ description TEXT,
156
+ code_example TEXT,
157
+ usage_count INTEGER DEFAULT 0,
158
+ score REAL DEFAULT 0.0,
159
+ source TEXT,
160
+ learned_at INTEGER NOT NULL,
161
+ updated_at INTEGER NOT NULL
162
+ )
163
+ `);
164
+ db.exec(
165
+ `CREATE INDEX IF NOT EXISTS idx_learned_practices_tech_type ON learned_practices(tech_type)`,
166
+ );
167
+ db.exec(
168
+ `CREATE INDEX IF NOT EXISTS idx_learned_practices_level ON learned_practices(level)`,
169
+ );
170
+ }
171
+
172
+ /* ── Parsers for dependency manifests ──────────────────────── */
173
+
174
+ function _readIfExists(filePath) {
175
+ try {
176
+ return fs.readFileSync(filePath, "utf-8");
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ function _parsePackageJson(text) {
183
+ if (!text) return null;
184
+ let json;
185
+ try {
186
+ json = JSON.parse(text);
187
+ } catch {
188
+ return null;
189
+ }
190
+ const deps = {
191
+ ...(json.dependencies || {}),
192
+ ...(json.devDependencies || {}),
193
+ ...(json.peerDependencies || {}),
194
+ };
195
+ return {
196
+ language: "javascript",
197
+ deps: Object.keys(deps).map((name) => ({
198
+ name,
199
+ version: deps[name],
200
+ })),
201
+ };
202
+ }
203
+
204
+ function _parseRequirementsTxt(text) {
205
+ if (!text) return null;
206
+ const deps = [];
207
+ for (const raw of text.split(/\r?\n/)) {
208
+ const line = raw.trim();
209
+ if (!line || line.startsWith("#") || line.startsWith("-")) continue;
210
+ // Strip environment markers + extras; grab "name" + optional version.
211
+ const match = line.match(/^([A-Za-z0-9_.\-[\]]+)\s*([<>=!~].*)?$/);
212
+ if (!match) continue;
213
+ const name = match[1].replace(/\[.*\]/, "").toLowerCase();
214
+ const version = match[2] ? match[2].trim() : null;
215
+ deps.push({ name, version });
216
+ }
217
+ return { language: "python", deps };
218
+ }
219
+
220
+ function _parseCargoToml(text) {
221
+ if (!text) return null;
222
+ const deps = [];
223
+ // Only mine the [dependencies] / [dev-dependencies] tables.
224
+ const tables = text.split(/^\[/m);
225
+ for (const chunk of tables) {
226
+ if (!/^dependencies]|^dev-dependencies]/.test(chunk)) continue;
227
+ const body = chunk.replace(/^[^\]]+\]\s*/, "");
228
+ for (const raw of body.split(/\r?\n/)) {
229
+ const line = raw.trim();
230
+ if (!line || line.startsWith("#") || line.startsWith("[")) continue;
231
+ const match = line.match(/^([A-Za-z0-9_\-]+)\s*=\s*(.+)$/);
232
+ if (!match) continue;
233
+ const version = match[2].match(/"([^"]+)"/);
234
+ deps.push({
235
+ name: match[1].toLowerCase(),
236
+ version: version ? version[1] : match[2].trim(),
237
+ });
238
+ }
239
+ }
240
+ return { language: "rust", deps };
241
+ }
242
+
243
+ function _parseGoMod(text) {
244
+ if (!text) return null;
245
+ const deps = [];
246
+ const requireBlock = text.match(/require\s*\(([\s\S]*?)\)/);
247
+ const lines = requireBlock
248
+ ? requireBlock[1].split(/\r?\n/)
249
+ : text.split(/\r?\n/).filter((l) => l.trim().startsWith("require "));
250
+ for (const raw of lines) {
251
+ const line = raw.replace(/^\s*require\s+/, "").trim();
252
+ if (!line || line.startsWith("//")) continue;
253
+ const match = line.match(/^(\S+)\s+(\S+)/);
254
+ if (!match) continue;
255
+ deps.push({ name: match[1].toLowerCase(), version: match[2] });
256
+ }
257
+ return { language: "go", deps };
258
+ }
259
+
260
+ export function analyzeTechStack(db, projectPath = process.cwd(), opts = {}) {
261
+ const absPath = path.resolve(projectPath);
262
+ const manifests = [
263
+ ["package.json", _parsePackageJson],
264
+ ["requirements.txt", _parseRequirementsTxt],
265
+ ["Cargo.toml", _parseCargoToml],
266
+ ["go.mod", _parseGoMod],
267
+ ];
268
+ const languages = new Set();
269
+ const deps = [];
270
+ for (const [file, parser] of manifests) {
271
+ const text = _readIfExists(path.join(absPath, file));
272
+ const parsed = parser(text);
273
+ if (parsed) {
274
+ languages.add(parsed.language);
275
+ for (const d of parsed.deps) deps.push({ ...d, sourceFile: file });
276
+ }
277
+ }
278
+
279
+ const classified = deps.map((d) => ({ ...d, type: _classify(d.name) }));
280
+ const frameworks = classified
281
+ .filter((d) => d.type === TECH_TYPES.FRAMEWORK)
282
+ .map((d) => d.name);
283
+ const databases = classified
284
+ .filter((d) => d.type === TECH_TYPES.DATABASE)
285
+ .map((d) => d.name);
286
+ const tools = classified
287
+ .filter((d) => d.type === TECH_TYPES.TOOL)
288
+ .map((d) => d.name);
289
+ const libraries = classified
290
+ .filter((d) => d.type === TECH_TYPES.LIBRARY)
291
+ .map((d) => d.name);
292
+
293
+ const now = Number(opts.now ?? Date.now());
294
+ const profileId = crypto.randomUUID();
295
+ const profile = {
296
+ profileId,
297
+ projectPath: absPath,
298
+ languages: [...languages],
299
+ frameworks,
300
+ databases,
301
+ tools,
302
+ libraries,
303
+ totalDependencies: deps.length,
304
+ analysisTimestamp: now,
305
+ createdAt: now,
306
+ updatedAt: now,
307
+ _seq: ++_seq,
308
+ };
309
+
310
+ _profiles.set(absPath, profile);
311
+
312
+ if (db) {
313
+ db.prepare(
314
+ `INSERT INTO tech_stack_profiles (profile_id, project_path, tech_stack, dependencies, languages, frameworks, analysis_timestamp, created_at, updated_at)
315
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
316
+ ).run(
317
+ profileId,
318
+ absPath,
319
+ JSON.stringify(classified),
320
+ JSON.stringify(deps),
321
+ JSON.stringify(profile.languages),
322
+ JSON.stringify(frameworks),
323
+ now,
324
+ now,
325
+ now,
326
+ );
327
+ }
328
+
329
+ const { _seq: _omit, ...rest } = profile;
330
+ void _omit;
331
+ return rest;
332
+ }
333
+
334
+ export function getProfile(projectPath) {
335
+ const absPath = path.resolve(projectPath);
336
+ const profile = _profiles.get(absPath);
337
+ if (!profile) return null;
338
+ const { _seq: _omit, ...rest } = profile;
339
+ void _omit;
340
+ return rest;
341
+ }
342
+
343
+ /* ── Anti-pattern detection (heuristics) ─────────────────── */
344
+
345
+ function _maxIndentDepth(source) {
346
+ let max = 0;
347
+ for (const line of source.split(/\r?\n/)) {
348
+ if (!line.trim()) continue;
349
+ const match = line.match(/^[\t ]+/);
350
+ if (!match) continue;
351
+ const leading = match[0];
352
+ const tabs = leading.match(/\t/g)?.length || 0;
353
+ const spaces = leading.length - tabs;
354
+ const depth = tabs + Math.floor(spaces / 2);
355
+ if (depth > max) max = depth;
356
+ }
357
+ return max;
358
+ }
359
+
360
+ function _countFunctions(source) {
361
+ const matches = source.match(
362
+ /\b(?:function|def|fn|async\s+function|public\s+\w+\s+\w+\s*\(|private\s+\w+\s+\w+\s*\()\b/g,
363
+ );
364
+ return matches ? matches.length : 0;
365
+ }
366
+
367
+ function _countImports(source) {
368
+ const es =
369
+ source.match(/^\s*import\s+[^;]*from\s+['"][^'"]+['"];?/gm)?.length || 0;
370
+ const req =
371
+ source.match(/\brequire\s*\(\s*['"][^'"]+['"]\s*\)/g)?.length || 0;
372
+ const py = source.match(/^\s*(?:from\s+\S+\s+)?import\s+/gm)?.length || 0;
373
+ return es + req + py;
374
+ }
375
+
376
+ function _findLongMethods(source, threshold = 80) {
377
+ const lines = source.split(/\r?\n/);
378
+ const findings = [];
379
+ for (let i = 0; i < lines.length; i++) {
380
+ const line = lines[i];
381
+ if (!/\b(function|=>\s*\{|def\s+\w+|fn\s+\w+)/.test(line)) continue;
382
+ const openIdx = line.indexOf("{");
383
+ let startLine = i;
384
+ if (openIdx === -1) {
385
+ // Python: indent-based. Measure until dedent.
386
+ if (/^\s*def\s+/.test(line)) {
387
+ const indent = (line.match(/^(\s*)/) || ["", ""])[1].length;
388
+ let end = i;
389
+ for (let j = i + 1; j < lines.length; j++) {
390
+ const body = lines[j];
391
+ if (!body.trim()) continue;
392
+ const leading = (body.match(/^(\s*)/) || ["", ""])[1].length;
393
+ if (leading <= indent) break;
394
+ end = j;
395
+ }
396
+ const bodyLen = end - startLine;
397
+ if (bodyLen > threshold) {
398
+ findings.push({ startLine: startLine + 1, length: bodyLen });
399
+ }
400
+ i = end;
401
+ }
402
+ continue;
403
+ }
404
+ // Brace-based: count matching braces.
405
+ let depth = 0;
406
+ let end = i;
407
+ for (let j = i; j < lines.length; j++) {
408
+ const body = lines[j];
409
+ for (const ch of body) {
410
+ if (ch === "{") depth++;
411
+ else if (ch === "}") depth--;
412
+ }
413
+ if (depth === 0 && j > i) {
414
+ end = j;
415
+ break;
416
+ }
417
+ end = j;
418
+ }
419
+ const bodyLen = end - startLine;
420
+ if (bodyLen > threshold) {
421
+ findings.push({ startLine: startLine + 1, length: bodyLen });
422
+ }
423
+ i = end;
424
+ }
425
+ return findings;
426
+ }
427
+
428
+ function _countMagicNumbers(source) {
429
+ // Exclude 0, 1, -1, 2, 10, 100 (common benign numbers).
430
+ const benign = new Set(["0", "1", "-1", "2", "10", "100"]);
431
+ // Strip strings and comments to avoid false positives.
432
+ const cleaned = source
433
+ .replace(/"(?:\\.|[^"\\])*"/g, '""')
434
+ .replace(/'(?:\\.|[^'\\])*'/g, "''")
435
+ .replace(/\/\/.*$/gm, "")
436
+ .replace(/\/\*[\s\S]*?\*\//g, "");
437
+ const numbers = cleaned.match(/(?<![A-Za-z_])-?\d+(?:\.\d+)?\b/g) || [];
438
+ let count = 0;
439
+ for (const n of numbers) {
440
+ if (!benign.has(n)) count++;
441
+ }
442
+ return count;
443
+ }
444
+
445
+ export function detectAntiPatterns(filePath, opts = {}) {
446
+ const source = opts.source ?? _readIfExists(filePath);
447
+ if (source == null) throw new Error(`File not found: ${filePath}`);
448
+ const lines = source.split(/\r?\n/).length;
449
+ const findings = [];
450
+
451
+ const functionCount = _countFunctions(source);
452
+ if (lines > 500 || functionCount > 20) {
453
+ findings.push({
454
+ type: ANTI_PATTERNS.GOD_OBJECT,
455
+ severity: lines > 800 || functionCount > 40 ? "high" : "medium",
456
+ detail: `file has ${lines} lines and ${functionCount} function declarations`,
457
+ });
458
+ }
459
+
460
+ const longMethods = _findLongMethods(source, opts.longMethodThreshold || 80);
461
+ for (const m of longMethods) {
462
+ findings.push({
463
+ type: ANTI_PATTERNS.LONG_METHOD,
464
+ severity: m.length > 160 ? "high" : "medium",
465
+ detail: `function at line ${m.startLine} spans ${m.length} lines`,
466
+ startLine: m.startLine,
467
+ });
468
+ }
469
+
470
+ const importCount = _countImports(source);
471
+ if (importCount > (opts.tightCouplingThreshold || 20)) {
472
+ findings.push({
473
+ type: ANTI_PATTERNS.TIGHT_COUPLING,
474
+ severity: importCount > 40 ? "high" : "medium",
475
+ detail: `${importCount} imports in single file`,
476
+ });
477
+ }
478
+
479
+ const magicCount = _countMagicNumbers(source);
480
+ if (magicCount > (opts.magicNumberThreshold || 10)) {
481
+ findings.push({
482
+ type: ANTI_PATTERNS.MAGIC_NUMBERS,
483
+ severity: magicCount > 25 ? "high" : "medium",
484
+ detail: `${magicCount} magic numbers detected`,
485
+ });
486
+ }
487
+
488
+ const indentDepth = _maxIndentDepth(source);
489
+ if (indentDepth > (opts.spaghettiDepthThreshold || 6)) {
490
+ findings.push({
491
+ type: ANTI_PATTERNS.SPAGHETTI_CODE,
492
+ severity: indentDepth > 9 ? "high" : "medium",
493
+ detail: `max nesting depth ${indentDepth}`,
494
+ });
495
+ }
496
+
497
+ return {
498
+ filePath,
499
+ lines,
500
+ functionCount,
501
+ importCount,
502
+ magicNumberCount: magicCount,
503
+ maxIndentDepth: indentDepth,
504
+ totalFindings: findings.length,
505
+ findings,
506
+ };
507
+ }
508
+
509
+ /* ── Practice store ─────────────────────────────────────────── */
510
+
511
+ export function recordPractice(db, config = {}) {
512
+ const techType = String(config.techType || "").toLowerCase();
513
+ if (!VALID_TECH_TYPES.has(techType)) {
514
+ throw new Error(
515
+ `Unknown tech type: ${config.techType} (known: ${[...VALID_TECH_TYPES].join("/")})`,
516
+ );
517
+ }
518
+ const level = String(config.level || "").toLowerCase();
519
+ if (!VALID_LEVELS.has(level)) {
520
+ throw new Error(
521
+ `Unknown level: ${config.level} (known: ${[...VALID_LEVELS].join("/")})`,
522
+ );
523
+ }
524
+ const techName = String(config.techName || "").trim();
525
+ const patternName = String(config.patternName || "").trim();
526
+ if (!techName) throw new Error("techName is required");
527
+ if (!patternName) throw new Error("patternName is required");
528
+
529
+ const now = Date.now();
530
+ const practiceId = crypto.randomUUID();
531
+ const practice = {
532
+ practiceId,
533
+ techType,
534
+ techName,
535
+ patternName,
536
+ level,
537
+ description: config.description || "",
538
+ codeExample: config.codeExample || "",
539
+ usageCount: 0,
540
+ score: Number(config.score ?? 0),
541
+ source: config.source || "manual",
542
+ learnedAt: now,
543
+ updatedAt: now,
544
+ _seq: ++_seq,
545
+ };
546
+ _practices.set(practiceId, practice);
547
+
548
+ if (db) {
549
+ db.prepare(
550
+ `INSERT INTO learned_practices (practice_id, tech_type, tech_name, pattern_name, level, description, code_example, usage_count, score, source, learned_at, updated_at)
551
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
552
+ ).run(
553
+ practiceId,
554
+ techType,
555
+ techName,
556
+ patternName,
557
+ level,
558
+ practice.description,
559
+ practice.codeExample,
560
+ 0,
561
+ practice.score,
562
+ practice.source,
563
+ now,
564
+ now,
565
+ );
566
+ }
567
+
568
+ const { _seq: _omit, ...rest } = practice;
569
+ void _omit;
570
+ return rest;
571
+ }
572
+
573
+ export function listPractices(opts = {}) {
574
+ let rows = [..._practices.values()];
575
+ if (opts.techType) {
576
+ const t = String(opts.techType).toLowerCase();
577
+ rows = rows.filter((p) => p.techType === t);
578
+ }
579
+ if (opts.techName) {
580
+ const n = String(opts.techName).toLowerCase();
581
+ rows = rows.filter((p) => p.techName.toLowerCase() === n);
582
+ }
583
+ if (opts.level) {
584
+ const l = String(opts.level).toLowerCase();
585
+ rows = rows.filter((p) => p.level === l);
586
+ }
587
+ rows.sort((a, b) => b.learnedAt - a.learnedAt || b._seq - a._seq);
588
+ const limit = opts.limit || 50;
589
+ return rows.slice(0, limit).map((p) => {
590
+ const { _seq: _omit, ...rest } = p;
591
+ void _omit;
592
+ return rest;
593
+ });
594
+ }
595
+
596
+ export function getRecommendations(opts = {}) {
597
+ // stackNames comes either explicitly or inferred from the most recent
598
+ // profile.
599
+ let stackNames = opts.stackNames;
600
+ if (!stackNames) {
601
+ const latest = [..._profiles.values()].sort(
602
+ (a, b) => b.analysisTimestamp - a.analysisTimestamp,
603
+ )[0];
604
+ if (!latest) {
605
+ return {
606
+ stackNames: [],
607
+ recommendations: [],
608
+ message:
609
+ "No analyzed stack; call analyzeTechStack() or pass stackNames.",
610
+ };
611
+ }
612
+ stackNames = [
613
+ ...latest.languages,
614
+ ...latest.frameworks,
615
+ ...latest.databases,
616
+ ...latest.tools,
617
+ ];
618
+ }
619
+ const normalized = stackNames.map((n) => String(n).toLowerCase());
620
+ const matches = [];
621
+ for (const p of _practices.values()) {
622
+ if (normalized.includes(p.techName.toLowerCase())) {
623
+ const { _seq: _omit, ...rest } = p;
624
+ void _omit;
625
+ matches.push(rest);
626
+ }
627
+ }
628
+ matches.sort((a, b) => {
629
+ const levelRank = {
630
+ beginner: 0,
631
+ intermediate: 1,
632
+ advanced: 2,
633
+ expert: 3,
634
+ };
635
+ return levelRank[b.level] - levelRank[a.level] || b.score - a.score;
636
+ });
637
+ return {
638
+ stackNames: normalized,
639
+ totalPractices: _practices.size,
640
+ totalMatches: matches.length,
641
+ recommendations: matches.slice(0, opts.limit || 20),
642
+ };
643
+ }
644
+
645
+ /* ── State reset (tests) ───────────────────────────────────── */
646
+
647
+ export function _resetState() {
648
+ _profiles.clear();
649
+ _practices.clear();
650
+ _seq = 0;
651
+ }