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.
@@ -0,0 +1,3829 @@
1
+ // src/scanner/filesystem.ts
2
+ import * as fs from "fs/promises";
3
+ import * as path from "path";
4
+
5
+ // src/core/knowledge.ts
6
+ import { createHash } from "crypto";
7
+
8
+ // src/core/quarantine.ts
9
+ function determineInitialStage(knowledge) {
10
+ if (knowledge.type === "fact" || knowledge.type === "architecture" || knowledge.confidenceTier === "high" || knowledge.confidence !== void 0 && knowledge.confidence >= 0.85) {
11
+ return "active";
12
+ }
13
+ if (knowledge.type === "rule" && ((knowledge.evidence?.length ?? 0) >= 2 || knowledge.confidence !== void 0 && knowledge.confidence >= 0.75)) {
14
+ return "probation";
15
+ }
16
+ if (knowledge.confidence !== void 0 && knowledge.confidence >= 0.7) {
17
+ return "probation";
18
+ }
19
+ return "candidate";
20
+ }
21
+ function evaluatePromotion(knowledge) {
22
+ const stage = knowledge.stage ?? "candidate";
23
+ const supportCount = knowledge.metrics.support_count;
24
+ const reuseCount = knowledge.metrics.reuse_count;
25
+ const successCount = Math.max(knowledge.outcomesSummary?.successful ?? 0, knowledge.metrics.successful_outcomes ?? 0);
26
+ const misleadingCount = Math.max(knowledge.outcomesSummary?.misleading ?? 0, knowledge.metrics.failed_outcomes ?? 0);
27
+ if (stage === "candidate") {
28
+ if (supportCount >= 2 || knowledge.evidence && knowledge.evidence.length >= 2 || knowledge.confidence >= 0.85) {
29
+ return {
30
+ promoted: true,
31
+ newStage: "probation",
32
+ reason: `Supported by ${supportCount} sources / ${knowledge.evidence.length} evidence nodes`
33
+ };
34
+ }
35
+ } else if (stage === "probation") {
36
+ if (successCount > 0 && misleadingCount === 0 || reuseCount >= 2 && misleadingCount === 0) {
37
+ return {
38
+ promoted: true,
39
+ newStage: "active",
40
+ reason: `Verified by ${successCount} successful task outcomes and ${reuseCount} reuses`
41
+ };
42
+ }
43
+ }
44
+ return { promoted: false, newStage: stage };
45
+ }
46
+ function calculateKnowledgeValue(knowledge) {
47
+ const reuse = knowledge.metrics.reuse_count;
48
+ const success = knowledge.outcomesSummary?.successful ?? knowledge.metrics.successful_outcomes;
49
+ const misleading = knowledge.outcomesSummary?.misleading ?? knowledge.metrics.failed_outcomes;
50
+ const evidenceCount = Math.min(knowledge.evidence.length, 6);
51
+ const confidence = knowledge.confidence;
52
+ let value = reuse * 2.5 + success * 4 + evidenceCount * 1.5 + confidence * 2 - misleading * 5;
53
+ if (knowledge.status === "active") value += 5;
54
+ if (knowledge.status === "needs_revalidation") value -= 3;
55
+ if (knowledge.status === "contradicted") value -= 8;
56
+ if (knowledge.status === "stale") value -= 4;
57
+ if (knowledge.stage === "active") value += 3;
58
+ if (knowledge.stage === "candidate") value -= 1;
59
+ return Math.round(value * 10) / 10;
60
+ }
61
+
62
+ // src/core/knowledge.ts
63
+ function tierToConfidence(tier) {
64
+ switch (tier) {
65
+ case "low":
66
+ return 0.5;
67
+ case "medium":
68
+ return 0.75;
69
+ case "high":
70
+ return 0.95;
71
+ }
72
+ }
73
+ function confidenceToTier(score) {
74
+ if (score >= 0.85) return "high";
75
+ if (score >= 0.65) return "medium";
76
+ return "low";
77
+ }
78
+ function hashString(input) {
79
+ return createHash("sha256").update(input.trim().toLowerCase()).digest("hex").slice(0, 16);
80
+ }
81
+ function createObservation(params) {
82
+ const provenance = params.provenance ?? {
83
+ type: "file",
84
+ path: params.source,
85
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
86
+ };
87
+ const contentForHash = `${params.source}:${params.claim}:${provenance.commit || provenance.timestamp || Date.now()}`;
88
+ const id = `obs_${hashString(contentForHash)}`;
89
+ return {
90
+ id,
91
+ source: params.source,
92
+ claim: params.claim.trim(),
93
+ provenance,
94
+ confidence: Math.min(1, Math.max(0, params.confidence ?? 0.8)),
95
+ positive: Array.from(new Set(params.positive?.map((p) => p.trim().toLowerCase()) ?? [])),
96
+ negative: Array.from(new Set(params.negative?.map((n) => n.trim().toLowerCase()) ?? [])),
97
+ details: params.details,
98
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
99
+ };
100
+ }
101
+ function createEvidence(observation, type = "supports") {
102
+ const sourceHash = hashString(observation.source);
103
+ const id = `evi_${sourceHash}`;
104
+ return {
105
+ id,
106
+ source: observation.source,
107
+ sourceHash,
108
+ observationIds: [observation.id],
109
+ type,
110
+ claim: observation.claim,
111
+ provenance: observation.provenance,
112
+ weight: 1,
113
+ firstObservedAt: observation.recordedAt,
114
+ lastObservedAt: observation.recordedAt
115
+ };
116
+ }
117
+ function createKnowledge(params) {
118
+ const now = (/* @__PURE__ */ new Date()).toISOString();
119
+ const confidenceTier = params.confidenceTier ?? (params.confidence !== void 0 ? confidenceToTier(params.confidence) : void 0);
120
+ const confidence = params.confidence !== void 0 ? params.confidence : confidenceTier ? tierToConfidence(confidenceTier) : 0.85;
121
+ const dependsOn = new Set(params.depends_on ?? []);
122
+ if (params.evidence) {
123
+ for (const ev of params.evidence) {
124
+ if (ev.startsWith("file:") || ev.startsWith("test:") || ev.startsWith("code:")) {
125
+ dependsOn.add(ev);
126
+ } else if (ev.includes("/") || ev.endsWith(".json") || ev.endsWith(".yaml") || ev.endsWith(".yml")) {
127
+ dependsOn.add(`file:${ev}`);
128
+ }
129
+ }
130
+ }
131
+ const initialStage = params.stage ?? determineInitialStage({
132
+ type: params.type ?? "lesson",
133
+ evidence: params.evidence ?? [],
134
+ confidence
135
+ });
136
+ const status = params.status ?? (initialStage === "candidate" ? "candidate" : "active");
137
+ const history = params.history ?? [
138
+ {
139
+ timestamp: now,
140
+ stage: initialStage,
141
+ status,
142
+ reason: `Initial knowledge recorded (${params.type ?? "lesson"})`,
143
+ source: params.evidence?.[0]
144
+ }
145
+ ];
146
+ const knowledgeItem = {
147
+ id: params.id.trim(),
148
+ type: params.type ?? "lesson",
149
+ status,
150
+ stage: initialStage,
151
+ statement: params.statement.trim(),
152
+ positive: Array.from(new Set(params.positive?.map((p) => p.trim().toLowerCase()) ?? [])),
153
+ negative: Array.from(new Set(params.negative?.map((n) => n.trim().toLowerCase()) ?? [])),
154
+ relations: Array.from(new Set(params.relations?.map((r) => r.trim()) ?? [])),
155
+ depends_on: Array.from(dependsOn),
156
+ resolves: params.resolves ? Array.from(new Set(params.resolves.map((r) => r.trim()))) : void 0,
157
+ question: params.question?.trim(),
158
+ confidence: Math.min(1, Math.max(0, confidence)),
159
+ confidenceTier,
160
+ evidence: Array.from(new Set(params.evidence ?? [])),
161
+ scope: params.scope ?? "project",
162
+ tags: Array.from(new Set(params.tags?.map((t) => t.trim().toLowerCase()) ?? [])),
163
+ temporal: {
164
+ revision: 1
165
+ },
166
+ metrics: {
167
+ reuse_count: 0,
168
+ support_count: 1,
169
+ contradiction_count: 0,
170
+ successful_outcomes: 0,
171
+ failed_outcomes: 0
172
+ },
173
+ outcomesSummary: params.outcomesSummary ?? {
174
+ applied: 0,
175
+ successful: 0,
176
+ misleading: 0,
177
+ neutral: 0
178
+ },
179
+ history,
180
+ value: 0,
181
+ createdAt: now,
182
+ updatedAt: now
183
+ };
184
+ knowledgeItem.value = calculateKnowledgeValue(knowledgeItem);
185
+ return knowledgeItem;
186
+ }
187
+ function calculateEffectiveConfidence(knowledge) {
188
+ const m = knowledge.metrics;
189
+ const base = knowledge.confidence;
190
+ const evRatio = (m.support_count + 1) / (m.support_count + 3 * m.contradiction_count + 1);
191
+ const totalOutcomes = m.successful_outcomes + m.failed_outcomes;
192
+ let outRatio = 1;
193
+ let evWeight = 0.6;
194
+ let outWeight = 0.4;
195
+ if (totalOutcomes === 0) {
196
+ evWeight = 0.85;
197
+ outWeight = 0.15;
198
+ } else {
199
+ outRatio = (m.successful_outcomes + 1) / (totalOutcomes + 1);
200
+ }
201
+ const effective = base * (evWeight * evRatio + outWeight * outRatio);
202
+ return Math.round(Math.min(1, Math.max(0.05, effective)) * 100) / 100;
203
+ }
204
+ var knowledgeTokenCache = /* @__PURE__ */ new WeakMap();
205
+ function tokenize(text) {
206
+ return new Set(
207
+ text.toLowerCase().replace(/[^a-z0-9_-]/g, " ").split(/\s+/).filter((t) => t.length > 2)
208
+ );
209
+ }
210
+ function getCachedTokens(k) {
211
+ let tokens = knowledgeTokenCache.get(k);
212
+ if (!tokens) {
213
+ tokens = tokenize(k.statement);
214
+ knowledgeTokenCache.set(k, tokens);
215
+ }
216
+ return tokens;
217
+ }
218
+ function jaccardSimilarity(setA, setB) {
219
+ if (setA.size === 0 && setB.size === 0) return 1;
220
+ let intersection = 0;
221
+ for (const item of setA) {
222
+ if (setB.has(item)) intersection++;
223
+ }
224
+ const union = setA.size + setB.size - intersection;
225
+ return union === 0 ? 0 : intersection / union;
226
+ }
227
+ var POLAR_ANTONYMS = [
228
+ ["enabled", "disabled"],
229
+ ["enable", "disable"],
230
+ ["active", "inactive"],
231
+ ["true", "false"],
232
+ ["on", "off"],
233
+ ["yes", "no"],
234
+ ["allowed", "disallowed"],
235
+ ["allowed", "prohibited"],
236
+ ["allowed", "forbidden"],
237
+ ["permit", "deny"],
238
+ ["allow", "forbid"],
239
+ ["allow", "prohibit"],
240
+ ["always", "never"],
241
+ ["should", "should not"],
242
+ ["must", "must not"],
243
+ ["required", "optional"],
244
+ ["required", "forbidden"],
245
+ ["valid", "invalid"],
246
+ ["safe", "unsafe"],
247
+ ["sync", "async"],
248
+ ["synchronous", "asynchronous"],
249
+ ["public", "private"]
250
+ ];
251
+ var NEGATION_PATTERNS = [
252
+ /\bnot\b/i,
253
+ /\bnever\b/i,
254
+ /\bno\b/i,
255
+ /\bcannot\b/i,
256
+ /\bwithout\b/i,
257
+ /\bdisallow\b/i,
258
+ /\bforbid\b/i,
259
+ /\bprohibit\b/i,
260
+ /\bavoid\b/i
261
+ ];
262
+ function detectPolarContradiction(stmtA, stmtB) {
263
+ const lowerA = stmtA.toLowerCase();
264
+ const lowerB = stmtB.toLowerCase();
265
+ for (const [pos, neg] of POLAR_ANTONYMS) {
266
+ const aHasPos = new RegExp(`\\b${pos}\\b`, "i").test(lowerA);
267
+ const aHasNeg = new RegExp(`\\b${neg}\\b`, "i").test(lowerA);
268
+ const bHasPos = new RegExp(`\\b${pos}\\b`, "i").test(lowerB);
269
+ const bHasNeg = new RegExp(`\\b${neg}\\b`, "i").test(lowerB);
270
+ if (aHasPos && bHasNeg || aHasNeg && bHasPos) {
271
+ return {
272
+ contradicted: true,
273
+ reason: `Polar opposite terms detected: '${pos}' vs '${neg}'`
274
+ };
275
+ }
276
+ }
277
+ const aHasNegation = NEGATION_PATTERNS.some((p) => p.test(lowerA));
278
+ const bHasNegation = NEGATION_PATTERNS.some((p) => p.test(lowerB));
279
+ if (aHasNegation !== bHasNegation) {
280
+ return {
281
+ contradicted: true,
282
+ reason: "Asymmetric negation detected (one statement asserts, the other negates)"
283
+ };
284
+ }
285
+ return { contradicted: false };
286
+ }
287
+ function reconcileKnowledge(candidate, existingList) {
288
+ const candidateId = candidate.id?.trim();
289
+ const candidateStmt = candidate.statement.trim();
290
+ const candidateTokens = tokenize(candidateStmt);
291
+ const candidatePos = new Set((candidate.positive ?? []).map((p) => p.trim().toLowerCase()));
292
+ if (candidateId) {
293
+ const directMatch = existingList.find((k) => k.id === candidateId);
294
+ if (directMatch) {
295
+ if (directMatch.statement.trim().toLowerCase() === candidateStmt.toLowerCase()) {
296
+ const hasNewEvidence = (candidate.evidence ?? []).some((e) => !directMatch.evidence.includes(e));
297
+ if (!hasNewEvidence) {
298
+ return {
299
+ action: "IGNORE",
300
+ targetId: directMatch.id,
301
+ knowledge: directMatch,
302
+ reason: "Identical statement and evidence already recorded"
303
+ };
304
+ }
305
+ return {
306
+ action: "SUPPORT",
307
+ targetId: directMatch.id,
308
+ knowledge: directMatch,
309
+ reason: "Confirms existing knowledge with new evidence source"
310
+ };
311
+ }
312
+ const opposingPositive = (candidate.negative ?? []).some((n) => directMatch.positive.includes(n));
313
+ const opposingNegative = (candidate.positive ?? []).some((p) => directMatch.negative.includes(p));
314
+ const polarCheck = detectPolarContradiction(candidateStmt, directMatch.statement);
315
+ if (opposingPositive || opposingNegative || polarCheck.contradicted) {
316
+ const distinctCues = Array.from(candidatePos).filter((c) => !directMatch.positive.includes(c));
317
+ const mentionsEnvA = /(development|dev|local|staging|test)/i.test(candidateStmt);
318
+ const mentionsEnvB = /(production|prod)/i.test(directMatch.statement);
319
+ const mentionsEnvBDev = /(development|dev|local|staging|test)/i.test(directMatch.statement);
320
+ const mentionsEnvAProd = /(production|prod)/i.test(candidateStmt);
321
+ if (distinctCues.length > 0 || mentionsEnvA && mentionsEnvB || mentionsEnvBDev && mentionsEnvAProd) {
322
+ return {
323
+ action: "SPECIALIZE",
324
+ targetId: directMatch.id,
325
+ knowledge: directMatch,
326
+ reason: `Conflicting claim applies under specialized context cues: [${distinctCues.join(", ")}] (${polarCheck.reason || "opposing polarity"})`
327
+ };
328
+ }
329
+ return {
330
+ action: "CONTRADICT",
331
+ targetId: directMatch.id,
332
+ knowledge: directMatch,
333
+ reason: `Contradicts existing statement (${polarCheck.reason || "opposing polarity"}) for ID "${directMatch.id}"`
334
+ };
335
+ }
336
+ return {
337
+ action: "UPDATE",
338
+ targetId: directMatch.id,
339
+ knowledge: directMatch,
340
+ reason: "Refines or updates existing knowledge statement"
341
+ };
342
+ }
343
+ }
344
+ let bestMatch = null;
345
+ let bestSim = 0;
346
+ let bestPolarCheck = { contradicted: false };
347
+ for (const existing of existingList) {
348
+ const existingTokens = getCachedTokens(existing);
349
+ const sim = jaccardSimilarity(candidateTokens, existingTokens);
350
+ const polarCheck = detectPolarContradiction(candidateStmt, existing.statement);
351
+ if (polarCheck.contradicted) {
352
+ let sharedCount = 0;
353
+ for (const t of candidateTokens) {
354
+ if (existingTokens.has(t)) sharedCount++;
355
+ }
356
+ const sharedCues = (candidate.positive ?? []).filter((p) => existing.positive.includes(p));
357
+ if (sharedCount >= 2 || sharedCues.length >= 1 && sim >= 0.35 || sim >= 0.4) {
358
+ bestMatch = existing;
359
+ bestSim = Math.max(sim, 0.85);
360
+ bestPolarCheck = polarCheck;
361
+ break;
362
+ }
363
+ }
364
+ if (sim > bestSim) {
365
+ bestSim = sim;
366
+ bestMatch = existing;
367
+ bestPolarCheck = polarCheck;
368
+ }
369
+ }
370
+ if (bestMatch && bestSim >= 0.7) {
371
+ const polarCheck = bestPolarCheck.contradicted ? bestPolarCheck : detectPolarContradiction(candidateStmt, bestMatch.statement);
372
+ const hasOpposingCues = (candidate.negative ?? []).some((n) => bestMatch.positive.includes(n)) || (candidate.positive ?? []).some((p) => bestMatch.negative.includes(p));
373
+ if (polarCheck.contradicted || hasOpposingCues) {
374
+ const distinctCues = Array.from(candidatePos).filter((c) => !bestMatch.positive.includes(c));
375
+ const mentionsEnvA = /(development|dev|local|staging|test)/i.test(candidateStmt);
376
+ const mentionsEnvB = /(production|prod)/i.test(bestMatch.statement);
377
+ const mentionsEnvBDev = /(development|dev|local|staging|test)/i.test(bestMatch.statement);
378
+ const mentionsEnvAProd = /(production|prod)/i.test(candidateStmt);
379
+ if (distinctCues.length > 0 || mentionsEnvA && mentionsEnvB || mentionsEnvBDev && mentionsEnvAProd) {
380
+ return {
381
+ action: "SPECIALIZE",
382
+ targetId: bestMatch.id,
383
+ knowledge: bestMatch,
384
+ reason: `High textual similarity (${Math.round(bestSim * 100)}%) with polar distinction across contexts (${polarCheck.reason || "opposing cues"})`,
385
+ score: bestSim
386
+ };
387
+ }
388
+ return {
389
+ action: "CONTRADICT",
390
+ targetId: bestMatch.id,
391
+ knowledge: bestMatch,
392
+ reason: `Contradicting statement (${polarCheck.reason || "opposing cues"}) despite high textual similarity (${Math.round(bestSim * 100)}%)`,
393
+ score: bestSim
394
+ };
395
+ }
396
+ if (bestSim >= 0.9) {
397
+ return {
398
+ action: "MERGE",
399
+ targetId: bestMatch.id,
400
+ knowledge: bestMatch,
401
+ reason: `Nearly identical statement (${Math.round(bestSim * 100)}% match) with compatible polarity; merging cues and evidence`,
402
+ score: bestSim
403
+ };
404
+ }
405
+ return {
406
+ action: "SUPPORT",
407
+ targetId: bestMatch.id,
408
+ knowledge: bestMatch,
409
+ reason: `Supports existing knowledge (${Math.round(bestSim * 100)}% similarity) with compatible polarity`,
410
+ score: bestSim
411
+ };
412
+ }
413
+ return {
414
+ action: "NEW",
415
+ reason: "No conflicting or matching knowledge found; creating new knowledge entry"
416
+ };
417
+ }
418
+ function applyReconciliation(reconciliation, candidate, existingList, storage) {
419
+ const now = (/* @__PURE__ */ new Date()).toISOString();
420
+ switch (reconciliation.action) {
421
+ case "SUPPORT": {
422
+ const target = existingList.find((k) => k.id === reconciliation.targetId);
423
+ target.metrics.support_count++;
424
+ target.metrics.last_used = now;
425
+ storage?.appendMetricEvent({
426
+ type: "knowledge_strengthened",
427
+ knowledgeId: target.id,
428
+ timestamp: Date.now()
429
+ });
430
+ if (candidate.evidence) {
431
+ for (const e of candidate.evidence) {
432
+ if (!target.evidence.includes(e)) {
433
+ target.evidence.push(e);
434
+ }
435
+ }
436
+ }
437
+ if (candidate.positive) {
438
+ for (const p of candidate.positive) {
439
+ const lp = p.trim().toLowerCase();
440
+ if (!target.positive.includes(lp)) target.positive.push(lp);
441
+ }
442
+ }
443
+ if (!target.history) {
444
+ target.history = [
445
+ {
446
+ timestamp: target.createdAt || now,
447
+ stage: target.stage,
448
+ status: target.status,
449
+ reason: "Initial knowledge recorded",
450
+ source: target.evidence?.[0]
451
+ }
452
+ ];
453
+ }
454
+ const promotion = evaluatePromotion(target);
455
+ if (promotion.promoted && promotion.newStage) {
456
+ target.stage = promotion.newStage;
457
+ if (target.stage === "active" && target.status === "candidate") {
458
+ target.status = "active";
459
+ }
460
+ target.history.push({
461
+ timestamp: now,
462
+ stage: target.stage,
463
+ status: target.status,
464
+ reason: promotion.reason || `Promoted to ${target.stage}`,
465
+ source: candidate.evidence?.[0]
466
+ });
467
+ } else {
468
+ target.history.push({
469
+ timestamp: now,
470
+ stage: target.stage,
471
+ status: target.status,
472
+ reason: reconciliation.reason || "Confirmed with supporting evidence",
473
+ source: candidate.evidence?.[0]
474
+ });
475
+ }
476
+ target.confidence = calculateEffectiveConfidence(target);
477
+ target.value = calculateKnowledgeValue(target);
478
+ target.updatedAt = now;
479
+ return { updatedKnowledge: target, action: "SUPPORT", createdId: target.id };
480
+ }
481
+ case "MERGE": {
482
+ const target = existingList.find((k) => k.id === reconciliation.targetId);
483
+ target.metrics.support_count++;
484
+ target.metrics.last_used = now;
485
+ storage?.appendMetricEvent({
486
+ type: "knowledge_merged",
487
+ knowledgeId: target.id,
488
+ targetId: reconciliation.targetId,
489
+ timestamp: Date.now()
490
+ });
491
+ if (candidate.evidence) {
492
+ for (const e of candidate.evidence) {
493
+ if (!target.evidence.includes(e)) target.evidence.push(e);
494
+ }
495
+ }
496
+ if (candidate.positive) {
497
+ for (const p of candidate.positive) {
498
+ const lp = p.trim().toLowerCase();
499
+ if (!target.positive.includes(lp)) target.positive.push(lp);
500
+ }
501
+ }
502
+ if (candidate.negative) {
503
+ for (const n of candidate.negative) {
504
+ const ln = n.trim().toLowerCase();
505
+ if (!target.negative.includes(ln)) target.negative.push(ln);
506
+ }
507
+ }
508
+ if (!target.history) {
509
+ target.history = [
510
+ {
511
+ timestamp: target.createdAt || now,
512
+ stage: target.stage,
513
+ status: target.status,
514
+ reason: "Initial knowledge recorded",
515
+ source: target.evidence?.[0]
516
+ }
517
+ ];
518
+ }
519
+ const promotion = evaluatePromotion(target);
520
+ if (promotion.promoted && promotion.newStage) {
521
+ target.stage = promotion.newStage;
522
+ if (target.stage === "active" && target.status === "candidate") {
523
+ target.status = "active";
524
+ }
525
+ target.history.push({
526
+ timestamp: now,
527
+ stage: target.stage,
528
+ status: target.status,
529
+ reason: promotion.reason || `Promoted to ${target.stage}`,
530
+ source: candidate.evidence?.[0]
531
+ });
532
+ } else {
533
+ target.history.push({
534
+ timestamp: now,
535
+ stage: target.stage,
536
+ status: target.status,
537
+ reason: reconciliation.reason || "Merged with compatible knowledge candidate",
538
+ source: candidate.evidence?.[0]
539
+ });
540
+ }
541
+ target.confidence = calculateEffectiveConfidence(target);
542
+ target.value = calculateKnowledgeValue(target);
543
+ target.updatedAt = now;
544
+ return { updatedKnowledge: target, action: "MERGE", createdId: target.id };
545
+ }
546
+ case "UPDATE": {
547
+ const target = existingList.find((k) => k.id === reconciliation.targetId);
548
+ target.statement = candidate.statement;
549
+ if (candidate.type) target.type = candidate.type;
550
+ if (candidate.positive) {
551
+ target.positive = Array.from(/* @__PURE__ */ new Set([...target.positive, ...candidate.positive.map((p) => p.toLowerCase())]));
552
+ }
553
+ if (candidate.negative) {
554
+ target.negative = Array.from(/* @__PURE__ */ new Set([...target.negative, ...candidate.negative.map((n) => n.toLowerCase())]));
555
+ }
556
+ if (candidate.evidence) {
557
+ target.evidence = Array.from(/* @__PURE__ */ new Set([...target.evidence, ...candidate.evidence]));
558
+ }
559
+ target.temporal = {
560
+ revision: (target.temporal?.revision ?? 1) + 1,
561
+ valid_from: { timestamp: now }
562
+ };
563
+ if (!target.history) {
564
+ target.history = [
565
+ {
566
+ timestamp: target.createdAt || now,
567
+ stage: target.stage,
568
+ status: target.status,
569
+ reason: "Initial knowledge recorded",
570
+ source: target.evidence?.[0]
571
+ }
572
+ ];
573
+ }
574
+ target.history.push({
575
+ timestamp: now,
576
+ stage: target.stage,
577
+ status: target.status,
578
+ reason: reconciliation.reason || "Updated statement and context",
579
+ source: candidate.evidence?.[0]
580
+ });
581
+ target.updatedAt = now;
582
+ target.confidence = calculateEffectiveConfidence(target);
583
+ target.value = calculateKnowledgeValue(target);
584
+ storage?.appendMetricEvent({
585
+ type: "knowledge_strengthened",
586
+ knowledgeId: target.id,
587
+ timestamp: Date.now()
588
+ });
589
+ return { updatedKnowledge: target, action: "UPDATE", createdId: target.id };
590
+ }
591
+ case "CONTRADICT": {
592
+ const target = existingList.find((k) => k.id === reconciliation.targetId);
593
+ target.metrics.contradiction_count++;
594
+ if (candidate.evidence) {
595
+ for (const e of candidate.evidence) {
596
+ if (!target.evidence.includes(e)) target.evidence.push(e);
597
+ }
598
+ }
599
+ if (target.metrics.contradiction_count > target.metrics.support_count) {
600
+ target.status = "contradicted";
601
+ }
602
+ if (!target.history) {
603
+ target.history = [
604
+ {
605
+ timestamp: target.createdAt || now,
606
+ stage: target.stage,
607
+ status: target.status,
608
+ reason: "Initial knowledge recorded",
609
+ source: target.evidence?.[0]
610
+ }
611
+ ];
612
+ }
613
+ target.history.push({
614
+ timestamp: now,
615
+ stage: target.stage,
616
+ status: target.status,
617
+ reason: reconciliation.reason || "Contradicted by opposing claim",
618
+ source: candidate.evidence?.[0]
619
+ });
620
+ target.confidence = calculateEffectiveConfidence(target);
621
+ target.value = calculateKnowledgeValue(target);
622
+ target.updatedAt = now;
623
+ storage?.appendMetricEvent({
624
+ type: "contradiction_detected",
625
+ knowledgeId: target.id,
626
+ timestamp: Date.now()
627
+ });
628
+ return { updatedKnowledge: target, action: "CONTRADICT", createdId: target.id };
629
+ }
630
+ case "SPECIALIZE": {
631
+ const parent = existingList.find((k) => k.id === reconciliation.targetId);
632
+ const subContext = candidate.positive?.[0] || "specialized";
633
+ const newId = candidate.id && candidate.id !== parent?.id ? candidate.id : `${parent ? parent.id : "knowledge"}.${subContext}`;
634
+ const created = createKnowledge({
635
+ id: newId,
636
+ type: candidate.type ?? parent?.type ?? "lesson",
637
+ statement: candidate.statement,
638
+ positive: candidate.positive ?? [],
639
+ negative: candidate.negative ?? [],
640
+ relations: parent ? [parent.id, ...candidate.relations ?? []] : candidate.relations ?? [],
641
+ confidence: candidate.confidence ?? 0.85,
642
+ evidence: candidate.evidence ?? [],
643
+ scope: candidate.scope ?? parent?.scope ?? "project",
644
+ tags: candidate.tags ?? parent?.tags ?? []
645
+ });
646
+ if (parent) {
647
+ if (!parent.relations.includes(newId)) {
648
+ parent.relations.push(newId);
649
+ }
650
+ if (!parent.history) {
651
+ parent.history = [
652
+ {
653
+ timestamp: parent.createdAt || now,
654
+ stage: parent.stage,
655
+ status: parent.status,
656
+ reason: "Initial knowledge recorded"
657
+ }
658
+ ];
659
+ }
660
+ parent.history.push({
661
+ timestamp: now,
662
+ stage: parent.stage,
663
+ status: parent.status,
664
+ reason: `Specialized into contextual branch ${newId}`
665
+ });
666
+ }
667
+ storage?.appendMetricEvent({
668
+ type: "knowledge_specialized",
669
+ knowledgeId: created.id,
670
+ timestamp: Date.now()
671
+ });
672
+ return { updatedKnowledge: created, action: "SPECIALIZE", createdId: created.id };
673
+ }
674
+ case "IGNORE": {
675
+ const target = existingList.find((k) => k.id === reconciliation.targetId);
676
+ storage?.appendMetricEvent({
677
+ type: "duplicate_ignored",
678
+ timestamp: Date.now()
679
+ });
680
+ return { updatedKnowledge: target, action: "IGNORE", createdId: target.id };
681
+ }
682
+ case "NEW":
683
+ default: {
684
+ const newId = candidate.id || `kn_${hashString(candidate.statement)}`;
685
+ const created = createKnowledge({
686
+ id: newId,
687
+ type: candidate.type,
688
+ statement: candidate.statement,
689
+ positive: candidate.positive,
690
+ negative: candidate.negative,
691
+ relations: candidate.relations,
692
+ confidence: candidate.confidence,
693
+ evidence: candidate.evidence,
694
+ scope: candidate.scope,
695
+ tags: candidate.tags
696
+ });
697
+ storage?.appendMetricEvent({
698
+ type: "knowledge_created",
699
+ knowledgeId: created.id,
700
+ timestamp: Date.now()
701
+ });
702
+ return { updatedKnowledge: created, action: "NEW", createdId: created.id };
703
+ }
704
+ }
705
+ }
706
+
707
+ // src/scanner/filesystem.ts
708
+ var DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([
709
+ ".git",
710
+ ".bilmem",
711
+ "node_modules",
712
+ "dist",
713
+ "build",
714
+ "target",
715
+ "vendor",
716
+ ".cache",
717
+ ".idea",
718
+ ".vscode",
719
+ "coverage",
720
+ ".next",
721
+ ".nuxt",
722
+ ".turbo",
723
+ ".output",
724
+ "tmp",
725
+ "temp"
726
+ ]);
727
+ function normalizeRelPath(rel) {
728
+ return rel.split(path.sep).join("/");
729
+ }
730
+ async function loadGitignorePatterns(rootDir) {
731
+ try {
732
+ const gitignorePath = path.join(rootDir, ".gitignore");
733
+ const content = await fs.readFile(gitignorePath, "utf-8");
734
+ const regexes = [];
735
+ for (const rawLine of content.split("\n")) {
736
+ const line = rawLine.trim();
737
+ if (!line || line.startsWith("#")) continue;
738
+ let pattern = line.replace(/^\//, "").replace(/\/$/, "");
739
+ if (!pattern) continue;
740
+ pattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
741
+ pattern = pattern.replace(/\*\*/g, ".*");
742
+ pattern = pattern.replace(/\*/g, "[^/]*");
743
+ pattern = pattern.replace(/\?/g, "[^/]");
744
+ try {
745
+ regexes.push(new RegExp(`(^|/)${pattern}($|/)`));
746
+ } catch {
747
+ }
748
+ }
749
+ return regexes;
750
+ } catch {
751
+ return [];
752
+ }
753
+ }
754
+ async function walkDirectory(dir, optionsOrRoot, legacyFileList) {
755
+ let root;
756
+ let options;
757
+ if (typeof optionsOrRoot === "string") {
758
+ root = optionsOrRoot;
759
+ options = {};
760
+ } else {
761
+ options = optionsOrRoot ?? {};
762
+ root = dir;
763
+ }
764
+ const fileList = legacyFileList ?? [];
765
+ const maxDepth = options.maxDepth ?? 15;
766
+ const maxFiles = options.maxFiles ?? 25e3;
767
+ const includeHidden = options.includeHidden ?? false;
768
+ const ignoredDirs = new Set(DEFAULT_IGNORED_DIRS);
769
+ if (options.ignoredDirs) {
770
+ for (const d of options.ignoredDirs) {
771
+ ignoredDirs.add(d.toLowerCase());
772
+ }
773
+ }
774
+ const gitignoreRules = options.readGitignore !== false ? await loadGitignorePatterns(root) : [];
775
+ const visitedRealPaths = /* @__PURE__ */ new Set();
776
+ async function walk(currentDir, currentDepth) {
777
+ if (currentDepth > maxDepth || fileList.length >= maxFiles) {
778
+ return;
779
+ }
780
+ try {
781
+ const real = await fs.realpath(currentDir);
782
+ if (visitedRealPaths.has(real)) {
783
+ return;
784
+ }
785
+ visitedRealPaths.add(real);
786
+ } catch {
787
+ return;
788
+ }
789
+ let entries;
790
+ try {
791
+ entries = await fs.readdir(currentDir, { withFileTypes: true });
792
+ } catch {
793
+ return;
794
+ }
795
+ for (const entry of entries) {
796
+ if (fileList.length >= maxFiles) break;
797
+ const fullPath = path.join(currentDir, entry.name);
798
+ const rawRelPath = path.relative(root, fullPath);
799
+ const relPath = normalizeRelPath(rawRelPath);
800
+ const lowerName = entry.name.toLowerCase();
801
+ if (gitignoreRules.some((rule) => rule.test(relPath))) {
802
+ continue;
803
+ }
804
+ if (entry.isDirectory()) {
805
+ if (ignoredDirs.has(lowerName)) continue;
806
+ if (!includeHidden && entry.name.startsWith(".") && lowerName !== ".github") {
807
+ continue;
808
+ }
809
+ await walk(fullPath, currentDepth + 1);
810
+ } else if (entry.isFile()) {
811
+ if (!includeHidden && entry.name.startsWith(".") && lowerName !== ".env.example") {
812
+ continue;
813
+ }
814
+ const ext = path.extname(entry.name).toLowerCase();
815
+ try {
816
+ const stat3 = await fs.stat(fullPath);
817
+ const fileInfo = {
818
+ path: fullPath,
819
+ relativePath: relPath,
820
+ name: entry.name,
821
+ ext,
822
+ size: stat3.size,
823
+ mtimeMs: stat3.mtimeMs,
824
+ isDirectory: false
825
+ };
826
+ if (!options.filter || options.filter(fileInfo)) {
827
+ fileList.push(fileInfo);
828
+ }
829
+ } catch {
830
+ }
831
+ }
832
+ }
833
+ }
834
+ await walk(dir, 0);
835
+ return fileList;
836
+ }
837
+ var ProjectContext = class {
838
+ rootDir;
839
+ files;
840
+ relPathSet;
841
+ fileNameSet;
842
+ textCache = /* @__PURE__ */ new Map();
843
+ jsonCache = /* @__PURE__ */ new Map();
844
+ constructor(files, rootDir) {
845
+ this.rootDir = rootDir;
846
+ this.files = files;
847
+ this.relPathSet = new Set(files.map((f) => f.relativePath.toLowerCase()));
848
+ this.fileNameSet = new Set(files.map((f) => f.name.toLowerCase()));
849
+ }
850
+ hasFile(nameOrRelPath) {
851
+ const lower = normalizeRelPath(nameOrRelPath).toLowerCase();
852
+ return this.fileNameSet.has(lower) || this.relPathSet.has(lower);
853
+ }
854
+ hasDirectory(dirNameOrRelPath) {
855
+ const prefix = normalizeRelPath(dirNameOrRelPath).toLowerCase().replace(/\/$/, "") + "/";
856
+ return Array.from(this.relPathSet).some((p) => p.startsWith(prefix));
857
+ }
858
+ findFiles(predicate) {
859
+ return this.files.filter(predicate);
860
+ }
861
+ async readText(relativePath, maxBytes = 64 * 1024) {
862
+ const key = normalizeRelPath(relativePath).toLowerCase();
863
+ if (this.textCache.has(key)) {
864
+ return this.textCache.get(key);
865
+ }
866
+ const fullPath = path.resolve(this.rootDir, relativePath);
867
+ try {
868
+ const handle = await fs.open(fullPath, "r");
869
+ const buffer = Buffer.alloc(maxBytes);
870
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
871
+ await handle.close();
872
+ const content = buffer.toString("utf-8", 0, bytesRead);
873
+ this.textCache.set(key, content);
874
+ return content;
875
+ } catch {
876
+ this.textCache.set(key, null);
877
+ return null;
878
+ }
879
+ }
880
+ async readJson(relativePath) {
881
+ const key = normalizeRelPath(relativePath).toLowerCase();
882
+ if (this.jsonCache.has(key)) {
883
+ return this.jsonCache.get(key);
884
+ }
885
+ const text = await this.readText(relativePath);
886
+ if (!text) {
887
+ this.jsonCache.set(key, null);
888
+ return null;
889
+ }
890
+ try {
891
+ const parsed = JSON.parse(text);
892
+ this.jsonCache.set(key, parsed);
893
+ return parsed;
894
+ } catch {
895
+ this.jsonCache.set(key, null);
896
+ return null;
897
+ }
898
+ }
899
+ };
900
+ var ConventionRegistry = class {
901
+ detectors = /* @__PURE__ */ new Map();
902
+ register(detector) {
903
+ this.detectors.set(detector.id, detector);
904
+ return this;
905
+ }
906
+ unregister(id) {
907
+ return this.detectors.delete(id);
908
+ }
909
+ get(id) {
910
+ return this.detectors.get(id);
911
+ }
912
+ list() {
913
+ return Array.from(this.detectors.values());
914
+ }
915
+ async run(ctx) {
916
+ const observations = [];
917
+ for (const detector of this.detectors.values()) {
918
+ try {
919
+ const result = await detector.detect(ctx);
920
+ if (result) {
921
+ if (Array.isArray(result)) {
922
+ observations.push(...result);
923
+ } else {
924
+ observations.push(result);
925
+ }
926
+ }
927
+ } catch {
928
+ }
929
+ }
930
+ return observations;
931
+ }
932
+ };
933
+ var BUILTIN_DETECTORS = [
934
+ // 1. Node.js & package.json
935
+ {
936
+ id: "node-manifest",
937
+ name: "Node.js Manifest Detector",
938
+ category: "runtime",
939
+ async detect(ctx) {
940
+ if (!ctx.hasFile("package.json")) return null;
941
+ const pkg = await ctx.readJson("package.json");
942
+ const now = (/* @__PURE__ */ new Date()).toISOString();
943
+ const scripts = pkg?.scripts ? Object.keys(pkg.scripts) : [];
944
+ const isEsm = pkg?.type === "module";
945
+ const scriptSummary = scripts.length > 0 ? ` with scripts: ${scripts.slice(0, 5).join(", ")}` : "";
946
+ const moduleType = isEsm ? " (ESM module)" : " (CommonJS)";
947
+ return createObservation({
948
+ source: "filesystem:package.json",
949
+ claim: `Project utilizes Node.js ecosystem with package.json manifest${moduleType}${scriptSummary}`,
950
+ provenance: { type: "file", path: "package.json", timestamp: now },
951
+ confidence: 0.99,
952
+ positive: ["node", "npm", "javascript", isEsm ? "esm" : "cjs", ...pkg?.name ? [pkg.name.toLowerCase()] : []],
953
+ details: {
954
+ ecosystem: "node",
955
+ name: pkg?.name,
956
+ version: pkg?.version,
957
+ type: pkg?.type,
958
+ scripts
959
+ }
960
+ });
961
+ }
962
+ },
963
+ // 2. Package Manager
964
+ {
965
+ id: "package-manager",
966
+ name: "Package Manager Detector",
967
+ category: "package-manager",
968
+ detect(ctx) {
969
+ const now = (/* @__PURE__ */ new Date()).toISOString();
970
+ if (ctx.hasFile("pnpm-lock.yaml")) {
971
+ return createObservation({
972
+ source: "filesystem:pnpm-lock.yaml",
973
+ claim: "Project utilizes pnpm as the package manager",
974
+ provenance: { type: "file", path: "pnpm-lock.yaml", timestamp: now },
975
+ confidence: 0.99,
976
+ positive: ["pnpm", "package-manager", "node"]
977
+ });
978
+ }
979
+ if (ctx.hasFile("bun.lockb") || ctx.hasFile("bun.lock")) {
980
+ return createObservation({
981
+ source: "filesystem:bun.lock",
982
+ claim: "Project utilizes Bun runtime and package manager",
983
+ provenance: { type: "file", path: "bun.lock", timestamp: now },
984
+ confidence: 0.99,
985
+ positive: ["bun", "package-manager"]
986
+ });
987
+ }
988
+ if (ctx.hasFile("yarn.lock")) {
989
+ return createObservation({
990
+ source: "filesystem:yarn.lock",
991
+ claim: "Project utilizes Yarn package manager",
992
+ provenance: { type: "file", path: "yarn.lock", timestamp: now },
993
+ confidence: 0.99,
994
+ positive: ["yarn", "package-manager", "node"]
995
+ });
996
+ }
997
+ if (ctx.hasFile("package-lock.json")) {
998
+ return createObservation({
999
+ source: "filesystem:package-lock.json",
1000
+ claim: "Project utilizes npm as package manager with package-lock.json",
1001
+ provenance: { type: "file", path: "package-lock.json", timestamp: now },
1002
+ confidence: 0.95,
1003
+ positive: ["npm", "package-manager", "node"]
1004
+ });
1005
+ }
1006
+ return null;
1007
+ }
1008
+ },
1009
+ // 3. TypeScript Configuration
1010
+ {
1011
+ id: "typescript-config",
1012
+ name: "TypeScript Compiler Detector",
1013
+ category: "language",
1014
+ async detect(ctx) {
1015
+ if (!ctx.hasFile("tsconfig.json")) return null;
1016
+ const tsconfig = await ctx.readJson("tsconfig.json");
1017
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1018
+ const opts = tsconfig?.compilerOptions;
1019
+ const targetStr = opts?.target ? ` target ${opts.target}` : "";
1020
+ const strictStr = opts?.strict ? " with strict mode" : "";
1021
+ return createObservation({
1022
+ source: "filesystem:tsconfig.json",
1023
+ claim: `Project is configured for TypeScript compilation${targetStr}${strictStr}`,
1024
+ provenance: { type: "file", path: "tsconfig.json", timestamp: now },
1025
+ confidence: 0.99,
1026
+ positive: ["typescript", "ts", ...opts?.target ? [opts.target.toLowerCase()] : []],
1027
+ details: { language: "typescript", target: opts?.target, strict: opts?.strict }
1028
+ });
1029
+ }
1030
+ },
1031
+ // 4. Rust & Cargo
1032
+ {
1033
+ id: "rust-cargo",
1034
+ name: "Rust & Cargo Detector",
1035
+ category: "language",
1036
+ async detect(ctx) {
1037
+ if (!ctx.hasFile("cargo.toml")) return null;
1038
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1039
+ const cargoText = await ctx.readText("Cargo.toml");
1040
+ const isWorkspace = cargoText?.includes("[workspace]") ?? false;
1041
+ const claim = isWorkspace ? "Project utilizes Rust with Cargo workspace configuration" : "Project utilizes Rust and Cargo";
1042
+ return createObservation({
1043
+ source: "filesystem:cargo.toml",
1044
+ claim,
1045
+ provenance: { type: "file", path: "Cargo.toml", timestamp: now },
1046
+ confidence: 0.99,
1047
+ positive: ["rust", "cargo", ...isWorkspace ? ["workspace"] : []],
1048
+ details: { ecosystem: "rust", isWorkspace }
1049
+ });
1050
+ }
1051
+ },
1052
+ // 5. Go Modules
1053
+ {
1054
+ id: "go-module",
1055
+ name: "Go Modules Detector",
1056
+ category: "language",
1057
+ async detect(ctx) {
1058
+ if (!ctx.hasFile("go.mod")) return null;
1059
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1060
+ const goModText = await ctx.readText("go.mod");
1061
+ const modMatch = goModText?.match(/^module\s+([^\s]+)/m);
1062
+ const modName = modMatch ? ` (${modMatch[1]})` : "";
1063
+ return createObservation({
1064
+ source: "filesystem:go.mod",
1065
+ claim: `Project utilizes Go modules${modName}`,
1066
+ provenance: { type: "file", path: "go.mod", timestamp: now },
1067
+ confidence: 0.99,
1068
+ positive: ["go", "golang"],
1069
+ details: { ecosystem: "go", module: modMatch ? modMatch[1] : void 0 }
1070
+ });
1071
+ }
1072
+ },
1073
+ // 6. Python Environment
1074
+ {
1075
+ id: "python-project",
1076
+ name: "Python Environment Detector",
1077
+ category: "language",
1078
+ detect(ctx) {
1079
+ const hasPyproject = ctx.hasFile("pyproject.toml");
1080
+ const hasReqs = ctx.hasFile("requirements.txt");
1081
+ const hasPipfile = ctx.hasFile("pipfile");
1082
+ const hasUvLock = ctx.hasFile("uv.lock");
1083
+ const hasPoetry = ctx.hasFile("poetry.lock");
1084
+ if (!hasPyproject && !hasReqs && !hasPipfile && !hasUvLock && !hasPoetry) {
1085
+ return null;
1086
+ }
1087
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1088
+ const tools = [];
1089
+ if (hasUvLock) tools.push("uv");
1090
+ if (hasPoetry) tools.push("poetry");
1091
+ if (hasPyproject) tools.push("pyproject.toml");
1092
+ if (hasReqs) tools.push("requirements.txt");
1093
+ return createObservation({
1094
+ source: "filesystem:python",
1095
+ claim: `Project contains Python dependencies (${tools.join(", ")})`,
1096
+ provenance: {
1097
+ type: "file",
1098
+ path: hasPyproject ? "pyproject.toml" : hasReqs ? "requirements.txt" : "uv.lock",
1099
+ timestamp: now
1100
+ },
1101
+ confidence: 0.95,
1102
+ positive: ["python", "py", ...tools],
1103
+ details: { ecosystem: "python", tools }
1104
+ });
1105
+ }
1106
+ },
1107
+ // 7. Monorepo & Workspaces
1108
+ {
1109
+ id: "monorepo",
1110
+ name: "Monorepo Workspace Detector",
1111
+ category: "monorepo",
1112
+ detect(ctx) {
1113
+ const hasTurbo = ctx.hasFile("turbo.json");
1114
+ const hasNx = ctx.hasFile("nx.json");
1115
+ const hasPnpmWs = ctx.hasFile("pnpm-workspace.yaml");
1116
+ const hasLerna = ctx.hasFile("lerna.json");
1117
+ if (!hasTurbo && !hasNx && !hasPnpmWs && !hasLerna) {
1118
+ return null;
1119
+ }
1120
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1121
+ const tools = [];
1122
+ if (hasTurbo) tools.push("turborepo");
1123
+ if (hasNx) tools.push("nx");
1124
+ if (hasPnpmWs) tools.push("pnpm-workspaces");
1125
+ if (hasLerna) tools.push("lerna");
1126
+ return createObservation({
1127
+ source: "filesystem:monorepo",
1128
+ claim: `Project is structured as a monorepo workspace managed by ${tools.join(", ")}`,
1129
+ provenance: { type: "file", path: hasTurbo ? "turbo.json" : hasPnpmWs ? "pnpm-workspace.yaml" : "nx.json", timestamp: now },
1130
+ confidence: 0.95,
1131
+ positive: ["monorepo", "workspace", ...tools]
1132
+ });
1133
+ }
1134
+ },
1135
+ // 8. Testing Suites
1136
+ {
1137
+ id: "testing-suite",
1138
+ name: "Testing Suite & Framework Detector",
1139
+ category: "testing",
1140
+ detect(ctx) {
1141
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1142
+ const hasVitest = ctx.hasFile("vitest.config.ts") || ctx.hasFile("vitest.config.js") || ctx.hasFile("vitest.config.mts");
1143
+ const hasJest = ctx.hasFile("jest.config.js") || ctx.hasFile("jest.config.ts");
1144
+ const hasPlaywright = ctx.hasFile("playwright.config.ts") || ctx.hasFile("playwright.config.js");
1145
+ const hasPytest = ctx.hasFile("pytest.ini") || ctx.hasFile("conftest.py");
1146
+ const testFiles = ctx.findFiles(
1147
+ (f) => f.relativePath.includes("test") || f.relativePath.includes("spec") || f.name.endsWith(".test.ts") || f.name.endsWith(".spec.ts") || f.name.endsWith(".test.js") || f.name.endsWith("_test.go") || f.name.startsWith("test_")
1148
+ );
1149
+ if (!hasVitest && !hasJest && !hasPlaywright && !hasPytest && testFiles.length === 0) {
1150
+ return null;
1151
+ }
1152
+ const frameworks = [];
1153
+ if (hasVitest) frameworks.push("Vitest");
1154
+ if (hasJest) frameworks.push("Jest");
1155
+ if (hasPlaywright) frameworks.push("Playwright");
1156
+ if (hasPytest) frameworks.push("Pytest");
1157
+ const fwStr = frameworks.length > 0 ? ` (${frameworks.join(", ")})` : "";
1158
+ return createObservation({
1159
+ source: "filesystem:tests",
1160
+ claim: `Project includes automated test suite${fwStr} with ${testFiles.length} test files detected`,
1161
+ provenance: { type: "file", path: "test", timestamp: now },
1162
+ confidence: 0.95,
1163
+ positive: ["tests", "testing", "qa", ...frameworks.map((f) => f.toLowerCase())],
1164
+ details: { testFilesCount: testFiles.length, frameworks }
1165
+ });
1166
+ }
1167
+ },
1168
+ // 9. Database & ORM
1169
+ {
1170
+ id: "database-orm",
1171
+ name: "Database & Schema Detector",
1172
+ category: "database",
1173
+ detect(ctx) {
1174
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1175
+ const hasPrisma = ctx.hasFile("schema.prisma") || ctx.hasDirectory("prisma");
1176
+ const hasDrizzle = ctx.hasFile("drizzle.config.ts") || ctx.hasFile("drizzle.config.js");
1177
+ const hasMigrations = ctx.hasDirectory("migrations") || ctx.hasDirectory("db/migrations");
1178
+ if (!hasPrisma && !hasDrizzle && !hasMigrations) {
1179
+ return null;
1180
+ }
1181
+ const tools = [];
1182
+ if (hasPrisma) tools.push("Prisma");
1183
+ if (hasDrizzle) tools.push("Drizzle");
1184
+ if (hasMigrations) tools.push("SQL Migrations");
1185
+ return createObservation({
1186
+ source: "filesystem:database",
1187
+ claim: `Project includes database schema and migration tooling: ${tools.join(", ")}`,
1188
+ provenance: { type: "file", path: hasPrisma ? "prisma/schema.prisma" : "migrations", timestamp: now },
1189
+ confidence: 0.92,
1190
+ positive: ["database", "orm", "migrations", ...tools.map((t) => t.toLowerCase())]
1191
+ });
1192
+ }
1193
+ },
1194
+ // 10. Container & DevOps
1195
+ {
1196
+ id: "container-devops",
1197
+ name: "Container & Docker Detector",
1198
+ category: "tooling",
1199
+ detect(ctx) {
1200
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1201
+ const hasDockerfile = ctx.hasFile("dockerfile");
1202
+ const hasCompose = ctx.hasFile("docker-compose.yml") || ctx.hasFile("compose.yaml") || ctx.hasFile("docker-compose.yaml");
1203
+ if (!hasDockerfile && !hasCompose) return null;
1204
+ return createObservation({
1205
+ source: "filesystem:docker",
1206
+ claim: "Project includes Docker container configuration",
1207
+ provenance: { type: "file", path: hasDockerfile ? "Dockerfile" : "docker-compose.yml", timestamp: now },
1208
+ confidence: 0.95,
1209
+ positive: ["docker", "containers", "devops"]
1210
+ });
1211
+ }
1212
+ },
1213
+ // 11. CI/CD Workflows
1214
+ {
1215
+ id: "ci-cd",
1216
+ name: "CI/CD Workflow Detector",
1217
+ category: "ci-cd",
1218
+ detect(ctx) {
1219
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1220
+ const ghWorkflows = ctx.findFiles((f) => f.relativePath.startsWith(".github/workflows/"));
1221
+ const hasGitlab = ctx.hasFile(".gitlab-ci.yml");
1222
+ if (ghWorkflows.length === 0 && !hasGitlab) return null;
1223
+ const claim = ghWorkflows.length > 0 ? `Project configures GitHub Actions CI/CD workflows (${ghWorkflows.length} workflow files)` : "Project configures GitLab CI pipeline";
1224
+ return createObservation({
1225
+ source: "filesystem:ci",
1226
+ claim,
1227
+ provenance: { type: "file", path: ghWorkflows.length > 0 ? ".github/workflows" : ".gitlab-ci.yml", timestamp: now },
1228
+ confidence: 0.95,
1229
+ positive: ["ci", "github-actions", "automation"]
1230
+ });
1231
+ }
1232
+ },
1233
+ // 12. Documentation
1234
+ {
1235
+ id: "documentation",
1236
+ name: "Project Documentation Detector",
1237
+ category: "documentation",
1238
+ detect(ctx) {
1239
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1240
+ const hasReadme = ctx.hasFile("readme.md");
1241
+ const hasDocs = ctx.hasDirectory("docs");
1242
+ const hasContributing = ctx.hasFile("contributing.md");
1243
+ if (!hasReadme && !hasDocs && !hasContributing) return null;
1244
+ return createObservation({
1245
+ source: "filesystem:readme.md",
1246
+ claim: "Root README.md is present for project documentation",
1247
+ provenance: { type: "file", path: "README.md", timestamp: now },
1248
+ confidence: 0.95,
1249
+ positive: ["documentation", "readme"]
1250
+ });
1251
+ }
1252
+ },
1253
+ // 13. Architecture & Code Layout
1254
+ {
1255
+ id: "code-layout",
1256
+ name: "Architecture & Layout Pattern Detector",
1257
+ category: "architecture",
1258
+ detect(ctx) {
1259
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1260
+ const isCleanArch = (ctx.hasDirectory("src/domain") || ctx.hasDirectory("domain")) && (ctx.hasDirectory("src/application") || ctx.hasDirectory("application"));
1261
+ if (isCleanArch) {
1262
+ return createObservation({
1263
+ source: "filesystem:architecture",
1264
+ claim: "Project follows Clean / Hexagonal Architecture layout (domain, application, infrastructure)",
1265
+ provenance: { type: "file", path: "src", timestamp: now },
1266
+ confidence: 0.88,
1267
+ positive: ["clean-architecture", "domain-driven", "hexagonal"]
1268
+ });
1269
+ }
1270
+ const isFeatureSliced = ctx.hasDirectory("src/features") || ctx.hasDirectory("src/modules");
1271
+ if (isFeatureSliced) {
1272
+ return createObservation({
1273
+ source: "filesystem:architecture",
1274
+ claim: "Project follows Feature-sliced or modular directory architecture",
1275
+ provenance: { type: "file", path: "src", timestamp: now },
1276
+ confidence: 0.85,
1277
+ positive: ["feature-sliced", "modular-architecture"]
1278
+ });
1279
+ }
1280
+ return null;
1281
+ }
1282
+ }
1283
+ ];
1284
+ var defaultRegistry = new ConventionRegistry();
1285
+ for (const detector of BUILTIN_DETECTORS) {
1286
+ defaultRegistry.register(detector);
1287
+ }
1288
+ function registerConventionDetector(detector) {
1289
+ defaultRegistry.register(detector);
1290
+ }
1291
+ async function analyzeProjectStructure(files, rootDir, registry = defaultRegistry) {
1292
+ const ctx = new ProjectContext(files, rootDir);
1293
+ return registry.run(ctx);
1294
+ }
1295
+
1296
+ // src/scanner/markdown.ts
1297
+ import * as fs2 from "fs/promises";
1298
+ import * as path2 from "path";
1299
+ import YAML from "yaml";
1300
+ function parseMarkdownDocument(content, filePath, relativePath) {
1301
+ const lines = content.split("\n");
1302
+ const baseName = path2.basename(relativePath, ".md").toLowerCase();
1303
+ let frontmatter = {};
1304
+ const headings = [];
1305
+ const alerts = [];
1306
+ const codeBlocks = [];
1307
+ const tables = [];
1308
+ const taskItems = [];
1309
+ const links = [];
1310
+ const prohibitions = [];
1311
+ let currentHeadingText = "";
1312
+ let inCodeBlock = false;
1313
+ let codeBlockLang = "";
1314
+ let codeBlockLines = [];
1315
+ let codeBlockStart = 0;
1316
+ let inTable = false;
1317
+ let tableHeaders = [];
1318
+ let tableRows = [];
1319
+ let tableStartLine = 0;
1320
+ let inFrontmatter = false;
1321
+ let frontmatterLines = [];
1322
+ let bodyStartIndex = 0;
1323
+ if (lines.length > 0 && lines[0].trim() === "---") {
1324
+ inFrontmatter = true;
1325
+ for (let i = 1; i < lines.length; i++) {
1326
+ if (lines[i].trim() === "---") {
1327
+ inFrontmatter = false;
1328
+ bodyStartIndex = i + 1;
1329
+ try {
1330
+ const yamlText = frontmatterLines.join("\n");
1331
+ const parsed = YAML.parse(yamlText);
1332
+ if (parsed && typeof parsed === "object") {
1333
+ frontmatter = parsed;
1334
+ }
1335
+ } catch {
1336
+ }
1337
+ break;
1338
+ }
1339
+ frontmatterLines.push(lines[i]);
1340
+ }
1341
+ }
1342
+ function commitTable() {
1343
+ if (inTable && tableHeaders.length > 0) {
1344
+ tables.push({
1345
+ headers: tableHeaders,
1346
+ rows: tableRows,
1347
+ line: tableStartLine,
1348
+ heading: currentHeadingText || void 0
1349
+ });
1350
+ inTable = false;
1351
+ tableHeaders = [];
1352
+ tableRows = [];
1353
+ }
1354
+ }
1355
+ for (let i = bodyStartIndex; i < lines.length; i++) {
1356
+ const rawLine = lines[i];
1357
+ const line = rawLine.trim();
1358
+ const codeMatch = line.match(/^```([a-zA-Z0-9_-]*)/);
1359
+ if (codeMatch) {
1360
+ if (!inCodeBlock) {
1361
+ commitTable();
1362
+ inCodeBlock = true;
1363
+ codeBlockLang = codeMatch[1] || "";
1364
+ codeBlockLines = [];
1365
+ codeBlockStart = i + 1;
1366
+ } else {
1367
+ inCodeBlock = false;
1368
+ codeBlocks.push({
1369
+ language: codeBlockLang,
1370
+ code: codeBlockLines.join("\n"),
1371
+ startLine: codeBlockStart,
1372
+ endLine: i + 1,
1373
+ heading: currentHeadingText || void 0
1374
+ });
1375
+ }
1376
+ continue;
1377
+ }
1378
+ if (inCodeBlock) {
1379
+ codeBlockLines.push(rawLine);
1380
+ continue;
1381
+ }
1382
+ if (line.startsWith("|") && line.endsWith("|")) {
1383
+ const cells = line.split("|").slice(1, -1).map((c) => c.trim());
1384
+ const isDelimiter = cells.every((c) => /^:?-+:?$/.test(c));
1385
+ if (!inTable) {
1386
+ commitTable();
1387
+ inTable = true;
1388
+ tableHeaders = cells;
1389
+ tableRows = [];
1390
+ tableStartLine = i + 1;
1391
+ } else if (!isDelimiter) {
1392
+ tableRows.push(cells);
1393
+ }
1394
+ continue;
1395
+ } else {
1396
+ commitTable();
1397
+ }
1398
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
1399
+ if (headingMatch) {
1400
+ const level = headingMatch[1].length;
1401
+ currentHeadingText = headingMatch[2].trim();
1402
+ headings.push({
1403
+ level,
1404
+ text: currentHeadingText,
1405
+ line: i + 1
1406
+ });
1407
+ continue;
1408
+ }
1409
+ const alertMatch = line.match(/^>\s*\[!(WARNING|CAUTION|IMPORTANT|NOTE|TIP)\]/i);
1410
+ if (alertMatch) {
1411
+ const alertStartLine = i + 1;
1412
+ const alertType = alertMatch[1].toUpperCase();
1413
+ const alertLines = [];
1414
+ let j = i + 1;
1415
+ while (j < lines.length && lines[j].trim().startsWith(">")) {
1416
+ alertLines.push(lines[j].trim().replace(/^>\s*/, ""));
1417
+ j++;
1418
+ }
1419
+ i = j - 1;
1420
+ alerts.push({
1421
+ type: alertType,
1422
+ body: alertLines.join(" ").trim(),
1423
+ line: alertStartLine,
1424
+ heading: currentHeadingText || void 0
1425
+ });
1426
+ continue;
1427
+ }
1428
+ const taskMatch = line.match(/^[-*]\s*\[([ xX])\]\s*(.+)$/);
1429
+ if (taskMatch) {
1430
+ const completed = taskMatch[1].toLowerCase() === "x";
1431
+ const text = taskMatch[2].trim();
1432
+ taskItems.push({
1433
+ completed,
1434
+ text,
1435
+ line: i + 1,
1436
+ heading: currentHeadingText || void 0
1437
+ });
1438
+ continue;
1439
+ }
1440
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
1441
+ let match;
1442
+ while ((match = linkRegex.exec(line)) !== null) {
1443
+ const linkText = match[1].trim();
1444
+ const linkTarget = match[2].trim();
1445
+ if (!linkTarget.startsWith("#")) {
1446
+ links.push({
1447
+ text: linkText,
1448
+ target: linkTarget,
1449
+ line: i + 1
1450
+ });
1451
+ }
1452
+ }
1453
+ const prohibMatch = line.match(/\b(Never|Must not|Do not|Cannot|Avoid)\s+([^.!?]+[.!?]?)/i);
1454
+ if (prohibMatch && !line.startsWith(">")) {
1455
+ prohibitions.push({
1456
+ rule: `${prohibMatch[1]} ${prohibMatch[2].trim()}`,
1457
+ line: i + 1,
1458
+ heading: currentHeadingText || void 0
1459
+ });
1460
+ }
1461
+ }
1462
+ commitTable();
1463
+ const sections = [];
1464
+ for (let idx = 0; idx < headings.length; idx++) {
1465
+ const h = headings[idx];
1466
+ const nextH = headings[idx + 1];
1467
+ const startLine = h.line;
1468
+ const endLine = nextH ? nextH.line - 1 : lines.length;
1469
+ const secLines = lines.slice(startLine - 1, endLine);
1470
+ sections.push({
1471
+ heading: h,
1472
+ content: secLines.join("\n").trim(),
1473
+ lines: secLines,
1474
+ startLine,
1475
+ endLine
1476
+ });
1477
+ }
1478
+ return {
1479
+ filePath,
1480
+ relativePath,
1481
+ baseName,
1482
+ raw: content,
1483
+ lines,
1484
+ frontmatter,
1485
+ headings,
1486
+ sections,
1487
+ alerts,
1488
+ codeBlocks,
1489
+ tables,
1490
+ taskItems,
1491
+ links,
1492
+ prohibitions
1493
+ };
1494
+ }
1495
+ var BUILTIN_MARKDOWN_EXTRACTORS = [
1496
+ // 1. Frontmatter Extractor
1497
+ {
1498
+ id: "frontmatter",
1499
+ name: "YAML Frontmatter Extractor",
1500
+ extract(doc) {
1501
+ const observations = [];
1502
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1503
+ const fm = doc.frontmatter;
1504
+ for (const [key, val] of Object.entries(fm)) {
1505
+ if (!val || typeof val === "object" && !Array.isArray(val)) continue;
1506
+ const valStr = Array.isArray(val) ? val.join(", ") : String(val);
1507
+ const lowerKey = key.toLowerCase();
1508
+ observations.push(
1509
+ createObservation({
1510
+ source: `doc:${doc.relativePath}:frontmatter:${lowerKey}`,
1511
+ claim: `Document metadata (${lowerKey}): ${valStr}`,
1512
+ provenance: { type: "doc", path: doc.relativePath, line: 1, timestamp: now },
1513
+ confidence: 0.92,
1514
+ positive: [doc.baseName, lowerKey, ...Array.isArray(val) ? val.map(String) : []],
1515
+ details: { frontmatterKey: lowerKey, value: val }
1516
+ })
1517
+ );
1518
+ }
1519
+ return { observations };
1520
+ }
1521
+ },
1522
+ // 2. GitHub Alerts Extractor (> [!WARNING], etc.)
1523
+ {
1524
+ id: "github-alerts",
1525
+ name: "GitHub Callouts & Alerts Extractor",
1526
+ extract(doc) {
1527
+ const observations = [];
1528
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1529
+ for (const alert of doc.alerts) {
1530
+ if (!alert.body) continue;
1531
+ const isCritical = alert.type === "WARNING" || alert.type === "CAUTION";
1532
+ observations.push(
1533
+ createObservation({
1534
+ source: `doc:${doc.relativePath}#L${alert.line}`,
1535
+ claim: `[${alert.type}] ${alert.body}`,
1536
+ provenance: { type: "doc", path: doc.relativePath, line: alert.line, timestamp: now },
1537
+ confidence: isCritical ? 0.96 : 0.88,
1538
+ positive: [doc.baseName, alert.type.toLowerCase(), ...alert.heading ? [alert.heading.toLowerCase()] : []],
1539
+ details: { alertType: alert.type, heading: alert.heading, line: alert.line }
1540
+ })
1541
+ );
1542
+ }
1543
+ return { observations };
1544
+ }
1545
+ },
1546
+ // 3. ADR & Architectural Decisions Extractor
1547
+ {
1548
+ id: "adr-decisions",
1549
+ name: "Architecture Decision Record (ADR/MADR) Extractor",
1550
+ extract(doc) {
1551
+ const observations = [];
1552
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1553
+ let hasContext = false;
1554
+ let decision = "";
1555
+ let status = "";
1556
+ let adrLine = 1;
1557
+ for (const sec of doc.sections) {
1558
+ const lowerHead = sec.heading.text.toLowerCase();
1559
+ if (lowerHead.includes("context")) {
1560
+ hasContext = true;
1561
+ }
1562
+ if (lowerHead.includes("decision") && !lowerHead.includes("options")) {
1563
+ for (const line of sec.lines) {
1564
+ const clean = line.replace(/^#{1,6}\s+/, "").trim();
1565
+ if (clean && clean.length > 5 && !clean.toLowerCase().includes("decision")) {
1566
+ decision = clean;
1567
+ adrLine = sec.heading.line;
1568
+ break;
1569
+ }
1570
+ }
1571
+ }
1572
+ if (lowerHead.includes("status")) {
1573
+ for (const line of sec.lines) {
1574
+ const clean = line.replace(/^#{1,6}\s+/, "").trim();
1575
+ if (clean && clean.length > 2 && !clean.toLowerCase().includes("status")) {
1576
+ status = clean;
1577
+ break;
1578
+ }
1579
+ }
1580
+ }
1581
+ }
1582
+ if (hasContext && decision) {
1583
+ observations.push(
1584
+ createObservation({
1585
+ source: `doc:${doc.relativePath}:adr#L${adrLine}`,
1586
+ claim: `Architectural Decision in ${doc.relativePath}: ${decision}`,
1587
+ provenance: { type: "doc", path: doc.relativePath, line: adrLine, timestamp: now },
1588
+ confidence: 0.95,
1589
+ positive: ["adr", "architecture", "decision", doc.baseName],
1590
+ details: { status: status || "Accepted", decision }
1591
+ })
1592
+ );
1593
+ }
1594
+ return { observations };
1595
+ }
1596
+ },
1597
+ // 4. Task Lists Extractor (- [ ] and - [x])
1598
+ {
1599
+ id: "task-items",
1600
+ name: "Task Items & TODOs Extractor",
1601
+ extract(doc) {
1602
+ const observations = [];
1603
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1604
+ for (const item of doc.taskItems) {
1605
+ const tag = item.completed ? "DONE" : "TODO";
1606
+ observations.push(
1607
+ createObservation({
1608
+ source: `doc:${doc.relativePath}#L${item.line}:todo`,
1609
+ claim: `[${tag}] ${item.text}`,
1610
+ provenance: { type: "doc", path: doc.relativePath, line: item.line, timestamp: now },
1611
+ confidence: item.completed ? 0.7 : 0.82,
1612
+ positive: ["todo", doc.baseName, ...item.heading ? [item.heading.toLowerCase()] : []],
1613
+ details: { completed: item.completed, heading: item.heading, line: item.line }
1614
+ })
1615
+ );
1616
+ }
1617
+ return { observations };
1618
+ }
1619
+ },
1620
+ // 5. Code Block Commands & Configurations
1621
+ {
1622
+ id: "code-blocks",
1623
+ name: "Code Block Commands Extractor",
1624
+ extract(doc) {
1625
+ const observations = [];
1626
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1627
+ for (const cb of doc.codeBlocks) {
1628
+ const lang = cb.language.toLowerCase();
1629
+ if (lang === "bash" || lang === "sh" || lang === "shell" || lang === "zsh") {
1630
+ const lines = cb.code.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
1631
+ for (const line of lines.slice(0, 3)) {
1632
+ if (/^(npm|pnpm|yarn|bun|cargo|go|docker|make|npx)\b/.test(line)) {
1633
+ observations.push(
1634
+ createObservation({
1635
+ source: `doc:${doc.relativePath}#L${cb.startLine}:cli`,
1636
+ claim: `Documented CLI command in ${doc.baseName}: ${line}`,
1637
+ provenance: { type: "doc", path: doc.relativePath, line: cb.startLine, timestamp: now },
1638
+ confidence: 0.88,
1639
+ positive: ["command", "cli", doc.baseName],
1640
+ details: { command: line, language: lang }
1641
+ })
1642
+ );
1643
+ }
1644
+ }
1645
+ }
1646
+ }
1647
+ return { observations };
1648
+ }
1649
+ },
1650
+ // 6. Markdown Tables (Configurations & Environment Variables)
1651
+ {
1652
+ id: "markdown-tables",
1653
+ name: "Markdown Table Key-Value Extractor",
1654
+ extract(doc) {
1655
+ const observations = [];
1656
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1657
+ for (const table of doc.tables) {
1658
+ if (table.headers.length < 2 || table.rows.length === 0) continue;
1659
+ const lowerHeaders = table.headers.map((h) => h.toLowerCase());
1660
+ const isConfigTable = lowerHeaders.some(
1661
+ (h) => h.includes("variable") || h.includes("env") || h.includes("setting") || h.includes("option") || h.includes("command") || h.includes("param") || h.includes("flag") || h.includes("property") || h.includes("key") || h.includes("config")
1662
+ );
1663
+ if (isConfigTable) {
1664
+ for (const row of table.rows.slice(0, 10)) {
1665
+ const key = row[0];
1666
+ const val = row[1];
1667
+ const desc = row[2] ? ` (${row[2]})` : "";
1668
+ if (key && val) {
1669
+ observations.push(
1670
+ createObservation({
1671
+ source: `doc:${doc.relativePath}#L${table.line}:table:${key}`,
1672
+ claim: `Configuration in ${doc.baseName}: ${key} = ${val}${desc}`,
1673
+ provenance: { type: "doc", path: doc.relativePath, line: table.line, timestamp: now },
1674
+ confidence: 0.88,
1675
+ positive: ["config", key.toLowerCase(), doc.baseName],
1676
+ details: { key, value: val, heading: table.heading }
1677
+ })
1678
+ );
1679
+ }
1680
+ }
1681
+ }
1682
+ }
1683
+ return { observations };
1684
+ }
1685
+ },
1686
+ // 7. Normative Rules & Prohibitions ("Never", "Do not", "Must not")
1687
+ {
1688
+ id: "rules-and-prohibitions",
1689
+ name: "Normative Rules & Prohibitions Extractor",
1690
+ extract(doc) {
1691
+ const observations = [];
1692
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1693
+ for (const p of doc.prohibitions) {
1694
+ observations.push(
1695
+ createObservation({
1696
+ source: `doc:${doc.relativePath}#L${p.line}:rule`,
1697
+ claim: `[RULE] in ${doc.baseName}: ${p.rule}`,
1698
+ provenance: { type: "doc", path: doc.relativePath, line: p.line, timestamp: now },
1699
+ confidence: 0.9,
1700
+ positive: ["rule", "constraint", doc.baseName],
1701
+ details: { rule: p.rule, heading: p.heading, line: p.line }
1702
+ })
1703
+ );
1704
+ }
1705
+ return { observations };
1706
+ }
1707
+ },
1708
+ // 8. Cross-Reference File Links
1709
+ {
1710
+ id: "file-links",
1711
+ name: "Cross-Reference File Links Extractor",
1712
+ extract(doc) {
1713
+ const relations = [];
1714
+ for (const link of doc.links) {
1715
+ const target = link.target;
1716
+ if (!target.startsWith("http://") && !target.startsWith("https://") && !target.startsWith("mailto:") && (target.startsWith(".") || target.includes("/") || /\.(md|ts|js|json|ya?ml|toml|rs|go|py)$/i.test(target))) {
1717
+ const rawTarget = target.split("#")[0].split("?")[0].trim();
1718
+ if (rawTarget) {
1719
+ const dir = path2.dirname(doc.relativePath);
1720
+ const resolved = dir === "." || path2.isAbsolute(rawTarget) ? rawTarget.replace(/^\.\//, "") : path2.normalize(path2.join(dir, rawTarget));
1721
+ relations.push({
1722
+ from: doc.relativePath,
1723
+ to: resolved,
1724
+ type: "DOCUMENTS"
1725
+ });
1726
+ }
1727
+ }
1728
+ }
1729
+ return { observations: [], relations };
1730
+ }
1731
+ }
1732
+ ];
1733
+ var MarkdownRegistry = class {
1734
+ extractors = /* @__PURE__ */ new Map();
1735
+ register(extractor) {
1736
+ this.extractors.set(extractor.id, extractor);
1737
+ return this;
1738
+ }
1739
+ unregister(id) {
1740
+ return this.extractors.delete(id);
1741
+ }
1742
+ get(id) {
1743
+ return this.extractors.get(id);
1744
+ }
1745
+ list() {
1746
+ return Array.from(this.extractors.values());
1747
+ }
1748
+ async run(doc) {
1749
+ const observations = [];
1750
+ const relations = [];
1751
+ for (const extractor of this.extractors.values()) {
1752
+ try {
1753
+ const result = await extractor.extract(doc);
1754
+ if (result.observations && result.observations.length > 0) {
1755
+ observations.push(...result.observations);
1756
+ }
1757
+ if (result.relations && result.relations.length > 0) {
1758
+ relations.push(...result.relations);
1759
+ }
1760
+ } catch {
1761
+ }
1762
+ }
1763
+ return { observations, relations };
1764
+ }
1765
+ };
1766
+ var defaultMarkdownRegistry = new MarkdownRegistry();
1767
+ for (const ext of BUILTIN_MARKDOWN_EXTRACTORS) {
1768
+ defaultMarkdownRegistry.register(ext);
1769
+ }
1770
+ function registerMarkdownExtractor(extractor) {
1771
+ defaultMarkdownRegistry.register(extractor);
1772
+ }
1773
+ async function parseMarkdownFile(filePath, relativePath, registry = defaultMarkdownRegistry) {
1774
+ let content = "";
1775
+ try {
1776
+ content = await fs2.readFile(filePath, "utf-8");
1777
+ } catch {
1778
+ return { observations: [], relations: [] };
1779
+ }
1780
+ const doc = parseMarkdownDocument(content, filePath, relativePath);
1781
+ return registry.run(doc);
1782
+ }
1783
+
1784
+ // src/scanner/git.ts
1785
+ import { execFile } from "child_process";
1786
+ import { promisify } from "util";
1787
+ var execFileAsync = promisify(execFile);
1788
+ async function runGit(args, cwd) {
1789
+ const { stdout } = await execFileAsync("git", args, {
1790
+ cwd,
1791
+ maxBuffer: 10 * 1024 * 1024
1792
+ });
1793
+ return stdout.trim();
1794
+ }
1795
+ async function isGitRepository(cwd) {
1796
+ try {
1797
+ const res = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
1798
+ return res === "true";
1799
+ } catch {
1800
+ return false;
1801
+ }
1802
+ }
1803
+ async function scanGitHistory(cwd, options = {}) {
1804
+ const isRepo = await isGitRepository(cwd);
1805
+ if (!isRepo) {
1806
+ return {
1807
+ isGitRepo: false,
1808
+ commitsScanned: 0,
1809
+ observations: [],
1810
+ relations: []
1811
+ };
1812
+ }
1813
+ let headCommit = "";
1814
+ try {
1815
+ headCommit = await runGit(["rev-parse", "HEAD"], cwd);
1816
+ } catch {
1817
+ return {
1818
+ isGitRepo: true,
1819
+ commitsScanned: 0,
1820
+ observations: [],
1821
+ relations: []
1822
+ };
1823
+ }
1824
+ let revisionRange = [];
1825
+ if (options.lastScanCommit && options.lastScanCommit !== headCommit) {
1826
+ try {
1827
+ await runGit(["cat-file", "-e", `${options.lastScanCommit}^{commit}`], cwd);
1828
+ revisionRange = [`${options.lastScanCommit}..HEAD`];
1829
+ } catch {
1830
+ revisionRange = ["-n", String(options.maxCommits ?? 100), "HEAD"];
1831
+ }
1832
+ } else {
1833
+ revisionRange = ["-n", String(options.maxCommits ?? 100), "HEAD"];
1834
+ }
1835
+ const logArgs = [
1836
+ "log",
1837
+ ...revisionRange,
1838
+ "--name-only",
1839
+ "--pretty=format:COMMIT:%H|%an|%aI|%s"
1840
+ ];
1841
+ let rawLog = "";
1842
+ try {
1843
+ rawLog = await runGit(logArgs, cwd);
1844
+ } catch {
1845
+ return {
1846
+ isGitRepo: true,
1847
+ headCommit,
1848
+ commitsScanned: 0,
1849
+ observations: [],
1850
+ relations: []
1851
+ };
1852
+ }
1853
+ const commits = [];
1854
+ const lines = rawLog.split("\n");
1855
+ let currentCommit = null;
1856
+ for (const line of lines) {
1857
+ const trimmed = line.trim();
1858
+ if (!trimmed) continue;
1859
+ if (trimmed.startsWith("COMMIT:")) {
1860
+ if (currentCommit) commits.push(currentCommit);
1861
+ const parts = trimmed.substring("COMMIT:".length).split("|");
1862
+ currentCommit = {
1863
+ hash: parts[0] || "",
1864
+ author: parts[1] || "",
1865
+ date: parts[2] || "",
1866
+ subject: parts.slice(3).join("|"),
1867
+ files: [],
1868
+ isMerge: false
1869
+ };
1870
+ } else if (currentCommit) {
1871
+ currentCommit.files.push(trimmed);
1872
+ }
1873
+ }
1874
+ if (currentCommit) commits.push(currentCommit);
1875
+ const observations = [];
1876
+ const relations = [];
1877
+ const coChangeMap = /* @__PURE__ */ new Map();
1878
+ const fileFixCount = /* @__PURE__ */ new Map();
1879
+ for (const commit of commits) {
1880
+ if (commit.files.length > 15) {
1881
+ commit.isMerge = true;
1882
+ continue;
1883
+ }
1884
+ const lowerSubject = commit.subject.toLowerCase();
1885
+ const isRevert = lowerSubject.startsWith("revert") || lowerSubject.includes("reverting");
1886
+ const isFix = lowerSubject.startsWith("fix") || lowerSubject.startsWith("hotfix") || lowerSubject.startsWith("bugfix") || lowerSubject.includes("fix:");
1887
+ if (isRevert && commit.files.length > 0) {
1888
+ observations.push(
1889
+ createObservation({
1890
+ source: `git:${commit.hash}`,
1891
+ claim: `Revert recorded in git: "${commit.subject}" affecting [${commit.files.slice(0, 5).join(", ")}]`,
1892
+ provenance: {
1893
+ type: "git",
1894
+ commit: commit.hash,
1895
+ author: commit.author,
1896
+ timestamp: commit.date
1897
+ },
1898
+ confidence: 0.95,
1899
+ positive: ["revert", ...commit.files.map((f) => f.split("/").pop()?.toLowerCase() || f)],
1900
+ negative: ["fragile-change"],
1901
+ details: { commit: commit.hash, files: commit.files }
1902
+ })
1903
+ );
1904
+ }
1905
+ if (isFix && commit.files.length > 1) {
1906
+ const testFiles = commit.files.filter((f) => f.includes("test") || f.includes("spec"));
1907
+ const srcFiles = commit.files.filter((f) => !f.includes("test") && !f.includes("spec"));
1908
+ if (testFiles.length > 0 && srcFiles.length > 0) {
1909
+ observations.push(
1910
+ createObservation({
1911
+ source: `git:${commit.hash}:bugfix-test`,
1912
+ claim: `Bugfix "${commit.subject}" included test verification in ${testFiles.join(", ")}`,
1913
+ provenance: {
1914
+ type: "git",
1915
+ commit: commit.hash,
1916
+ author: commit.author,
1917
+ timestamp: commit.date
1918
+ },
1919
+ confidence: 0.92,
1920
+ positive: ["bugfix", "test-added", ...srcFiles.map((f) => f.split("/").pop()?.toLowerCase() || f)],
1921
+ details: { srcFiles, testFiles }
1922
+ })
1923
+ );
1924
+ }
1925
+ }
1926
+ if (isFix) {
1927
+ for (const file of commit.files) {
1928
+ fileFixCount.set(file, (fileFixCount.get(file) ?? 0) + 1);
1929
+ }
1930
+ }
1931
+ if (commit.files.length >= 2 && commit.files.length <= 8) {
1932
+ for (let i = 0; i < commit.files.length; i++) {
1933
+ for (let j = i + 1; j < commit.files.length; j++) {
1934
+ const a = commit.files[i];
1935
+ const b = commit.files[j];
1936
+ const key = a < b ? `${a} <-> ${b}` : `${b} <-> ${a}`;
1937
+ coChangeMap.set(key, (coChangeMap.get(key) ?? 0) + 1);
1938
+ }
1939
+ }
1940
+ }
1941
+ }
1942
+ for (const [file, count] of fileFixCount.entries()) {
1943
+ if (count >= 3) {
1944
+ observations.push(
1945
+ createObservation({
1946
+ source: `git:hotspot:${file}`,
1947
+ claim: `Fragile hotspot: "${file}" required ${count} fixes in recent history`,
1948
+ provenance: { type: "git", path: file, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
1949
+ confidence: 0.88,
1950
+ positive: ["hotspot", "fragile", file.split("/").pop()?.toLowerCase() || file],
1951
+ details: { fixCount: count }
1952
+ })
1953
+ );
1954
+ }
1955
+ }
1956
+ for (const [key, count] of coChangeMap.entries()) {
1957
+ if (count >= 2) {
1958
+ const [from, to] = key.split(" <-> ");
1959
+ relations.push({
1960
+ from,
1961
+ to,
1962
+ type: "CO_CHANGED",
1963
+ weight: count
1964
+ });
1965
+ relations.push({
1966
+ from: to,
1967
+ to: from,
1968
+ type: "CO_CHANGED",
1969
+ weight: count
1970
+ });
1971
+ }
1972
+ }
1973
+ return {
1974
+ isGitRepo: true,
1975
+ headCommit,
1976
+ commitsScanned: commits.length,
1977
+ observations,
1978
+ relations
1979
+ };
1980
+ }
1981
+
1982
+ // src/scanner/source.ts
1983
+ import * as fs3 from "fs/promises";
1984
+ import * as fsSync from "fs";
1985
+ import * as path3 from "path";
1986
+ import { createRequire } from "module";
1987
+ import Parser from "web-tree-sitter";
1988
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
1989
+ ".ts",
1990
+ ".tsx",
1991
+ ".js",
1992
+ ".jsx",
1993
+ ".mjs",
1994
+ ".cjs",
1995
+ ".py",
1996
+ ".go",
1997
+ ".rs",
1998
+ ".java",
1999
+ ".c",
2000
+ ".cpp",
2001
+ ".h",
2002
+ ".hpp",
2003
+ ".rb",
2004
+ ".php",
2005
+ ".cs",
2006
+ ".swift",
2007
+ ".kt",
2008
+ ".scala",
2009
+ ".sh",
2010
+ ".bash",
2011
+ ".zsh",
2012
+ ".sql"
2013
+ ]);
2014
+ function isSourceFile(ext) {
2015
+ return SOURCE_EXTENSIONS.has(ext.toLowerCase());
2016
+ }
2017
+ var EXT_TO_WASM = {
2018
+ ".ts": "tree-sitter-typescript.wasm",
2019
+ ".mts": "tree-sitter-typescript.wasm",
2020
+ ".cts": "tree-sitter-typescript.wasm",
2021
+ ".tsx": "tree-sitter-tsx.wasm",
2022
+ ".js": "tree-sitter-javascript.wasm",
2023
+ ".mjs": "tree-sitter-javascript.wasm",
2024
+ ".cjs": "tree-sitter-javascript.wasm",
2025
+ ".jsx": "tree-sitter-javascript.wasm",
2026
+ ".py": "tree-sitter-python.wasm",
2027
+ ".go": "tree-sitter-go.wasm",
2028
+ ".rs": "tree-sitter-rust.wasm"
2029
+ };
2030
+ var wasmBaseDir = null;
2031
+ var parserInitialized = false;
2032
+ var parserInstance = null;
2033
+ var languageCache = /* @__PURE__ */ new Map();
2034
+ function getWasmDir() {
2035
+ if (wasmBaseDir) return wasmBaseDir;
2036
+ try {
2037
+ const req = createRequire(import.meta.url);
2038
+ const pkgJson = req.resolve("tree-sitter-wasms/package.json");
2039
+ const outDir = path3.join(path3.dirname(pkgJson), "out");
2040
+ if (fsSync.existsSync(outDir)) {
2041
+ wasmBaseDir = outDir;
2042
+ return wasmBaseDir;
2043
+ }
2044
+ } catch {
2045
+ }
2046
+ const candidateDirs = [
2047
+ path3.resolve(process.cwd(), "node_modules/tree-sitter-wasms/out"),
2048
+ path3.resolve(process.cwd(), "../node_modules/tree-sitter-wasms/out")
2049
+ ];
2050
+ for (const dir of candidateDirs) {
2051
+ if (fsSync.existsSync(dir)) {
2052
+ wasmBaseDir = dir;
2053
+ return wasmBaseDir;
2054
+ }
2055
+ }
2056
+ return null;
2057
+ }
2058
+ async function getParserForWasm(wasmName) {
2059
+ const wasmDir = getWasmDir();
2060
+ if (!wasmDir) return null;
2061
+ const wasmPath = path3.join(wasmDir, wasmName);
2062
+ if (!fsSync.existsSync(wasmPath)) return null;
2063
+ try {
2064
+ if (!parserInitialized) {
2065
+ await Parser.init();
2066
+ parserInitialized = true;
2067
+ }
2068
+ if (!parserInstance) {
2069
+ parserInstance = new Parser();
2070
+ }
2071
+ let lang = languageCache.get(wasmName);
2072
+ if (!lang) {
2073
+ lang = await Parser.Language.load(wasmPath);
2074
+ languageCache.set(wasmName, lang);
2075
+ }
2076
+ parserInstance.setLanguage(lang);
2077
+ return parserInstance;
2078
+ } catch {
2079
+ return null;
2080
+ }
2081
+ }
2082
+ function cleanQuote(str) {
2083
+ return str.replace(/^['"`]|['"`]$/g, "").trim();
2084
+ }
2085
+ function getEnclosingScope(node) {
2086
+ let curr = node.parent;
2087
+ const parts = [];
2088
+ while (curr) {
2089
+ if (curr.type === "class_declaration" || curr.type === "class_definition" || curr.type === "function_declaration" || curr.type === "function_definition" || curr.type === "method_definition" || curr.type === "interface_declaration" || curr.type === "struct_item" || curr.type === "impl_item") {
2090
+ const name = curr.childForFieldName("name")?.text;
2091
+ if (name) parts.unshift(name);
2092
+ }
2093
+ curr = curr.parent;
2094
+ }
2095
+ return parts.length > 0 ? parts.join(".") : void 0;
2096
+ }
2097
+ function extractCommentObservation(node, relativePath, baseName, now) {
2098
+ const text = node.text;
2099
+ const match = text.match(/\b(TODO|FIXME|HACK|DEPRECATED|WARN|NOTE|BUG)\b:?\s*(.*)$/i);
2100
+ if (!match) return null;
2101
+ const tag = match[1].toUpperCase();
2102
+ const commentText = match[2].trim() || "Unspecified";
2103
+ const scope = getEnclosingScope(node);
2104
+ const line = node.startPosition.row + 1;
2105
+ const scopePrefix = scope ? ` in ${scope}` : "";
2106
+ const positive = [tag.toLowerCase(), baseName.toLowerCase()];
2107
+ if (scope) {
2108
+ for (const part of scope.split(".")) {
2109
+ if (part) positive.push(part.toLowerCase());
2110
+ }
2111
+ }
2112
+ const confidence = tag === "DEPRECATED" || tag === "FIXME" || tag === "BUG" ? 0.9 : 0.8;
2113
+ return createObservation({
2114
+ source: `code:${relativePath}#L${line}`,
2115
+ claim: `[${tag}${scopePrefix}] in ${baseName}: ${commentText}`,
2116
+ provenance: { type: "code", path: relativePath, line, timestamp: now },
2117
+ confidence,
2118
+ positive,
2119
+ details: { tag, text: commentText, scope, line }
2120
+ });
2121
+ }
2122
+ function extractFromTsJs(root, relativePath, baseName, now, observations, relations) {
2123
+ function walk(node) {
2124
+ if (node.type === "import_statement") {
2125
+ const source = node.childForFieldName("source");
2126
+ if (source) {
2127
+ const target = cleanQuote(source.text);
2128
+ relations.push({ from: relativePath, to: target, type: "IMPORTS" });
2129
+ }
2130
+ }
2131
+ if (node.type === "call_expression") {
2132
+ const fn = node.childForFieldName("function");
2133
+ if (fn && (fn.text === "require" || fn.text === "import")) {
2134
+ const args = node.childForFieldName("arguments");
2135
+ if (args && args.namedChildCount > 0) {
2136
+ const firstArg = args.namedChild(0);
2137
+ if (firstArg && (firstArg.type === "string" || firstArg.type === "template_string")) {
2138
+ const target = cleanQuote(firstArg.text);
2139
+ if (target) {
2140
+ relations.push({ from: relativePath, to: target, type: "IMPORTS" });
2141
+ }
2142
+ }
2143
+ }
2144
+ }
2145
+ }
2146
+ if (node.type === "export_statement") {
2147
+ const source = node.childForFieldName("source");
2148
+ if (source) {
2149
+ const target = cleanQuote(source.text);
2150
+ relations.push({ from: relativePath, to: target, type: "IMPORTS" });
2151
+ }
2152
+ for (let i = 0; i < node.namedChildCount; i++) {
2153
+ const child = node.namedChild(i);
2154
+ if (!child) continue;
2155
+ if (child.type === "function_declaration" || child.type === "class_declaration" || child.type === "interface_declaration" || child.type === "type_alias_declaration" || child.type === "enum_declaration") {
2156
+ const name = child.childForFieldName("name")?.text;
2157
+ const kind = child.type.replace("_declaration", "");
2158
+ const line = child.startPosition.row + 1;
2159
+ if (name) {
2160
+ observations.push(
2161
+ createObservation({
2162
+ source: `code:${relativePath}#L${line}`,
2163
+ claim: `[export ${kind}] ${name} in ${baseName}`,
2164
+ provenance: { type: "code", path: relativePath, line, timestamp: now },
2165
+ confidence: 0.85,
2166
+ positive: [name.toLowerCase(), kind, baseName.toLowerCase()],
2167
+ details: { kind, name, exported: true, line }
2168
+ })
2169
+ );
2170
+ relations.push({
2171
+ from: relativePath,
2172
+ to: `${relativePath}#${name}`,
2173
+ type: "DEFINES"
2174
+ });
2175
+ }
2176
+ } else if (child.type === "lexical_declaration") {
2177
+ for (let j = 0; j < child.namedChildCount; j++) {
2178
+ const decl = child.namedChild(j);
2179
+ if (decl && decl.type === "variable_declarator") {
2180
+ const name = decl.childForFieldName("name")?.text;
2181
+ const line = decl.startPosition.row + 1;
2182
+ if (name) {
2183
+ observations.push(
2184
+ createObservation({
2185
+ source: `code:${relativePath}#L${line}`,
2186
+ claim: `[export const] ${name} in ${baseName}`,
2187
+ provenance: { type: "code", path: relativePath, line, timestamp: now },
2188
+ confidence: 0.8,
2189
+ positive: [name.toLowerCase(), "const", baseName.toLowerCase()],
2190
+ details: { kind: "const", name, exported: true, line }
2191
+ })
2192
+ );
2193
+ relations.push({
2194
+ from: relativePath,
2195
+ to: `${relativePath}#${name}`,
2196
+ type: "DEFINES"
2197
+ });
2198
+ }
2199
+ }
2200
+ }
2201
+ }
2202
+ }
2203
+ }
2204
+ if (node.type === "comment") {
2205
+ const obs = extractCommentObservation(node, relativePath, baseName, now);
2206
+ if (obs) observations.push(obs);
2207
+ }
2208
+ for (let i = 0; i < node.childCount; i++) {
2209
+ const ch = node.child(i);
2210
+ if (ch) walk(ch);
2211
+ }
2212
+ }
2213
+ walk(root);
2214
+ }
2215
+ function extractFromPython(root, relativePath, baseName, now, observations, relations) {
2216
+ function walk(node) {
2217
+ if (node.type === "import_statement") {
2218
+ for (let i = 0; i < node.namedChildCount; i++) {
2219
+ const child = node.namedChild(i);
2220
+ if (child && (child.type === "dotted_name" || child.type === "aliased_import")) {
2221
+ const target = child.type === "aliased_import" ? child.childForFieldName("name")?.text : child.text;
2222
+ if (target) {
2223
+ relations.push({ from: relativePath, to: target, type: "IMPORTS" });
2224
+ }
2225
+ }
2226
+ }
2227
+ }
2228
+ if (node.type === "import_from_statement") {
2229
+ const moduleName = node.childForFieldName("module_name")?.text;
2230
+ if (moduleName) {
2231
+ relations.push({ from: relativePath, to: moduleName, type: "IMPORTS" });
2232
+ }
2233
+ }
2234
+ if (node.type === "class_definition" || node.type === "function_definition") {
2235
+ const name = node.childForFieldName("name")?.text;
2236
+ const kind = node.type === "class_definition" ? "class" : "def";
2237
+ const line = node.startPosition.row + 1;
2238
+ if (name && (node.parent?.type === "module" || node.type === "class_definition")) {
2239
+ observations.push(
2240
+ createObservation({
2241
+ source: `code:${relativePath}#L${line}`,
2242
+ claim: `[${kind}] ${name} in ${baseName}`,
2243
+ provenance: { type: "code", path: relativePath, line, timestamp: now },
2244
+ confidence: 0.85,
2245
+ positive: [name.toLowerCase(), kind, baseName.toLowerCase()],
2246
+ details: { kind, name, line }
2247
+ })
2248
+ );
2249
+ relations.push({
2250
+ from: relativePath,
2251
+ to: `${relativePath}#${name}`,
2252
+ type: "DEFINES"
2253
+ });
2254
+ }
2255
+ }
2256
+ if (node.type === "comment") {
2257
+ const obs = extractCommentObservation(node, relativePath, baseName, now);
2258
+ if (obs) observations.push(obs);
2259
+ }
2260
+ for (let i = 0; i < node.childCount; i++) {
2261
+ const ch = node.child(i);
2262
+ if (ch) walk(ch);
2263
+ }
2264
+ }
2265
+ walk(root);
2266
+ }
2267
+ function extractFromGo(root, relativePath, baseName, now, observations, relations) {
2268
+ function walk(node) {
2269
+ if (node.type === "import_spec") {
2270
+ const pathNode = node.childForFieldName("path");
2271
+ if (pathNode) {
2272
+ const target = cleanQuote(pathNode.text);
2273
+ relations.push({ from: relativePath, to: target, type: "IMPORTS" });
2274
+ }
2275
+ }
2276
+ if (node.type === "function_declaration" || node.type === "method_declaration") {
2277
+ const name = node.childForFieldName("name")?.text;
2278
+ const line = node.startPosition.row + 1;
2279
+ if (name) {
2280
+ observations.push(
2281
+ createObservation({
2282
+ source: `code:${relativePath}#L${line}`,
2283
+ claim: `[func] ${name} in ${baseName}`,
2284
+ provenance: { type: "code", path: relativePath, line, timestamp: now },
2285
+ confidence: 0.85,
2286
+ positive: [name.toLowerCase(), "func", baseName.toLowerCase()],
2287
+ details: { kind: "func", name, line }
2288
+ })
2289
+ );
2290
+ relations.push({
2291
+ from: relativePath,
2292
+ to: `${relativePath}#${name}`,
2293
+ type: "DEFINES"
2294
+ });
2295
+ }
2296
+ }
2297
+ if (node.type === "type_spec") {
2298
+ const name = node.childForFieldName("name")?.text;
2299
+ const line = node.startPosition.row + 1;
2300
+ if (name) {
2301
+ observations.push(
2302
+ createObservation({
2303
+ source: `code:${relativePath}#L${line}`,
2304
+ claim: `[type] ${name} in ${baseName}`,
2305
+ provenance: { type: "code", path: relativePath, line, timestamp: now },
2306
+ confidence: 0.85,
2307
+ positive: [name.toLowerCase(), "type", baseName.toLowerCase()],
2308
+ details: { kind: "type", name, line }
2309
+ })
2310
+ );
2311
+ relations.push({
2312
+ from: relativePath,
2313
+ to: `${relativePath}#${name}`,
2314
+ type: "DEFINES"
2315
+ });
2316
+ }
2317
+ }
2318
+ if (node.type === "comment") {
2319
+ const obs = extractCommentObservation(node, relativePath, baseName, now);
2320
+ if (obs) observations.push(obs);
2321
+ }
2322
+ for (let i = 0; i < node.childCount; i++) {
2323
+ const ch = node.child(i);
2324
+ if (ch) walk(ch);
2325
+ }
2326
+ }
2327
+ walk(root);
2328
+ }
2329
+ function extractFromRust(root, relativePath, baseName, now, observations, relations) {
2330
+ function walk(node) {
2331
+ if (node.type === "use_declaration") {
2332
+ const text = node.text.replace(/^use\s+/, "").replace(/;$/, "").trim();
2333
+ const rootCrate = text.split("::")[0];
2334
+ if (rootCrate) {
2335
+ relations.push({ from: relativePath, to: rootCrate, type: "IMPORTS" });
2336
+ }
2337
+ }
2338
+ if (node.type === "function_item" || node.type === "struct_item" || node.type === "enum_item" || node.type === "trait_item") {
2339
+ const name = node.childForFieldName("name")?.text;
2340
+ const kind = node.type.replace("_item", "");
2341
+ const line = node.startPosition.row + 1;
2342
+ if (name) {
2343
+ observations.push(
2344
+ createObservation({
2345
+ source: `code:${relativePath}#L${line}`,
2346
+ claim: `[${kind}] ${name} in ${baseName}`,
2347
+ provenance: { type: "code", path: relativePath, line, timestamp: now },
2348
+ confidence: 0.85,
2349
+ positive: [name.toLowerCase(), kind, baseName.toLowerCase()],
2350
+ details: { kind, name, line }
2351
+ })
2352
+ );
2353
+ relations.push({
2354
+ from: relativePath,
2355
+ to: `${relativePath}#${name}`,
2356
+ type: "DEFINES"
2357
+ });
2358
+ }
2359
+ }
2360
+ if (node.type === "line_comment" || node.type === "block_comment") {
2361
+ const obs = extractCommentObservation(node, relativePath, baseName, now);
2362
+ if (obs) observations.push(obs);
2363
+ }
2364
+ for (let i = 0; i < node.childCount; i++) {
2365
+ const ch = node.child(i);
2366
+ if (ch) walk(ch);
2367
+ }
2368
+ }
2369
+ walk(root);
2370
+ }
2371
+ function analyzeSourceLexical(content, relativePath, baseName, now) {
2372
+ const observations = [];
2373
+ const relations = [];
2374
+ const lines = content.split("\n");
2375
+ for (let i = 0; i < lines.length; i++) {
2376
+ const line = lines[i].trim();
2377
+ if (!line) continue;
2378
+ const jsImportMatch = line.match(/(?:import\s+.*?\s+from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/);
2379
+ if (jsImportMatch) {
2380
+ const target = jsImportMatch[1] || jsImportMatch[2];
2381
+ relations.push({ from: relativePath, to: target, type: "IMPORTS" });
2382
+ }
2383
+ const pyImportMatch = line.match(/^(?:from\s+([a-zA-Z0-9_.]+)\s+import|import\s+([a-zA-Z0-9_.]+))/);
2384
+ if (pyImportMatch && relativePath.endsWith(".py")) {
2385
+ const target = pyImportMatch[1] || pyImportMatch[2];
2386
+ relations.push({ from: relativePath, to: target, type: "IMPORTS" });
2387
+ }
2388
+ const rustUseMatch = line.match(/^use\s+([a-zA-Z0-9_]+)::/);
2389
+ if (rustUseMatch && relativePath.endsWith(".rs")) {
2390
+ relations.push({ from: relativePath, to: rustUseMatch[1], type: "IMPORTS" });
2391
+ }
2392
+ const goImportMatch = line.match(/^import\s+["']([^"']+)["']/);
2393
+ if (goImportMatch && relativePath.endsWith(".go")) {
2394
+ relations.push({ from: relativePath, to: goImportMatch[1], type: "IMPORTS" });
2395
+ }
2396
+ const commentMatch = line.match(/\b(TODO|FIXME|HACK|DEPRECATED|WARN|NOTE|BUG)\b:?\s*(.*)$/i);
2397
+ if (commentMatch) {
2398
+ const tag = commentMatch[1].toUpperCase();
2399
+ const text = commentMatch[2].trim() || "Unspecified";
2400
+ const confidence = tag === "DEPRECATED" || tag === "FIXME" || tag === "BUG" ? 0.9 : 0.8;
2401
+ observations.push(
2402
+ createObservation({
2403
+ source: `code:${relativePath}#L${i + 1}`,
2404
+ claim: `[${tag}] in ${baseName}: ${text}`,
2405
+ provenance: { type: "code", path: relativePath, line: i + 1, timestamp: now },
2406
+ confidence,
2407
+ positive: [tag.toLowerCase(), baseName.toLowerCase()],
2408
+ details: { tag, text, line: i + 1 }
2409
+ })
2410
+ );
2411
+ }
2412
+ }
2413
+ return { observations, relations };
2414
+ }
2415
+ async function analyzeSourceFile(filePath, relativePath) {
2416
+ const observations = [];
2417
+ const relations = [];
2418
+ let content = "";
2419
+ try {
2420
+ content = await fs3.readFile(filePath, "utf-8");
2421
+ } catch {
2422
+ return { observations, relations };
2423
+ }
2424
+ if (content.length > 500 * 1024) {
2425
+ return { observations, relations };
2426
+ }
2427
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2428
+ const baseName = path3.basename(relativePath);
2429
+ const ext = path3.extname(relativePath).toLowerCase();
2430
+ const wasmName = EXT_TO_WASM[ext];
2431
+ if (wasmName) {
2432
+ try {
2433
+ const parser = await getParserForWasm(wasmName);
2434
+ if (parser) {
2435
+ const tree = parser.parse(content);
2436
+ const root = tree.rootNode;
2437
+ if (ext === ".ts" || ext === ".mts" || ext === ".cts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") {
2438
+ extractFromTsJs(root, relativePath, baseName, now, observations, relations);
2439
+ } else if (ext === ".py") {
2440
+ extractFromPython(root, relativePath, baseName, now, observations, relations);
2441
+ } else if (ext === ".go") {
2442
+ extractFromGo(root, relativePath, baseName, now, observations, relations);
2443
+ } else if (ext === ".rs") {
2444
+ extractFromRust(root, relativePath, baseName, now, observations, relations);
2445
+ }
2446
+ return { observations, relations };
2447
+ }
2448
+ } catch {
2449
+ }
2450
+ }
2451
+ return analyzeSourceLexical(content, relativePath, baseName, now);
2452
+ }
2453
+
2454
+ // src/scanner/engine.ts
2455
+ import * as path7 from "path";
2456
+
2457
+ // src/core/unknown.ts
2458
+ function createUnknown(params) {
2459
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2460
+ return {
2461
+ id: params.id.trim(),
2462
+ state: params.state ?? "UNKNOWN",
2463
+ reason: params.reason ?? "NOT_OBSERVED",
2464
+ statement: params.statement?.trim(),
2465
+ checked: Array.from(new Set(params.checked?.map((c) => c.trim()) ?? [])),
2466
+ next: params.next,
2467
+ scope: params.scope ?? "project",
2468
+ createdAt: now,
2469
+ updatedAt: now
2470
+ };
2471
+ }
2472
+
2473
+ // src/core/relations.ts
2474
+ var EvidenceGraph = class _EvidenceGraph {
2475
+ fromMap = /* @__PURE__ */ new Map();
2476
+ toMap = /* @__PURE__ */ new Map();
2477
+ constructor(relations = []) {
2478
+ for (const rel of relations) {
2479
+ this.addRelation(rel);
2480
+ }
2481
+ }
2482
+ addRelation(relation) {
2483
+ if (!relation || !relation.from || !relation.to || !relation.type) {
2484
+ return;
2485
+ }
2486
+ const existing = this.getRelationsBetween(relation.from, relation.to);
2487
+ const dup = existing.find((r) => r.type === relation.type);
2488
+ if (dup) {
2489
+ dup.weight = (dup.weight ?? 1) + (relation.weight ?? 1);
2490
+ return;
2491
+ }
2492
+ const fromList = this.fromMap.get(relation.from) ?? [];
2493
+ fromList.push(relation);
2494
+ this.fromMap.set(relation.from, fromList);
2495
+ const toList = this.toMap.get(relation.to) ?? [];
2496
+ toList.push(relation);
2497
+ this.toMap.set(relation.to, toList);
2498
+ }
2499
+ getRelationsFrom(fromId) {
2500
+ return this.fromMap.get(fromId) ?? [];
2501
+ }
2502
+ getRelationsTo(toId) {
2503
+ return this.toMap.get(toId) ?? [];
2504
+ }
2505
+ getRelationsBetween(fromId, toId) {
2506
+ const fromList = this.fromMap.get(fromId);
2507
+ if (!fromList) return [];
2508
+ return fromList.filter((r) => r.to === toId);
2509
+ }
2510
+ getAllRelations() {
2511
+ const all = [];
2512
+ for (const list of this.fromMap.values()) {
2513
+ all.push(...list);
2514
+ }
2515
+ return all;
2516
+ }
2517
+ findCoChanged(file, threshold = 1) {
2518
+ const relations = this.getRelationsFrom(file).filter((r) => r.type === "CO_CHANGED");
2519
+ const result = [];
2520
+ for (const rel of relations) {
2521
+ const w = rel.weight ?? 1;
2522
+ if (w >= threshold) {
2523
+ result.push({ file: rel.to, weight: w });
2524
+ }
2525
+ }
2526
+ return result.sort((a, b) => b.weight - a.weight);
2527
+ }
2528
+ findConnected(id, maxDepth = 2) {
2529
+ const visited = /* @__PURE__ */ new Set();
2530
+ const queue = [{ node: id, depth: 0 }];
2531
+ while (queue.length > 0) {
2532
+ const { node, depth } = queue.shift();
2533
+ if (visited.has(node)) continue;
2534
+ visited.add(node);
2535
+ if (depth < maxDepth) {
2536
+ const outRels = this.getRelationsFrom(node);
2537
+ for (const r of outRels) {
2538
+ if (!visited.has(r.to)) {
2539
+ queue.push({ node: r.to, depth: depth + 1 });
2540
+ }
2541
+ }
2542
+ const inRels = this.getRelationsTo(node);
2543
+ for (const r of inRels) {
2544
+ if (!visited.has(r.from)) {
2545
+ queue.push({ node: r.from, depth: depth + 1 });
2546
+ }
2547
+ }
2548
+ }
2549
+ }
2550
+ visited.delete(id);
2551
+ return Array.from(visited);
2552
+ }
2553
+ toJSON() {
2554
+ return this.getAllRelations();
2555
+ }
2556
+ static fromJSON(relations) {
2557
+ return new _EvidenceGraph(relations);
2558
+ }
2559
+ };
2560
+
2561
+ // src/storage/canonical.ts
2562
+ import * as fs6 from "fs/promises";
2563
+ import * as path6 from "path";
2564
+
2565
+ // src/rules/loader.ts
2566
+ import YAML2 from "yaml";
2567
+ import * as fs4 from "fs/promises";
2568
+ import * as path4 from "path";
2569
+ function validateRuleDefinition(rule, index = 0) {
2570
+ const errors = [];
2571
+ if (!rule || typeof rule !== "object") {
2572
+ errors.push(`Rule at index ${index} must be an object.`);
2573
+ return errors;
2574
+ }
2575
+ const r = rule;
2576
+ if (!r.id || typeof r.id !== "string" || !r.id.trim()) {
2577
+ errors.push(`Rule at index ${index} requires a non-empty string 'id'.`);
2578
+ }
2579
+ if (!r.statement || typeof r.statement !== "string" || !r.statement.trim()) {
2580
+ errors.push(`Rule '${r.id || index}' requires a non-empty string 'statement'.`);
2581
+ }
2582
+ if (r.type) {
2583
+ const validTypes = [
2584
+ "rule",
2585
+ "convention",
2586
+ "decision",
2587
+ "lesson",
2588
+ "architecture",
2589
+ "fact"
2590
+ ];
2591
+ if (!validTypes.includes(r.type)) {
2592
+ errors.push(`Rule '${r.id || index}' has invalid type '${r.type}'. Allowed: ${validTypes.join(", ")}.`);
2593
+ }
2594
+ }
2595
+ if (r.confidence !== void 0) {
2596
+ if (typeof r.confidence !== "number" || r.confidence < 0 || r.confidence > 1) {
2597
+ errors.push(`Rule '${r.id || index}' confidence must be a number between 0.0 and 1.0.`);
2598
+ }
2599
+ }
2600
+ if (r.positive !== void 0 && !Array.isArray(r.positive)) {
2601
+ errors.push(`Rule '${r.id || index}' positive cues must be an array of strings.`);
2602
+ }
2603
+ if (r.negative !== void 0 && !Array.isArray(r.negative)) {
2604
+ errors.push(`Rule '${r.id || index}' negative cues must be an array of strings.`);
2605
+ }
2606
+ return errors;
2607
+ }
2608
+ function validateRuleSet(data) {
2609
+ const errors = [];
2610
+ if (!data || typeof data !== "object") {
2611
+ return { valid: false, errors: ["RuleSet content must be an object or an array of rules."] };
2612
+ }
2613
+ let rules = [];
2614
+ if (Array.isArray(data)) {
2615
+ rules = data;
2616
+ } else if ("rules" in data && Array.isArray(data.rules)) {
2617
+ rules = data.rules;
2618
+ } else if ("id" in data && "statement" in data) {
2619
+ rules = [data];
2620
+ } else {
2621
+ return { valid: false, errors: ['RuleSet must contain a "rules" array or be a list of rule definitions.'] };
2622
+ }
2623
+ if (rules.length === 0) {
2624
+ errors.push("RuleSet contains no rules.");
2625
+ }
2626
+ rules.forEach((rule, idx) => {
2627
+ errors.push(...validateRuleDefinition(rule, idx));
2628
+ });
2629
+ return {
2630
+ valid: errors.length === 0,
2631
+ errors
2632
+ };
2633
+ }
2634
+ function parseRuleSetYaml(content, sourcePath = "inline.yml") {
2635
+ let parsed;
2636
+ try {
2637
+ parsed = YAML2.parse(content);
2638
+ } catch (err) {
2639
+ throw new Error(`YAML syntax error in ${sourcePath}: ${err.message}`);
2640
+ }
2641
+ const validation = validateRuleSet(parsed);
2642
+ if (!validation.valid) {
2643
+ throw new Error(`Invalid RuleSet in ${sourcePath}:
2644
+ - ${validation.errors.join("\n- ")}`);
2645
+ }
2646
+ if (Array.isArray(parsed)) {
2647
+ return {
2648
+ name: path4.basename(sourcePath, path4.extname(sourcePath)),
2649
+ rules: parsed
2650
+ };
2651
+ }
2652
+ if ("id" in parsed && "statement" in parsed && !("rules" in parsed)) {
2653
+ return {
2654
+ name: parsed.id,
2655
+ rules: [parsed]
2656
+ };
2657
+ }
2658
+ const set = parsed;
2659
+ return {
2660
+ name: set.name ?? path4.basename(sourcePath, path4.extname(sourcePath)),
2661
+ version: set.version ?? 1,
2662
+ scope: set.scope ?? "project",
2663
+ description: set.description,
2664
+ rules: set.rules
2665
+ };
2666
+ }
2667
+ function stringifyRuleSetYaml(ruleSet) {
2668
+ const payload = {};
2669
+ if (ruleSet.name) payload.name = ruleSet.name;
2670
+ if (ruleSet.version !== void 0) payload.version = ruleSet.version;
2671
+ if (ruleSet.scope) payload.scope = ruleSet.scope;
2672
+ if (ruleSet.description) payload.description = ruleSet.description;
2673
+ payload.rules = ruleSet.rules.map((r) => {
2674
+ const ruleObj = {
2675
+ id: r.id
2676
+ };
2677
+ if (r.name) ruleObj.name = r.name;
2678
+ ruleObj.statement = r.statement;
2679
+ if (r.type && r.type !== "rule") ruleObj.type = r.type;
2680
+ if (r.confidence !== void 0 && r.confidence !== 0.95) ruleObj.confidence = r.confidence;
2681
+ if (r.scope && r.scope !== "project") ruleObj.scope = r.scope;
2682
+ if (r.positive && r.positive.length > 0) ruleObj.positive = r.positive;
2683
+ if (r.negative && r.negative.length > 0) ruleObj.negative = r.negative;
2684
+ if (r.tags && r.tags.length > 0) ruleObj.tags = r.tags;
2685
+ if (r.when && Object.keys(r.when).length > 0) ruleObj.when = r.when;
2686
+ if (r.details && Object.keys(r.details).length > 0) ruleObj.details = r.details;
2687
+ return ruleObj;
2688
+ });
2689
+ return YAML2.stringify(payload, { indent: 2, lineWidth: 100 });
2690
+ }
2691
+ async function loadRuleSetFile(filePath) {
2692
+ const content = await fs4.readFile(filePath, "utf-8");
2693
+ return parseRuleSetYaml(content, filePath);
2694
+ }
2695
+ async function findRuleFiles(rootDir, storageRulesDir) {
2696
+ const candidates = [];
2697
+ const rulesDir = storageRulesDir || path4.resolve(rootDir, ".bilmem", "rules");
2698
+ try {
2699
+ const entries = await fs4.readdir(rulesDir, { withFileTypes: true });
2700
+ for (const entry of entries) {
2701
+ if (entry.isFile() && (entry.name.endsWith(".yml") || entry.name.endsWith(".yaml"))) {
2702
+ candidates.push(path4.join(rulesDir, entry.name));
2703
+ }
2704
+ }
2705
+ } catch {
2706
+ }
2707
+ for (const name of ["rules.yml", "rules.yaml"]) {
2708
+ const p = path4.resolve(rootDir, ".bilmem", name);
2709
+ try {
2710
+ const st = await fs4.stat(p);
2711
+ if (st.isFile()) candidates.push(p);
2712
+ } catch {
2713
+ }
2714
+ }
2715
+ for (const name of ["bilmem.rules.yml", "bilmem.rules.yaml", "rules.yml", "rules.yaml"]) {
2716
+ const p = path4.resolve(rootDir, name);
2717
+ try {
2718
+ const st = await fs4.stat(p);
2719
+ if (st.isFile()) candidates.push(p);
2720
+ } catch {
2721
+ }
2722
+ }
2723
+ return Array.from(new Set(candidates));
2724
+ }
2725
+ async function loadAllProjectRuleSets(rootDir, storageRulesDir) {
2726
+ const files = await findRuleFiles(rootDir, storageRulesDir);
2727
+ const ruleSets = [];
2728
+ const loadedFiles = [];
2729
+ for (const file of files) {
2730
+ try {
2731
+ const set = await loadRuleSetFile(file);
2732
+ ruleSets.push(set);
2733
+ loadedFiles.push(file);
2734
+ } catch (err) {
2735
+ console.warn(`[bilmem] Warning: Failed to load rule set from ${file}: ${err.message}`);
2736
+ }
2737
+ }
2738
+ return { ruleSets, sourceFiles: loadedFiles };
2739
+ }
2740
+
2741
+ // src/stats/events.ts
2742
+ import * as fs5 from "fs/promises";
2743
+ import * as path5 from "path";
2744
+ function getEventsFilePath(rootDir) {
2745
+ return path5.join(rootDir, ".bilmem", "events.jsonl");
2746
+ }
2747
+ async function readMetricEvents(rootDir, filter) {
2748
+ const filePath = getEventsFilePath(rootDir);
2749
+ let raw;
2750
+ try {
2751
+ raw = await fs5.readFile(filePath, "utf-8");
2752
+ } catch (err) {
2753
+ if (err.code === "ENOENT") {
2754
+ return [];
2755
+ }
2756
+ return [];
2757
+ }
2758
+ const lines = raw.split("\n");
2759
+ const events = [];
2760
+ for (const line of lines) {
2761
+ const trimmed = line.trim();
2762
+ if (!trimmed) continue;
2763
+ try {
2764
+ const parsed = JSON.parse(trimmed);
2765
+ if (!parsed || typeof parsed !== "object" || typeof parsed.type !== "string") {
2766
+ continue;
2767
+ }
2768
+ if (typeof parsed.timestamp !== "number") {
2769
+ continue;
2770
+ }
2771
+ if (filter?.sinceTimestamp !== void 0 && parsed.timestamp < filter.sinceTimestamp) {
2772
+ continue;
2773
+ }
2774
+ if (filter?.knowledgeId !== void 0) {
2775
+ const kid = parsed.knowledgeId;
2776
+ if (kid !== filter.knowledgeId) {
2777
+ continue;
2778
+ }
2779
+ }
2780
+ events.push(parsed);
2781
+ } catch {
2782
+ continue;
2783
+ }
2784
+ }
2785
+ return events;
2786
+ }
2787
+ async function resetMetricEvents(rootDir) {
2788
+ const filePath = getEventsFilePath(rootDir);
2789
+ try {
2790
+ await fs5.mkdir(path5.dirname(filePath), { recursive: true });
2791
+ await fs5.writeFile(filePath, "", "utf-8");
2792
+ } catch (err) {
2793
+ if (err.code !== "ENOENT") throw err;
2794
+ }
2795
+ }
2796
+
2797
+ // src/storage/canonical.ts
2798
+ function sanitizeFileName(id) {
2799
+ return encodeURIComponent(id).replace(/%/g, "_");
2800
+ }
2801
+ var Storage = class {
2802
+ baseDir;
2803
+ knowledgeDir;
2804
+ evidenceDir;
2805
+ observationDir;
2806
+ unknownDir;
2807
+ rulesDir;
2808
+ configFile;
2809
+ relationsFile;
2810
+ outcomesFile;
2811
+ metricsFile;
2812
+ revisionsFile;
2813
+ knowledgeJsonlFile;
2814
+ evidenceJsonlFile;
2815
+ observationJsonlFile;
2816
+ unknownsJsonlFile;
2817
+ receiptsDir;
2818
+ receiptsJsonlFile;
2819
+ eventsFile;
2820
+ constructor(rootDir = process.cwd()) {
2821
+ this.baseDir = path6.resolve(rootDir, ".bilmem");
2822
+ this.knowledgeDir = path6.join(this.baseDir, "knowledge");
2823
+ this.evidenceDir = path6.join(this.baseDir, "evidence");
2824
+ this.observationDir = path6.join(this.baseDir, "observations");
2825
+ this.unknownDir = path6.join(this.baseDir, "unknowns");
2826
+ this.rulesDir = path6.join(this.baseDir, "rules");
2827
+ this.receiptsDir = path6.join(this.baseDir, "receipts");
2828
+ this.configFile = path6.join(this.baseDir, "config.json");
2829
+ this.relationsFile = path6.join(this.baseDir, "relations.json");
2830
+ this.outcomesFile = path6.join(this.baseDir, "outcomes.jsonl");
2831
+ this.metricsFile = path6.join(this.baseDir, "metrics.json");
2832
+ this.revisionsFile = path6.join(this.baseDir, "revisions.jsonl");
2833
+ this.knowledgeJsonlFile = path6.join(this.baseDir, "knowledge.jsonl");
2834
+ this.evidenceJsonlFile = path6.join(this.baseDir, "evidence.jsonl");
2835
+ this.observationJsonlFile = path6.join(this.baseDir, "observations.jsonl");
2836
+ this.unknownsJsonlFile = path6.join(this.baseDir, "unknowns.jsonl");
2837
+ this.receiptsJsonlFile = path6.join(this.baseDir, "receipts.jsonl");
2838
+ this.eventsFile = path6.join(this.baseDir, "events.jsonl");
2839
+ }
2840
+ getBaseDir() {
2841
+ return this.baseDir;
2842
+ }
2843
+ async isInitialized() {
2844
+ try {
2845
+ await fs6.access(this.configFile);
2846
+ return true;
2847
+ } catch {
2848
+ return false;
2849
+ }
2850
+ }
2851
+ async init(options = {}) {
2852
+ await fs6.mkdir(this.knowledgeDir, { recursive: true });
2853
+ await fs6.mkdir(this.evidenceDir, { recursive: true });
2854
+ await fs6.mkdir(this.observationDir, { recursive: true });
2855
+ await fs6.mkdir(this.unknownDir, { recursive: true });
2856
+ await fs6.mkdir(this.rulesDir, { recursive: true });
2857
+ await fs6.mkdir(this.receiptsDir, { recursive: true });
2858
+ let config;
2859
+ if (await this.isInitialized()) {
2860
+ config = await this.getConfig();
2861
+ } else {
2862
+ config = {
2863
+ schemaVersion: 1,
2864
+ projectName: options.projectName ?? path6.basename(path6.dirname(this.baseDir)) ?? "project",
2865
+ defaultScope: options.defaultScope ?? "project",
2866
+ dataDir: this.baseDir
2867
+ };
2868
+ await this.saveConfig(config);
2869
+ }
2870
+ try {
2871
+ await fs6.access(this.relationsFile);
2872
+ } catch {
2873
+ await fs6.writeFile(this.relationsFile, JSON.stringify([], null, 2), "utf-8");
2874
+ }
2875
+ try {
2876
+ await fs6.access(this.metricsFile);
2877
+ } catch {
2878
+ const defaultMetrics = {
2879
+ knowledgeHits: 0,
2880
+ knowledgeMisses: 0,
2881
+ knowledgeReused: 0,
2882
+ unknownsResolved: 0,
2883
+ repeatedInvestigationsPrevented: 0,
2884
+ candidateKnowledgeMerged: 0,
2885
+ staleKnowledgeDetected: 0,
2886
+ contradictionsDetected: 0,
2887
+ knowledgePromoted: 0,
2888
+ knowledgeSuperseded: 0
2889
+ };
2890
+ await fs6.writeFile(this.metricsFile, JSON.stringify(defaultMetrics, null, 2), "utf-8");
2891
+ }
2892
+ return config;
2893
+ }
2894
+ // --- Config ---
2895
+ async getConfig() {
2896
+ const raw = await fs6.readFile(this.configFile, "utf-8");
2897
+ return JSON.parse(raw);
2898
+ }
2899
+ async saveConfig(config) {
2900
+ await fs6.writeFile(this.configFile, JSON.stringify(config, null, 2), "utf-8");
2901
+ }
2902
+ async updateConfig(updates) {
2903
+ const current = await this.getConfig();
2904
+ const updated = { ...current, ...updates };
2905
+ await this.saveConfig(updated);
2906
+ return updated;
2907
+ }
2908
+ // --- Knowledge ---
2909
+ async saveKnowledge(knowledge) {
2910
+ await fs6.mkdir(this.knowledgeDir, { recursive: true });
2911
+ const filename = `${sanitizeFileName(knowledge.id)}.json`;
2912
+ const filePath = path6.join(this.knowledgeDir, filename);
2913
+ await fs6.writeFile(filePath, JSON.stringify(knowledge, null, 2), "utf-8");
2914
+ await fs6.appendFile(this.knowledgeJsonlFile, JSON.stringify(knowledge) + "\n", "utf-8");
2915
+ }
2916
+ async getKnowledge(id) {
2917
+ const filename = `${sanitizeFileName(id)}.json`;
2918
+ const filePath = path6.join(this.knowledgeDir, filename);
2919
+ try {
2920
+ const raw = await fs6.readFile(filePath, "utf-8");
2921
+ return JSON.parse(raw);
2922
+ } catch {
2923
+ return void 0;
2924
+ }
2925
+ }
2926
+ async getAllKnowledge() {
2927
+ try {
2928
+ const files = await fs6.readdir(this.knowledgeDir);
2929
+ const items = [];
2930
+ for (const file of files) {
2931
+ if (!file.endsWith(".json")) continue;
2932
+ const raw = await fs6.readFile(path6.join(this.knowledgeDir, file), "utf-8");
2933
+ try {
2934
+ items.push(JSON.parse(raw));
2935
+ } catch {
2936
+ }
2937
+ }
2938
+ return items;
2939
+ } catch {
2940
+ return [];
2941
+ }
2942
+ }
2943
+ async deleteKnowledge(id) {
2944
+ const filename = `${sanitizeFileName(id)}.json`;
2945
+ const filePath = path6.join(this.knowledgeDir, filename);
2946
+ try {
2947
+ await fs6.unlink(filePath);
2948
+ return true;
2949
+ } catch {
2950
+ return false;
2951
+ }
2952
+ }
2953
+ // --- Evidence ---
2954
+ async saveEvidence(evidence) {
2955
+ await fs6.mkdir(this.evidenceDir, { recursive: true });
2956
+ const filename = `${sanitizeFileName(evidence.id)}.json`;
2957
+ const filePath = path6.join(this.evidenceDir, filename);
2958
+ await fs6.writeFile(filePath, JSON.stringify(evidence, null, 2), "utf-8");
2959
+ await fs6.appendFile(this.evidenceJsonlFile, JSON.stringify(evidence) + "\n", "utf-8");
2960
+ }
2961
+ async getEvidence(id) {
2962
+ const filename = `${sanitizeFileName(id)}.json`;
2963
+ const filePath = path6.join(this.evidenceDir, filename);
2964
+ try {
2965
+ const raw = await fs6.readFile(filePath, "utf-8");
2966
+ return JSON.parse(raw);
2967
+ } catch {
2968
+ return void 0;
2969
+ }
2970
+ }
2971
+ async getAllEvidence() {
2972
+ try {
2973
+ const files = await fs6.readdir(this.evidenceDir);
2974
+ const items = [];
2975
+ for (const file of files) {
2976
+ if (!file.endsWith(".json")) continue;
2977
+ const raw = await fs6.readFile(path6.join(this.evidenceDir, file), "utf-8");
2978
+ try {
2979
+ items.push(JSON.parse(raw));
2980
+ } catch {
2981
+ }
2982
+ }
2983
+ return items;
2984
+ } catch {
2985
+ return [];
2986
+ }
2987
+ }
2988
+ // --- Observations ---
2989
+ async saveObservation(obs) {
2990
+ await fs6.mkdir(this.observationDir, { recursive: true });
2991
+ const filename = `${sanitizeFileName(obs.id)}.json`;
2992
+ const filePath = path6.join(this.observationDir, filename);
2993
+ await fs6.writeFile(filePath, JSON.stringify(obs, null, 2), "utf-8");
2994
+ await fs6.appendFile(this.observationJsonlFile, JSON.stringify(obs) + "\n", "utf-8");
2995
+ }
2996
+ async getObservation(id) {
2997
+ const filename = `${sanitizeFileName(id)}.json`;
2998
+ const filePath = path6.join(this.observationDir, filename);
2999
+ try {
3000
+ const raw = await fs6.readFile(filePath, "utf-8");
3001
+ return JSON.parse(raw);
3002
+ } catch {
3003
+ return void 0;
3004
+ }
3005
+ }
3006
+ async getAllObservations() {
3007
+ try {
3008
+ const files = await fs6.readdir(this.observationDir);
3009
+ const items = [];
3010
+ for (const file of files) {
3011
+ if (!file.endsWith(".json")) continue;
3012
+ const raw = await fs6.readFile(path6.join(this.observationDir, file), "utf-8");
3013
+ try {
3014
+ items.push(JSON.parse(raw));
3015
+ } catch {
3016
+ }
3017
+ }
3018
+ return items;
3019
+ } catch {
3020
+ return [];
3021
+ }
3022
+ }
3023
+ async deleteObservation(id) {
3024
+ const filename = `${sanitizeFileName(id)}.json`;
3025
+ const filePath = path6.join(this.observationDir, filename);
3026
+ try {
3027
+ await fs6.unlink(filePath);
3028
+ return true;
3029
+ } catch {
3030
+ return false;
3031
+ }
3032
+ }
3033
+ // --- Unknowns ---
3034
+ async saveUnknown(record) {
3035
+ await fs6.mkdir(this.unknownDir, { recursive: true });
3036
+ const filename = `${sanitizeFileName(record.id)}.json`;
3037
+ const filePath = path6.join(this.unknownDir, filename);
3038
+ await fs6.writeFile(filePath, JSON.stringify(record, null, 2), "utf-8");
3039
+ await fs6.appendFile(this.unknownsJsonlFile, JSON.stringify(record) + "\n", "utf-8");
3040
+ }
3041
+ async getUnknown(id) {
3042
+ const filename = `${sanitizeFileName(id)}.json`;
3043
+ const filePath = path6.join(this.unknownDir, filename);
3044
+ try {
3045
+ const raw = await fs6.readFile(filePath, "utf-8");
3046
+ return JSON.parse(raw);
3047
+ } catch {
3048
+ return void 0;
3049
+ }
3050
+ }
3051
+ async getAllUnknowns() {
3052
+ try {
3053
+ const files = await fs6.readdir(this.unknownDir);
3054
+ const items = [];
3055
+ for (const file of files) {
3056
+ if (!file.endsWith(".json")) continue;
3057
+ const raw = await fs6.readFile(path6.join(this.unknownDir, file), "utf-8");
3058
+ try {
3059
+ items.push(JSON.parse(raw));
3060
+ } catch {
3061
+ }
3062
+ }
3063
+ return items;
3064
+ } catch {
3065
+ return [];
3066
+ }
3067
+ }
3068
+ async deleteUnknown(id) {
3069
+ const filename = `${sanitizeFileName(id)}.json`;
3070
+ const filePath = path6.join(this.unknownDir, filename);
3071
+ try {
3072
+ await fs6.unlink(filePath);
3073
+ return true;
3074
+ } catch {
3075
+ return false;
3076
+ }
3077
+ }
3078
+ // --- Outcomes (Append-oriented log) ---
3079
+ async appendOutcome(outcome) {
3080
+ const line = JSON.stringify(outcome) + "\n";
3081
+ await fs6.appendFile(this.outcomesFile, line, "utf-8");
3082
+ if (outcome.knowledgeId) {
3083
+ await this.appendMetricEvent({
3084
+ type: "outcome",
3085
+ knowledgeId: outcome.knowledgeId,
3086
+ result: outcome.result,
3087
+ timestamp: Date.now()
3088
+ });
3089
+ if (outcome.result === "success") {
3090
+ await this.appendMetricEvent({
3091
+ type: "question_resolved",
3092
+ questionId: outcome.knowledgeId,
3093
+ timestamp: Date.now()
3094
+ });
3095
+ }
3096
+ }
3097
+ }
3098
+ async getAllOutcomes() {
3099
+ try {
3100
+ const raw = await fs6.readFile(this.outcomesFile, "utf-8");
3101
+ const lines = raw.split("\n").filter((l) => l.trim().length > 0);
3102
+ return lines.map((l) => JSON.parse(l));
3103
+ } catch {
3104
+ return [];
3105
+ }
3106
+ }
3107
+ // --- Metric Events (Append-only local log) ---
3108
+ async appendMetricEvent(event) {
3109
+ try {
3110
+ await fs6.mkdir(this.baseDir, { recursive: true });
3111
+ await fs6.appendFile(this.eventsFile, JSON.stringify(event) + "\n", "utf-8");
3112
+ } catch {
3113
+ }
3114
+ }
3115
+ async getMetricEvents(filter) {
3116
+ return readMetricEvents(path6.dirname(this.baseDir), filter);
3117
+ }
3118
+ async resetMetricEvents() {
3119
+ return resetMetricEvents(path6.dirname(this.baseDir));
3120
+ }
3121
+ // --- Investigation Receipts ---
3122
+ async saveReceipt(receipt) {
3123
+ await fs6.mkdir(this.receiptsDir, { recursive: true });
3124
+ const filename = `${sanitizeFileName(receipt.id)}.json`;
3125
+ const filePath = path6.join(this.receiptsDir, filename);
3126
+ await fs6.writeFile(filePath, JSON.stringify(receipt, null, 2), "utf-8");
3127
+ await fs6.appendFile(this.receiptsJsonlFile, JSON.stringify(receipt) + "\n", "utf-8");
3128
+ }
3129
+ async getReceipt(id) {
3130
+ const filename = `${sanitizeFileName(id)}.json`;
3131
+ const filePath = path6.join(this.receiptsDir, filename);
3132
+ try {
3133
+ const raw = await fs6.readFile(filePath, "utf-8");
3134
+ return JSON.parse(raw);
3135
+ } catch {
3136
+ return void 0;
3137
+ }
3138
+ }
3139
+ async getAllReceipts() {
3140
+ try {
3141
+ const files = await fs6.readdir(this.receiptsDir);
3142
+ const items = [];
3143
+ for (const file of files) {
3144
+ if (!file.endsWith(".json")) continue;
3145
+ const raw = await fs6.readFile(path6.join(this.receiptsDir, file), "utf-8");
3146
+ try {
3147
+ items.push(JSON.parse(raw));
3148
+ } catch {
3149
+ }
3150
+ }
3151
+ return items;
3152
+ } catch {
3153
+ return [];
3154
+ }
3155
+ }
3156
+ // --- Relations ---
3157
+ async getRelations() {
3158
+ try {
3159
+ const raw = await fs6.readFile(this.relationsFile, "utf-8");
3160
+ return JSON.parse(raw);
3161
+ } catch {
3162
+ return [];
3163
+ }
3164
+ }
3165
+ async saveRelations(relations) {
3166
+ await fs6.writeFile(this.relationsFile, JSON.stringify(relations, null, 2), "utf-8");
3167
+ }
3168
+ // --- Metrics ---
3169
+ async getMetrics() {
3170
+ try {
3171
+ const raw = await fs6.readFile(this.metricsFile, "utf-8");
3172
+ return JSON.parse(raw);
3173
+ } catch {
3174
+ return {
3175
+ knowledgeHits: 0,
3176
+ knowledgeMisses: 0,
3177
+ knowledgeReused: 0,
3178
+ unknownsResolved: 0,
3179
+ repeatedInvestigationsPrevented: 0,
3180
+ candidateKnowledgeMerged: 0,
3181
+ staleKnowledgeDetected: 0,
3182
+ contradictionsDetected: 0,
3183
+ knowledgePromoted: 0,
3184
+ knowledgeSuperseded: 0
3185
+ };
3186
+ }
3187
+ }
3188
+ async saveMetrics(metrics) {
3189
+ await fs6.writeFile(this.metricsFile, JSON.stringify(metrics, null, 2), "utf-8");
3190
+ }
3191
+ // --- Rules ---
3192
+ getRulesDir() {
3193
+ return this.rulesDir;
3194
+ }
3195
+ async loadRuleSets() {
3196
+ const files = await findRuleFiles(path6.dirname(this.baseDir), this.rulesDir);
3197
+ const sets = [];
3198
+ for (const f of files) {
3199
+ try {
3200
+ sets.push(await loadRuleSetFile(f));
3201
+ } catch {
3202
+ }
3203
+ }
3204
+ return sets;
3205
+ }
3206
+ async saveRuleSet(ruleSet, fileName = "default.yml") {
3207
+ await fs6.mkdir(this.rulesDir, { recursive: true });
3208
+ const cleanName = fileName.endsWith(".yml") || fileName.endsWith(".yaml") ? fileName : `${fileName}.yml`;
3209
+ const targetPath = path6.join(this.rulesDir, cleanName);
3210
+ const yaml = stringifyRuleSetYaml(ruleSet);
3211
+ await fs6.writeFile(targetPath, yaml, "utf-8");
3212
+ return targetPath;
3213
+ }
3214
+ async addRule(rule, fileName = "default.yml") {
3215
+ await fs6.mkdir(this.rulesDir, { recursive: true });
3216
+ const cleanName = fileName.endsWith(".yml") || fileName.endsWith(".yaml") ? fileName : `${fileName}.yml`;
3217
+ const targetPath = path6.join(this.rulesDir, cleanName);
3218
+ let existingSet;
3219
+ try {
3220
+ existingSet = await loadRuleSetFile(targetPath);
3221
+ } catch {
3222
+ existingSet = {
3223
+ name: path6.basename(cleanName, path6.extname(cleanName)),
3224
+ rules: []
3225
+ };
3226
+ }
3227
+ const existingIdx = existingSet.rules.findIndex((r) => r.id === rule.id);
3228
+ if (existingIdx >= 0) {
3229
+ existingSet.rules[existingIdx] = rule;
3230
+ } else {
3231
+ existingSet.rules.push(rule);
3232
+ }
3233
+ const yaml = stringifyRuleSetYaml(existingSet);
3234
+ await fs6.writeFile(targetPath, yaml, "utf-8");
3235
+ return targetPath;
3236
+ }
3237
+ // --- Revisions & Incremental Learning Diff ---
3238
+ async appendRevision(snapshot) {
3239
+ const line = JSON.stringify(snapshot) + "\n";
3240
+ await fs6.appendFile(this.revisionsFile, line, "utf-8");
3241
+ }
3242
+ async getRevisions() {
3243
+ try {
3244
+ const raw = await fs6.readFile(this.revisionsFile, "utf-8");
3245
+ const lines = raw.split("\n").filter((l) => l.trim().length > 0);
3246
+ return lines.map((l) => JSON.parse(l));
3247
+ } catch {
3248
+ return [];
3249
+ }
3250
+ }
3251
+ async getLatestRevision() {
3252
+ const revs = await this.getRevisions();
3253
+ return revs.length > 0 ? revs[revs.length - 1] : void 0;
3254
+ }
3255
+ async getDiff(sinceRef) {
3256
+ const revisions = await this.getRevisions();
3257
+ let baselineSnapshot;
3258
+ let targetSnapshot;
3259
+ if (sinceRef && sinceRef.includes("..")) {
3260
+ const [startRef, endRef] = sinceRef.split("..").map((s) => s.trim().toLowerCase());
3261
+ baselineSnapshot = revisions.find(
3262
+ (r) => r.id.toLowerCase() === startRef || r.commit?.toLowerCase() === startRef || r.commit?.toLowerCase().startsWith(startRef) || r.id.toLowerCase().startsWith(startRef)
3263
+ );
3264
+ targetSnapshot = revisions.find(
3265
+ (r) => r.id.toLowerCase() === endRef || r.commit?.toLowerCase() === endRef || r.commit?.toLowerCase().startsWith(endRef) || r.id.toLowerCase().startsWith(endRef)
3266
+ );
3267
+ } else if (sinceRef) {
3268
+ const cleanRef = sinceRef.trim().toLowerCase();
3269
+ baselineSnapshot = revisions.find(
3270
+ (r) => r.id.toLowerCase() === cleanRef || r.commit?.toLowerCase() === cleanRef || r.commit?.toLowerCase().startsWith(cleanRef) || r.id.toLowerCase().startsWith(cleanRef)
3271
+ );
3272
+ } else {
3273
+ if (revisions.length >= 2) {
3274
+ baselineSnapshot = revisions[revisions.length - 2];
3275
+ } else if (revisions.length === 1) {
3276
+ baselineSnapshot = revisions[0];
3277
+ }
3278
+ }
3279
+ const baselineTimestamp = baselineSnapshot?.timestamp ?? "1970-01-01T00:00:00.000Z";
3280
+ const baselineTime = new Date(baselineTimestamp).getTime();
3281
+ const targetTime = targetSnapshot ? new Date(targetSnapshot.timestamp).getTime() : Date.now();
3282
+ const inWindow = (time) => time >= baselineTime && time <= targetTime;
3283
+ const allObservations = await this.getAllObservations();
3284
+ const allKnowledge = await this.getAllKnowledge();
3285
+ const allUnknowns = await this.getAllUnknowns();
3286
+ let observationsAdded = 0;
3287
+ for (const obs of allObservations) {
3288
+ const obsTime = new Date(obs.recordedAt || obs.provenance?.timestamp || 0).getTime();
3289
+ if (inWindow(obsTime)) {
3290
+ observationsAdded++;
3291
+ }
3292
+ }
3293
+ let candidatesAdded = 0;
3294
+ let strengthenedCount = 0;
3295
+ let revisedCount = 0;
3296
+ let contradictionCount = 0;
3297
+ let invalidatedCount = 0;
3298
+ let needsRevalidationCount = 0;
3299
+ const strengthened = [];
3300
+ const revised = [];
3301
+ const contradictions = [];
3302
+ const invalidated = [];
3303
+ const needsRevalidation = [];
3304
+ for (const k of allKnowledge) {
3305
+ const createdTime = new Date(k.createdAt).getTime();
3306
+ const updatedTime = new Date(k.updatedAt).getTime();
3307
+ if ((k.status === "invalid" || k.status === "invalidated") && inWindow(updatedTime)) {
3308
+ invalidatedCount++;
3309
+ invalidated.push(k.id);
3310
+ continue;
3311
+ }
3312
+ if (k.status === "contradicted" && inWindow(updatedTime)) {
3313
+ contradictionCount++;
3314
+ contradictions.push(k.id);
3315
+ continue;
3316
+ }
3317
+ if ((k.status === "needs_revalidation" || k.status === "stale") && inWindow(updatedTime)) {
3318
+ needsRevalidationCount++;
3319
+ needsRevalidation.push(k.id);
3320
+ continue;
3321
+ }
3322
+ if (inWindow(createdTime)) {
3323
+ candidatesAdded++;
3324
+ } else if (inWindow(updatedTime)) {
3325
+ if (k.metrics.support_count > 1 || k.confidence >= 0.85 || k.metrics.successful_outcomes > 0) {
3326
+ strengthenedCount++;
3327
+ strengthened.push(k.id);
3328
+ } else {
3329
+ revisedCount++;
3330
+ revised.push(k.id);
3331
+ }
3332
+ }
3333
+ }
3334
+ let unknownsCount = 0;
3335
+ const unknownsList = [];
3336
+ for (const u of allUnknowns) {
3337
+ const uTime = new Date(u.createdAt || u.updatedAt || 0).getTime();
3338
+ if (inWindow(uTime)) {
3339
+ unknownsCount++;
3340
+ unknownsList.push(u.id);
3341
+ }
3342
+ }
3343
+ const refLabel = sinceRef ?? (baselineSnapshot ? baselineSnapshot.commit?.slice(0, 7) || baselineSnapshot.id.slice(0, 7) : void 0);
3344
+ return {
3345
+ sinceRef: refLabel,
3346
+ observationsAdded,
3347
+ candidatesAdded,
3348
+ strengthenedCount,
3349
+ revisedCount,
3350
+ contradictionCount,
3351
+ unknownsCount,
3352
+ needsRevalidationCount,
3353
+ invalidatedCount,
3354
+ details: {
3355
+ strengthened,
3356
+ revised,
3357
+ contradictions,
3358
+ unknowns: unknownsList,
3359
+ needsRevalidation,
3360
+ invalidated
3361
+ }
3362
+ };
3363
+ }
3364
+ };
3365
+
3366
+ // src/search/indexer.ts
3367
+ import { create, insert, remove, search } from "@orama/orama";
3368
+ var SearchIndex = class {
3369
+ db = null;
3370
+ idToDocId = /* @__PURE__ */ new Map();
3371
+ async init() {
3372
+ this.db = await create({
3373
+ schema: {
3374
+ id: "string",
3375
+ statement: "string",
3376
+ type: "string",
3377
+ scope: "string",
3378
+ status: "string",
3379
+ positive: "string[]",
3380
+ negative: "string[]",
3381
+ tags: "string[]"
3382
+ }
3383
+ });
3384
+ this.idToDocId.clear();
3385
+ }
3386
+ async buildIndex(knowledgeList) {
3387
+ await this.init();
3388
+ for (const item of knowledgeList) {
3389
+ await this.upsert(item);
3390
+ }
3391
+ }
3392
+ async upsert(item) {
3393
+ if (!this.db) {
3394
+ await this.init();
3395
+ }
3396
+ const existingDocId = this.idToDocId.get(item.id);
3397
+ if (existingDocId) {
3398
+ try {
3399
+ await remove(this.db, existingDocId);
3400
+ } catch {
3401
+ }
3402
+ this.idToDocId.delete(item.id);
3403
+ }
3404
+ const docId = await insert(this.db, {
3405
+ id: item.id,
3406
+ statement: item.statement,
3407
+ type: item.type,
3408
+ scope: item.scope,
3409
+ status: item.status,
3410
+ positive: item.positive,
3411
+ negative: item.negative,
3412
+ tags: item.tags ?? []
3413
+ });
3414
+ this.idToDocId.set(item.id, docId);
3415
+ }
3416
+ async delete(id) {
3417
+ if (!this.db) return;
3418
+ const docId = this.idToDocId.get(id);
3419
+ if (docId) {
3420
+ try {
3421
+ await remove(this.db, docId);
3422
+ } catch {
3423
+ }
3424
+ this.idToDocId.delete(id);
3425
+ }
3426
+ }
3427
+ async searchCandidates(term, limit = 25) {
3428
+ if (!this.db) {
3429
+ return [];
3430
+ }
3431
+ const cleanTerm = term.trim();
3432
+ if (!cleanTerm) {
3433
+ return Array.from(this.idToDocId.keys()).slice(0, limit);
3434
+ }
3435
+ try {
3436
+ const results = await search(this.db, {
3437
+ term: cleanTerm,
3438
+ limit,
3439
+ threshold: 0.2
3440
+ });
3441
+ return results.hits.map((h) => h.document.id);
3442
+ } catch {
3443
+ return Array.from(this.idToDocId.keys()).slice(0, limit);
3444
+ }
3445
+ }
3446
+ size() {
3447
+ return this.idToDocId.size;
3448
+ }
3449
+ };
3450
+
3451
+ // src/rules/evaluator.ts
3452
+ function globToRegex(glob) {
3453
+ let pattern = glob.trim().replace(/^\//, "").replace(/\/$/, "");
3454
+ pattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
3455
+ pattern = pattern.replace(/\*\*/g, ".*");
3456
+ pattern = pattern.replace(/\*/g, "[^/]*");
3457
+ pattern = pattern.replace(/\?/g, "[^/]");
3458
+ return new RegExp(`(^|/)${pattern}$`, "i");
3459
+ }
3460
+ async function evaluateRule(rule, context) {
3461
+ const cond = rule.when;
3462
+ if (!cond || Object.keys(cond).length === 0) {
3463
+ return {
3464
+ rule,
3465
+ matched: true,
3466
+ reason: "Always active project rule"
3467
+ };
3468
+ }
3469
+ const matchedFiles = [];
3470
+ if (cond.has_file) {
3471
+ const files = Array.isArray(cond.has_file) ? cond.has_file : [cond.has_file];
3472
+ for (const f of files) {
3473
+ if (!context.hasFile(f)) {
3474
+ return {
3475
+ rule,
3476
+ matched: false,
3477
+ reason: `Required file '${f}' not found in project`
3478
+ };
3479
+ }
3480
+ matchedFiles.push(f);
3481
+ }
3482
+ }
3483
+ if (cond.missing_file) {
3484
+ const files = Array.isArray(cond.missing_file) ? cond.missing_file : [cond.missing_file];
3485
+ for (const f of files) {
3486
+ if (context.hasFile(f)) {
3487
+ return {
3488
+ rule,
3489
+ matched: false,
3490
+ reason: `Prohibited file '${f}' is present in project`
3491
+ };
3492
+ }
3493
+ }
3494
+ }
3495
+ if (cond.any_file && cond.any_file.length > 0) {
3496
+ let anyFound = false;
3497
+ for (const f of cond.any_file) {
3498
+ if (context.hasFile(f)) {
3499
+ anyFound = true;
3500
+ matchedFiles.push(f);
3501
+ break;
3502
+ }
3503
+ }
3504
+ if (!anyFound) {
3505
+ return {
3506
+ rule,
3507
+ matched: false,
3508
+ reason: `None of the expected files were found: ${cond.any_file.join(", ")}`
3509
+ };
3510
+ }
3511
+ }
3512
+ if (cond.file_pattern) {
3513
+ const regex = globToRegex(cond.file_pattern);
3514
+ const matches = context.files.filter(
3515
+ (f) => regex.test(f.relativePath) || regex.test(f.name)
3516
+ );
3517
+ if (matches.length === 0) {
3518
+ return {
3519
+ rule,
3520
+ matched: false,
3521
+ reason: `No files matched pattern '${cond.file_pattern}'`
3522
+ };
3523
+ }
3524
+ matchedFiles.push(...matches.slice(0, 5).map((m) => m.relativePath));
3525
+ }
3526
+ if (cond.content_match) {
3527
+ const { files: targetFile, pattern } = cond.content_match;
3528
+ const regex = new RegExp(pattern, "m");
3529
+ if (targetFile) {
3530
+ const content = await context.readText(targetFile);
3531
+ if (!content || !regex.test(content)) {
3532
+ return {
3533
+ rule,
3534
+ matched: false,
3535
+ reason: `Pattern '${pattern}' was not found in file '${targetFile}'`
3536
+ };
3537
+ }
3538
+ matchedFiles.push(targetFile);
3539
+ } else {
3540
+ let contentFound = false;
3541
+ for (const file of context.files.slice(0, 20)) {
3542
+ const content = await context.readText(file.relativePath);
3543
+ if (content && regex.test(content)) {
3544
+ contentFound = true;
3545
+ matchedFiles.push(file.relativePath);
3546
+ break;
3547
+ }
3548
+ }
3549
+ if (!contentFound) {
3550
+ return {
3551
+ rule,
3552
+ matched: false,
3553
+ reason: `Pattern '${pattern}' was not found in project files`
3554
+ };
3555
+ }
3556
+ }
3557
+ }
3558
+ return {
3559
+ rule,
3560
+ matched: true,
3561
+ reason: "Rule condition matched repository structure",
3562
+ matchedFiles: matchedFiles.length > 0 ? Array.from(new Set(matchedFiles)) : void 0
3563
+ };
3564
+ }
3565
+ function ruleToObservation(rule, evaluated, sourceFile) {
3566
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3567
+ const ruleType = rule.type ?? "rule";
3568
+ const positive = [
3569
+ ...rule.positive ?? [],
3570
+ rule.id,
3571
+ ruleType,
3572
+ ...rule.tags ?? []
3573
+ ];
3574
+ return createObservation({
3575
+ source: `rule:${rule.id}`,
3576
+ claim: rule.statement,
3577
+ confidence: rule.confidence ?? 0.95,
3578
+ provenance: {
3579
+ type: "file",
3580
+ path: sourceFile,
3581
+ timestamp: now
3582
+ },
3583
+ positive: Array.from(new Set(positive)),
3584
+ negative: rule.negative ? [...rule.negative] : [],
3585
+ details: {
3586
+ ruleId: rule.id,
3587
+ ruleName: rule.name,
3588
+ ruleType,
3589
+ matchedReason: evaluated.reason,
3590
+ matchedFiles: evaluated.matchedFiles,
3591
+ scope: rule.scope ?? "project",
3592
+ ...rule.details ?? {}
3593
+ }
3594
+ });
3595
+ }
3596
+ function ruleToCandidateLesson(rule, evaluated, obsSource) {
3597
+ const ruleType = rule.type ?? "rule";
3598
+ const positive = [
3599
+ ...rule.positive ?? [],
3600
+ rule.id,
3601
+ ruleType,
3602
+ ...rule.tags ?? []
3603
+ ];
3604
+ return {
3605
+ id: `rule.${rule.id}`,
3606
+ type: ruleType,
3607
+ statement: rule.statement,
3608
+ positive: Array.from(new Set(positive)),
3609
+ negative: rule.negative ? [...rule.negative] : [],
3610
+ confidence: rule.confidence ?? 0.95,
3611
+ evidence: [obsSource],
3612
+ scope: rule.scope ?? "project",
3613
+ tags: rule.tags ? [...rule.tags] : []
3614
+ };
3615
+ }
3616
+
3617
+ // src/scanner/engine.ts
3618
+ async function scanRepository(rootDir = process.cwd(), options = {}) {
3619
+ const storage = new Storage(rootDir);
3620
+ const dryRun = options.dryRun ?? false;
3621
+ const plannedActions = [];
3622
+ const config = dryRun ? await storage.isInitialized() ? await storage.getConfig() : {
3623
+ schemaVersion: 1,
3624
+ projectName: path7.basename(rootDir),
3625
+ defaultScope: "project",
3626
+ dataDir: path7.join(rootDir, ".bilmem")
3627
+ } : await storage.init();
3628
+ const incremental = options.incremental ?? false;
3629
+ const lastScanCommit = incremental ? config.lastScanCommit : void 0;
3630
+ const scannedFiles = await walkDirectory(rootDir);
3631
+ const fsObs = await analyzeProjectStructure(scannedFiles, rootDir);
3632
+ const projectContext = new ProjectContext(scannedFiles, rootDir);
3633
+ const ruleObs = [];
3634
+ const ruleCandidateLessons = [];
3635
+ const { ruleSets, sourceFiles } = await loadAllProjectRuleSets(rootDir, storage.getRulesDir());
3636
+ for (let idx = 0; idx < ruleSets.length; idx++) {
3637
+ const set = ruleSets[idx];
3638
+ const sourceFile = sourceFiles[idx] || "rules.yml";
3639
+ for (const rule of set.rules) {
3640
+ const evalResult = await evaluateRule(rule, projectContext);
3641
+ if (evalResult.matched) {
3642
+ const obs = ruleToObservation(rule, evalResult, sourceFile);
3643
+ ruleObs.push(obs);
3644
+ ruleCandidateLessons.push(ruleToCandidateLesson(rule, evalResult, obs.source));
3645
+ }
3646
+ }
3647
+ }
3648
+ const mdObs = [];
3649
+ const srcObs = [];
3650
+ const relations = [];
3651
+ for (const file of scannedFiles) {
3652
+ if (file.ext === ".md") {
3653
+ const res = await parseMarkdownFile(file.path, file.relativePath);
3654
+ mdObs.push(...res.observations);
3655
+ if (res.relations && res.relations.length > 0) {
3656
+ relations.push(...res.relations);
3657
+ }
3658
+ } else if (isSourceFile(file.ext)) {
3659
+ const res = await analyzeSourceFile(file.path, file.relativePath);
3660
+ srcObs.push(...res.observations);
3661
+ relations.push(...res.relations);
3662
+ }
3663
+ }
3664
+ const gitResult = await scanGitHistory(rootDir, {
3665
+ lastScanCommit,
3666
+ maxCommits: options.maxCommits ?? 100
3667
+ });
3668
+ relations.push(...gitResult.relations);
3669
+ const allObs = [...fsObs, ...ruleObs, ...mdObs, ...srcObs, ...gitResult.observations];
3670
+ if (!dryRun) {
3671
+ for (const obs of allObs) {
3672
+ await storage.saveObservation(obs);
3673
+ }
3674
+ const graph = new EvidenceGraph(await storage.getRelations());
3675
+ for (const r of relations) {
3676
+ graph.addRelation(r);
3677
+ }
3678
+ await storage.saveRelations(graph.getAllRelations());
3679
+ } else {
3680
+ plannedActions.push(`Would persist ${allObs.length} observations`);
3681
+ plannedActions.push(`Would record ${relations.length} evidence relations`);
3682
+ }
3683
+ const evidenceNodes = [];
3684
+ for (const obs of allObs) {
3685
+ const ev = createEvidence(obs, "supports");
3686
+ if (!dryRun) {
3687
+ await storage.saveEvidence(ev);
3688
+ }
3689
+ evidenceNodes.push(ev);
3690
+ }
3691
+ if (dryRun) {
3692
+ plannedActions.push(`Would create ${evidenceNodes.length} evidence nodes`);
3693
+ }
3694
+ const candidateLessons = [
3695
+ ...ruleCandidateLessons
3696
+ ];
3697
+ for (const o of mdObs) {
3698
+ if (o.source.includes(":adr") || o.claim.startsWith("[WARNING]") || o.claim.startsWith("[CAUTION]")) {
3699
+ const id = `decision.${path7.basename(o.provenance.path || "doc", ".md")}.${o.provenance.line || "adr"}`;
3700
+ candidateLessons.push({
3701
+ id,
3702
+ type: "decision",
3703
+ statement: o.claim,
3704
+ positive: o.positive ?? [],
3705
+ negative: o.negative ?? [],
3706
+ confidence: o.confidence,
3707
+ evidence: [o.source],
3708
+ scope: "project"
3709
+ });
3710
+ }
3711
+ }
3712
+ for (const o of gitResult.observations) {
3713
+ if (o.positive?.includes("revert")) {
3714
+ candidateLessons.push({
3715
+ id: `git.lesson.${o.provenance.commit || "revert"}`,
3716
+ type: "lesson",
3717
+ statement: `Caution: ${o.claim}`,
3718
+ positive: o.positive,
3719
+ negative: ["untested-assumption"],
3720
+ confidence: o.confidence,
3721
+ evidence: [o.source],
3722
+ scope: "project"
3723
+ });
3724
+ }
3725
+ }
3726
+ const relPaths = new Set(scannedFiles.map((f) => f.relativePath.toLowerCase()));
3727
+ const hasMigrations = Array.from(relPaths).some((p) => p.includes("migration") || p.includes("migrate"));
3728
+ const hasDb = Array.from(relPaths).some((p) => p.includes("db") || p.includes("database") || p.includes("sql"));
3729
+ let unknownCount = 0;
3730
+ if (hasDb && !hasMigrations) {
3731
+ unknownCount++;
3732
+ const unknownRec = createUnknown({
3733
+ id: "project.database.migration",
3734
+ state: "UNKNOWN",
3735
+ reason: "UNRESOLVABLE_LOCALLY",
3736
+ statement: "Database directory exists, but no explicit migration tool or folder was found locally.",
3737
+ checked: ["package.json", "src/db/**", "scripts/**"],
3738
+ next: {
3739
+ type: "ask",
3740
+ question: "How are database migrations performed in this project?"
3741
+ },
3742
+ scope: "project"
3743
+ });
3744
+ if (!dryRun) {
3745
+ await storage.saveUnknown(unknownRec);
3746
+ } else {
3747
+ plannedActions.push(`Would register unknown: "${unknownRec.statement}"`);
3748
+ }
3749
+ }
3750
+ const existingKnowledge = await storage.getAllKnowledge();
3751
+ let createdCount = 0;
3752
+ let updatedCount = 0;
3753
+ for (const cand of candidateLessons) {
3754
+ const rec = reconcileKnowledge(cand, existingKnowledge);
3755
+ if (!dryRun) {
3756
+ const { updatedKnowledge, action } = applyReconciliation(rec, cand, existingKnowledge, storage);
3757
+ await storage.saveKnowledge(updatedKnowledge);
3758
+ if (action === "NEW" || action === "SPECIALIZE") {
3759
+ createdCount++;
3760
+ existingKnowledge.push(updatedKnowledge);
3761
+ } else if (action === "SUPPORT" || action === "UPDATE" || action === "MERGE" || action === "CONTRADICT") {
3762
+ updatedCount++;
3763
+ }
3764
+ } else {
3765
+ plannedActions.push(`Would reconcile [${rec.action}]: "${cand.statement}" (${rec.reason})`);
3766
+ if (rec.action === "NEW" || rec.action === "SPECIALIZE") {
3767
+ createdCount++;
3768
+ } else if (rec.action !== "IGNORE") {
3769
+ updatedCount++;
3770
+ }
3771
+ }
3772
+ }
3773
+ if (!dryRun) {
3774
+ const index = new SearchIndex();
3775
+ await index.buildIndex(existingKnowledge);
3776
+ await storage.updateConfig({
3777
+ lastScanCommit: gitResult.headCommit ?? config.lastScanCommit,
3778
+ lastScanTimestamp: (/* @__PURE__ */ new Date()).toISOString()
3779
+ });
3780
+ await storage.appendRevision({
3781
+ id: `rev_${Date.now().toString(36)}`,
3782
+ commit: gitResult.headCommit,
3783
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3784
+ action: "scan",
3785
+ observationCount: allObs.length,
3786
+ evidenceCount: evidenceNodes.length,
3787
+ knowledgeCount: existingKnowledge.length,
3788
+ activeCount: existingKnowledge.filter((k) => k.status === "active").length,
3789
+ contradictedCount: existingKnowledge.filter((k) => k.status === "contradicted").length,
3790
+ staleCount: existingKnowledge.filter((k) => k.status === "stale").length,
3791
+ changesSummary: `Scanned ${scannedFiles.length} files: +${allObs.length} obs, +${createdCount} candidates, ~${updatedCount} updated.`
3792
+ });
3793
+ }
3794
+ const unknowns = dryRun ? [] : await storage.getAllUnknowns();
3795
+ return {
3796
+ filesScanned: scannedFiles.length,
3797
+ observationsCreated: allObs.length,
3798
+ evidenceCreated: evidenceNodes.length,
3799
+ knowledgeCreated: createdCount,
3800
+ knowledgeUpdated: updatedCount,
3801
+ unknownsCount: dryRun ? unknownCount : unknowns.length,
3802
+ gitCommitsScanned: gitResult.commitsScanned,
3803
+ headCommit: gitResult.headCommit,
3804
+ incremental,
3805
+ dryRun,
3806
+ plannedActions: dryRun ? plannedActions : void 0
3807
+ };
3808
+ }
3809
+ export {
3810
+ BUILTIN_DETECTORS,
3811
+ BUILTIN_MARKDOWN_EXTRACTORS,
3812
+ ConventionRegistry,
3813
+ MarkdownRegistry,
3814
+ ProjectContext,
3815
+ analyzeProjectStructure,
3816
+ analyzeSourceFile,
3817
+ defaultMarkdownRegistry,
3818
+ defaultRegistry,
3819
+ isGitRepository,
3820
+ isSourceFile,
3821
+ parseMarkdownDocument,
3822
+ parseMarkdownFile,
3823
+ registerConventionDetector,
3824
+ registerMarkdownExtractor,
3825
+ scanGitHistory,
3826
+ scanRepository,
3827
+ walkDirectory
3828
+ };
3829
+ //# sourceMappingURL=index.js.map