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,595 @@
1
+ /**
2
+ * Natural Language Programming — CLI port of Phase 28
3
+ * (docs/design/modules/28_自然语言编程.md).
4
+ *
5
+ * Desktop uses SpecTranslator (9-step pipeline with LLM enhancement),
6
+ * RequirementParser (intent classification + entity extraction),
7
+ * and ProjectStyleAnalyzer (6-category code style learning).
8
+ * CLI port ships:
9
+ *
10
+ * - Translation recording with intent classification and spec
11
+ * - Heuristic intent classification (keyword-based)
12
+ * - Heuristic entity extraction (noun phrases)
13
+ * - Convention/style recording and retrieval
14
+ * - Completeness scoring and ambiguity tracking
15
+ *
16
+ * What does NOT port: LLM-driven translation, real AST analysis,
17
+ * KG context integration, instinct memory patterns, live project
18
+ * style analysis, spec refinement with feedback loops.
19
+ */
20
+
21
+ import crypto from "crypto";
22
+
23
+ /* ── Constants ──────────────────────────────────────────── */
24
+
25
+ export const INTENT = Object.freeze({
26
+ CREATE_COMPONENT: "create_component",
27
+ ADD_FEATURE: "add_feature",
28
+ FIX_BUG: "fix_bug",
29
+ REFACTOR: "refactor",
30
+ ADD_API: "add_api",
31
+ ADD_TEST: "add_test",
32
+ UPDATE_STYLE: "update_style",
33
+ CONFIGURE: "configure",
34
+ GENERAL: "general",
35
+ });
36
+
37
+ export const TRANSLATION_STATUS = Object.freeze({
38
+ DRAFT: "draft",
39
+ COMPLETE: "complete",
40
+ REFINED: "refined",
41
+ });
42
+
43
+ export const STYLE_CATEGORY = Object.freeze({
44
+ NAMING: "naming",
45
+ ARCHITECTURE: "architecture",
46
+ TESTING: "testing",
47
+ STYLE: "style",
48
+ IMPORTS: "imports",
49
+ COMPONENTS: "components",
50
+ });
51
+
52
+ /* ── State ──────────────────────────────────────────────── */
53
+
54
+ let _translations = new Map();
55
+ let _conventions = new Map();
56
+
57
+ function _id() {
58
+ return crypto.randomUUID();
59
+ }
60
+ function _now() {
61
+ return Date.now();
62
+ }
63
+
64
+ function _strip(row) {
65
+ if (!row) return null;
66
+ const out = {};
67
+ for (const [k, v] of Object.entries(row)) {
68
+ if (k !== "_rowid_" && k !== "rowid") out[k] = v;
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /* ── Schema ─────────────────────────────────────────────── */
74
+
75
+ export function ensureNlProgrammingTables(db) {
76
+ db.exec(`CREATE TABLE IF NOT EXISTS nl_programs (
77
+ id TEXT PRIMARY KEY,
78
+ input_text TEXT NOT NULL,
79
+ intent TEXT,
80
+ entities TEXT,
81
+ tech_stack TEXT,
82
+ spec TEXT,
83
+ completeness_score REAL DEFAULT 0.0,
84
+ ambiguities TEXT,
85
+ status TEXT DEFAULT 'draft',
86
+ created_at INTEGER NOT NULL,
87
+ updated_at INTEGER NOT NULL
88
+ )`);
89
+
90
+ db.exec(`CREATE TABLE IF NOT EXISTS nl_program_conventions (
91
+ id TEXT PRIMARY KEY,
92
+ category TEXT NOT NULL,
93
+ pattern TEXT NOT NULL,
94
+ examples TEXT,
95
+ confidence REAL DEFAULT 0.5,
96
+ source_files TEXT,
97
+ created_at INTEGER NOT NULL,
98
+ updated_at INTEGER NOT NULL
99
+ )`);
100
+
101
+ _loadAll(db);
102
+ }
103
+
104
+ function _loadAll(db) {
105
+ _translations.clear();
106
+ _conventions.clear();
107
+
108
+ const tables = [
109
+ ["nl_programs", _translations, "id"],
110
+ ["nl_program_conventions", _conventions, "id"],
111
+ ];
112
+ for (const [table, map, key] of tables) {
113
+ try {
114
+ for (const row of db.prepare(`SELECT * FROM ${table}`).all()) {
115
+ const r = _strip(row);
116
+ map.set(r[key], r);
117
+ }
118
+ } catch (_e) {
119
+ /* table may not exist */
120
+ }
121
+ }
122
+ }
123
+
124
+ /* ── Intent Classification (heuristic) ──────────────────── */
125
+
126
+ const INTENT_KEYWORDS = {
127
+ create_component: [
128
+ "create",
129
+ "new",
130
+ "build",
131
+ "make",
132
+ "scaffold",
133
+ "generate",
134
+ "创建",
135
+ "新建",
136
+ "生成",
137
+ ],
138
+ add_feature: [
139
+ "add",
140
+ "implement",
141
+ "include",
142
+ "extend",
143
+ "添加",
144
+ "实现",
145
+ "增加",
146
+ "给",
147
+ ],
148
+ fix_bug: [
149
+ "fix",
150
+ "repair",
151
+ "resolve",
152
+ "debug",
153
+ "patch",
154
+ "修复",
155
+ "修改",
156
+ "解决",
157
+ "bug",
158
+ ],
159
+ refactor: [
160
+ "refactor",
161
+ "restructure",
162
+ "reorganize",
163
+ "clean",
164
+ "重构",
165
+ "整理",
166
+ "优化",
167
+ ],
168
+ add_api: ["api", "endpoint", "route", "接口", "端点"],
169
+ add_test: ["test", "spec", "测试", "单元测试"],
170
+ update_style: [
171
+ "style",
172
+ "css",
173
+ "theme",
174
+ "color",
175
+ "样式",
176
+ "主题",
177
+ "颜色",
178
+ "UI",
179
+ ],
180
+ configure: ["config", "setting", "setup", "配置", "设置", "安装"],
181
+ };
182
+
183
+ export function classifyIntent(text) {
184
+ if (!text) return { intent: "general", confidence: 0 };
185
+
186
+ const lower = text.toLowerCase();
187
+ let bestIntent = "general";
188
+ let bestScore = 0;
189
+
190
+ for (const [intent, keywords] of Object.entries(INTENT_KEYWORDS)) {
191
+ let score = 0;
192
+ for (const kw of keywords) {
193
+ if (lower.includes(kw)) score += 1;
194
+ }
195
+ if (score > bestScore) {
196
+ bestScore = score;
197
+ bestIntent = intent;
198
+ }
199
+ }
200
+
201
+ const confidence = bestScore > 0 ? Math.min(1, bestScore * 0.3) : 0;
202
+ return {
203
+ intent: bestIntent,
204
+ confidence: Math.round(confidence * 1000) / 1000,
205
+ };
206
+ }
207
+
208
+ /* ── Entity Extraction (heuristic) ──────────────────────── */
209
+
210
+ export function extractEntities(text) {
211
+ if (!text) return { entities: [], count: 0 };
212
+
213
+ // Simple heuristic: extract quoted strings and capitalized phrases
214
+ const entities = [];
215
+
216
+ // Quoted strings
217
+ const quotedMatches = text.match(/["'`]([^"'`]+)["'`]/g);
218
+ if (quotedMatches) {
219
+ for (const m of quotedMatches) {
220
+ entities.push({ type: "quoted", value: m.slice(1, -1) });
221
+ }
222
+ }
223
+
224
+ // Chinese noun phrases (after keywords like 给/的/个)
225
+ const cnMatches = text.match(/(?:给|的|个|一个)([^\s,,。、]+)/g);
226
+ if (cnMatches) {
227
+ for (const m of cnMatches) {
228
+ const value = m.replace(/^(?:给|的|个|一个)/, "").trim();
229
+ if (value.length > 1) entities.push({ type: "noun_phrase", value });
230
+ }
231
+ }
232
+
233
+ // Technical terms (PascalCase, kebab-case patterns)
234
+ const techMatches = text.match(/\b[A-Z][a-zA-Z]+(?:[A-Z][a-z]+)+\b/g);
235
+ if (techMatches) {
236
+ for (const m of techMatches) {
237
+ entities.push({ type: "technical", value: m });
238
+ }
239
+ }
240
+
241
+ // Deduplicate
242
+ const seen = new Set();
243
+ const unique = entities.filter((e) => {
244
+ const key = `${e.type}:${e.value}`;
245
+ if (seen.has(key)) return false;
246
+ seen.add(key);
247
+ return true;
248
+ });
249
+
250
+ return { entities: unique, count: unique.length };
251
+ }
252
+
253
+ /* ── Tech Stack Detection (heuristic) ───────────────────── */
254
+
255
+ const TECH_PATTERNS = {
256
+ vue: ["vue", "Vue", "组件", "component", "composable"],
257
+ react: ["react", "React", "jsx", "tsx", "hook"],
258
+ typescript: ["typescript", "TypeScript", "ts", "类型"],
259
+ javascript: ["javascript", "js", "JavaScript"],
260
+ python: ["python", "Python", "pip", "django", "flask", "fastapi"],
261
+ java: ["java", "Java", "spring", "Spring"],
262
+ go: ["golang", "go", "Go"],
263
+ rust: ["rust", "Rust", "cargo"],
264
+ };
265
+
266
+ export function detectTechStack(text) {
267
+ if (!text) return { detected: [], primary: null };
268
+
269
+ const lower = text.toLowerCase();
270
+ const detected = [];
271
+
272
+ for (const [tech, patterns] of Object.entries(TECH_PATTERNS)) {
273
+ for (const p of patterns) {
274
+ if (lower.includes(p.toLowerCase())) {
275
+ detected.push(tech);
276
+ break;
277
+ }
278
+ }
279
+ }
280
+
281
+ return { detected: [...new Set(detected)], primary: detected[0] || null };
282
+ }
283
+
284
+ /* ── Completeness Scoring ───────────────────────────────── */
285
+
286
+ export function scoreCompleteness(spec) {
287
+ if (!spec) return { score: 0, missing: ["spec"] };
288
+
289
+ let score = 0;
290
+ const missing = [];
291
+ const total = 6;
292
+
293
+ if (spec.intent && spec.intent !== "general") score += 1;
294
+ else missing.push("intent");
295
+
296
+ if (spec.entities && spec.entities.length > 0) score += 1;
297
+ else missing.push("entities");
298
+
299
+ if (spec.techStack && spec.techStack.length > 0) score += 1;
300
+ else missing.push("tech_stack");
301
+
302
+ if (spec.inputText && spec.inputText.length > 10) score += 1;
303
+ else missing.push("detailed_input");
304
+
305
+ if (!spec.ambiguities || spec.ambiguities.length === 0) score += 1;
306
+ else missing.push("no_ambiguities");
307
+
308
+ if (spec.conventions && spec.conventions.length > 0) score += 1;
309
+ else missing.push("conventions");
310
+
311
+ return {
312
+ score: Math.round((score / total) * 1000) / 1000,
313
+ missing,
314
+ fulfilled: total - missing.length,
315
+ total,
316
+ };
317
+ }
318
+
319
+ /* ── Translation CRUD ───────────────────────────────────── */
320
+
321
+ const VALID_STATUSES = new Set(Object.values(TRANSLATION_STATUS));
322
+
323
+ export function translate(
324
+ db,
325
+ { text, intent, entities, techStack, spec, ambiguities } = {},
326
+ ) {
327
+ if (!text) return { translated: false, reason: "missing_text" };
328
+
329
+ const id = _id();
330
+ const now = _now();
331
+
332
+ // Auto-classify if not provided
333
+ const classified = intent || classifyIntent(text).intent;
334
+ const extracted = entities || JSON.stringify(extractEntities(text).entities);
335
+ const stack = techStack || JSON.stringify(detectTechStack(text).detected);
336
+ const entitiesJson =
337
+ typeof extracted === "string" ? extracted : JSON.stringify(extracted);
338
+ const stackJson = typeof stack === "string" ? stack : JSON.stringify(stack);
339
+ const specJson = spec
340
+ ? typeof spec === "string"
341
+ ? spec
342
+ : JSON.stringify(spec)
343
+ : null;
344
+ const ambigJson = ambiguities
345
+ ? typeof ambiguities === "string"
346
+ ? ambiguities
347
+ : JSON.stringify(ambiguities)
348
+ : null;
349
+
350
+ // Score completeness
351
+ const completeness = scoreCompleteness({
352
+ intent: classified,
353
+ entities: JSON.parse(entitiesJson),
354
+ techStack: JSON.parse(stackJson),
355
+ inputText: text,
356
+ ambiguities: ambigJson ? JSON.parse(ambigJson) : [],
357
+ });
358
+
359
+ const entry = {
360
+ id,
361
+ input_text: text,
362
+ intent: classified,
363
+ entities: entitiesJson,
364
+ tech_stack: stackJson,
365
+ spec: specJson,
366
+ completeness_score: completeness.score,
367
+ ambiguities: ambigJson,
368
+ status: "draft",
369
+ created_at: now,
370
+ updated_at: now,
371
+ };
372
+
373
+ db.prepare(
374
+ `INSERT INTO nl_programs (id, input_text, intent, entities, tech_stack, spec, completeness_score, ambiguities, status, created_at, updated_at)
375
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
376
+ ).run(
377
+ id,
378
+ text,
379
+ entry.intent,
380
+ entry.entities,
381
+ entry.tech_stack,
382
+ entry.spec,
383
+ entry.completeness_score,
384
+ entry.ambiguities,
385
+ "draft",
386
+ now,
387
+ now,
388
+ );
389
+
390
+ _translations.set(id, entry);
391
+ return {
392
+ translated: true,
393
+ translationId: id,
394
+ intent: classified,
395
+ completeness: completeness.score,
396
+ };
397
+ }
398
+
399
+ export function getTranslation(db, id) {
400
+ const t = _translations.get(id);
401
+ return t ? { ...t } : null;
402
+ }
403
+
404
+ export function listTranslations(db, { intent, status, limit = 50 } = {}) {
405
+ let results = [..._translations.values()];
406
+ if (intent) results = results.filter((t) => t.intent === intent);
407
+ if (status) results = results.filter((t) => t.status === status);
408
+ return results
409
+ .sort((a, b) => b.created_at - a.created_at)
410
+ .slice(0, limit)
411
+ .map((t) => ({ ...t }));
412
+ }
413
+
414
+ export function updateTranslationStatus(db, id, status) {
415
+ const t = _translations.get(id);
416
+ if (!t) return { updated: false, reason: "not_found" };
417
+ if (!VALID_STATUSES.has(status))
418
+ return { updated: false, reason: "invalid_status" };
419
+
420
+ const now = _now();
421
+ t.status = status;
422
+ t.updated_at = now;
423
+
424
+ db.prepare(
425
+ "UPDATE nl_programs SET status = ?, updated_at = ? WHERE id = ?",
426
+ ).run(status, now, id);
427
+
428
+ return { updated: true, status };
429
+ }
430
+
431
+ export function refineTranslation(
432
+ db,
433
+ id,
434
+ { spec, ambiguities, feedback } = {},
435
+ ) {
436
+ const t = _translations.get(id);
437
+ if (!t) return { refined: false, reason: "not_found" };
438
+
439
+ const now = _now();
440
+ if (spec) t.spec = typeof spec === "string" ? spec : JSON.stringify(spec);
441
+ if (ambiguities)
442
+ t.ambiguities =
443
+ typeof ambiguities === "string"
444
+ ? ambiguities
445
+ : JSON.stringify(ambiguities);
446
+ t.status = "refined";
447
+ t.updated_at = now;
448
+
449
+ // Rescore completeness
450
+ const completeness = scoreCompleteness({
451
+ intent: t.intent,
452
+ entities: JSON.parse(t.entities || "[]"),
453
+ techStack: JSON.parse(t.tech_stack || "[]"),
454
+ inputText: t.input_text,
455
+ ambiguities: t.ambiguities ? JSON.parse(t.ambiguities) : [],
456
+ });
457
+ t.completeness_score = completeness.score;
458
+
459
+ db.prepare(
460
+ "UPDATE nl_programs SET spec = ?, ambiguities = ?, completeness_score = ?, status = 'refined', updated_at = ? WHERE id = ?",
461
+ ).run(t.spec, t.ambiguities, t.completeness_score, now, id);
462
+
463
+ return { refined: true, completeness: completeness.score };
464
+ }
465
+
466
+ export function removeTranslation(db, id) {
467
+ const t = _translations.get(id);
468
+ if (!t) return { removed: false, reason: "not_found" };
469
+
470
+ _translations.delete(id);
471
+ db.prepare("DELETE FROM nl_programs WHERE id = ?").run(id);
472
+
473
+ return { removed: true };
474
+ }
475
+
476
+ /* ── Conventions ────────────────────────────────────────── */
477
+
478
+ const VALID_CATEGORIES = new Set(Object.values(STYLE_CATEGORY));
479
+
480
+ export function addConvention(
481
+ db,
482
+ { category, pattern, examples, confidence, sourceFiles } = {},
483
+ ) {
484
+ if (!category || !VALID_CATEGORIES.has(category))
485
+ return { added: false, reason: "invalid_category" };
486
+ if (!pattern) return { added: false, reason: "missing_pattern" };
487
+
488
+ const id = _id();
489
+ const now = _now();
490
+ const examplesJson = examples
491
+ ? typeof examples === "string"
492
+ ? examples
493
+ : JSON.stringify(examples)
494
+ : null;
495
+ const sourceJson = sourceFiles
496
+ ? typeof sourceFiles === "string"
497
+ ? sourceFiles
498
+ : JSON.stringify(sourceFiles)
499
+ : null;
500
+ const conf = confidence != null ? Math.max(0, Math.min(1, confidence)) : 0.5;
501
+
502
+ const entry = {
503
+ id,
504
+ category,
505
+ pattern,
506
+ examples: examplesJson,
507
+ confidence: conf,
508
+ source_files: sourceJson,
509
+ created_at: now,
510
+ updated_at: now,
511
+ };
512
+
513
+ db.prepare(
514
+ `INSERT INTO nl_program_conventions (id, category, pattern, examples, confidence, source_files, created_at, updated_at)
515
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
516
+ ).run(id, category, pattern, examplesJson, conf, sourceJson, now, now);
517
+
518
+ _conventions.set(id, entry);
519
+ return { added: true, conventionId: id };
520
+ }
521
+
522
+ export function getConvention(db, id) {
523
+ const c = _conventions.get(id);
524
+ return c ? { ...c } : null;
525
+ }
526
+
527
+ export function listConventions(db, { category, limit = 50 } = {}) {
528
+ let results = [..._conventions.values()];
529
+ if (category) results = results.filter((c) => c.category === category);
530
+ return results
531
+ .sort((a, b) => b.confidence - a.confidence)
532
+ .slice(0, limit)
533
+ .map((c) => ({ ...c }));
534
+ }
535
+
536
+ export function removeConvention(db, id) {
537
+ const c = _conventions.get(id);
538
+ if (!c) return { removed: false, reason: "not_found" };
539
+
540
+ _conventions.delete(id);
541
+ db.prepare("DELETE FROM nl_program_conventions WHERE id = ?").run(id);
542
+
543
+ return { removed: true };
544
+ }
545
+
546
+ /* ── Stats ──────────────────────────────────────────────── */
547
+
548
+ export function getNlProgrammingStats(db) {
549
+ const translations = [..._translations.values()];
550
+ const conventions = [..._conventions.values()];
551
+
552
+ const byIntent = {};
553
+ for (const i of Object.values(INTENT)) byIntent[i] = 0;
554
+ for (const t of translations)
555
+ byIntent[t.intent] = (byIntent[t.intent] || 0) + 1;
556
+
557
+ const byStatus = {};
558
+ for (const s of Object.values(TRANSLATION_STATUS)) byStatus[s] = 0;
559
+ for (const t of translations)
560
+ byStatus[t.status] = (byStatus[t.status] || 0) + 1;
561
+
562
+ const byCategory = {};
563
+ for (const c of Object.values(STYLE_CATEGORY)) byCategory[c] = 0;
564
+ for (const c of conventions)
565
+ byCategory[c.category] = (byCategory[c.category] || 0) + 1;
566
+
567
+ const avgCompleteness =
568
+ translations.length > 0
569
+ ? Math.round(
570
+ (translations.reduce((s, t) => s + t.completeness_score, 0) /
571
+ translations.length) *
572
+ 1000,
573
+ ) / 1000
574
+ : 0;
575
+
576
+ return {
577
+ translations: {
578
+ total: translations.length,
579
+ byIntent,
580
+ byStatus,
581
+ avgCompleteness,
582
+ },
583
+ conventions: {
584
+ total: conventions.length,
585
+ byCategory,
586
+ },
587
+ };
588
+ }
589
+
590
+ /* ── Reset (tests) ──────────────────────────────────────── */
591
+
592
+ export function _resetState() {
593
+ _translations.clear();
594
+ _conventions.clear();
595
+ }