bilmem 0.1.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.
- package/README.md +439 -0
- package/dist/canonical-CglQQ0xc.d.ts +262 -0
- package/dist/cli/index.js +7601 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/index.d.ts +311 -0
- package/dist/core/index.js +1975 -0
- package/dist/core/index.js.map +1 -0
- package/dist/filesystem-B3qAej0l.d.ts +69 -0
- package/dist/index.d.ts +163 -0
- package/dist/index.js +7765 -0
- package/dist/index.js.map +1 -0
- package/dist/install/index.d.ts +373 -0
- package/dist/install/index.js +6632 -0
- package/dist/install/index.js.map +1 -0
- package/dist/jev/index.d.ts +32 -0
- package/dist/jev/index.js +269 -0
- package/dist/jev/index.js.map +1 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +5479 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/rules/index.d.ts +59 -0
- package/dist/rules/index.js +388 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/scanner/index.d.ts +169 -0
- package/dist/scanner/index.js +3829 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/search/index.d.ts +44 -0
- package/dist/search/index.js +301 -0
- package/dist/search/index.js.map +1 -0
- package/dist/stats/index.d.ts +60 -0
- package/dist/stats/index.js +1373 -0
- package/dist/stats/index.js.map +1 -0
- package/dist/storage/index.d.ts +82 -0
- package/dist/storage/index.js +1748 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/types-Nxq5Gzay.d.ts +390 -0
- package/package.json +71 -0
|
@@ -0,0 +1,1373 @@
|
|
|
1
|
+
// src/storage/canonical.ts
|
|
2
|
+
import * as fs3 from "fs/promises";
|
|
3
|
+
import * as path3 from "path";
|
|
4
|
+
|
|
5
|
+
// src/rules/loader.ts
|
|
6
|
+
import YAML from "yaml";
|
|
7
|
+
import * as fs from "fs/promises";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
function validateRuleDefinition(rule, index = 0) {
|
|
10
|
+
const errors = [];
|
|
11
|
+
if (!rule || typeof rule !== "object") {
|
|
12
|
+
errors.push(`Rule at index ${index} must be an object.`);
|
|
13
|
+
return errors;
|
|
14
|
+
}
|
|
15
|
+
const r = rule;
|
|
16
|
+
if (!r.id || typeof r.id !== "string" || !r.id.trim()) {
|
|
17
|
+
errors.push(`Rule at index ${index} requires a non-empty string 'id'.`);
|
|
18
|
+
}
|
|
19
|
+
if (!r.statement || typeof r.statement !== "string" || !r.statement.trim()) {
|
|
20
|
+
errors.push(`Rule '${r.id || index}' requires a non-empty string 'statement'.`);
|
|
21
|
+
}
|
|
22
|
+
if (r.type) {
|
|
23
|
+
const validTypes = [
|
|
24
|
+
"rule",
|
|
25
|
+
"convention",
|
|
26
|
+
"decision",
|
|
27
|
+
"lesson",
|
|
28
|
+
"architecture",
|
|
29
|
+
"fact"
|
|
30
|
+
];
|
|
31
|
+
if (!validTypes.includes(r.type)) {
|
|
32
|
+
errors.push(`Rule '${r.id || index}' has invalid type '${r.type}'. Allowed: ${validTypes.join(", ")}.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (r.confidence !== void 0) {
|
|
36
|
+
if (typeof r.confidence !== "number" || r.confidence < 0 || r.confidence > 1) {
|
|
37
|
+
errors.push(`Rule '${r.id || index}' confidence must be a number between 0.0 and 1.0.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (r.positive !== void 0 && !Array.isArray(r.positive)) {
|
|
41
|
+
errors.push(`Rule '${r.id || index}' positive cues must be an array of strings.`);
|
|
42
|
+
}
|
|
43
|
+
if (r.negative !== void 0 && !Array.isArray(r.negative)) {
|
|
44
|
+
errors.push(`Rule '${r.id || index}' negative cues must be an array of strings.`);
|
|
45
|
+
}
|
|
46
|
+
return errors;
|
|
47
|
+
}
|
|
48
|
+
function validateRuleSet(data) {
|
|
49
|
+
const errors = [];
|
|
50
|
+
if (!data || typeof data !== "object") {
|
|
51
|
+
return { valid: false, errors: ["RuleSet content must be an object or an array of rules."] };
|
|
52
|
+
}
|
|
53
|
+
let rules = [];
|
|
54
|
+
if (Array.isArray(data)) {
|
|
55
|
+
rules = data;
|
|
56
|
+
} else if ("rules" in data && Array.isArray(data.rules)) {
|
|
57
|
+
rules = data.rules;
|
|
58
|
+
} else if ("id" in data && "statement" in data) {
|
|
59
|
+
rules = [data];
|
|
60
|
+
} else {
|
|
61
|
+
return { valid: false, errors: ['RuleSet must contain a "rules" array or be a list of rule definitions.'] };
|
|
62
|
+
}
|
|
63
|
+
if (rules.length === 0) {
|
|
64
|
+
errors.push("RuleSet contains no rules.");
|
|
65
|
+
}
|
|
66
|
+
rules.forEach((rule, idx) => {
|
|
67
|
+
errors.push(...validateRuleDefinition(rule, idx));
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
valid: errors.length === 0,
|
|
71
|
+
errors
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function parseRuleSetYaml(content, sourcePath = "inline.yml") {
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = YAML.parse(content);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
throw new Error(`YAML syntax error in ${sourcePath}: ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
const validation = validateRuleSet(parsed);
|
|
82
|
+
if (!validation.valid) {
|
|
83
|
+
throw new Error(`Invalid RuleSet in ${sourcePath}:
|
|
84
|
+
- ${validation.errors.join("\n- ")}`);
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(parsed)) {
|
|
87
|
+
return {
|
|
88
|
+
name: path.basename(sourcePath, path.extname(sourcePath)),
|
|
89
|
+
rules: parsed
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if ("id" in parsed && "statement" in parsed && !("rules" in parsed)) {
|
|
93
|
+
return {
|
|
94
|
+
name: parsed.id,
|
|
95
|
+
rules: [parsed]
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const set = parsed;
|
|
99
|
+
return {
|
|
100
|
+
name: set.name ?? path.basename(sourcePath, path.extname(sourcePath)),
|
|
101
|
+
version: set.version ?? 1,
|
|
102
|
+
scope: set.scope ?? "project",
|
|
103
|
+
description: set.description,
|
|
104
|
+
rules: set.rules
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function stringifyRuleSetYaml(ruleSet) {
|
|
108
|
+
const payload = {};
|
|
109
|
+
if (ruleSet.name) payload.name = ruleSet.name;
|
|
110
|
+
if (ruleSet.version !== void 0) payload.version = ruleSet.version;
|
|
111
|
+
if (ruleSet.scope) payload.scope = ruleSet.scope;
|
|
112
|
+
if (ruleSet.description) payload.description = ruleSet.description;
|
|
113
|
+
payload.rules = ruleSet.rules.map((r) => {
|
|
114
|
+
const ruleObj = {
|
|
115
|
+
id: r.id
|
|
116
|
+
};
|
|
117
|
+
if (r.name) ruleObj.name = r.name;
|
|
118
|
+
ruleObj.statement = r.statement;
|
|
119
|
+
if (r.type && r.type !== "rule") ruleObj.type = r.type;
|
|
120
|
+
if (r.confidence !== void 0 && r.confidence !== 0.95) ruleObj.confidence = r.confidence;
|
|
121
|
+
if (r.scope && r.scope !== "project") ruleObj.scope = r.scope;
|
|
122
|
+
if (r.positive && r.positive.length > 0) ruleObj.positive = r.positive;
|
|
123
|
+
if (r.negative && r.negative.length > 0) ruleObj.negative = r.negative;
|
|
124
|
+
if (r.tags && r.tags.length > 0) ruleObj.tags = r.tags;
|
|
125
|
+
if (r.when && Object.keys(r.when).length > 0) ruleObj.when = r.when;
|
|
126
|
+
if (r.details && Object.keys(r.details).length > 0) ruleObj.details = r.details;
|
|
127
|
+
return ruleObj;
|
|
128
|
+
});
|
|
129
|
+
return YAML.stringify(payload, { indent: 2, lineWidth: 100 });
|
|
130
|
+
}
|
|
131
|
+
async function loadRuleSetFile(filePath) {
|
|
132
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
133
|
+
return parseRuleSetYaml(content, filePath);
|
|
134
|
+
}
|
|
135
|
+
async function findRuleFiles(rootDir, storageRulesDir) {
|
|
136
|
+
const candidates = [];
|
|
137
|
+
const rulesDir = storageRulesDir || path.resolve(rootDir, ".bilmem", "rules");
|
|
138
|
+
try {
|
|
139
|
+
const entries = await fs.readdir(rulesDir, { withFileTypes: true });
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (entry.isFile() && (entry.name.endsWith(".yml") || entry.name.endsWith(".yaml"))) {
|
|
142
|
+
candidates.push(path.join(rulesDir, entry.name));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
for (const name of ["rules.yml", "rules.yaml"]) {
|
|
148
|
+
const p = path.resolve(rootDir, ".bilmem", name);
|
|
149
|
+
try {
|
|
150
|
+
const st = await fs.stat(p);
|
|
151
|
+
if (st.isFile()) candidates.push(p);
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const name of ["bilmem.rules.yml", "bilmem.rules.yaml", "rules.yml", "rules.yaml"]) {
|
|
156
|
+
const p = path.resolve(rootDir, name);
|
|
157
|
+
try {
|
|
158
|
+
const st = await fs.stat(p);
|
|
159
|
+
if (st.isFile()) candidates.push(p);
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return Array.from(new Set(candidates));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/stats/events.ts
|
|
167
|
+
import * as fs2 from "fs/promises";
|
|
168
|
+
import * as path2 from "path";
|
|
169
|
+
function getEventsFilePath(rootDir) {
|
|
170
|
+
return path2.join(rootDir, ".bilmem", "events.jsonl");
|
|
171
|
+
}
|
|
172
|
+
async function appendMetricEvent(rootDir, event) {
|
|
173
|
+
const filePath = getEventsFilePath(rootDir);
|
|
174
|
+
try {
|
|
175
|
+
await fs2.mkdir(path2.dirname(filePath), { recursive: true });
|
|
176
|
+
const line = JSON.stringify(event) + "\n";
|
|
177
|
+
await fs2.appendFile(filePath, line, "utf-8");
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function readMetricEvents(rootDir, filter) {
|
|
182
|
+
const filePath = getEventsFilePath(rootDir);
|
|
183
|
+
let raw;
|
|
184
|
+
try {
|
|
185
|
+
raw = await fs2.readFile(filePath, "utf-8");
|
|
186
|
+
} catch (err) {
|
|
187
|
+
if (err.code === "ENOENT") {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
const lines = raw.split("\n");
|
|
193
|
+
const events = [];
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
const trimmed = line.trim();
|
|
196
|
+
if (!trimmed) continue;
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(trimmed);
|
|
199
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.type !== "string") {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (typeof parsed.timestamp !== "number") {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (filter?.sinceTimestamp !== void 0 && parsed.timestamp < filter.sinceTimestamp) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (filter?.knowledgeId !== void 0) {
|
|
209
|
+
const kid = parsed.knowledgeId;
|
|
210
|
+
if (kid !== filter.knowledgeId) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
events.push(parsed);
|
|
215
|
+
} catch {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return events;
|
|
220
|
+
}
|
|
221
|
+
async function resetMetricEvents(rootDir) {
|
|
222
|
+
const filePath = getEventsFilePath(rootDir);
|
|
223
|
+
try {
|
|
224
|
+
await fs2.mkdir(path2.dirname(filePath), { recursive: true });
|
|
225
|
+
await fs2.writeFile(filePath, "", "utf-8");
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if (err.code !== "ENOENT") throw err;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/storage/canonical.ts
|
|
232
|
+
function sanitizeFileName(id) {
|
|
233
|
+
return encodeURIComponent(id).replace(/%/g, "_");
|
|
234
|
+
}
|
|
235
|
+
var Storage = class {
|
|
236
|
+
baseDir;
|
|
237
|
+
knowledgeDir;
|
|
238
|
+
evidenceDir;
|
|
239
|
+
observationDir;
|
|
240
|
+
unknownDir;
|
|
241
|
+
rulesDir;
|
|
242
|
+
configFile;
|
|
243
|
+
relationsFile;
|
|
244
|
+
outcomesFile;
|
|
245
|
+
metricsFile;
|
|
246
|
+
revisionsFile;
|
|
247
|
+
knowledgeJsonlFile;
|
|
248
|
+
evidenceJsonlFile;
|
|
249
|
+
observationJsonlFile;
|
|
250
|
+
unknownsJsonlFile;
|
|
251
|
+
receiptsDir;
|
|
252
|
+
receiptsJsonlFile;
|
|
253
|
+
eventsFile;
|
|
254
|
+
constructor(rootDir = process.cwd()) {
|
|
255
|
+
this.baseDir = path3.resolve(rootDir, ".bilmem");
|
|
256
|
+
this.knowledgeDir = path3.join(this.baseDir, "knowledge");
|
|
257
|
+
this.evidenceDir = path3.join(this.baseDir, "evidence");
|
|
258
|
+
this.observationDir = path3.join(this.baseDir, "observations");
|
|
259
|
+
this.unknownDir = path3.join(this.baseDir, "unknowns");
|
|
260
|
+
this.rulesDir = path3.join(this.baseDir, "rules");
|
|
261
|
+
this.receiptsDir = path3.join(this.baseDir, "receipts");
|
|
262
|
+
this.configFile = path3.join(this.baseDir, "config.json");
|
|
263
|
+
this.relationsFile = path3.join(this.baseDir, "relations.json");
|
|
264
|
+
this.outcomesFile = path3.join(this.baseDir, "outcomes.jsonl");
|
|
265
|
+
this.metricsFile = path3.join(this.baseDir, "metrics.json");
|
|
266
|
+
this.revisionsFile = path3.join(this.baseDir, "revisions.jsonl");
|
|
267
|
+
this.knowledgeJsonlFile = path3.join(this.baseDir, "knowledge.jsonl");
|
|
268
|
+
this.evidenceJsonlFile = path3.join(this.baseDir, "evidence.jsonl");
|
|
269
|
+
this.observationJsonlFile = path3.join(this.baseDir, "observations.jsonl");
|
|
270
|
+
this.unknownsJsonlFile = path3.join(this.baseDir, "unknowns.jsonl");
|
|
271
|
+
this.receiptsJsonlFile = path3.join(this.baseDir, "receipts.jsonl");
|
|
272
|
+
this.eventsFile = path3.join(this.baseDir, "events.jsonl");
|
|
273
|
+
}
|
|
274
|
+
getBaseDir() {
|
|
275
|
+
return this.baseDir;
|
|
276
|
+
}
|
|
277
|
+
async isInitialized() {
|
|
278
|
+
try {
|
|
279
|
+
await fs3.access(this.configFile);
|
|
280
|
+
return true;
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async init(options = {}) {
|
|
286
|
+
await fs3.mkdir(this.knowledgeDir, { recursive: true });
|
|
287
|
+
await fs3.mkdir(this.evidenceDir, { recursive: true });
|
|
288
|
+
await fs3.mkdir(this.observationDir, { recursive: true });
|
|
289
|
+
await fs3.mkdir(this.unknownDir, { recursive: true });
|
|
290
|
+
await fs3.mkdir(this.rulesDir, { recursive: true });
|
|
291
|
+
await fs3.mkdir(this.receiptsDir, { recursive: true });
|
|
292
|
+
let config;
|
|
293
|
+
if (await this.isInitialized()) {
|
|
294
|
+
config = await this.getConfig();
|
|
295
|
+
} else {
|
|
296
|
+
config = {
|
|
297
|
+
schemaVersion: 1,
|
|
298
|
+
projectName: options.projectName ?? path3.basename(path3.dirname(this.baseDir)) ?? "project",
|
|
299
|
+
defaultScope: options.defaultScope ?? "project",
|
|
300
|
+
dataDir: this.baseDir
|
|
301
|
+
};
|
|
302
|
+
await this.saveConfig(config);
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await fs3.access(this.relationsFile);
|
|
306
|
+
} catch {
|
|
307
|
+
await fs3.writeFile(this.relationsFile, JSON.stringify([], null, 2), "utf-8");
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
await fs3.access(this.metricsFile);
|
|
311
|
+
} catch {
|
|
312
|
+
const defaultMetrics = {
|
|
313
|
+
knowledgeHits: 0,
|
|
314
|
+
knowledgeMisses: 0,
|
|
315
|
+
knowledgeReused: 0,
|
|
316
|
+
unknownsResolved: 0,
|
|
317
|
+
repeatedInvestigationsPrevented: 0,
|
|
318
|
+
candidateKnowledgeMerged: 0,
|
|
319
|
+
staleKnowledgeDetected: 0,
|
|
320
|
+
contradictionsDetected: 0,
|
|
321
|
+
knowledgePromoted: 0,
|
|
322
|
+
knowledgeSuperseded: 0
|
|
323
|
+
};
|
|
324
|
+
await fs3.writeFile(this.metricsFile, JSON.stringify(defaultMetrics, null, 2), "utf-8");
|
|
325
|
+
}
|
|
326
|
+
return config;
|
|
327
|
+
}
|
|
328
|
+
// --- Config ---
|
|
329
|
+
async getConfig() {
|
|
330
|
+
const raw = await fs3.readFile(this.configFile, "utf-8");
|
|
331
|
+
return JSON.parse(raw);
|
|
332
|
+
}
|
|
333
|
+
async saveConfig(config) {
|
|
334
|
+
await fs3.writeFile(this.configFile, JSON.stringify(config, null, 2), "utf-8");
|
|
335
|
+
}
|
|
336
|
+
async updateConfig(updates) {
|
|
337
|
+
const current = await this.getConfig();
|
|
338
|
+
const updated = { ...current, ...updates };
|
|
339
|
+
await this.saveConfig(updated);
|
|
340
|
+
return updated;
|
|
341
|
+
}
|
|
342
|
+
// --- Knowledge ---
|
|
343
|
+
async saveKnowledge(knowledge) {
|
|
344
|
+
await fs3.mkdir(this.knowledgeDir, { recursive: true });
|
|
345
|
+
const filename = `${sanitizeFileName(knowledge.id)}.json`;
|
|
346
|
+
const filePath = path3.join(this.knowledgeDir, filename);
|
|
347
|
+
await fs3.writeFile(filePath, JSON.stringify(knowledge, null, 2), "utf-8");
|
|
348
|
+
await fs3.appendFile(this.knowledgeJsonlFile, JSON.stringify(knowledge) + "\n", "utf-8");
|
|
349
|
+
}
|
|
350
|
+
async getKnowledge(id) {
|
|
351
|
+
const filename = `${sanitizeFileName(id)}.json`;
|
|
352
|
+
const filePath = path3.join(this.knowledgeDir, filename);
|
|
353
|
+
try {
|
|
354
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
355
|
+
return JSON.parse(raw);
|
|
356
|
+
} catch {
|
|
357
|
+
return void 0;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async getAllKnowledge() {
|
|
361
|
+
try {
|
|
362
|
+
const files = await fs3.readdir(this.knowledgeDir);
|
|
363
|
+
const items = [];
|
|
364
|
+
for (const file of files) {
|
|
365
|
+
if (!file.endsWith(".json")) continue;
|
|
366
|
+
const raw = await fs3.readFile(path3.join(this.knowledgeDir, file), "utf-8");
|
|
367
|
+
try {
|
|
368
|
+
items.push(JSON.parse(raw));
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return items;
|
|
373
|
+
} catch {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async deleteKnowledge(id) {
|
|
378
|
+
const filename = `${sanitizeFileName(id)}.json`;
|
|
379
|
+
const filePath = path3.join(this.knowledgeDir, filename);
|
|
380
|
+
try {
|
|
381
|
+
await fs3.unlink(filePath);
|
|
382
|
+
return true;
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// --- Evidence ---
|
|
388
|
+
async saveEvidence(evidence) {
|
|
389
|
+
await fs3.mkdir(this.evidenceDir, { recursive: true });
|
|
390
|
+
const filename = `${sanitizeFileName(evidence.id)}.json`;
|
|
391
|
+
const filePath = path3.join(this.evidenceDir, filename);
|
|
392
|
+
await fs3.writeFile(filePath, JSON.stringify(evidence, null, 2), "utf-8");
|
|
393
|
+
await fs3.appendFile(this.evidenceJsonlFile, JSON.stringify(evidence) + "\n", "utf-8");
|
|
394
|
+
}
|
|
395
|
+
async getEvidence(id) {
|
|
396
|
+
const filename = `${sanitizeFileName(id)}.json`;
|
|
397
|
+
const filePath = path3.join(this.evidenceDir, filename);
|
|
398
|
+
try {
|
|
399
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
400
|
+
return JSON.parse(raw);
|
|
401
|
+
} catch {
|
|
402
|
+
return void 0;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async getAllEvidence() {
|
|
406
|
+
try {
|
|
407
|
+
const files = await fs3.readdir(this.evidenceDir);
|
|
408
|
+
const items = [];
|
|
409
|
+
for (const file of files) {
|
|
410
|
+
if (!file.endsWith(".json")) continue;
|
|
411
|
+
const raw = await fs3.readFile(path3.join(this.evidenceDir, file), "utf-8");
|
|
412
|
+
try {
|
|
413
|
+
items.push(JSON.parse(raw));
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return items;
|
|
418
|
+
} catch {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// --- Observations ---
|
|
423
|
+
async saveObservation(obs) {
|
|
424
|
+
await fs3.mkdir(this.observationDir, { recursive: true });
|
|
425
|
+
const filename = `${sanitizeFileName(obs.id)}.json`;
|
|
426
|
+
const filePath = path3.join(this.observationDir, filename);
|
|
427
|
+
await fs3.writeFile(filePath, JSON.stringify(obs, null, 2), "utf-8");
|
|
428
|
+
await fs3.appendFile(this.observationJsonlFile, JSON.stringify(obs) + "\n", "utf-8");
|
|
429
|
+
}
|
|
430
|
+
async getObservation(id) {
|
|
431
|
+
const filename = `${sanitizeFileName(id)}.json`;
|
|
432
|
+
const filePath = path3.join(this.observationDir, filename);
|
|
433
|
+
try {
|
|
434
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
435
|
+
return JSON.parse(raw);
|
|
436
|
+
} catch {
|
|
437
|
+
return void 0;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async getAllObservations() {
|
|
441
|
+
try {
|
|
442
|
+
const files = await fs3.readdir(this.observationDir);
|
|
443
|
+
const items = [];
|
|
444
|
+
for (const file of files) {
|
|
445
|
+
if (!file.endsWith(".json")) continue;
|
|
446
|
+
const raw = await fs3.readFile(path3.join(this.observationDir, file), "utf-8");
|
|
447
|
+
try {
|
|
448
|
+
items.push(JSON.parse(raw));
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return items;
|
|
453
|
+
} catch {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async deleteObservation(id) {
|
|
458
|
+
const filename = `${sanitizeFileName(id)}.json`;
|
|
459
|
+
const filePath = path3.join(this.observationDir, filename);
|
|
460
|
+
try {
|
|
461
|
+
await fs3.unlink(filePath);
|
|
462
|
+
return true;
|
|
463
|
+
} catch {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// --- Unknowns ---
|
|
468
|
+
async saveUnknown(record) {
|
|
469
|
+
await fs3.mkdir(this.unknownDir, { recursive: true });
|
|
470
|
+
const filename = `${sanitizeFileName(record.id)}.json`;
|
|
471
|
+
const filePath = path3.join(this.unknownDir, filename);
|
|
472
|
+
await fs3.writeFile(filePath, JSON.stringify(record, null, 2), "utf-8");
|
|
473
|
+
await fs3.appendFile(this.unknownsJsonlFile, JSON.stringify(record) + "\n", "utf-8");
|
|
474
|
+
}
|
|
475
|
+
async getUnknown(id) {
|
|
476
|
+
const filename = `${sanitizeFileName(id)}.json`;
|
|
477
|
+
const filePath = path3.join(this.unknownDir, filename);
|
|
478
|
+
try {
|
|
479
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
480
|
+
return JSON.parse(raw);
|
|
481
|
+
} catch {
|
|
482
|
+
return void 0;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async getAllUnknowns() {
|
|
486
|
+
try {
|
|
487
|
+
const files = await fs3.readdir(this.unknownDir);
|
|
488
|
+
const items = [];
|
|
489
|
+
for (const file of files) {
|
|
490
|
+
if (!file.endsWith(".json")) continue;
|
|
491
|
+
const raw = await fs3.readFile(path3.join(this.unknownDir, file), "utf-8");
|
|
492
|
+
try {
|
|
493
|
+
items.push(JSON.parse(raw));
|
|
494
|
+
} catch {
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return items;
|
|
498
|
+
} catch {
|
|
499
|
+
return [];
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async deleteUnknown(id) {
|
|
503
|
+
const filename = `${sanitizeFileName(id)}.json`;
|
|
504
|
+
const filePath = path3.join(this.unknownDir, filename);
|
|
505
|
+
try {
|
|
506
|
+
await fs3.unlink(filePath);
|
|
507
|
+
return true;
|
|
508
|
+
} catch {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// --- Outcomes (Append-oriented log) ---
|
|
513
|
+
async appendOutcome(outcome) {
|
|
514
|
+
const line = JSON.stringify(outcome) + "\n";
|
|
515
|
+
await fs3.appendFile(this.outcomesFile, line, "utf-8");
|
|
516
|
+
if (outcome.knowledgeId) {
|
|
517
|
+
await this.appendMetricEvent({
|
|
518
|
+
type: "outcome",
|
|
519
|
+
knowledgeId: outcome.knowledgeId,
|
|
520
|
+
result: outcome.result,
|
|
521
|
+
timestamp: Date.now()
|
|
522
|
+
});
|
|
523
|
+
if (outcome.result === "success") {
|
|
524
|
+
await this.appendMetricEvent({
|
|
525
|
+
type: "question_resolved",
|
|
526
|
+
questionId: outcome.knowledgeId,
|
|
527
|
+
timestamp: Date.now()
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async getAllOutcomes() {
|
|
533
|
+
try {
|
|
534
|
+
const raw = await fs3.readFile(this.outcomesFile, "utf-8");
|
|
535
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
536
|
+
return lines.map((l) => JSON.parse(l));
|
|
537
|
+
} catch {
|
|
538
|
+
return [];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// --- Metric Events (Append-only local log) ---
|
|
542
|
+
async appendMetricEvent(event) {
|
|
543
|
+
try {
|
|
544
|
+
await fs3.mkdir(this.baseDir, { recursive: true });
|
|
545
|
+
await fs3.appendFile(this.eventsFile, JSON.stringify(event) + "\n", "utf-8");
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async getMetricEvents(filter) {
|
|
550
|
+
return readMetricEvents(path3.dirname(this.baseDir), filter);
|
|
551
|
+
}
|
|
552
|
+
async resetMetricEvents() {
|
|
553
|
+
return resetMetricEvents(path3.dirname(this.baseDir));
|
|
554
|
+
}
|
|
555
|
+
// --- Investigation Receipts ---
|
|
556
|
+
async saveReceipt(receipt) {
|
|
557
|
+
await fs3.mkdir(this.receiptsDir, { recursive: true });
|
|
558
|
+
const filename = `${sanitizeFileName(receipt.id)}.json`;
|
|
559
|
+
const filePath = path3.join(this.receiptsDir, filename);
|
|
560
|
+
await fs3.writeFile(filePath, JSON.stringify(receipt, null, 2), "utf-8");
|
|
561
|
+
await fs3.appendFile(this.receiptsJsonlFile, JSON.stringify(receipt) + "\n", "utf-8");
|
|
562
|
+
}
|
|
563
|
+
async getReceipt(id) {
|
|
564
|
+
const filename = `${sanitizeFileName(id)}.json`;
|
|
565
|
+
const filePath = path3.join(this.receiptsDir, filename);
|
|
566
|
+
try {
|
|
567
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
568
|
+
return JSON.parse(raw);
|
|
569
|
+
} catch {
|
|
570
|
+
return void 0;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async getAllReceipts() {
|
|
574
|
+
try {
|
|
575
|
+
const files = await fs3.readdir(this.receiptsDir);
|
|
576
|
+
const items = [];
|
|
577
|
+
for (const file of files) {
|
|
578
|
+
if (!file.endsWith(".json")) continue;
|
|
579
|
+
const raw = await fs3.readFile(path3.join(this.receiptsDir, file), "utf-8");
|
|
580
|
+
try {
|
|
581
|
+
items.push(JSON.parse(raw));
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return items;
|
|
586
|
+
} catch {
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// --- Relations ---
|
|
591
|
+
async getRelations() {
|
|
592
|
+
try {
|
|
593
|
+
const raw = await fs3.readFile(this.relationsFile, "utf-8");
|
|
594
|
+
return JSON.parse(raw);
|
|
595
|
+
} catch {
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async saveRelations(relations) {
|
|
600
|
+
await fs3.writeFile(this.relationsFile, JSON.stringify(relations, null, 2), "utf-8");
|
|
601
|
+
}
|
|
602
|
+
// --- Metrics ---
|
|
603
|
+
async getMetrics() {
|
|
604
|
+
try {
|
|
605
|
+
const raw = await fs3.readFile(this.metricsFile, "utf-8");
|
|
606
|
+
return JSON.parse(raw);
|
|
607
|
+
} catch {
|
|
608
|
+
return {
|
|
609
|
+
knowledgeHits: 0,
|
|
610
|
+
knowledgeMisses: 0,
|
|
611
|
+
knowledgeReused: 0,
|
|
612
|
+
unknownsResolved: 0,
|
|
613
|
+
repeatedInvestigationsPrevented: 0,
|
|
614
|
+
candidateKnowledgeMerged: 0,
|
|
615
|
+
staleKnowledgeDetected: 0,
|
|
616
|
+
contradictionsDetected: 0,
|
|
617
|
+
knowledgePromoted: 0,
|
|
618
|
+
knowledgeSuperseded: 0
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async saveMetrics(metrics) {
|
|
623
|
+
await fs3.writeFile(this.metricsFile, JSON.stringify(metrics, null, 2), "utf-8");
|
|
624
|
+
}
|
|
625
|
+
// --- Rules ---
|
|
626
|
+
getRulesDir() {
|
|
627
|
+
return this.rulesDir;
|
|
628
|
+
}
|
|
629
|
+
async loadRuleSets() {
|
|
630
|
+
const files = await findRuleFiles(path3.dirname(this.baseDir), this.rulesDir);
|
|
631
|
+
const sets = [];
|
|
632
|
+
for (const f of files) {
|
|
633
|
+
try {
|
|
634
|
+
sets.push(await loadRuleSetFile(f));
|
|
635
|
+
} catch {
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return sets;
|
|
639
|
+
}
|
|
640
|
+
async saveRuleSet(ruleSet, fileName = "default.yml") {
|
|
641
|
+
await fs3.mkdir(this.rulesDir, { recursive: true });
|
|
642
|
+
const cleanName = fileName.endsWith(".yml") || fileName.endsWith(".yaml") ? fileName : `${fileName}.yml`;
|
|
643
|
+
const targetPath = path3.join(this.rulesDir, cleanName);
|
|
644
|
+
const yaml = stringifyRuleSetYaml(ruleSet);
|
|
645
|
+
await fs3.writeFile(targetPath, yaml, "utf-8");
|
|
646
|
+
return targetPath;
|
|
647
|
+
}
|
|
648
|
+
async addRule(rule, fileName = "default.yml") {
|
|
649
|
+
await fs3.mkdir(this.rulesDir, { recursive: true });
|
|
650
|
+
const cleanName = fileName.endsWith(".yml") || fileName.endsWith(".yaml") ? fileName : `${fileName}.yml`;
|
|
651
|
+
const targetPath = path3.join(this.rulesDir, cleanName);
|
|
652
|
+
let existingSet;
|
|
653
|
+
try {
|
|
654
|
+
existingSet = await loadRuleSetFile(targetPath);
|
|
655
|
+
} catch {
|
|
656
|
+
existingSet = {
|
|
657
|
+
name: path3.basename(cleanName, path3.extname(cleanName)),
|
|
658
|
+
rules: []
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const existingIdx = existingSet.rules.findIndex((r) => r.id === rule.id);
|
|
662
|
+
if (existingIdx >= 0) {
|
|
663
|
+
existingSet.rules[existingIdx] = rule;
|
|
664
|
+
} else {
|
|
665
|
+
existingSet.rules.push(rule);
|
|
666
|
+
}
|
|
667
|
+
const yaml = stringifyRuleSetYaml(existingSet);
|
|
668
|
+
await fs3.writeFile(targetPath, yaml, "utf-8");
|
|
669
|
+
return targetPath;
|
|
670
|
+
}
|
|
671
|
+
// --- Revisions & Incremental Learning Diff ---
|
|
672
|
+
async appendRevision(snapshot) {
|
|
673
|
+
const line = JSON.stringify(snapshot) + "\n";
|
|
674
|
+
await fs3.appendFile(this.revisionsFile, line, "utf-8");
|
|
675
|
+
}
|
|
676
|
+
async getRevisions() {
|
|
677
|
+
try {
|
|
678
|
+
const raw = await fs3.readFile(this.revisionsFile, "utf-8");
|
|
679
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
680
|
+
return lines.map((l) => JSON.parse(l));
|
|
681
|
+
} catch {
|
|
682
|
+
return [];
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async getLatestRevision() {
|
|
686
|
+
const revs = await this.getRevisions();
|
|
687
|
+
return revs.length > 0 ? revs[revs.length - 1] : void 0;
|
|
688
|
+
}
|
|
689
|
+
async getDiff(sinceRef) {
|
|
690
|
+
const revisions = await this.getRevisions();
|
|
691
|
+
let baselineSnapshot;
|
|
692
|
+
let targetSnapshot;
|
|
693
|
+
if (sinceRef && sinceRef.includes("..")) {
|
|
694
|
+
const [startRef, endRef] = sinceRef.split("..").map((s) => s.trim().toLowerCase());
|
|
695
|
+
baselineSnapshot = revisions.find(
|
|
696
|
+
(r) => r.id.toLowerCase() === startRef || r.commit?.toLowerCase() === startRef || r.commit?.toLowerCase().startsWith(startRef) || r.id.toLowerCase().startsWith(startRef)
|
|
697
|
+
);
|
|
698
|
+
targetSnapshot = revisions.find(
|
|
699
|
+
(r) => r.id.toLowerCase() === endRef || r.commit?.toLowerCase() === endRef || r.commit?.toLowerCase().startsWith(endRef) || r.id.toLowerCase().startsWith(endRef)
|
|
700
|
+
);
|
|
701
|
+
} else if (sinceRef) {
|
|
702
|
+
const cleanRef = sinceRef.trim().toLowerCase();
|
|
703
|
+
baselineSnapshot = revisions.find(
|
|
704
|
+
(r) => r.id.toLowerCase() === cleanRef || r.commit?.toLowerCase() === cleanRef || r.commit?.toLowerCase().startsWith(cleanRef) || r.id.toLowerCase().startsWith(cleanRef)
|
|
705
|
+
);
|
|
706
|
+
} else {
|
|
707
|
+
if (revisions.length >= 2) {
|
|
708
|
+
baselineSnapshot = revisions[revisions.length - 2];
|
|
709
|
+
} else if (revisions.length === 1) {
|
|
710
|
+
baselineSnapshot = revisions[0];
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const baselineTimestamp = baselineSnapshot?.timestamp ?? "1970-01-01T00:00:00.000Z";
|
|
714
|
+
const baselineTime = new Date(baselineTimestamp).getTime();
|
|
715
|
+
const targetTime = targetSnapshot ? new Date(targetSnapshot.timestamp).getTime() : Date.now();
|
|
716
|
+
const inWindow = (time) => time >= baselineTime && time <= targetTime;
|
|
717
|
+
const allObservations = await this.getAllObservations();
|
|
718
|
+
const allKnowledge = await this.getAllKnowledge();
|
|
719
|
+
const allUnknowns = await this.getAllUnknowns();
|
|
720
|
+
let observationsAdded = 0;
|
|
721
|
+
for (const obs of allObservations) {
|
|
722
|
+
const obsTime = new Date(obs.recordedAt || obs.provenance?.timestamp || 0).getTime();
|
|
723
|
+
if (inWindow(obsTime)) {
|
|
724
|
+
observationsAdded++;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
let candidatesAdded = 0;
|
|
728
|
+
let strengthenedCount = 0;
|
|
729
|
+
let revisedCount = 0;
|
|
730
|
+
let contradictionCount = 0;
|
|
731
|
+
let invalidatedCount = 0;
|
|
732
|
+
let needsRevalidationCount = 0;
|
|
733
|
+
const strengthened = [];
|
|
734
|
+
const revised = [];
|
|
735
|
+
const contradictions = [];
|
|
736
|
+
const invalidated = [];
|
|
737
|
+
const needsRevalidation = [];
|
|
738
|
+
for (const k of allKnowledge) {
|
|
739
|
+
const createdTime = new Date(k.createdAt).getTime();
|
|
740
|
+
const updatedTime = new Date(k.updatedAt).getTime();
|
|
741
|
+
if ((k.status === "invalid" || k.status === "invalidated") && inWindow(updatedTime)) {
|
|
742
|
+
invalidatedCount++;
|
|
743
|
+
invalidated.push(k.id);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (k.status === "contradicted" && inWindow(updatedTime)) {
|
|
747
|
+
contradictionCount++;
|
|
748
|
+
contradictions.push(k.id);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if ((k.status === "needs_revalidation" || k.status === "stale") && inWindow(updatedTime)) {
|
|
752
|
+
needsRevalidationCount++;
|
|
753
|
+
needsRevalidation.push(k.id);
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (inWindow(createdTime)) {
|
|
757
|
+
candidatesAdded++;
|
|
758
|
+
} else if (inWindow(updatedTime)) {
|
|
759
|
+
if (k.metrics.support_count > 1 || k.confidence >= 0.85 || k.metrics.successful_outcomes > 0) {
|
|
760
|
+
strengthenedCount++;
|
|
761
|
+
strengthened.push(k.id);
|
|
762
|
+
} else {
|
|
763
|
+
revisedCount++;
|
|
764
|
+
revised.push(k.id);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
let unknownsCount = 0;
|
|
769
|
+
const unknownsList = [];
|
|
770
|
+
for (const u of allUnknowns) {
|
|
771
|
+
const uTime = new Date(u.createdAt || u.updatedAt || 0).getTime();
|
|
772
|
+
if (inWindow(uTime)) {
|
|
773
|
+
unknownsCount++;
|
|
774
|
+
unknownsList.push(u.id);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const refLabel = sinceRef ?? (baselineSnapshot ? baselineSnapshot.commit?.slice(0, 7) || baselineSnapshot.id.slice(0, 7) : void 0);
|
|
778
|
+
return {
|
|
779
|
+
sinceRef: refLabel,
|
|
780
|
+
observationsAdded,
|
|
781
|
+
candidatesAdded,
|
|
782
|
+
strengthenedCount,
|
|
783
|
+
revisedCount,
|
|
784
|
+
contradictionCount,
|
|
785
|
+
unknownsCount,
|
|
786
|
+
needsRevalidationCount,
|
|
787
|
+
invalidatedCount,
|
|
788
|
+
details: {
|
|
789
|
+
strengthened,
|
|
790
|
+
revised,
|
|
791
|
+
contradictions,
|
|
792
|
+
unknowns: unknownsList,
|
|
793
|
+
needsRevalidation,
|
|
794
|
+
invalidated
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// src/stats/aggregator.ts
|
|
801
|
+
function formatPercentage(ratio) {
|
|
802
|
+
if (ratio === void 0 || isNaN(ratio)) {
|
|
803
|
+
return "\u2014";
|
|
804
|
+
}
|
|
805
|
+
return `${(ratio * 100).toFixed(1)}%`;
|
|
806
|
+
}
|
|
807
|
+
function formatRelativeTime(dateStr) {
|
|
808
|
+
if (!dateStr) return void 0;
|
|
809
|
+
const ts = typeof dateStr === "number" ? dateStr : Date.parse(dateStr);
|
|
810
|
+
if (isNaN(ts)) return void 0;
|
|
811
|
+
const diffMs = Date.now() - ts;
|
|
812
|
+
const diffSec = Math.floor(diffMs / 1e3);
|
|
813
|
+
if (diffSec < 60) return `${diffSec}s ago`;
|
|
814
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
815
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
816
|
+
const diffHr = Math.floor(diffMin / 60);
|
|
817
|
+
if (diffHr < 24) return `${diffHr}h ago`;
|
|
818
|
+
const diffDays = Math.floor(diffHr / 24);
|
|
819
|
+
return `${diffDays}d ago`;
|
|
820
|
+
}
|
|
821
|
+
function classifyKnowledgeHealth(knowledge, knowledgeEvents, independentSourcesCount) {
|
|
822
|
+
if (knowledge.status === "needs_revalidation" || knowledge.status === "stale") {
|
|
823
|
+
return {
|
|
824
|
+
category: "NEEDS_REVALIDATION",
|
|
825
|
+
reason: "Underlying source or dependency changed since last validation"
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
const outcomes = knowledgeEvents.filter((e) => e.type === "outcome");
|
|
829
|
+
const misleadingCount = outcomes.filter(
|
|
830
|
+
(o) => o.result === "failure" || o.result === "reverted" || o.result === "misleading"
|
|
831
|
+
).length + (knowledge.outcomesSummary?.misleading ?? 0);
|
|
832
|
+
const successCount = outcomes.filter((o) => o.result === "success").length + (knowledge.metrics.successful_outcomes ?? 0);
|
|
833
|
+
if (misleadingCount > 0 && misleadingCount >= successCount) {
|
|
834
|
+
return {
|
|
835
|
+
category: "FREQUENTLY_MISLEADING",
|
|
836
|
+
reason: `${misleadingCount} negative or misleading outcome(s) recorded`
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
const jevEvents = knowledgeEvents.filter((e) => e.type === "jev_result");
|
|
840
|
+
const rejectCount = jevEvents.filter((j) => j.result === "REJECT").length;
|
|
841
|
+
if (jevEvents.length >= 3 && rejectCount / jevEvents.length >= 0.7) {
|
|
842
|
+
return {
|
|
843
|
+
category: "FREQUENTLY_REJECTED",
|
|
844
|
+
reason: `Rejected in ${rejectCount} of ${jevEvents.length} JEV evaluations`
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
if (independentSourcesCount < 2 && knowledge.confidence < 0.75) {
|
|
848
|
+
return {
|
|
849
|
+
category: "LOW_EVIDENCE",
|
|
850
|
+
reason: `Only ${independentSourcesCount} independent source(s) and confidence < 0.75`
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
const reuseCount = knowledge.metrics.reuse_count + knowledgeEvents.filter((e) => e.type === "knowledge_reused").length;
|
|
854
|
+
const createdAtMs = Date.parse(knowledge.createdAt);
|
|
855
|
+
const ageHours = !isNaN(createdAtMs) ? (Date.now() - createdAtMs) / (1e3 * 3600) : 0;
|
|
856
|
+
if (reuseCount === 0 && ageHours > 1) {
|
|
857
|
+
return {
|
|
858
|
+
category: "UNUSED",
|
|
859
|
+
reason: "No reuse recorded since creation"
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
return {
|
|
863
|
+
category: "HEALTHY",
|
|
864
|
+
reason: "Active with verified evidence and clean outcome history"
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
async function computeStats(storage, events, options) {
|
|
868
|
+
const allKnowledge = await storage.getAllKnowledge();
|
|
869
|
+
const allUnknowns = await storage.getAllUnknowns();
|
|
870
|
+
let active = 0;
|
|
871
|
+
let candidates = 0;
|
|
872
|
+
let conflicted = 0;
|
|
873
|
+
let needsRevalidation = 0;
|
|
874
|
+
let stale = 0;
|
|
875
|
+
for (const k of allKnowledge) {
|
|
876
|
+
if (k.status === "active") active++;
|
|
877
|
+
else if (k.status === "candidate" || k.stage === "candidate") candidates++;
|
|
878
|
+
else if (k.status === "contradicted") conflicted++;
|
|
879
|
+
else if (k.status === "needs_revalidation") needsRevalidation++;
|
|
880
|
+
else if (k.status === "stale") stale++;
|
|
881
|
+
}
|
|
882
|
+
const learning = {
|
|
883
|
+
observations: 0,
|
|
884
|
+
new: 0,
|
|
885
|
+
merged: 0,
|
|
886
|
+
strengthened: 0,
|
|
887
|
+
specialized: 0,
|
|
888
|
+
superseded: 0,
|
|
889
|
+
contradictions: 0,
|
|
890
|
+
ignoredDuplicates: 0
|
|
891
|
+
};
|
|
892
|
+
const resolution = {
|
|
893
|
+
resolveCalls: 0,
|
|
894
|
+
known: 0,
|
|
895
|
+
partiallyKnown: 0,
|
|
896
|
+
unknown: 0,
|
|
897
|
+
conflicted: 0,
|
|
898
|
+
stale: 0
|
|
899
|
+
};
|
|
900
|
+
const investigation = {
|
|
901
|
+
repeatedChecksAvoided: 0,
|
|
902
|
+
knowledgeReused: 0,
|
|
903
|
+
questionsSuggested: 0,
|
|
904
|
+
questionsResolved: 0,
|
|
905
|
+
broadSearchesAvoided: 0,
|
|
906
|
+
sourceReadsAvoided: 0,
|
|
907
|
+
investigationsReused: 0
|
|
908
|
+
};
|
|
909
|
+
const jev = {
|
|
910
|
+
candidates: 0,
|
|
911
|
+
match: 0,
|
|
912
|
+
possible: 0,
|
|
913
|
+
rejected: 0,
|
|
914
|
+
hardNegativeRejects: 0,
|
|
915
|
+
reasons: {
|
|
916
|
+
hardNegative: 0,
|
|
917
|
+
scopeMismatch: 0,
|
|
918
|
+
insufficientContext: 0,
|
|
919
|
+
staleKnowledge: 0
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
const actions = {};
|
|
923
|
+
for (const e of events) {
|
|
924
|
+
switch (e.type) {
|
|
925
|
+
case "knowledge_created":
|
|
926
|
+
learning.new++;
|
|
927
|
+
break;
|
|
928
|
+
case "knowledge_merged":
|
|
929
|
+
learning.merged++;
|
|
930
|
+
break;
|
|
931
|
+
case "knowledge_strengthened":
|
|
932
|
+
learning.strengthened++;
|
|
933
|
+
break;
|
|
934
|
+
case "knowledge_specialized":
|
|
935
|
+
learning.specialized++;
|
|
936
|
+
break;
|
|
937
|
+
case "knowledge_superseded":
|
|
938
|
+
learning.superseded++;
|
|
939
|
+
break;
|
|
940
|
+
case "contradiction_detected":
|
|
941
|
+
learning.contradictions++;
|
|
942
|
+
break;
|
|
943
|
+
case "duplicate_ignored":
|
|
944
|
+
learning.ignoredDuplicates++;
|
|
945
|
+
break;
|
|
946
|
+
case "resolve":
|
|
947
|
+
resolution.resolveCalls++;
|
|
948
|
+
if (e.state === "KNOWN") resolution.known++;
|
|
949
|
+
else if (e.state === "PARTIALLY_KNOWN") resolution.partiallyKnown++;
|
|
950
|
+
else if (e.state === "UNKNOWN") resolution.unknown++;
|
|
951
|
+
else if (e.state === "CONFLICTED") resolution.conflicted++;
|
|
952
|
+
else if (e.state === "STALE") resolution.stale++;
|
|
953
|
+
if (e.action) {
|
|
954
|
+
actions[e.action] = (actions[e.action] ?? 0) + 1;
|
|
955
|
+
}
|
|
956
|
+
break;
|
|
957
|
+
case "knowledge_reused":
|
|
958
|
+
investigation.knowledgeReused++;
|
|
959
|
+
break;
|
|
960
|
+
case "question_suggested":
|
|
961
|
+
investigation.questionsSuggested++;
|
|
962
|
+
break;
|
|
963
|
+
case "question_resolved":
|
|
964
|
+
investigation.questionsResolved++;
|
|
965
|
+
break;
|
|
966
|
+
case "broad_search_avoided":
|
|
967
|
+
investigation.broadSearchesAvoided++;
|
|
968
|
+
break;
|
|
969
|
+
case "source_read_avoided":
|
|
970
|
+
investigation.sourceReadsAvoided++;
|
|
971
|
+
investigation.repeatedChecksAvoided++;
|
|
972
|
+
break;
|
|
973
|
+
case "investigation_reused":
|
|
974
|
+
investigation.investigationsReused++;
|
|
975
|
+
investigation.repeatedChecksAvoided++;
|
|
976
|
+
break;
|
|
977
|
+
case "jev_result":
|
|
978
|
+
jev.candidates++;
|
|
979
|
+
if (e.result === "MATCH") jev.match++;
|
|
980
|
+
else if (e.result === "POSSIBLE") jev.possible++;
|
|
981
|
+
else if (e.result === "REJECT") {
|
|
982
|
+
jev.rejected++;
|
|
983
|
+
const reason = String(e.reason || "").toUpperCase();
|
|
984
|
+
if (reason.includes("HARD_NEGATIVE") || reason.includes("FORBIDS")) {
|
|
985
|
+
jev.hardNegativeRejects++;
|
|
986
|
+
jev.reasons.hardNegative++;
|
|
987
|
+
} else if (reason.includes("SCOPE")) {
|
|
988
|
+
jev.reasons.scopeMismatch++;
|
|
989
|
+
} else if (reason.includes("STALE")) {
|
|
990
|
+
jev.reasons.staleKnowledge++;
|
|
991
|
+
} else {
|
|
992
|
+
jev.reasons.insufficientContext++;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
break;
|
|
996
|
+
case "jev_hard_negative":
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const allObs = await storage.getAllObservations();
|
|
1002
|
+
learning.observations = allObs.length;
|
|
1003
|
+
} catch {
|
|
1004
|
+
learning.observations = 0;
|
|
1005
|
+
}
|
|
1006
|
+
const reuseRatio = resolution.resolveCalls > 0 ? Math.min(1, (resolution.known + Math.min(resolution.partiallyKnown, investigation.knowledgeReused)) / resolution.resolveCalls) : void 0;
|
|
1007
|
+
const knowledgeReuseRate = {
|
|
1008
|
+
value: reuseRatio,
|
|
1009
|
+
formatted: reuseRatio !== void 0 ? formatPercentage(reuseRatio) : "\u2014 (Not enough observations)",
|
|
1010
|
+
formula: "resolve operations that reused existing applicable knowledge / all resolve operations"
|
|
1011
|
+
};
|
|
1012
|
+
const resolveSuccessRatio = resolution.resolveCalls > 0 ? Math.min(1, (resolution.known + investigation.questionsResolved) / resolution.resolveCalls) : void 0;
|
|
1013
|
+
const resolveSuccessRate = {
|
|
1014
|
+
value: resolveSuccessRatio,
|
|
1015
|
+
formatted: resolveSuccessRatio !== void 0 ? formatPercentage(resolveSuccessRatio) : "\u2014 (Not enough observations)",
|
|
1016
|
+
formula: "(KNOWN + successfully resolved PARTIALLY_KNOWN) / all completed resolve attempts"
|
|
1017
|
+
};
|
|
1018
|
+
const questionRatio = investigation.questionsSuggested > 0 ? Math.min(1, investigation.questionsResolved / investigation.questionsSuggested) : void 0;
|
|
1019
|
+
const questionResolutionRate = {
|
|
1020
|
+
value: questionRatio,
|
|
1021
|
+
formatted: questionRatio !== void 0 ? formatPercentage(questionRatio) : "\u2014 (Not enough observations)",
|
|
1022
|
+
formula: "questions resolved / questions suggested"
|
|
1023
|
+
};
|
|
1024
|
+
const jevRatio = jev.candidates > 0 ? jev.rejected / jev.candidates : void 0;
|
|
1025
|
+
const jevRejectionRate = {
|
|
1026
|
+
value: jevRatio,
|
|
1027
|
+
formatted: jevRatio !== void 0 ? formatPercentage(jevRatio) : "\u2014 (Not enough observations)",
|
|
1028
|
+
formula: "JEV rejected candidates / all JEV candidates evaluated"
|
|
1029
|
+
};
|
|
1030
|
+
const health = {
|
|
1031
|
+
healthy: 0,
|
|
1032
|
+
needsRevalidation: 0,
|
|
1033
|
+
lowEvidence: 0,
|
|
1034
|
+
frequentlyRejected: 0,
|
|
1035
|
+
frequentlyMisleading: 0,
|
|
1036
|
+
unused: 0
|
|
1037
|
+
};
|
|
1038
|
+
const eventsByKnowledge = /* @__PURE__ */ new Map();
|
|
1039
|
+
for (const e of events) {
|
|
1040
|
+
const kid = e.knowledgeId;
|
|
1041
|
+
if (kid) {
|
|
1042
|
+
const list = eventsByKnowledge.get(kid) || [];
|
|
1043
|
+
list.push(e);
|
|
1044
|
+
eventsByKnowledge.set(kid, list);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
for (const k of allKnowledge) {
|
|
1048
|
+
const kEvents = eventsByKnowledge.get(k.id) || [];
|
|
1049
|
+
const uniqueSources = new Set(k.evidence.map((ev) => ev.replace(/^(code|file|test|doc):/, "")));
|
|
1050
|
+
const { category } = classifyKnowledgeHealth(k, kEvents, uniqueSources.size);
|
|
1051
|
+
switch (category) {
|
|
1052
|
+
case "HEALTHY":
|
|
1053
|
+
health.healthy++;
|
|
1054
|
+
break;
|
|
1055
|
+
case "NEEDS_REVALIDATION":
|
|
1056
|
+
health.needsRevalidation++;
|
|
1057
|
+
break;
|
|
1058
|
+
case "LOW_EVIDENCE":
|
|
1059
|
+
health.lowEvidence++;
|
|
1060
|
+
break;
|
|
1061
|
+
case "FREQUENTLY_REJECTED":
|
|
1062
|
+
health.frequentlyRejected++;
|
|
1063
|
+
break;
|
|
1064
|
+
case "FREQUENTLY_MISLEADING":
|
|
1065
|
+
health.frequentlyMisleading++;
|
|
1066
|
+
break;
|
|
1067
|
+
case "UNUSED":
|
|
1068
|
+
health.unused++;
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
since: options?.since,
|
|
1074
|
+
sinceTimestamp: options?.sinceTimestamp,
|
|
1075
|
+
knowledge: {
|
|
1076
|
+
active,
|
|
1077
|
+
candidates,
|
|
1078
|
+
unknown: allUnknowns.length,
|
|
1079
|
+
conflicted,
|
|
1080
|
+
needsRevalidation,
|
|
1081
|
+
stale,
|
|
1082
|
+
total: allKnowledge.length
|
|
1083
|
+
},
|
|
1084
|
+
learning,
|
|
1085
|
+
resolution,
|
|
1086
|
+
investigation,
|
|
1087
|
+
jev,
|
|
1088
|
+
actions,
|
|
1089
|
+
effectiveness: {
|
|
1090
|
+
knowledgeReuseRate,
|
|
1091
|
+
resolveSuccessRate,
|
|
1092
|
+
questionResolutionRate,
|
|
1093
|
+
jevRejectionRate
|
|
1094
|
+
},
|
|
1095
|
+
health
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
async function computeKnowledgeStats(storage, knowledgeId, events) {
|
|
1099
|
+
const knowledge = await storage.getKnowledge(knowledgeId);
|
|
1100
|
+
if (!knowledge) {
|
|
1101
|
+
return void 0;
|
|
1102
|
+
}
|
|
1103
|
+
const kEvents = events.filter((e) => e.knowledgeId === knowledgeId);
|
|
1104
|
+
const outcomes = kEvents.filter((e) => e.type === "outcome");
|
|
1105
|
+
const successfulOutcomes = knowledge.metrics.successful_outcomes + outcomes.filter((o) => o.result === "success").length;
|
|
1106
|
+
const neutralOutcomes = (knowledge.outcomesSummary?.neutral ?? 0) + outcomes.filter((o) => o.result === "neutral").length;
|
|
1107
|
+
const misleadingOutcomes = (knowledge.outcomesSummary?.misleading ?? 0) + outcomes.filter((o) => o.result === "misleading").length;
|
|
1108
|
+
const failedOutcomes = knowledge.metrics.failed_outcomes + outcomes.filter((o) => o.result === "failure" || o.result === "reverted").length;
|
|
1109
|
+
const used = knowledge.metrics.reuse_count + kEvents.filter((e) => e.type === "knowledge_reused").length;
|
|
1110
|
+
const jevEvents = kEvents.filter((e) => e.type === "jev_result");
|
|
1111
|
+
const jevMatch = jevEvents.filter((j) => j.result === "MATCH").length;
|
|
1112
|
+
const jevPossible = jevEvents.filter((j) => j.result === "POSSIBLE").length;
|
|
1113
|
+
const jevRejected = jevEvents.filter((j) => j.result === "REJECT").length;
|
|
1114
|
+
const jevHardNegativeRejects = jevEvents.filter(
|
|
1115
|
+
(j) => String(j.reason || "").toUpperCase().includes("HARD_NEGATIVE")
|
|
1116
|
+
).length + kEvents.filter((e) => e.type === "jev_hard_negative").length;
|
|
1117
|
+
const uniqueSources = new Set(knowledge.evidence.map((ev) => ev.replace(/^(code|file|test|doc):/, "")));
|
|
1118
|
+
const revisions = knowledge.temporal?.revision || knowledge.history?.length || 1;
|
|
1119
|
+
const strengthened = knowledge.metrics.support_count + kEvents.filter((e) => e.type === "knowledge_strengthened").length;
|
|
1120
|
+
const contradictions = knowledge.metrics.contradiction_count + kEvents.filter((e) => e.type === "contradiction_detected").length;
|
|
1121
|
+
const lastUsedStr = formatRelativeTime(knowledge.metrics.last_used);
|
|
1122
|
+
const lastValidatedStr = formatRelativeTime(knowledge.updatedAt);
|
|
1123
|
+
const { category: healthCategory, reason: healthReason } = classifyKnowledgeHealth(
|
|
1124
|
+
knowledge,
|
|
1125
|
+
kEvents,
|
|
1126
|
+
uniqueSources.size
|
|
1127
|
+
);
|
|
1128
|
+
return {
|
|
1129
|
+
id: knowledge.id,
|
|
1130
|
+
status: knowledge.status,
|
|
1131
|
+
stage: knowledge.stage,
|
|
1132
|
+
statement: knowledge.statement,
|
|
1133
|
+
usage: {
|
|
1134
|
+
used,
|
|
1135
|
+
successfulOutcomes,
|
|
1136
|
+
neutralOutcomes,
|
|
1137
|
+
misleadingOutcomes,
|
|
1138
|
+
failedOutcomes
|
|
1139
|
+
},
|
|
1140
|
+
jev: {
|
|
1141
|
+
match: jevMatch,
|
|
1142
|
+
possible: jevPossible,
|
|
1143
|
+
rejected: jevRejected,
|
|
1144
|
+
hardNegativeRejects: jevHardNegativeRejects
|
|
1145
|
+
},
|
|
1146
|
+
evidence: {
|
|
1147
|
+
sources: knowledge.evidence.length,
|
|
1148
|
+
independentSources: uniqueSources.size,
|
|
1149
|
+
sourcesList: knowledge.evidence
|
|
1150
|
+
},
|
|
1151
|
+
lifecycle: {
|
|
1152
|
+
revisions,
|
|
1153
|
+
strengthened,
|
|
1154
|
+
contradictions,
|
|
1155
|
+
lastUsed: lastUsedStr,
|
|
1156
|
+
lastValidated: lastValidatedStr,
|
|
1157
|
+
health: healthCategory,
|
|
1158
|
+
healthReason
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/stats/filter.ts
|
|
1164
|
+
import { execFile } from "child_process";
|
|
1165
|
+
import { promisify } from "util";
|
|
1166
|
+
var execFileAsync = promisify(execFile);
|
|
1167
|
+
async function resolveSinceTimestamp(since, projectDir = process.cwd()) {
|
|
1168
|
+
const trimmed = since.trim();
|
|
1169
|
+
const durationMatch = trimmed.match(/^(\d+)\s*([smhdw])$/i);
|
|
1170
|
+
if (durationMatch) {
|
|
1171
|
+
const value = parseInt(durationMatch[1], 10);
|
|
1172
|
+
const unit = durationMatch[2].toLowerCase();
|
|
1173
|
+
let ms = 0;
|
|
1174
|
+
switch (unit) {
|
|
1175
|
+
case "s":
|
|
1176
|
+
ms = value * 1e3;
|
|
1177
|
+
break;
|
|
1178
|
+
case "m":
|
|
1179
|
+
ms = value * 60 * 1e3;
|
|
1180
|
+
break;
|
|
1181
|
+
case "h":
|
|
1182
|
+
ms = value * 3600 * 1e3;
|
|
1183
|
+
break;
|
|
1184
|
+
case "d":
|
|
1185
|
+
ms = value * 86400 * 1e3;
|
|
1186
|
+
break;
|
|
1187
|
+
case "w":
|
|
1188
|
+
ms = value * 7 * 86400 * 1e3;
|
|
1189
|
+
break;
|
|
1190
|
+
}
|
|
1191
|
+
return Date.now() - ms;
|
|
1192
|
+
}
|
|
1193
|
+
const parsedDate = Date.parse(trimmed);
|
|
1194
|
+
if (!isNaN(parsedDate) && trimmed.includes("-")) {
|
|
1195
|
+
return parsedDate;
|
|
1196
|
+
}
|
|
1197
|
+
try {
|
|
1198
|
+
const { stdout } = await execFileAsync(
|
|
1199
|
+
"git",
|
|
1200
|
+
["log", "-1", "--format=%ct", trimmed],
|
|
1201
|
+
{ cwd: projectDir }
|
|
1202
|
+
);
|
|
1203
|
+
const epochSec = parseInt(stdout.trim(), 10);
|
|
1204
|
+
if (!isNaN(epochSec) && epochSec > 0) {
|
|
1205
|
+
return epochSec * 1e3;
|
|
1206
|
+
}
|
|
1207
|
+
throw new Error(`Invalid git output: ${stdout}`);
|
|
1208
|
+
} catch {
|
|
1209
|
+
throw new Error(`Unable to resolve Git revision: ${trimmed}`);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/stats/format.ts
|
|
1214
|
+
function padRow(label, value, width = 26) {
|
|
1215
|
+
return ` ${label.padEnd(width)} ${String(value).padStart(5)}`;
|
|
1216
|
+
}
|
|
1217
|
+
function formatStatsReport(report) {
|
|
1218
|
+
const lines = [];
|
|
1219
|
+
if (report.since) {
|
|
1220
|
+
lines.push(`Time range: since ${report.since}
|
|
1221
|
+
`);
|
|
1222
|
+
}
|
|
1223
|
+
lines.push("Knowledge");
|
|
1224
|
+
lines.push(padRow("Active", report.knowledge.active));
|
|
1225
|
+
lines.push(padRow("Candidates", report.knowledge.candidates));
|
|
1226
|
+
lines.push(padRow("Unknown", report.knowledge.unknown));
|
|
1227
|
+
lines.push(padRow("Conflicted", report.knowledge.conflicted));
|
|
1228
|
+
lines.push(padRow("Needs revalidation", report.knowledge.needsRevalidation));
|
|
1229
|
+
lines.push("");
|
|
1230
|
+
lines.push("Learning");
|
|
1231
|
+
lines.push(padRow("Observations", report.learning.observations));
|
|
1232
|
+
lines.push(padRow("New", report.learning.new));
|
|
1233
|
+
lines.push(padRow("Merged", report.learning.merged));
|
|
1234
|
+
lines.push(padRow("Strengthened", report.learning.strengthened));
|
|
1235
|
+
lines.push(padRow("Specialized", report.learning.specialized));
|
|
1236
|
+
lines.push(padRow("Superseded", report.learning.superseded));
|
|
1237
|
+
lines.push(padRow("Contradictions", report.learning.contradictions));
|
|
1238
|
+
lines.push(padRow("Ignored duplicates", report.learning.ignoredDuplicates));
|
|
1239
|
+
lines.push("");
|
|
1240
|
+
lines.push("Resolution");
|
|
1241
|
+
lines.push(padRow("Resolve calls", report.resolution.resolveCalls));
|
|
1242
|
+
lines.push(padRow("Known", report.resolution.known));
|
|
1243
|
+
lines.push(padRow("Partially known", report.resolution.partiallyKnown));
|
|
1244
|
+
lines.push(padRow("Unknown", report.resolution.unknown));
|
|
1245
|
+
lines.push(padRow("Conflicted", report.resolution.conflicted));
|
|
1246
|
+
lines.push("");
|
|
1247
|
+
lines.push("Investigation");
|
|
1248
|
+
lines.push(padRow("Repeated checks avoided", report.investigation.repeatedChecksAvoided));
|
|
1249
|
+
lines.push(padRow("Knowledge reused", report.investigation.knowledgeReused));
|
|
1250
|
+
lines.push(padRow("Questions suggested", report.investigation.questionsSuggested));
|
|
1251
|
+
lines.push(padRow("Questions resolved", report.investigation.questionsResolved));
|
|
1252
|
+
lines.push(padRow("Broad searches avoided", report.investigation.broadSearchesAvoided));
|
|
1253
|
+
lines.push("");
|
|
1254
|
+
lines.push("JEV");
|
|
1255
|
+
lines.push(padRow("Candidates", report.jev.candidates));
|
|
1256
|
+
lines.push(padRow("Match", report.jev.match));
|
|
1257
|
+
lines.push(padRow("Possible", report.jev.possible));
|
|
1258
|
+
lines.push(padRow("Rejected", report.jev.rejected));
|
|
1259
|
+
lines.push(padRow("Hard negative rejects", report.jev.hardNegativeRejects));
|
|
1260
|
+
lines.push("");
|
|
1261
|
+
const actionKeys = Object.keys(report.actions);
|
|
1262
|
+
if (actionKeys.length > 0) {
|
|
1263
|
+
const actionLabels = {
|
|
1264
|
+
USE_KNOWLEDGE: "Use existing knowledge",
|
|
1265
|
+
INSPECT_FILE: "Inspect single source",
|
|
1266
|
+
INSPECT_TEST: "Inspect test",
|
|
1267
|
+
INSPECT_SYMBOL: "Inspect symbol",
|
|
1268
|
+
CHECK_GIT: "Check Git",
|
|
1269
|
+
CHECK_DOCUMENTATION: "Check documentation",
|
|
1270
|
+
SEARCH_REPOSITORY: "Search repository",
|
|
1271
|
+
ASK_USER: "Ask user",
|
|
1272
|
+
STOP: "Stop unresolved"
|
|
1273
|
+
};
|
|
1274
|
+
lines.push("Resolution Actions");
|
|
1275
|
+
for (const key of actionKeys) {
|
|
1276
|
+
const label = actionLabels[key] || key;
|
|
1277
|
+
lines.push(padRow(label, report.actions[key]));
|
|
1278
|
+
}
|
|
1279
|
+
lines.push("");
|
|
1280
|
+
}
|
|
1281
|
+
lines.push("Effectiveness");
|
|
1282
|
+
lines.push(padRow("Knowledge reuse rate", report.effectiveness.knowledgeReuseRate.formatted));
|
|
1283
|
+
lines.push(padRow("Resolve success rate", report.effectiveness.resolveSuccessRate.formatted));
|
|
1284
|
+
lines.push(padRow("Question resolution rate", report.effectiveness.questionResolutionRate.formatted));
|
|
1285
|
+
lines.push(padRow("JEV rejection rate", report.effectiveness.jevRejectionRate.formatted));
|
|
1286
|
+
lines.push("");
|
|
1287
|
+
lines.push("Knowledge Health");
|
|
1288
|
+
lines.push(padRow("Healthy", report.health.healthy));
|
|
1289
|
+
lines.push(padRow("Needs revalidation", report.health.needsRevalidation));
|
|
1290
|
+
lines.push(padRow("Low evidence", report.health.lowEvidence));
|
|
1291
|
+
lines.push(padRow("Frequently rejected", report.health.frequentlyRejected));
|
|
1292
|
+
lines.push(padRow("Frequently misleading", report.health.frequentlyMisleading));
|
|
1293
|
+
lines.push(padRow("Unused", report.health.unused));
|
|
1294
|
+
return lines.join("\n");
|
|
1295
|
+
}
|
|
1296
|
+
function formatKnowledgeStatsReport(report) {
|
|
1297
|
+
const lines = [];
|
|
1298
|
+
lines.push(report.id);
|
|
1299
|
+
lines.push(`"${report.statement}"
|
|
1300
|
+
`);
|
|
1301
|
+
lines.push("Status");
|
|
1302
|
+
lines.push(` ${report.status.toUpperCase()}${report.stage ? ` (${report.stage})` : ""}
|
|
1303
|
+
`);
|
|
1304
|
+
lines.push("Usage");
|
|
1305
|
+
lines.push(padRow("Used", report.usage.used));
|
|
1306
|
+
lines.push(padRow("Successful outcomes", report.usage.successfulOutcomes));
|
|
1307
|
+
lines.push(padRow("Neutral", report.usage.neutralOutcomes));
|
|
1308
|
+
lines.push(padRow("Misleading", report.usage.misleadingOutcomes));
|
|
1309
|
+
lines.push("");
|
|
1310
|
+
lines.push("JEV");
|
|
1311
|
+
lines.push(padRow("Match", report.jev.match));
|
|
1312
|
+
lines.push(padRow("Possible", report.jev.possible));
|
|
1313
|
+
lines.push(padRow("Rejected", report.jev.rejected));
|
|
1314
|
+
lines.push(padRow("Hard negative rejects", report.jev.hardNegativeRejects));
|
|
1315
|
+
lines.push("");
|
|
1316
|
+
lines.push("Evidence");
|
|
1317
|
+
lines.push(padRow("Sources", report.evidence.sources));
|
|
1318
|
+
lines.push(padRow("Independent sources", report.evidence.independentSources));
|
|
1319
|
+
lines.push("");
|
|
1320
|
+
lines.push("Lifecycle");
|
|
1321
|
+
lines.push(padRow("Revisions", report.lifecycle.revisions));
|
|
1322
|
+
lines.push(padRow("Strengthened", report.lifecycle.strengthened));
|
|
1323
|
+
lines.push(padRow("Contradictions", report.lifecycle.contradictions));
|
|
1324
|
+
if (report.lifecycle.lastUsed) {
|
|
1325
|
+
lines.push(padRow("Last used", report.lifecycle.lastUsed));
|
|
1326
|
+
}
|
|
1327
|
+
if (report.lifecycle.lastValidated) {
|
|
1328
|
+
lines.push(padRow("Last validated", report.lifecycle.lastValidated));
|
|
1329
|
+
}
|
|
1330
|
+
lines.push(padRow("Health", `${report.lifecycle.health} (${report.lifecycle.healthReason})`));
|
|
1331
|
+
return lines.join("\n");
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/stats/index.ts
|
|
1335
|
+
async function runStats(options = {}) {
|
|
1336
|
+
const rootDir = options.rootDir || process.cwd();
|
|
1337
|
+
const storage = new Storage(rootDir);
|
|
1338
|
+
if (options.reset) {
|
|
1339
|
+
await resetMetricEvents(rootDir);
|
|
1340
|
+
return { reset: true };
|
|
1341
|
+
}
|
|
1342
|
+
let sinceTimestamp;
|
|
1343
|
+
if (options.since) {
|
|
1344
|
+
sinceTimestamp = await resolveSinceTimestamp(options.since, rootDir);
|
|
1345
|
+
}
|
|
1346
|
+
const events = await readMetricEvents(rootDir, {
|
|
1347
|
+
sinceTimestamp,
|
|
1348
|
+
knowledgeId: options.knowledgeId
|
|
1349
|
+
});
|
|
1350
|
+
if (options.knowledgeId) {
|
|
1351
|
+
const knowledgeReport = await computeKnowledgeStats(storage, options.knowledgeId, events);
|
|
1352
|
+
return { knowledgeReport };
|
|
1353
|
+
}
|
|
1354
|
+
const report = await computeStats(storage, events, {
|
|
1355
|
+
since: options.since,
|
|
1356
|
+
sinceTimestamp
|
|
1357
|
+
});
|
|
1358
|
+
return { report };
|
|
1359
|
+
}
|
|
1360
|
+
export {
|
|
1361
|
+
appendMetricEvent,
|
|
1362
|
+
classifyKnowledgeHealth,
|
|
1363
|
+
computeKnowledgeStats,
|
|
1364
|
+
computeStats,
|
|
1365
|
+
formatKnowledgeStatsReport,
|
|
1366
|
+
formatStatsReport,
|
|
1367
|
+
getEventsFilePath,
|
|
1368
|
+
readMetricEvents,
|
|
1369
|
+
resetMetricEvents,
|
|
1370
|
+
resolveSinceTimestamp,
|
|
1371
|
+
runStats
|
|
1372
|
+
};
|
|
1373
|
+
//# sourceMappingURL=index.js.map
|