@plumpslabs/kuma 2.3.0 → 2.3.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.
- package/README.md +1 -1
- package/dist/{agentDetector-FDRVUNJY.js → agentDetector-G4RBMZ6X.js} +1 -1
- package/dist/chunk-2IIDVJPW.js +836 -0
- package/dist/{chunk-OUPFJQKW.js → chunk-4CKTTORL.js} +43 -2
- package/dist/{chunk-5A6AKUJZ.js → chunk-5WSPGLXS.js} +1 -1
- package/dist/{chunk-TT37TE4P.js → chunk-6W7YV4AF.js} +37 -2
- package/dist/{chunk-IXMWW5WA.js → chunk-E2KFPEBT.js} +80 -1
- package/dist/{chunk-WP3WRPDC.js → chunk-WG47POMW.js} +2 -2
- package/dist/{chunk-IMO4QAFJ.js → chunk-ZDQVSBFB.js} +120 -5
- package/dist/index.js +355 -98
- package/dist/{init-O4MBXFZL.js → init-3MBHSTWD.js} +2 -2
- package/dist/kumaDb-PA7XVERC.js +59 -0
- package/dist/{kumaGraph-GW5SWOHQ.js → kumaGraph-VUTLTOFY.js} +3 -3
- package/dist/{kumaMemory-IO7TVE7W.js → kumaMemory-YEIEXGNW.js} +3 -3
- package/dist/pathValidator-V4DC6U6Z.js +22 -0
- package/dist/{safetyScore-7FU7XSY5.js → safetyScore-OQFYLLP7.js} +5 -5
- package/dist/{sessionMemory-52ECPXZ6.js → sessionMemory-JQT5GEWO.js} +1 -1
- package/package.json +1 -1
- package/dist/chunk-6NEMSUKP.js +0 -304
- package/dist/kumaDb-KN7Z4B5V.js +0 -19
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sessionMemory
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-6W7YV4AF.js";
|
|
4
4
|
import {
|
|
5
5
|
getProjectRoot
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-E2KFPEBT.js";
|
|
7
7
|
|
|
8
8
|
// src/engine/kumaMemory.ts
|
|
9
9
|
import fs from "fs";
|
|
@@ -73,11 +73,52 @@ function recordDecision(decision) {
|
|
|
73
73
|
const existing = sessionMemory.getMemoryContent("decisions");
|
|
74
74
|
sessionMemory.writeMemory("decisions", existing + entry);
|
|
75
75
|
sessionMemory.recordToolCall("kuma_decision", { title: decision.title });
|
|
76
|
+
recordDecisionToGraph(decision).catch(() => {
|
|
77
|
+
});
|
|
76
78
|
return `\u2705 Decision "${decision.title}" recorded.`;
|
|
77
79
|
} catch (err) {
|
|
78
80
|
return `Error recording decision: ${err}`;
|
|
79
81
|
}
|
|
80
82
|
}
|
|
83
|
+
async function recordDecisionToGraph(decision) {
|
|
84
|
+
try {
|
|
85
|
+
const { upsertNode, addEdge } = await import("./kumaGraph-VUTLTOFY.js");
|
|
86
|
+
const decisionId = `decision::${decision.title.replace(/[^a-zA-Z0-9_\-\s]/g, "").trim().replace(/\s+/g, "-")}`;
|
|
87
|
+
await upsertNode({
|
|
88
|
+
id: decisionId,
|
|
89
|
+
type: "variable",
|
|
90
|
+
name: `ADR: ${decision.title.substring(0, 80)}`,
|
|
91
|
+
metadata: {
|
|
92
|
+
type: "architectural-decision",
|
|
93
|
+
context: decision.context.substring(0, 200),
|
|
94
|
+
rationale: decision.rationale.substring(0, 200),
|
|
95
|
+
outcome: decision.outcome,
|
|
96
|
+
timestamp: decision.timestamp
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const filePathMatches = decision.context.matchAll(/["']?([\w./-]+\.\w+)["']?/g);
|
|
100
|
+
for (const match of filePathMatches) {
|
|
101
|
+
const possiblePath = match[1];
|
|
102
|
+
if (possiblePath.includes("/") && (possiblePath.endsWith(".ts") || possiblePath.endsWith(".js") || possiblePath.endsWith(".json") || possiblePath.endsWith(".md"))) {
|
|
103
|
+
try {
|
|
104
|
+
const fileId = `file::${possiblePath}`;
|
|
105
|
+
await upsertNode({ id: fileId, type: "file", name: possiblePath });
|
|
106
|
+
await addEdge({ sourceId: decisionId, targetId: fileId, type: "depends_on", metadata: { reason: "adr-context" } });
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (decision.outcome && decision.outcome !== "implemented") {
|
|
112
|
+
try {
|
|
113
|
+
const outcomeId = `decision-outcome::${decision.outcome.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase()}`;
|
|
114
|
+
await upsertNode({ id: outcomeId, type: "variable", name: `Outcome: ${decision.outcome.substring(0, 60)}` });
|
|
115
|
+
await addEdge({ sourceId: decisionId, targetId: outcomeId, type: "depends_on" });
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
81
122
|
var decisionCooldown = 0;
|
|
82
123
|
function shouldRecordDecision() {
|
|
83
124
|
const history = sessionMemory.getToolCallHistory(30);
|
|
@@ -300,15 +300,41 @@ No unresolved issues.`;
|
|
|
300
300
|
}
|
|
301
301
|
recordToolCall(toolName, params) {
|
|
302
302
|
this.ensureInit();
|
|
303
|
+
const timestamp = Date.now();
|
|
303
304
|
this.state.toolCalls.push({
|
|
304
305
|
toolName,
|
|
305
306
|
params,
|
|
306
|
-
timestamp
|
|
307
|
+
timestamp
|
|
307
308
|
});
|
|
308
309
|
if (this.state.toolCalls.length > 100) {
|
|
309
310
|
this.state.toolCalls = this.state.toolCalls.slice(-100);
|
|
310
311
|
}
|
|
311
312
|
this.save();
|
|
313
|
+
this.autoTrackToDb(toolName, params, timestamp).catch(() => {
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Auto-track tool calls to the database sessions + tool_calls + experiences tables.
|
|
318
|
+
* This is the passive observability pipeline — every tool call gets recorded
|
|
319
|
+
* automatically without requiring the tool to explicitly write to the DB.
|
|
320
|
+
*/
|
|
321
|
+
async autoTrackToDb(toolName, params, timestamp) {
|
|
322
|
+
try {
|
|
323
|
+
const { getDb, saveDb } = await import("./kumaDb-PA7XVERC.js");
|
|
324
|
+
const db = await getDb();
|
|
325
|
+
db.run(
|
|
326
|
+
`INSERT INTO tool_calls (tool_name, params, success, duration_ms, created_at) VALUES (?, ?, 1, 0, ?)`,
|
|
327
|
+
[toolName, JSON.stringify(params), Math.floor(timestamp / 1e3)]
|
|
328
|
+
);
|
|
329
|
+
const paramsHash = simpleHash(JSON.stringify(params));
|
|
330
|
+
const filePath = params.filePath || params.file_path || params.target || null;
|
|
331
|
+
db.run(
|
|
332
|
+
`INSERT INTO experiences (tool_name, params_hash, success, context_file, context_action, created_at) VALUES (?, ?, 1, ?, ?, ?)`,
|
|
333
|
+
[toolName, paramsHash, filePath, params.action || null, Math.floor(timestamp / 1e3)]
|
|
334
|
+
);
|
|
335
|
+
saveDb(db);
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
312
338
|
}
|
|
313
339
|
setConventions(conventions) {
|
|
314
340
|
this.ensureInit();
|
|
@@ -571,7 +597,7 @@ No unresolved issues.`;
|
|
|
571
597
|
this.addModifiedFile(filePath);
|
|
572
598
|
}
|
|
573
599
|
try {
|
|
574
|
-
const { recordChange } = await import("./kumaDb-
|
|
600
|
+
const { recordChange } = await import("./kumaDb-PA7XVERC.js");
|
|
575
601
|
const gitHash = this.getGitHead();
|
|
576
602
|
await recordChange({ filePath, changeType, symbol, gitCommitHash: gitHash || void 0 });
|
|
577
603
|
} catch {
|
|
@@ -608,6 +634,15 @@ No unresolved issues.`;
|
|
|
608
634
|
}
|
|
609
635
|
}
|
|
610
636
|
};
|
|
637
|
+
function simpleHash(str) {
|
|
638
|
+
let hash = 0;
|
|
639
|
+
for (let i = 0; i < str.length; i++) {
|
|
640
|
+
const char = str.charCodeAt(i);
|
|
641
|
+
hash = (hash << 5) - hash + char;
|
|
642
|
+
hash = hash & hash;
|
|
643
|
+
}
|
|
644
|
+
return Math.abs(hash).toString(36);
|
|
645
|
+
}
|
|
611
646
|
var sessionMemory = new SessionMemory();
|
|
612
647
|
|
|
613
648
|
export {
|
|
@@ -89,16 +89,95 @@ function validateFilePath(filePath, projectRoot) {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
function validateFileExtension(filePath) {
|
|
93
|
+
const allowedExtensions = [
|
|
94
|
+
".ts",
|
|
95
|
+
".tsx",
|
|
96
|
+
".js",
|
|
97
|
+
".jsx",
|
|
98
|
+
".mjs",
|
|
99
|
+
".cjs",
|
|
100
|
+
".json",
|
|
101
|
+
".md",
|
|
102
|
+
".css",
|
|
103
|
+
".html",
|
|
104
|
+
".htm",
|
|
105
|
+
".env",
|
|
106
|
+
".env.example",
|
|
107
|
+
".env.local",
|
|
108
|
+
".yml",
|
|
109
|
+
".yaml",
|
|
110
|
+
".toml",
|
|
111
|
+
".svg",
|
|
112
|
+
".png",
|
|
113
|
+
".jpg",
|
|
114
|
+
".gif",
|
|
115
|
+
".sh",
|
|
116
|
+
".bat",
|
|
117
|
+
".ps1",
|
|
118
|
+
".sql",
|
|
119
|
+
".graphql",
|
|
120
|
+
".gql",
|
|
121
|
+
".vue",
|
|
122
|
+
".svelte",
|
|
123
|
+
".astro",
|
|
124
|
+
".prisma",
|
|
125
|
+
".proto",
|
|
126
|
+
".txt",
|
|
127
|
+
".log"
|
|
128
|
+
];
|
|
129
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
130
|
+
return allowedExtensions.includes(ext);
|
|
131
|
+
}
|
|
92
132
|
function getProjectRoot() {
|
|
93
133
|
return process.env.AGENT_PROJECT_ROOT ?? process.cwd();
|
|
94
134
|
}
|
|
135
|
+
function isReadable(filePath) {
|
|
136
|
+
try {
|
|
137
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
138
|
+
return true;
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function isWritable(dirPath) {
|
|
144
|
+
try {
|
|
145
|
+
fs.accessSync(dirPath, fs.constants.W_OK);
|
|
146
|
+
return true;
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function getBackupPath(filePath, timestamp) {
|
|
152
|
+
const ts = timestamp ?? Date.now();
|
|
153
|
+
const root = getProjectRoot();
|
|
154
|
+
const relativePath = path.relative(root, filePath);
|
|
155
|
+
return path.join(root, ".kuma", "backups", String(ts), relativePath);
|
|
156
|
+
}
|
|
157
|
+
function ensureBackupDir() {
|
|
158
|
+
const root = getProjectRoot();
|
|
159
|
+
const backupDir = path.join(root, ".kuma", "backups");
|
|
160
|
+
if (!fs.existsSync(backupDir)) {
|
|
161
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
162
|
+
}
|
|
163
|
+
return backupDir;
|
|
164
|
+
}
|
|
95
165
|
function getKumaDir() {
|
|
96
166
|
const root = getProjectRoot();
|
|
97
167
|
return path.join(root, ".kuma");
|
|
98
168
|
}
|
|
169
|
+
function getKumaBackupsDir() {
|
|
170
|
+
return path.join(getKumaDir(), "backups");
|
|
171
|
+
}
|
|
99
172
|
|
|
100
173
|
export {
|
|
101
174
|
validateFilePath,
|
|
175
|
+
validateFileExtension,
|
|
102
176
|
getProjectRoot,
|
|
103
|
-
|
|
177
|
+
isReadable,
|
|
178
|
+
isWritable,
|
|
179
|
+
getBackupPath,
|
|
180
|
+
ensureBackupDir,
|
|
181
|
+
getKumaDir,
|
|
182
|
+
getKumaBackupsDir
|
|
104
183
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getDb,
|
|
3
3
|
saveDb
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2IIDVJPW.js";
|
|
5
5
|
import {
|
|
6
6
|
getProjectRoot
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-E2KFPEBT.js";
|
|
8
8
|
|
|
9
9
|
// src/engine/kumaSelfHeal.ts
|
|
10
10
|
import { execSync } from "child_process";
|
|
@@ -458,7 +458,7 @@ The two nodes may not be connected in the knowledge graph yet. Use more tools to
|
|
|
458
458
|
}
|
|
459
459
|
async function buildFromSessionMemory() {
|
|
460
460
|
try {
|
|
461
|
-
const { sessionMemory } = await import("./sessionMemory-
|
|
461
|
+
const { sessionMemory } = await import("./sessionMemory-JQT5GEWO.js");
|
|
462
462
|
const toolCalls = sessionMemory.getToolCallHistory(50);
|
|
463
463
|
let edgeCount = 0;
|
|
464
464
|
for (const call of toolCalls) {
|
|
@@ -532,7 +532,7 @@ async function searchGraph(query, limit = 20) {
|
|
|
532
532
|
}
|
|
533
533
|
stmt.free();
|
|
534
534
|
if (results.length === 0) {
|
|
535
|
-
return
|
|
535
|
+
return await codebaseSearchFallback(query, limit);
|
|
536
536
|
}
|
|
537
537
|
const lines = [
|
|
538
538
|
`\u{1F50D} **Graph Search** \u2014 ${results.length} result(s) for "${query}"`,
|
|
@@ -546,6 +546,89 @@ async function searchGraph(query, limit = 20) {
|
|
|
546
546
|
return `Error searching graph: ${err}`;
|
|
547
547
|
}
|
|
548
548
|
}
|
|
549
|
+
async function codebaseSearchFallback(query, limit = 20) {
|
|
550
|
+
try {
|
|
551
|
+
const fg = (await import("fast-glob")).default;
|
|
552
|
+
const { getProjectRoot: getProjectRoot2 } = await import("./pathValidator-V4DC6U6Z.js");
|
|
553
|
+
const root = getProjectRoot2();
|
|
554
|
+
const patterns = [
|
|
555
|
+
`**/*${query}*`,
|
|
556
|
+
`**/*${query}*/**`,
|
|
557
|
+
query.includes(".") ? `**/${query}` : null
|
|
558
|
+
].filter(Boolean);
|
|
559
|
+
const ignorePatterns = ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/.kuma/**"];
|
|
560
|
+
const files = await fg(patterns, {
|
|
561
|
+
cwd: root,
|
|
562
|
+
ignore: ignorePatterns,
|
|
563
|
+
onlyFiles: true,
|
|
564
|
+
deep: 6,
|
|
565
|
+
dot: false
|
|
566
|
+
});
|
|
567
|
+
if (files.length === 0) {
|
|
568
|
+
return `\u{1F50D} **Codebase Search** \u2014 No results for "${query}". Try a different search term or research the topic first.`;
|
|
569
|
+
}
|
|
570
|
+
const lines = [
|
|
571
|
+
`\u{1F50D} **Codebase Search** \u2014 ${files.length} result(s) for "${query}" (graph fallback)`,
|
|
572
|
+
`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
|
|
573
|
+
""
|
|
574
|
+
];
|
|
575
|
+
for (const file of files.slice(0, limit)) {
|
|
576
|
+
lines.push(` \u{1F4C4} **${file}**`);
|
|
577
|
+
}
|
|
578
|
+
if (files.length > limit) {
|
|
579
|
+
lines.push(` ... and ${files.length - limit} more`);
|
|
580
|
+
}
|
|
581
|
+
lines.push(
|
|
582
|
+
"",
|
|
583
|
+
"\u{1F4A1} The knowledge graph is empty or has no matches.",
|
|
584
|
+
"\u{1F4A1} Run research or use more tools to build the graph for richer results."
|
|
585
|
+
);
|
|
586
|
+
return lines.join("\n");
|
|
587
|
+
} catch {
|
|
588
|
+
try {
|
|
589
|
+
let walk2 = function(dir) {
|
|
590
|
+
if (results.length >= limit) return;
|
|
591
|
+
let entries = [];
|
|
592
|
+
try {
|
|
593
|
+
entries = fs2.readdirSync(dir);
|
|
594
|
+
} catch {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
for (const entry of entries) {
|
|
598
|
+
if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "build") continue;
|
|
599
|
+
const full = path2.join(dir, entry);
|
|
600
|
+
try {
|
|
601
|
+
const stat = fs2.statSync(full);
|
|
602
|
+
if (stat.isDirectory()) {
|
|
603
|
+
walk2(full);
|
|
604
|
+
} else if (entry.toLowerCase().includes(query.toLowerCase())) {
|
|
605
|
+
results.push(path2.relative(root, full));
|
|
606
|
+
}
|
|
607
|
+
} catch {
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
var walk = walk2;
|
|
612
|
+
const fs2 = await import("fs");
|
|
613
|
+
const path2 = await import("path");
|
|
614
|
+
const root = process.cwd();
|
|
615
|
+
const results = [];
|
|
616
|
+
walk2(root);
|
|
617
|
+
if (results.length === 0) {
|
|
618
|
+
return `\u{1F50D} **Codebase Search** \u2014 No results for "${query}". Try a different search term.`;
|
|
619
|
+
}
|
|
620
|
+
return [
|
|
621
|
+
`\u{1F50D} **Codebase Search** \u2014 ${results.length} result(s) for "${query}" (basic fallback)`,
|
|
622
|
+
"",
|
|
623
|
+
...results.slice(0, limit).map((f) => ` \u{1F4C4} **${f}**`),
|
|
624
|
+
"",
|
|
625
|
+
"\u{1F4A1} Install fast-glob for faster, more comprehensive codebase searches."
|
|
626
|
+
].join("\n");
|
|
627
|
+
} catch {
|
|
628
|
+
return `\u{1F50D} **Search** \u2014 No results for "${query}". The knowledge graph is empty. Try researching first.`;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
549
632
|
async function getGraphStats() {
|
|
550
633
|
try {
|
|
551
634
|
const db = await getDb();
|
|
@@ -589,6 +672,10 @@ async function getGraphStats() {
|
|
|
589
672
|
async function analyzeImpact(target) {
|
|
590
673
|
try {
|
|
591
674
|
const db = await getDb();
|
|
675
|
+
const nodeCount = db.exec("SELECT COUNT(*) as c FROM nodes")[0]?.values[0][0] ?? 0;
|
|
676
|
+
if (nodeCount === 0) {
|
|
677
|
+
return await codebaseImpactFallback(target);
|
|
678
|
+
}
|
|
592
679
|
const nodeStmt = db.prepare(`
|
|
593
680
|
SELECT id, name, type, file_path FROM nodes
|
|
594
681
|
WHERE name LIKE ? OR file_path = ? OR id = ?
|
|
@@ -604,7 +691,7 @@ async function analyzeImpact(target) {
|
|
|
604
691
|
}
|
|
605
692
|
nodeStmt.free();
|
|
606
693
|
if (!nodeId2) {
|
|
607
|
-
return
|
|
694
|
+
return await codebaseImpactFallback(target);
|
|
608
695
|
}
|
|
609
696
|
const refStmt = db.prepare(`
|
|
610
697
|
SELECT COUNT(*) as cnt, COUNT(DISTINCT CASE WHEN e.source_id != ? THEN e.source_id ELSE e.target_id END) as file_count
|
|
@@ -651,6 +738,34 @@ async function analyzeImpact(target) {
|
|
|
651
738
|
return { symbol: target, references: 0, files: 0, testFiles: 0, entryPoints: [], risk: "low" };
|
|
652
739
|
}
|
|
653
740
|
}
|
|
741
|
+
async function codebaseImpactFallback(target) {
|
|
742
|
+
try {
|
|
743
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
744
|
+
const root = process.cwd();
|
|
745
|
+
const grepResult = execSync2(
|
|
746
|
+
`grep -rn --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.json" -l "${target.replace(/"/g, '\\"')}" . 2>/dev/null | grep -v node_modules | grep -v .git | grep -v dist | grep -v .kuma | head -50`,
|
|
747
|
+
{ cwd: root, encoding: "utf-8", timeout: 5e3, maxBuffer: 1024 * 1024 }
|
|
748
|
+
).trim();
|
|
749
|
+
const files = grepResult ? grepResult.split("\n").filter(Boolean).filter((f) => !f.startsWith("./")) : [];
|
|
750
|
+
if (files.length === 0) {
|
|
751
|
+
return { symbol: target, references: 0, files: 0, testFiles: 0, entryPoints: [], risk: "low" };
|
|
752
|
+
}
|
|
753
|
+
const fileCount = files.length;
|
|
754
|
+
const testFiles = files.filter((f) => f.includes("test") || f.includes("spec") || f.includes("__tests__")).length;
|
|
755
|
+
const risk = fileCount > 20 ? "high" : fileCount > 5 ? "medium" : "low";
|
|
756
|
+
return {
|
|
757
|
+
symbol: target,
|
|
758
|
+
references: fileCount,
|
|
759
|
+
// file-level count since we're approximating
|
|
760
|
+
files: fileCount,
|
|
761
|
+
testFiles,
|
|
762
|
+
entryPoints: files.slice(0, 5).map((f) => `${f} (grep match)`),
|
|
763
|
+
risk
|
|
764
|
+
};
|
|
765
|
+
} catch {
|
|
766
|
+
return { symbol: target, references: 0, files: 0, testFiles: 0, entryPoints: [], risk: "low" };
|
|
767
|
+
}
|
|
768
|
+
}
|
|
654
769
|
function formatImpact(result) {
|
|
655
770
|
const icon = result.risk === "high" ? "\u{1F534}" : result.risk === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
656
771
|
const lines = [
|