skyloom 1.13.5 → 1.13.7

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 (195) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +220 -159
  3. package/config/providers.yaml +39 -39
  4. package/config/skills/api_integrator/SKILL.md +15 -15
  5. package/config/skills/arch_designer/SKILL.md +13 -13
  6. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  7. package/config/skills/code_analysis/SKILL.md +13 -13
  8. package/config/skills/code_generator/SKILL.md +12 -12
  9. package/config/skills/code_reviewer/SKILL.md +13 -13
  10. package/config/skills/content_writer/SKILL.md +14 -14
  11. package/config/skills/data_transformer/SKILL.md +15 -15
  12. package/config/skills/document_analysis/SKILL.md +13 -13
  13. package/config/skills/emotional_companion/SKILL.md +15 -15
  14. package/config/skills/performance_checker/SKILL.md +14 -14
  15. package/config/skills/security_auditor/SKILL.md +14 -14
  16. package/config/skills/self_evolve/SKILL.md +13 -13
  17. package/config/skills/sys_operator/SKILL.md +15 -15
  18. package/config/skills/task_planner/SKILL.md +14 -14
  19. package/config/skills/web_research/SKILL.md +14 -14
  20. package/config/skills/workflow_designer/SKILL.md +13 -13
  21. package/dist/agents/dew.js +52 -52
  22. package/dist/agents/fair.js +84 -84
  23. package/dist/agents/fog.js +30 -30
  24. package/dist/agents/frost.js +32 -32
  25. package/dist/agents/rain.js +32 -32
  26. package/dist/agents/snow.js +68 -68
  27. package/dist/cli/commands_md.d.ts +41 -0
  28. package/dist/cli/commands_md.d.ts.map +1 -0
  29. package/dist/cli/commands_md.js +140 -0
  30. package/dist/cli/commands_md.js.map +1 -0
  31. package/dist/cli/input_macros.d.ts +28 -0
  32. package/dist/cli/input_macros.d.ts.map +1 -0
  33. package/dist/cli/input_macros.js +120 -0
  34. package/dist/cli/input_macros.js.map +1 -0
  35. package/dist/cli/loom.d.ts +220 -0
  36. package/dist/cli/loom.d.ts.map +1 -0
  37. package/dist/cli/loom.js +1094 -0
  38. package/dist/cli/loom.js.map +1 -0
  39. package/dist/cli/loom_chat.d.ts +20 -0
  40. package/dist/cli/loom_chat.d.ts.map +1 -0
  41. package/dist/cli/loom_chat.js +685 -0
  42. package/dist/cli/loom_chat.js.map +1 -0
  43. package/dist/cli/main.js +310 -14
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/tui.d.ts.map +1 -1
  46. package/dist/cli/tui.js +7 -1
  47. package/dist/cli/tui.js.map +1 -1
  48. package/dist/core/agent/guard.d.ts +45 -0
  49. package/dist/core/agent/guard.d.ts.map +1 -0
  50. package/dist/core/agent/guard.js +113 -0
  51. package/dist/core/agent/guard.js.map +1 -0
  52. package/dist/core/agent.d.ts +17 -0
  53. package/dist/core/agent.d.ts.map +1 -1
  54. package/dist/core/agent.js +182 -93
  55. package/dist/core/agent.js.map +1 -1
  56. package/dist/core/factory.d.ts.map +1 -1
  57. package/dist/core/factory.js +34 -2
  58. package/dist/core/factory.js.map +1 -1
  59. package/dist/core/file_checkpoint.d.ts +57 -0
  60. package/dist/core/file_checkpoint.d.ts.map +1 -0
  61. package/dist/core/file_checkpoint.js +162 -0
  62. package/dist/core/file_checkpoint.js.map +1 -0
  63. package/dist/core/hooks.d.ts +43 -0
  64. package/dist/core/hooks.d.ts.map +1 -0
  65. package/dist/core/hooks.js +110 -0
  66. package/dist/core/hooks.js.map +1 -0
  67. package/dist/core/llm.d.ts.map +1 -1
  68. package/dist/core/llm.js +15 -9
  69. package/dist/core/llm.js.map +1 -1
  70. package/dist/core/longdoc.js +5 -5
  71. package/dist/core/mcp.d.ts +16 -0
  72. package/dist/core/mcp.d.ts.map +1 -1
  73. package/dist/core/mcp.js +55 -0
  74. package/dist/core/mcp.js.map +1 -1
  75. package/dist/core/model_config.d.ts +40 -0
  76. package/dist/core/model_config.d.ts.map +1 -0
  77. package/dist/core/model_config.js +191 -0
  78. package/dist/core/model_config.js.map +1 -0
  79. package/dist/core/skill.d.ts +7 -0
  80. package/dist/core/skill.d.ts.map +1 -1
  81. package/dist/core/skill.js +47 -0
  82. package/dist/core/skill.js.map +1 -1
  83. package/dist/core/skymd.d.ts +39 -0
  84. package/dist/core/skymd.d.ts.map +1 -0
  85. package/dist/core/skymd.js +177 -0
  86. package/dist/core/skymd.js.map +1 -0
  87. package/dist/core/tool.d.ts +12 -0
  88. package/dist/core/tool.d.ts.map +1 -1
  89. package/dist/core/tool.js +30 -0
  90. package/dist/core/tool.js.map +1 -1
  91. package/dist/core/verify.d.ts +27 -0
  92. package/dist/core/verify.d.ts.map +1 -0
  93. package/dist/core/verify.js +62 -0
  94. package/dist/core/verify.js.map +1 -0
  95. package/dist/skills/loader.d.ts +22 -2
  96. package/dist/skills/loader.d.ts.map +1 -1
  97. package/dist/skills/loader.js +45 -15
  98. package/dist/skills/loader.js.map +1 -1
  99. package/dist/tools/builtin.d.ts.map +1 -1
  100. package/dist/tools/builtin.js +13 -3
  101. package/dist/tools/builtin.js.map +1 -1
  102. package/dist/tools/model_tool.d.ts +11 -0
  103. package/dist/tools/model_tool.d.ts.map +1 -0
  104. package/dist/tools/model_tool.js +71 -0
  105. package/dist/tools/model_tool.js.map +1 -0
  106. package/dist/tools/todo.d.ts +30 -0
  107. package/dist/tools/todo.d.ts.map +1 -0
  108. package/dist/tools/todo.js +78 -0
  109. package/dist/tools/todo.js.map +1 -0
  110. package/docs/AESTHETIC_DESIGN.md +152 -144
  111. package/docs/OPTIMIZATION_PLAN.md +178 -178
  112. package/package.json +1 -1
  113. package/scripts/install.js +48 -48
  114. package/scripts/link.js +10 -10
  115. package/setup.bat +79 -79
  116. package/skill-test-ty2fOA/test.md +10 -10
  117. package/src/agents/dew.ts +70 -70
  118. package/src/agents/fair.ts +102 -102
  119. package/src/agents/fog.ts +48 -48
  120. package/src/agents/frost.ts +50 -50
  121. package/src/agents/rain.ts +50 -50
  122. package/src/agents/snow.ts +239 -239
  123. package/src/cli/commands_md.ts +112 -0
  124. package/src/cli/input_macros.ts +83 -0
  125. package/src/cli/loom.ts +982 -0
  126. package/src/cli/loom_chat.ts +598 -0
  127. package/src/cli/main.ts +255 -9
  128. package/src/cli/mode.ts +58 -58
  129. package/src/cli/tui.ts +228 -222
  130. package/src/core/agent/guard.ts +134 -0
  131. package/src/core/agent/task.ts +100 -100
  132. package/src/core/agent.ts +177 -95
  133. package/src/core/arbitrate.ts +162 -162
  134. package/src/core/catalog.ts +178 -178
  135. package/src/core/checkpoint.ts +94 -94
  136. package/src/core/estimate.ts +104 -104
  137. package/src/core/evolve.ts +191 -191
  138. package/src/core/factory.ts +31 -2
  139. package/src/core/file_checkpoint.ts +136 -0
  140. package/src/core/filter.ts +103 -103
  141. package/src/core/graph.ts +156 -156
  142. package/src/core/hooks.ts +126 -0
  143. package/src/core/icons.ts +53 -53
  144. package/src/core/index.ts +37 -37
  145. package/src/core/learn.ts +146 -146
  146. package/src/core/llm.ts +15 -9
  147. package/src/core/longdoc.ts +155 -155
  148. package/src/core/mcp.ts +48 -0
  149. package/src/core/mcp_server.ts +176 -176
  150. package/src/core/model_config.ts +157 -0
  151. package/src/core/profile.ts +255 -255
  152. package/src/core/router.ts +124 -124
  153. package/src/core/sandbox.ts +142 -142
  154. package/src/core/security.ts +243 -243
  155. package/src/core/skill.ts +42 -0
  156. package/src/core/skymd.ts +143 -0
  157. package/src/core/theme.ts +65 -65
  158. package/src/core/tool.ts +30 -0
  159. package/src/core/tool_router.ts +193 -193
  160. package/src/core/vector.ts +152 -152
  161. package/src/core/verify.ts +71 -0
  162. package/src/core/workspace.ts +150 -150
  163. package/src/plugins/loader.ts +66 -66
  164. package/src/skills/loader.ts +45 -16
  165. package/src/sql.js.d.ts +29 -29
  166. package/src/tools/builtin.ts +13 -3
  167. package/src/tools/computer.ts +269 -269
  168. package/src/tools/delegate.ts +49 -49
  169. package/src/tools/model_tool.ts +74 -0
  170. package/src/tools/todo.ts +76 -0
  171. package/src/web/tts.ts +93 -93
  172. package/tests/agent.test.ts +159 -159
  173. package/tests/agent_helpers.test.ts +48 -48
  174. package/tests/bus.test.ts +121 -121
  175. package/tests/catalog.test.ts +86 -86
  176. package/tests/checkpoint_commands.test.ts +124 -0
  177. package/tests/claude_compat.test.ts +110 -0
  178. package/tests/config.test.ts +41 -41
  179. package/tests/guard.test.ts +75 -0
  180. package/tests/icons.test.ts +45 -45
  181. package/tests/loom.test.ts +248 -0
  182. package/tests/memory.test.ts +170 -170
  183. package/tests/model_config.test.ts +109 -0
  184. package/tests/router.test.ts +86 -86
  185. package/tests/schemas.test.ts +51 -51
  186. package/tests/semantic.test.ts +83 -83
  187. package/tests/setup.ts +10 -10
  188. package/tests/skill.test.ts +172 -172
  189. package/tests/skymd.test.ts +146 -0
  190. package/tests/task.test.ts +60 -60
  191. package/tests/todo_toolstats.test.ts +94 -0
  192. package/tests/tool.test.ts +108 -108
  193. package/tests/tool_router.test.ts +71 -71
  194. package/tests/tui.test.ts +67 -67
  195. package/vitest.config.ts +17 -17
@@ -1,162 +1,162 @@
1
- /**
2
- * 多Agent冲突仲裁 — majority voting, quality scoring, tie-breaking.
3
- *
4
- * When multiple agents produce conflicting outputs on the same task
5
- * (or when a reviewer disagrees with an executor), this module
6
- * provides structured conflict resolution.
7
- */
8
-
9
- import type { TaskExecutionResult } from "./factory";
10
-
11
- /* ═══════════════════════════════════════
12
- Conflict detection
13
- ═══════════════════════════════════════ */
14
- export interface Conflict {
15
- taskId: string;
16
- results: TaskExecutionResult[];
17
- description: string;
18
- severity: "low" | "medium" | "high";
19
- }
20
-
21
- /** Detect if two results conflict based on success status and content overlap. */
22
- export function detectConflicts(results: TaskExecutionResult[]): Conflict[] {
23
- const byTask = new Map<string, TaskExecutionResult[]>();
24
- for (const r of results) { const id = r.id; if (!byTask.has(id)) byTask.set(id, []); byTask.get(id)!.push(r); }
25
-
26
- const conflicts: Conflict[] = [];
27
- for (const [id, items] of byTask) {
28
- if (items.length < 2) continue;
29
-
30
- const successes = items.filter(r => r.success);
31
- const failures = items.filter(r => !r.success);
32
-
33
- // All succeeded — check content divergence
34
- if (successes.length >= 2) {
35
- const contents = successes.map(r => (r.content || "").toLowerCase());
36
- const similarity = pairwiseSimilarity(contents);
37
- if (similarity < 0.3) {
38
- conflicts.push({ taskId: id, results: successes, description: "Multiple agents produced divergent successful outputs", severity: "medium" });
39
- }
40
- }
41
-
42
- // Mix of success and failure
43
- if (successes.length > 0 && failures.length > 0) {
44
- conflicts.push({ taskId: id, results: items, description: `${successes.length} succeeded, ${failures.length} failed — need tiebreaker`, severity: "medium" });
45
- }
46
-
47
- // All failed
48
- if (failures.length >= 2 && successes.length === 0) {
49
- conflicts.push({ taskId: id, results: failures, description: "All agents failed on this task", severity: "high" });
50
- }
51
- }
52
-
53
- return conflicts;
54
- }
55
-
56
- /* ═══════════════════════════════════════
57
- Content similarity (n-gram Jaccard)
58
- ═══════════════════════════════════════ */
59
- function pairwiseSimilarity(texts: string[]): number {
60
- if (texts.length < 2) return 1.0;
61
- let total = 0;
62
- let count = 0;
63
- for (let i = 0; i < texts.length; i++) {
64
- for (let j = i + 1; j < texts.length; j++) {
65
- total += ngramJaccard(texts[i], texts[j], 3);
66
- count++;
67
- }
68
- }
69
- return count === 0 ? 0 : total / count;
70
- }
71
-
72
- function ngramJaccard(a: string, b: string, n: number): number {
73
- const as = ngrams(a, n), bs = ngrams(b, n);
74
- if (as.size === 0 && bs.size === 0) return 1;
75
- let intersection = 0;
76
- for (const g of as) { if (bs.has(g)) intersection++; }
77
- const union = as.size + bs.size - intersection;
78
- return union === 0 ? 0 : intersection / union;
79
- }
80
-
81
- function ngrams(s: string, n: number): Set<string> {
82
- const out = new Set<string>();
83
- for (let i = 0; i <= s.length - n; i++) out.add(s.slice(i, i + n));
84
- return out;
85
- }
86
-
87
- /* ═══════════════════════════════════════
88
- Majority voting / arbitration
89
- ═══════════════════════════════════════ */
90
- export interface ArbitrationResult {
91
- winner: TaskExecutionResult;
92
- method: "unanimous" | "majority" | "tiebreaker" | "single";
93
- confidence: number; // 0-1
94
- reasoning: string;
95
- }
96
-
97
- /** Pick the best result from conflicting ones via majority vote. */
98
- export function arbitrate(results: TaskExecutionResult[]): ArbitrationResult {
99
- if (results.length === 0) throw new Error("No results to arbitrate");
100
- if (results.length === 1) return { winner: results[0], method: "single", confidence: 0.8, reasoning: "Only one result available" };
101
-
102
- const success = results.filter(r => r.success);
103
- const fail = results.filter(r => !r.success);
104
-
105
- // All agree (success)
106
- if (success.length === results.length) {
107
- const longest = success.reduce((a, b) => (b.content || "").length > (a.content || "").length ? b : a);
108
- return { winner: longest, method: "unanimous", confidence: 0.95, reasoning: `${results.length}/${results.length} agents agreed` };
109
- }
110
-
111
- // Majority success
112
- if (success.length > fail.length) {
113
- // Pick the longest successful content (most detailed)
114
- const best = success.reduce((a, b) => (b.content || "").length > (a.content || "").length ? b : a);
115
- return { winner: best, method: "majority", confidence: success.length / results.length, reasoning: `${success.length}/${results.length} succeeded, selected most detailed` };
116
- }
117
-
118
- // Majority failure — pick the "closest to success" (longest content)
119
- if (fail.length > success.length) {
120
- const best = fail.reduce((a, b) => (b.content || "").length > (a.content || "").length ? b : a);
121
- return { winner: best, method: "majority", confidence: 0.3, reasoning: `Majority failed (${fail.length}/${results.length}), best-effort from partial output` };
122
- }
123
-
124
- // Tie — prefer success, or longest content
125
- const tie = success.length > 0 ? success[0] : fail[0];
126
- return { winner: tie, method: "tiebreaker", confidence: 0.5, reasoning: `Tie — selected ${tie.success ? "success" : "longest"} result` };
127
- }
128
-
129
- /* ═══════════════════════════════════════
130
- Quality scoring for individual results
131
- ═══════════════════════════════════════ */
132
- export interface QualityScore {
133
- score: number; // 0-100
134
- completeness: number; // how much of the task was addressed
135
- richness: number; // detail level of the output
136
- correctness: number; // did it match expectations (requires ground truth)
137
- }
138
-
139
- export function scoreQuality(result: TaskExecutionResult): QualityScore {
140
- const content = result.content || "";
141
-
142
- // Completeness: length is a weak proxy but useful
143
- const completeness = content.length > 500 ? 80 : content.length > 100 ? 50 : content.length > 0 ? 20 : 0;
144
-
145
- // Richness: code blocks, structured output, bullet points
146
- let richness = 50;
147
- if (/```/.test(content)) richness += 20;
148
- if (/\|.*\|.*\|/.test(content)) richness += 15; // tables
149
- if (/^[-*] /.test(content)) richness += 10; // bullets
150
- if (/\d+\./.test(content)) richness += 10; // numbered lists
151
- richness = Math.min(100, richness);
152
-
153
- // Correctness: basic sanity checks
154
- let correctness = 70;
155
- if (content.includes("Error") || content.includes("error")) correctness -= 20;
156
- if (content.includes("[REDACTED]")) correctness -= 10;
157
- if (content.includes("truncated")) correctness -= 15;
158
- correctness = Math.max(0, correctness);
159
-
160
- const score = Math.round((completeness * 0.3 + richness * 0.3 + correctness * 0.4));
161
- return { score, completeness, richness, correctness };
162
- }
1
+ /**
2
+ * 多Agent冲突仲裁 — majority voting, quality scoring, tie-breaking.
3
+ *
4
+ * When multiple agents produce conflicting outputs on the same task
5
+ * (or when a reviewer disagrees with an executor), this module
6
+ * provides structured conflict resolution.
7
+ */
8
+
9
+ import type { TaskExecutionResult } from "./factory";
10
+
11
+ /* ═══════════════════════════════════════
12
+ Conflict detection
13
+ ═══════════════════════════════════════ */
14
+ export interface Conflict {
15
+ taskId: string;
16
+ results: TaskExecutionResult[];
17
+ description: string;
18
+ severity: "low" | "medium" | "high";
19
+ }
20
+
21
+ /** Detect if two results conflict based on success status and content overlap. */
22
+ export function detectConflicts(results: TaskExecutionResult[]): Conflict[] {
23
+ const byTask = new Map<string, TaskExecutionResult[]>();
24
+ for (const r of results) { const id = r.id; if (!byTask.has(id)) byTask.set(id, []); byTask.get(id)!.push(r); }
25
+
26
+ const conflicts: Conflict[] = [];
27
+ for (const [id, items] of byTask) {
28
+ if (items.length < 2) continue;
29
+
30
+ const successes = items.filter(r => r.success);
31
+ const failures = items.filter(r => !r.success);
32
+
33
+ // All succeeded — check content divergence
34
+ if (successes.length >= 2) {
35
+ const contents = successes.map(r => (r.content || "").toLowerCase());
36
+ const similarity = pairwiseSimilarity(contents);
37
+ if (similarity < 0.3) {
38
+ conflicts.push({ taskId: id, results: successes, description: "Multiple agents produced divergent successful outputs", severity: "medium" });
39
+ }
40
+ }
41
+
42
+ // Mix of success and failure
43
+ if (successes.length > 0 && failures.length > 0) {
44
+ conflicts.push({ taskId: id, results: items, description: `${successes.length} succeeded, ${failures.length} failed — need tiebreaker`, severity: "medium" });
45
+ }
46
+
47
+ // All failed
48
+ if (failures.length >= 2 && successes.length === 0) {
49
+ conflicts.push({ taskId: id, results: failures, description: "All agents failed on this task", severity: "high" });
50
+ }
51
+ }
52
+
53
+ return conflicts;
54
+ }
55
+
56
+ /* ═══════════════════════════════════════
57
+ Content similarity (n-gram Jaccard)
58
+ ═══════════════════════════════════════ */
59
+ function pairwiseSimilarity(texts: string[]): number {
60
+ if (texts.length < 2) return 1.0;
61
+ let total = 0;
62
+ let count = 0;
63
+ for (let i = 0; i < texts.length; i++) {
64
+ for (let j = i + 1; j < texts.length; j++) {
65
+ total += ngramJaccard(texts[i], texts[j], 3);
66
+ count++;
67
+ }
68
+ }
69
+ return count === 0 ? 0 : total / count;
70
+ }
71
+
72
+ function ngramJaccard(a: string, b: string, n: number): number {
73
+ const as = ngrams(a, n), bs = ngrams(b, n);
74
+ if (as.size === 0 && bs.size === 0) return 1;
75
+ let intersection = 0;
76
+ for (const g of as) { if (bs.has(g)) intersection++; }
77
+ const union = as.size + bs.size - intersection;
78
+ return union === 0 ? 0 : intersection / union;
79
+ }
80
+
81
+ function ngrams(s: string, n: number): Set<string> {
82
+ const out = new Set<string>();
83
+ for (let i = 0; i <= s.length - n; i++) out.add(s.slice(i, i + n));
84
+ return out;
85
+ }
86
+
87
+ /* ═══════════════════════════════════════
88
+ Majority voting / arbitration
89
+ ═══════════════════════════════════════ */
90
+ export interface ArbitrationResult {
91
+ winner: TaskExecutionResult;
92
+ method: "unanimous" | "majority" | "tiebreaker" | "single";
93
+ confidence: number; // 0-1
94
+ reasoning: string;
95
+ }
96
+
97
+ /** Pick the best result from conflicting ones via majority vote. */
98
+ export function arbitrate(results: TaskExecutionResult[]): ArbitrationResult {
99
+ if (results.length === 0) throw new Error("No results to arbitrate");
100
+ if (results.length === 1) return { winner: results[0], method: "single", confidence: 0.8, reasoning: "Only one result available" };
101
+
102
+ const success = results.filter(r => r.success);
103
+ const fail = results.filter(r => !r.success);
104
+
105
+ // All agree (success)
106
+ if (success.length === results.length) {
107
+ const longest = success.reduce((a, b) => (b.content || "").length > (a.content || "").length ? b : a);
108
+ return { winner: longest, method: "unanimous", confidence: 0.95, reasoning: `${results.length}/${results.length} agents agreed` };
109
+ }
110
+
111
+ // Majority success
112
+ if (success.length > fail.length) {
113
+ // Pick the longest successful content (most detailed)
114
+ const best = success.reduce((a, b) => (b.content || "").length > (a.content || "").length ? b : a);
115
+ return { winner: best, method: "majority", confidence: success.length / results.length, reasoning: `${success.length}/${results.length} succeeded, selected most detailed` };
116
+ }
117
+
118
+ // Majority failure — pick the "closest to success" (longest content)
119
+ if (fail.length > success.length) {
120
+ const best = fail.reduce((a, b) => (b.content || "").length > (a.content || "").length ? b : a);
121
+ return { winner: best, method: "majority", confidence: 0.3, reasoning: `Majority failed (${fail.length}/${results.length}), best-effort from partial output` };
122
+ }
123
+
124
+ // Tie — prefer success, or longest content
125
+ const tie = success.length > 0 ? success[0] : fail[0];
126
+ return { winner: tie, method: "tiebreaker", confidence: 0.5, reasoning: `Tie — selected ${tie.success ? "success" : "longest"} result` };
127
+ }
128
+
129
+ /* ═══════════════════════════════════════
130
+ Quality scoring for individual results
131
+ ═══════════════════════════════════════ */
132
+ export interface QualityScore {
133
+ score: number; // 0-100
134
+ completeness: number; // how much of the task was addressed
135
+ richness: number; // detail level of the output
136
+ correctness: number; // did it match expectations (requires ground truth)
137
+ }
138
+
139
+ export function scoreQuality(result: TaskExecutionResult): QualityScore {
140
+ const content = result.content || "";
141
+
142
+ // Completeness: length is a weak proxy but useful
143
+ const completeness = content.length > 500 ? 80 : content.length > 100 ? 50 : content.length > 0 ? 20 : 0;
144
+
145
+ // Richness: code blocks, structured output, bullet points
146
+ let richness = 50;
147
+ if (/```/.test(content)) richness += 20;
148
+ if (/\|.*\|.*\|/.test(content)) richness += 15; // tables
149
+ if (/^[-*] /.test(content)) richness += 10; // bullets
150
+ if (/\d+\./.test(content)) richness += 10; // numbered lists
151
+ richness = Math.min(100, richness);
152
+
153
+ // Correctness: basic sanity checks
154
+ let correctness = 70;
155
+ if (content.includes("Error") || content.includes("error")) correctness -= 20;
156
+ if (content.includes("[REDACTED]")) correctness -= 10;
157
+ if (content.includes("truncated")) correctness -= 15;
158
+ correctness = Math.max(0, correctness);
159
+
160
+ const score = Math.round((completeness * 0.3 + richness * 0.3 + correctness * 0.4));
161
+ return { score, completeness, richness, correctness };
162
+ }