@plumpslabs/kuma 2.3.19 → 2.3.22
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 +18 -0
- package/dist/index.js +10221 -332
- package/package.json +2 -2
- package/packages/ide/studio/dist/index.js +37 -5
- package/packages/ide/studio/public/index.html +284 -63
- package/dist/agentDetector-YOWQVJFR.js +0 -186
- package/dist/chunk-3BRBJZ7P.js +0 -1055
- package/dist/chunk-3OHYYXYN.js +0 -71
- package/dist/chunk-ABKE45T4.js +0 -1264
- package/dist/chunk-E2KFPEBT.js +0 -183
- package/dist/chunk-FKRSI5U5.js +0 -282
- package/dist/chunk-GFLSAXAH.js +0 -155
- package/dist/chunk-L7F67KUP.js +0 -172
- package/dist/chunk-LVKOGXLC.js +0 -658
- package/dist/chunk-PRUTTZBS.js +0 -1113
- package/dist/contextDigest-QB5XHPXE.js +0 -177
- package/dist/domainRules-QLPAQASB.js +0 -20
- package/dist/init-PL4XL662.js +0 -15
- package/dist/kumaAstValidator-CNM7FHYA.js +0 -150
- package/dist/kumaCheckpoint-J2LDQMEO.js +0 -207
- package/dist/kumaCodeScanner-J6B2EHGI.js +0 -563
- package/dist/kumaContractEngine-KX27T4N7.js +0 -305
- package/dist/kumaDb-4XZ5S2LH.js +0 -65
- package/dist/kumaDriftDetector-TOORILSZ.js +0 -237
- package/dist/kumaGotchas-XRGFFBTA.js +0 -151
- package/dist/kumaGraph-UMXZNGYF.js +0 -44
- package/dist/kumaMemory-FBJMV77G.js +0 -16
- package/dist/kumaMiner-XJETL7TL.js +0 -176
- package/dist/kumaPolicyEngine-2QDJDLM7.js +0 -311
- package/dist/kumaProgressiveContext-MWEDRXOH.js +0 -231
- package/dist/kumaSearch-PV4QTKE7.js +0 -321
- package/dist/kumaTrajectory-7NOAVNG2.js +0 -460
- package/dist/kumaVerifier-6YEGC77M.js +0 -265
- package/dist/kumaVisualize-264OEBGJ.js +0 -264
- package/dist/pathValidator-V4DC6U6Z.js +0 -22
- package/dist/safetyAudit-O45SPNTS.js +0 -12
- package/dist/safetyScore-TMMRD2MV.js +0 -333
- package/dist/sessionMemory-YPKVIOMV.js +0 -6
- package/dist/skillGenerator-PWEJKZNX.js +0 -304
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getProjectRoot
|
|
3
|
-
} from "./chunk-E2KFPEBT.js";
|
|
4
|
-
|
|
5
|
-
// src/engine/kumaContractEngine.ts
|
|
6
|
-
import fs from "fs";
|
|
7
|
-
import path from "path";
|
|
8
|
-
var DEFAULT_CONTRACTS = {
|
|
9
|
-
version: 1,
|
|
10
|
-
contracts: [
|
|
11
|
-
{
|
|
12
|
-
id: "no-delete-tests-without-replacement",
|
|
13
|
-
description: "Test files should not be deleted without replacement",
|
|
14
|
-
appliesTo: ["*"],
|
|
15
|
-
preConditions: [],
|
|
16
|
-
postConditions: [
|
|
17
|
-
{
|
|
18
|
-
type: "not_contains",
|
|
19
|
-
target: "*/.test.*",
|
|
20
|
-
value: "test.skip",
|
|
21
|
-
description: "Tests should not be skipped without reason"
|
|
22
|
-
}
|
|
23
|
-
],
|
|
24
|
-
severity: "warning"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
id: "no-silent-empty-catch",
|
|
28
|
-
description: "Catch blocks must not be empty",
|
|
29
|
-
appliesTo: ["*"],
|
|
30
|
-
preConditions: [],
|
|
31
|
-
postConditions: [
|
|
32
|
-
{
|
|
33
|
-
type: "not_contains",
|
|
34
|
-
target: "*.ts",
|
|
35
|
-
value: "catch {}",
|
|
36
|
-
description: "Empty catch blocks suppress errors silently"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
type: "not_contains",
|
|
40
|
-
target: "*.ts",
|
|
41
|
-
value: "catch (e) {}",
|
|
42
|
-
description: "Empty catch (e) {} suppresses errors silently"
|
|
43
|
-
}
|
|
44
|
-
],
|
|
45
|
-
severity: "error"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: "api-response-shape",
|
|
49
|
-
description: "API response shape must be preserved",
|
|
50
|
-
appliesTo: ["src/api/*", "src/routes/*"],
|
|
51
|
-
preConditions: [],
|
|
52
|
-
postConditions: [],
|
|
53
|
-
severity: "warning"
|
|
54
|
-
}
|
|
55
|
-
]
|
|
56
|
-
};
|
|
57
|
-
function contractsDir() {
|
|
58
|
-
return path.join(getProjectRoot(), ".kuma", "contracts");
|
|
59
|
-
}
|
|
60
|
-
function ensureContractsDir() {
|
|
61
|
-
const dir = contractsDir();
|
|
62
|
-
if (!fs.existsSync(dir)) {
|
|
63
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
64
|
-
const defaultPath = path.join(dir, "default.json");
|
|
65
|
-
if (!fs.existsSync(defaultPath)) {
|
|
66
|
-
fs.writeFileSync(
|
|
67
|
-
defaultPath,
|
|
68
|
-
JSON.stringify(DEFAULT_CONTRACTS, null, 2),
|
|
69
|
-
"utf-8"
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function loadContracts() {
|
|
75
|
-
ensureContractsDir();
|
|
76
|
-
const dir = contractsDir();
|
|
77
|
-
const contracts = [];
|
|
78
|
-
try {
|
|
79
|
-
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
80
|
-
for (const file of files) {
|
|
81
|
-
try {
|
|
82
|
-
const content = fs.readFileSync(path.join(dir, file), "utf-8");
|
|
83
|
-
const config = JSON.parse(content);
|
|
84
|
-
contracts.push(config);
|
|
85
|
-
} catch (err) {
|
|
86
|
-
console.error(`[ContractEngine] Failed to load ${file}: ${err}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
} catch {
|
|
90
|
-
}
|
|
91
|
-
if (contracts.length === 0) {
|
|
92
|
-
contracts.push(DEFAULT_CONTRACTS);
|
|
93
|
-
}
|
|
94
|
-
return contracts;
|
|
95
|
-
}
|
|
96
|
-
function evaluatePreConditions(toolName, filePath) {
|
|
97
|
-
const contracts = loadContracts();
|
|
98
|
-
const verdicts = [];
|
|
99
|
-
for (const config of contracts) {
|
|
100
|
-
for (const contract of config.contracts) {
|
|
101
|
-
if (!appliesToContract(contract, toolName, filePath)) continue;
|
|
102
|
-
const violations = [];
|
|
103
|
-
for (const condition of contract.preConditions) {
|
|
104
|
-
const result = evaluateCondition(condition, filePath);
|
|
105
|
-
if (!result.passed) {
|
|
106
|
-
violations.push({
|
|
107
|
-
condition,
|
|
108
|
-
message: result.message,
|
|
109
|
-
severity: contract.severity
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
verdicts.push({
|
|
114
|
-
contractId: contract.id,
|
|
115
|
-
description: contract.description,
|
|
116
|
-
passed: violations.length === 0,
|
|
117
|
-
violations
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return verdicts;
|
|
122
|
-
}
|
|
123
|
-
function evaluatePostConditions(toolName, filePath) {
|
|
124
|
-
const contracts = loadContracts();
|
|
125
|
-
const verdicts = [];
|
|
126
|
-
for (const config of contracts) {
|
|
127
|
-
for (const contract of config.contracts) {
|
|
128
|
-
if (!appliesToContract(contract, toolName, filePath)) continue;
|
|
129
|
-
const violations = [];
|
|
130
|
-
for (const condition of contract.postConditions) {
|
|
131
|
-
const result = evaluateCondition(condition, filePath);
|
|
132
|
-
if (!result.passed) {
|
|
133
|
-
violations.push({
|
|
134
|
-
condition,
|
|
135
|
-
message: result.message,
|
|
136
|
-
severity: contract.severity
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
verdicts.push({
|
|
141
|
-
contractId: contract.id,
|
|
142
|
-
description: contract.description,
|
|
143
|
-
passed: violations.length === 0,
|
|
144
|
-
violations
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return verdicts;
|
|
149
|
-
}
|
|
150
|
-
function evaluateCondition(condition, contextPath) {
|
|
151
|
-
const root = getProjectRoot();
|
|
152
|
-
switch (condition.type) {
|
|
153
|
-
case "file_exists": {
|
|
154
|
-
const targetPath = resolveTarget(condition.target, contextPath, root);
|
|
155
|
-
const exists = targetPath.some((p) => fs.existsSync(p));
|
|
156
|
-
return {
|
|
157
|
-
passed: exists,
|
|
158
|
-
message: exists ? `\u2705 File exists: ${condition.target}` : `\u274C Required file not found: ${condition.target} (${condition.description})`
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
case "file_not_exists": {
|
|
162
|
-
const targetPath = resolveTarget(condition.target, contextPath, root);
|
|
163
|
-
const exists = targetPath.some((p) => fs.existsSync(p));
|
|
164
|
-
return {
|
|
165
|
-
passed: !exists,
|
|
166
|
-
message: !exists ? `\u2705 File does not exist: ${condition.target}` : `\u274C File should not exist: ${condition.target} (${condition.description})`
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
case "contains": {
|
|
170
|
-
if (!contextPath) return { passed: true, message: "\u26A0\uFE0F No file to check" };
|
|
171
|
-
const fullPath = path.resolve(root, contextPath);
|
|
172
|
-
if (!fs.existsSync(fullPath)) return { passed: true, message: "\u26A0\uFE0F File not found" };
|
|
173
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
174
|
-
const hasContent = condition.value ? content.includes(condition.value) : false;
|
|
175
|
-
return {
|
|
176
|
-
passed: hasContent,
|
|
177
|
-
message: hasContent ? `\u2705 Contains expected content` : `\u274C Missing expected content: "${condition.value}" (${condition.description})`
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
case "not_contains": {
|
|
181
|
-
if (!contextPath) return { passed: true, message: "\u26A0\uFE0F No file to check" };
|
|
182
|
-
const root2 = getProjectRoot();
|
|
183
|
-
const targets = resolveTarget(condition.target, contextPath, root2);
|
|
184
|
-
for (const t of targets) {
|
|
185
|
-
if (fs.existsSync(t)) {
|
|
186
|
-
const content = fs.readFileSync(t, "utf-8");
|
|
187
|
-
if (condition.value && content.includes(condition.value)) {
|
|
188
|
-
return {
|
|
189
|
-
passed: false,
|
|
190
|
-
message: `\u274C Found forbidden pattern "${condition.value}" in ${path.relative(root2, t)} (${condition.description})`
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return { passed: true, message: "\u2705 No forbidden patterns found" };
|
|
196
|
-
}
|
|
197
|
-
case "has_test_file": {
|
|
198
|
-
if (!contextPath) return { passed: true, message: "\u26A0\uFE0F No file to check" };
|
|
199
|
-
const basename = path.basename(contextPath, path.extname(contextPath));
|
|
200
|
-
const dir = path.dirname(contextPath);
|
|
201
|
-
const possibleTests = [
|
|
202
|
-
path.join(dir, `${basename}.test.ts`),
|
|
203
|
-
path.join(dir, `${basename}.spec.ts`),
|
|
204
|
-
path.join(dir, `__tests__/${basename}.test.ts`),
|
|
205
|
-
path.join(dir, `__tests__/${basename}.spec.ts`)
|
|
206
|
-
];
|
|
207
|
-
const hasTest = possibleTests.some((t) => fs.existsSync(t));
|
|
208
|
-
return {
|
|
209
|
-
passed: hasTest,
|
|
210
|
-
message: hasTest ? `\u2705 Test file exists` : `\u274C No test file found for ${contextPath} (${condition.description})`
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
default:
|
|
214
|
-
return { passed: true, message: `\u26A0\uFE0F Unknown condition type: ${condition.type}` };
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
function appliesToContract(contract, toolName, filePath) {
|
|
218
|
-
if (contract.appliesTo.includes("*")) return true;
|
|
219
|
-
if (contract.appliesTo.includes(toolName)) return true;
|
|
220
|
-
if (filePath) {
|
|
221
|
-
return contract.appliesTo.some((pattern) => {
|
|
222
|
-
return filePath.includes(pattern.replace("*", ""));
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
function resolveTarget(pattern, contextPath, root) {
|
|
228
|
-
const r = root || getProjectRoot();
|
|
229
|
-
if (pattern.startsWith("*")) {
|
|
230
|
-
if (contextPath) {
|
|
231
|
-
const dir = path.dirname(path.resolve(r, contextPath));
|
|
232
|
-
const suffix = pattern.replace("*", "");
|
|
233
|
-
try {
|
|
234
|
-
const files = fs.readdirSync(dir);
|
|
235
|
-
return files.filter((f) => f.endsWith(suffix) || f.includes(suffix)).map((f) => path.join(dir, f));
|
|
236
|
-
} catch {
|
|
237
|
-
return [path.join(dir, pattern.replace("*", ""))];
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return [];
|
|
241
|
-
}
|
|
242
|
-
return [path.resolve(r, pattern)];
|
|
243
|
-
}
|
|
244
|
-
async function runContractChecks(toolName, filePath, phase = "pre") {
|
|
245
|
-
const verdicts = phase === "pre" ? evaluatePreConditions(toolName, filePath) : evaluatePostConditions(toolName, filePath);
|
|
246
|
-
if (verdicts.length === 0) {
|
|
247
|
-
return "\u2705 **No applicable contracts** for this action.";
|
|
248
|
-
}
|
|
249
|
-
const lines = [
|
|
250
|
-
`\u{1F4DC} **Contract Check** (${phase}-conditions)`,
|
|
251
|
-
`\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`,
|
|
252
|
-
``
|
|
253
|
-
];
|
|
254
|
-
let totalViolations = 0;
|
|
255
|
-
let errors = 0;
|
|
256
|
-
for (const v of verdicts) {
|
|
257
|
-
if (v.passed) {
|
|
258
|
-
lines.push(` \u2705 **${v.description}** \u2014 passed`);
|
|
259
|
-
} else {
|
|
260
|
-
totalViolations += v.violations.length;
|
|
261
|
-
const errorCount = v.violations.filter((x) => x.severity === "error").length;
|
|
262
|
-
errors += errorCount;
|
|
263
|
-
lines.push(` ${errorCount > 0 ? "\u{1F534}" : "\u{1F7E1}"} **${v.description}** \u2014 ${v.violations.length} violation(s)`);
|
|
264
|
-
for (const violation of v.violations) {
|
|
265
|
-
lines.push(` ${violation.severity === "error" ? "\u{1F534}" : "\u{1F7E1}"} ${violation.message.substring(0, 120)}`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
lines.push("");
|
|
270
|
-
if (errors > 0) {
|
|
271
|
-
lines.push(`\u{1F534} **${errors} error(s)** \u2014 blocking. Resolve before proceeding.`);
|
|
272
|
-
} else if (totalViolations > 0) {
|
|
273
|
-
lines.push(`\u{1F7E1} **${totalViolations} warning(s)** \u2014 review recommended.`);
|
|
274
|
-
} else {
|
|
275
|
-
lines.push("\u2705 All contract checks passed.");
|
|
276
|
-
}
|
|
277
|
-
return lines.join("\n");
|
|
278
|
-
}
|
|
279
|
-
function listContracts() {
|
|
280
|
-
const configs = loadContracts();
|
|
281
|
-
const lines = [
|
|
282
|
-
"\u{1F4DC} **Active Contracts**",
|
|
283
|
-
"\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
|
|
284
|
-
""
|
|
285
|
-
];
|
|
286
|
-
for (const config of configs) {
|
|
287
|
-
for (const contract of config.contracts) {
|
|
288
|
-
const appliesTo = contract.appliesTo.join(", ");
|
|
289
|
-
lines.push(` \u{1F4CC} **${contract.id}**`);
|
|
290
|
-
lines.push(` ${contract.description}`);
|
|
291
|
-
lines.push(` Applies to: ${appliesTo}`);
|
|
292
|
-
lines.push(` Pre-conditions: ${contract.preConditions.length}`);
|
|
293
|
-
lines.push(` Post-conditions: ${contract.postConditions.length}`);
|
|
294
|
-
lines.push("");
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return lines.join("\n");
|
|
298
|
-
}
|
|
299
|
-
export {
|
|
300
|
-
evaluatePostConditions,
|
|
301
|
-
evaluatePreConditions,
|
|
302
|
-
listContracts,
|
|
303
|
-
loadContracts,
|
|
304
|
-
runContractChecks
|
|
305
|
-
};
|
package/dist/kumaDb-4XZ5S2LH.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
addContextNote,
|
|
3
|
-
addSecurityFinding,
|
|
4
|
-
addTodo,
|
|
5
|
-
checkPortability,
|
|
6
|
-
ensureGitignore,
|
|
7
|
-
getBenchmarkDiff,
|
|
8
|
-
getChanges,
|
|
9
|
-
getDb,
|
|
10
|
-
getFileSummary,
|
|
11
|
-
getLatestVerifications,
|
|
12
|
-
getResearchCache,
|
|
13
|
-
getSecurityFindings,
|
|
14
|
-
listContextNotes,
|
|
15
|
-
listDecisionLog,
|
|
16
|
-
listResearchCache,
|
|
17
|
-
listTodos,
|
|
18
|
-
recordChange,
|
|
19
|
-
recordDecisionLog,
|
|
20
|
-
resetDbInstance,
|
|
21
|
-
rollbackChange,
|
|
22
|
-
runDoctor,
|
|
23
|
-
runGarbageCollection,
|
|
24
|
-
saveBenchmark,
|
|
25
|
-
saveDb,
|
|
26
|
-
saveFileSummary,
|
|
27
|
-
saveHealthSnapshot,
|
|
28
|
-
saveResearchCache,
|
|
29
|
-
saveVerification,
|
|
30
|
-
updateDecisionStatus,
|
|
31
|
-
updateTodoStatus
|
|
32
|
-
} from "./chunk-3BRBJZ7P.js";
|
|
33
|
-
import "./chunk-E2KFPEBT.js";
|
|
34
|
-
export {
|
|
35
|
-
addContextNote,
|
|
36
|
-
addSecurityFinding,
|
|
37
|
-
addTodo,
|
|
38
|
-
checkPortability,
|
|
39
|
-
ensureGitignore,
|
|
40
|
-
getBenchmarkDiff,
|
|
41
|
-
getChanges,
|
|
42
|
-
getDb,
|
|
43
|
-
getFileSummary,
|
|
44
|
-
getLatestVerifications,
|
|
45
|
-
getResearchCache,
|
|
46
|
-
getSecurityFindings,
|
|
47
|
-
listContextNotes,
|
|
48
|
-
listDecisionLog,
|
|
49
|
-
listResearchCache,
|
|
50
|
-
listTodos,
|
|
51
|
-
recordChange,
|
|
52
|
-
recordDecisionLog,
|
|
53
|
-
resetDbInstance,
|
|
54
|
-
rollbackChange,
|
|
55
|
-
runDoctor,
|
|
56
|
-
runGarbageCollection,
|
|
57
|
-
saveBenchmark,
|
|
58
|
-
saveDb,
|
|
59
|
-
saveFileSummary,
|
|
60
|
-
saveHealthSnapshot,
|
|
61
|
-
saveResearchCache,
|
|
62
|
-
saveVerification,
|
|
63
|
-
updateDecisionStatus,
|
|
64
|
-
updateTodoStatus
|
|
65
|
-
};
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getDb,
|
|
3
|
-
saveDb
|
|
4
|
-
} from "./chunk-3BRBJZ7P.js";
|
|
5
|
-
import {
|
|
6
|
-
getProjectRoot
|
|
7
|
-
} from "./chunk-E2KFPEBT.js";
|
|
8
|
-
|
|
9
|
-
// src/engine/kumaDriftDetector.ts
|
|
10
|
-
import fs from "fs";
|
|
11
|
-
import path from "path";
|
|
12
|
-
import crypto from "crypto";
|
|
13
|
-
function hashFile(filePath) {
|
|
14
|
-
try {
|
|
15
|
-
const root = getProjectRoot();
|
|
16
|
-
const fullPath = path.resolve(root, filePath);
|
|
17
|
-
if (!fs.existsSync(fullPath)) return null;
|
|
18
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
19
|
-
return crypto.createHash("sha256").update(content).digest("hex");
|
|
20
|
-
} catch {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
async function detectDrift() {
|
|
25
|
-
const records = [];
|
|
26
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
27
|
-
try {
|
|
28
|
-
const db = await getDb();
|
|
29
|
-
const rcStmt = db.prepare(
|
|
30
|
-
"SELECT id, scope, content_hash, updated_at FROM research_cache WHERE content_hash IS NOT NULL AND length(content_hash) > 0 LIMIT 100"
|
|
31
|
-
);
|
|
32
|
-
while (rcStmt.step()) {
|
|
33
|
-
const row = rcStmt.getAsObject();
|
|
34
|
-
const scope = row.scope;
|
|
35
|
-
const storedHash = row.content_hash;
|
|
36
|
-
const updatedAt = row.updated_at;
|
|
37
|
-
const age = Math.floor((now - updatedAt) / 86400);
|
|
38
|
-
const currentHash = computeScopeHash(scope);
|
|
39
|
-
const stale = currentHash && currentHash !== storedHash;
|
|
40
|
-
records.push({
|
|
41
|
-
id: row.id,
|
|
42
|
-
source: "research_cache",
|
|
43
|
-
description: scope,
|
|
44
|
-
filePath: null,
|
|
45
|
-
oldHash: storedHash,
|
|
46
|
-
currentHash: currentHash || "",
|
|
47
|
-
age: `${age}d`,
|
|
48
|
-
severity: !currentHash ? "fresh" : stale ? "stale" : age > 30 ? "warning" : "fresh"
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
rcStmt.free();
|
|
52
|
-
const fsStmt = db.prepare(
|
|
53
|
-
"SELECT id, file_path, content_hash FROM file_summaries WHERE content_hash IS NOT NULL AND length(content_hash) > 0 LIMIT 100"
|
|
54
|
-
);
|
|
55
|
-
while (fsStmt.step()) {
|
|
56
|
-
const row = fsStmt.getAsObject();
|
|
57
|
-
const filePath = row.file_path;
|
|
58
|
-
const storedHash = row.content_hash;
|
|
59
|
-
const currentHash = hashFile(filePath);
|
|
60
|
-
if (currentHash === null) {
|
|
61
|
-
records.push({
|
|
62
|
-
id: row.id,
|
|
63
|
-
source: "file_summaries",
|
|
64
|
-
description: filePath,
|
|
65
|
-
filePath,
|
|
66
|
-
oldHash: storedHash,
|
|
67
|
-
currentHash: "",
|
|
68
|
-
age: "\u2014",
|
|
69
|
-
severity: "missing"
|
|
70
|
-
});
|
|
71
|
-
} else if (currentHash !== storedHash) {
|
|
72
|
-
records.push({
|
|
73
|
-
id: row.id,
|
|
74
|
-
source: "file_summaries",
|
|
75
|
-
description: filePath,
|
|
76
|
-
filePath,
|
|
77
|
-
oldHash: storedHash,
|
|
78
|
-
currentHash,
|
|
79
|
-
age: "\u2014",
|
|
80
|
-
severity: "stale"
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
fsStmt.free();
|
|
85
|
-
const dlStmt = db.prepare(
|
|
86
|
-
"SELECT id, title, file_paths, created_at FROM decision_log WHERE file_paths IS NOT NULL ORDER BY created_at DESC LIMIT 50"
|
|
87
|
-
);
|
|
88
|
-
while (dlStmt.step()) {
|
|
89
|
-
const row = dlStmt.getAsObject();
|
|
90
|
-
const filePaths = [];
|
|
91
|
-
try {
|
|
92
|
-
filePaths.push(...JSON.parse(row.file_paths));
|
|
93
|
-
} catch {
|
|
94
|
-
}
|
|
95
|
-
let hasStaleFile = false;
|
|
96
|
-
for (const fp of filePaths) {
|
|
97
|
-
if (!fp) continue;
|
|
98
|
-
const fullPath = path.resolve(getProjectRoot(), fp);
|
|
99
|
-
if (!fs.existsSync(fullPath)) {
|
|
100
|
-
hasStaleFile = true;
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (hasStaleFile) {
|
|
105
|
-
records.push({
|
|
106
|
-
id: row.id,
|
|
107
|
-
source: "decision_log",
|
|
108
|
-
description: row.title,
|
|
109
|
-
filePath: filePaths[0] || null,
|
|
110
|
-
oldHash: "",
|
|
111
|
-
currentHash: "",
|
|
112
|
-
age: "\u2014",
|
|
113
|
-
severity: "stale"
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
dlStmt.free();
|
|
118
|
-
} catch (err) {
|
|
119
|
-
console.error(`[DriftDetector] Error: ${err}`);
|
|
120
|
-
}
|
|
121
|
-
return records;
|
|
122
|
-
}
|
|
123
|
-
function computeScopeHash(scope) {
|
|
124
|
-
try {
|
|
125
|
-
const hash = crypto.createHash("sha256");
|
|
126
|
-
const root = getProjectRoot();
|
|
127
|
-
hash.update(scope);
|
|
128
|
-
let found = false;
|
|
129
|
-
try {
|
|
130
|
-
const files = fs.readdirSync(root);
|
|
131
|
-
for (const file of files) {
|
|
132
|
-
if (file.toLowerCase().includes(scope.toLowerCase())) {
|
|
133
|
-
try {
|
|
134
|
-
if (fs.statSync(path.join(root, file)).isFile()) {
|
|
135
|
-
const content = fs.readFileSync(path.join(root, file), "utf-8");
|
|
136
|
-
hash.update(file);
|
|
137
|
-
hash.update(content.substring(0, 1e4));
|
|
138
|
-
found = true;
|
|
139
|
-
}
|
|
140
|
-
} catch {
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
} catch {
|
|
145
|
-
}
|
|
146
|
-
const researchDir = path.join(root, ".kuma", "research");
|
|
147
|
-
if (fs.existsSync(researchDir)) {
|
|
148
|
-
const scopeFile = path.join(researchDir, `${scope}.json`);
|
|
149
|
-
if (fs.existsSync(scopeFile)) {
|
|
150
|
-
const content = fs.readFileSync(scopeFile, "utf-8");
|
|
151
|
-
hash.update(content);
|
|
152
|
-
found = true;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (!found) return null;
|
|
156
|
-
return hash.digest("hex").substring(0, 16);
|
|
157
|
-
} catch {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
async function flagStaleRecords() {
|
|
162
|
-
const staleRecords = await detectDrift();
|
|
163
|
-
let flagged = 0;
|
|
164
|
-
try {
|
|
165
|
-
const db = await getDb();
|
|
166
|
-
for (const record of staleRecords) {
|
|
167
|
-
if (record.severity !== "stale" && record.severity !== "missing") continue;
|
|
168
|
-
if (record.source === "research_cache") {
|
|
169
|
-
db.run(
|
|
170
|
-
`UPDATE research_cache SET confidence = MAX(confidence * 0.5, 0.1) WHERE id = ?`,
|
|
171
|
-
[record.id]
|
|
172
|
-
);
|
|
173
|
-
flagged++;
|
|
174
|
-
} else if (record.source === "file_summaries") {
|
|
175
|
-
db.run(
|
|
176
|
-
`UPDATE file_summaries SET content_hash = '' WHERE id = ?`,
|
|
177
|
-
[record.id]
|
|
178
|
-
);
|
|
179
|
-
flagged++;
|
|
180
|
-
} else if (record.source === "decision_log") {
|
|
181
|
-
db.run(
|
|
182
|
-
`UPDATE decision_log SET status = 'deprecated' WHERE id = ?`,
|
|
183
|
-
[record.id]
|
|
184
|
-
);
|
|
185
|
-
flagged++;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
saveDb();
|
|
189
|
-
} catch (err) {
|
|
190
|
-
console.error(`[DriftDetector] Flag error: ${err}`);
|
|
191
|
-
}
|
|
192
|
-
return { flagged, total: staleRecords.length };
|
|
193
|
-
}
|
|
194
|
-
function formatDriftReport(records) {
|
|
195
|
-
if (records.length === 0) {
|
|
196
|
-
return "\u2705 **Drift Detection** \u2014 All memory records are fresh. No code drift detected.";
|
|
197
|
-
}
|
|
198
|
-
const fresh = records.filter((r) => r.severity === "fresh").length;
|
|
199
|
-
const warning = records.filter((r) => r.severity === "warning").length;
|
|
200
|
-
const stale = records.filter((r) => r.severity === "stale").length;
|
|
201
|
-
const missing = records.filter((r) => r.severity === "missing").length;
|
|
202
|
-
const lines = [
|
|
203
|
-
"\u{1F50D} **Code Drift Detection Report**",
|
|
204
|
-
"\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",
|
|
205
|
-
"",
|
|
206
|
-
`\u{1F4CA} ${records.length} total records checked: ${fresh} fresh, ${warning} aging, ${stale} stale, ${missing} missing`,
|
|
207
|
-
""
|
|
208
|
-
];
|
|
209
|
-
if (stale > 0 || missing > 0) {
|
|
210
|
-
lines.push("**Stale/Missing Records:**");
|
|
211
|
-
for (const r of records) {
|
|
212
|
-
if (r.severity === "stale" || r.severity === "missing") {
|
|
213
|
-
const icon = r.severity === "missing" ? "\u274C" : "\u26A0\uFE0F";
|
|
214
|
-
lines.push(` ${icon} [${r.source}] ${r.description}`);
|
|
215
|
-
if (r.filePath) lines.push(` \u{1F4CD} ${r.filePath}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
lines.push("");
|
|
219
|
-
lines.push("\u{1F4A1} Run kuma_memory({ action: 'heal' }) to repair stale graph entries.");
|
|
220
|
-
lines.push("\u{1F4A1} Run kuma_context({ action: 'research', scope: '<stale-scope>' }) to refresh.");
|
|
221
|
-
}
|
|
222
|
-
return lines.join("\n");
|
|
223
|
-
}
|
|
224
|
-
async function getDriftSummary() {
|
|
225
|
-
const records = await detectDrift();
|
|
226
|
-
if (records.length === 0) return "\u2705 No code drift detected";
|
|
227
|
-
const staleCount = records.filter((r) => r.severity === "stale" || r.severity === "missing").length;
|
|
228
|
-
if (staleCount === 0) return "\u2705 All records fresh";
|
|
229
|
-
return `\u26A0\uFE0F ${staleCount} stale record(s) detected. Use kuma_memory({ action: 'heal' }) to repair.`;
|
|
230
|
-
}
|
|
231
|
-
export {
|
|
232
|
-
detectDrift,
|
|
233
|
-
flagStaleRecords,
|
|
234
|
-
formatDriftReport,
|
|
235
|
-
getDriftSummary,
|
|
236
|
-
hashFile
|
|
237
|
-
};
|