memorix 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3577,7 +3577,7 @@ async function promoteToMiniSkill(projectDir2, projectId, observations2, options
3577
3577
  const title = generateTitle(observations2);
3578
3578
  const instruction = options?.instruction || generateInstruction(observations2);
3579
3579
  const trigger = options?.trigger || generateTrigger(observations2);
3580
- const facts = extractFacts(observations2);
3580
+ const facts = extractFacts2(observations2);
3581
3581
  const tags = [
3582
3582
  ...options?.tags || [],
3583
3583
  ...extractTags(observations2)
@@ -3685,7 +3685,7 @@ function generateTrigger(observations2) {
3685
3685
  }
3686
3686
  return parts.length > 0 ? parts.join("; ") : `Related to ${obs.title}`;
3687
3687
  }
3688
- function extractFacts(observations2) {
3688
+ function extractFacts2(observations2) {
3689
3689
  const facts = /* @__PURE__ */ new Set();
3690
3690
  for (const obs of observations2) {
3691
3691
  for (const f of obs.facts) {
@@ -5841,6 +5841,10 @@ var KnowledgeGraphManager = class {
5841
5841
  );
5842
5842
  await this.save();
5843
5843
  }
5844
+ /** Get all entity names (for Formation Pipeline entity resolution) */
5845
+ getEntityNames() {
5846
+ return this.entities.map((e) => e.name);
5847
+ }
5844
5848
  /** Read the entire graph */
5845
5849
  async readGraph() {
5846
5850
  await this.init();
@@ -8207,6 +8211,588 @@ var WorkspaceSyncEngine = class _WorkspaceSyncEngine {
8207
8211
  // src/server.ts
8208
8212
  init_provider2();
8209
8213
  init_memory_manager();
8214
+
8215
+ // src/memory/formation/index.ts
8216
+ init_esm_shims();
8217
+
8218
+ // src/memory/formation/extract.ts
8219
+ init_esm_shims();
8220
+ var FACT_PATTERNS = [
8221
+ // Key: Value pairs (e.g., "Port: 3000", "Timeout = 60s")
8222
+ {
8223
+ pattern: /\b([A-Z][a-zA-Z_-]{2,30})\s*[:=]\s*([^\n,;]{2,60})/g,
8224
+ format: (m) => `${m[1]}: ${m[2].trim()}`
8225
+ },
8226
+ // Arrow notation (e.g., "MySQL → PostgreSQL", "v1.0 → v2.0")
8227
+ {
8228
+ pattern: /\b(\S{2,30})\s*[→➜\->]+\s*(\S{2,30})/g,
8229
+ format: (m) => `${m[1]} \u2192 ${m[2]}`
8230
+ },
8231
+ // Version numbers (e.g., "v1.2.3", "version 2.0")
8232
+ {
8233
+ pattern: /\b(?:v(?:ersion)?\s*)(\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?)\b/gi,
8234
+ format: (m) => `Version: ${m[1]}`
8235
+ },
8236
+ // Error messages (e.g., "Error: ...", "ERR_...")
8237
+ {
8238
+ pattern: /\b(?:Error|ERR|ENOENT|ECONNREFUSED|TypeError|RangeError|SyntaxError|ReferenceError)[:\s]+([^\n]{5,80})/gi,
8239
+ format: (m) => `Error: ${m[1].trim()}`
8240
+ },
8241
+ // Port numbers in context
8242
+ {
8243
+ pattern: /\b(?:port|PORT)\s*[:=]?\s*(\d{2,5})\b/gi,
8244
+ format: (m) => `Port: ${m[1]}`
8245
+ },
8246
+ // Environment variables
8247
+ {
8248
+ pattern: /\b([A-Z][A-Z0-9_]{3,30})\s*=\s*(\S{1,60})/g,
8249
+ format: (m) => `${m[1]}=${m[2]}`
8250
+ },
8251
+ // npm/package versions (e.g., "react@18.2.0")
8252
+ {
8253
+ pattern: /\b([@a-z][\w./-]+)@(\d+\.\d+\.\d+(?:-[\w.]+)?)\b/g,
8254
+ format: (m) => `${m[1]}@${m[2]}`
8255
+ }
8256
+ ];
8257
+ var GENERIC_TITLE_PATTERNS = [
8258
+ /^Updated \S+\.\w+$/i,
8259
+ /^Created \S+\.\w+$/i,
8260
+ /^Deleted \S+\.\w+$/i,
8261
+ /^Modified \S+\.\w+$/i,
8262
+ /^Changed \S+\.\w+$/i,
8263
+ /^Session activity/i,
8264
+ /^Activity \(/i,
8265
+ /^Used \w+$/i,
8266
+ /^Ran: /i
8267
+ ];
8268
+ var TYPE_SIGNALS = [
8269
+ {
8270
+ type: "problem-solution",
8271
+ patterns: [
8272
+ /\b(fix|fixed|bug|error|issue|crash|broken|resolved|workaround|patch)\b/i,
8273
+ /\b(修复|修正|解决|报错|崩溃|异常)\b/
8274
+ ]
8275
+ },
8276
+ {
8277
+ type: "gotcha",
8278
+ patterns: [
8279
+ /\b(gotcha|pitfall|trap|careful|warning|caveat|footgun|unexpected|beware)\b/i,
8280
+ /\b(坑|陷阱|注意|小心|踩坑)\b/
8281
+ ]
8282
+ },
8283
+ {
8284
+ type: "decision",
8285
+ patterns: [
8286
+ /\b(decided|chose|chosen|selected|adopted|rejected|evaluated|compared)\b/i,
8287
+ /\b(决定|选择|采用|弃用|对比|评估)\b/
8288
+ ]
8289
+ },
8290
+ {
8291
+ type: "what-changed",
8292
+ patterns: [
8293
+ /\b(changed|migrated|upgraded|refactored|replaced|renamed|moved|removed|added)\b/i,
8294
+ /\b(改|迁移|升级|重构|替换|重命名|删除|新增)\b/
8295
+ ]
8296
+ },
8297
+ {
8298
+ type: "how-it-works",
8299
+ patterns: [
8300
+ /\b(works by|architecture|mechanism|pipeline|flow|under the hood|internally)\b/i,
8301
+ /\b(原理|机制|流程|架构|内部)\b/
8302
+ ]
8303
+ },
8304
+ {
8305
+ type: "trade-off",
8306
+ patterns: [
8307
+ /\b(trade.?off|compromise|downside|cost|benefit|pro|con|versus|vs)\b/i,
8308
+ /\b(权衡|折中|代价|收益|优缺点)\b/
8309
+ ]
8310
+ }
8311
+ ];
8312
+ function extractFacts(narrative, existingFacts) {
8313
+ const existingLower = new Set(existingFacts.map((f) => f.toLowerCase().trim()));
8314
+ const extracted = [];
8315
+ const seen = /* @__PURE__ */ new Set();
8316
+ for (const { pattern, format } of FACT_PATTERNS) {
8317
+ pattern.lastIndex = 0;
8318
+ let match;
8319
+ while ((match = pattern.exec(narrative)) !== null) {
8320
+ const fact = format(match);
8321
+ const normalized = fact.toLowerCase().trim();
8322
+ if (existingLower.has(normalized) || seen.has(normalized)) continue;
8323
+ if (fact.length < 5 || fact.length > 120) continue;
8324
+ seen.add(normalized);
8325
+ extracted.push(fact);
8326
+ }
8327
+ }
8328
+ return extracted.slice(0, 10);
8329
+ }
8330
+ function improveTitle(title, narrative) {
8331
+ const isGeneric = GENERIC_TITLE_PATTERNS.some((p) => p.test(title));
8332
+ if (!isGeneric) return { title, improved: false };
8333
+ const sentences = narrative.replace(/```[\s\S]*?```/g, "").split(/[.。!!?\n]/).map((s) => s.trim()).filter((s) => s.length >= 15);
8334
+ if (sentences.length > 0) {
8335
+ return { title: sentences[0].slice(0, 60), improved: true };
8336
+ }
8337
+ return { title, improved: false };
8338
+ }
8339
+ function resolveEntity(entityName, existingEntities) {
8340
+ if (existingEntities.length === 0) return { entityName, resolved: false };
8341
+ const lower = entityName.toLowerCase().replace(/[-_]/g, "");
8342
+ for (const existing of existingEntities) {
8343
+ const existingLower = existing.toLowerCase().replace(/[-_]/g, "");
8344
+ if (lower === existingLower) {
8345
+ return { entityName: existing, resolved: existing !== entityName };
8346
+ }
8347
+ if (lower.length >= 3 && existingLower.length >= 3) {
8348
+ if (existingLower.includes(lower) || lower.includes(existingLower)) {
8349
+ const canonical = existing.length >= entityName.length ? existing : entityName;
8350
+ return { entityName: canonical, resolved: canonical !== entityName };
8351
+ }
8352
+ }
8353
+ }
8354
+ return { entityName, resolved: false };
8355
+ }
8356
+ function verifyType(declaredType, narrative, title) {
8357
+ const content = `${title} ${narrative}`;
8358
+ const scores = [];
8359
+ for (const { type, patterns } of TYPE_SIGNALS) {
8360
+ let score = 0;
8361
+ for (const p of patterns) {
8362
+ const regex = new RegExp(p.source, p.flags.includes("g") ? p.flags : p.flags + "g");
8363
+ const matches = [...content.matchAll(regex)];
8364
+ score += matches.length;
8365
+ }
8366
+ if (score > 0) scores.push({ type, score });
8367
+ }
8368
+ if (scores.length === 0) return { type: declaredType, corrected: false };
8369
+ scores.sort((a, b) => b.score - a.score);
8370
+ const best = scores[0];
8371
+ if (best.type !== declaredType && best.score >= 2) {
8372
+ const declaredScore = scores.find((s) => s.type === declaredType)?.score ?? 0;
8373
+ if (declaredScore === 0) {
8374
+ return { type: best.type, corrected: true };
8375
+ }
8376
+ }
8377
+ return { type: declaredType, corrected: false };
8378
+ }
8379
+ function runExtract(input, existingEntities) {
8380
+ const callerFacts = input.facts ?? [];
8381
+ const extractedFacts = extractFacts(input.narrative, callerFacts);
8382
+ const allFacts = [...callerFacts, ...extractedFacts];
8383
+ const { title, improved: titleImproved } = improveTitle(input.title, input.narrative);
8384
+ const { entityName, resolved: entityResolved } = resolveEntity(
8385
+ input.entityName,
8386
+ existingEntities
8387
+ );
8388
+ const { type, corrected: typeCorrected } = verifyType(
8389
+ input.type,
8390
+ input.narrative,
8391
+ input.title
8392
+ );
8393
+ return {
8394
+ title,
8395
+ titleImproved,
8396
+ narrative: input.narrative,
8397
+ facts: allFacts,
8398
+ extractedFacts,
8399
+ entityName,
8400
+ entityResolved,
8401
+ type,
8402
+ typeCorrected
8403
+ };
8404
+ }
8405
+
8406
+ // src/memory/formation/resolve.ts
8407
+ init_esm_shims();
8408
+ var SIMILARITY_HIGH2 = 0.75;
8409
+ var SIMILARITY_MEDIUM2 = 0.5;
8410
+ var SIMILARITY_DUPLICATE = 0.9;
8411
+ function wordOverlap(a, b) {
8412
+ const wordsA = new Set(a.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
8413
+ const wordsB = new Set(b.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
8414
+ if (wordsA.size === 0 || wordsB.size === 0) return 0;
8415
+ let intersection = 0;
8416
+ for (const w of wordsA) {
8417
+ if (wordsB.has(w)) intersection++;
8418
+ }
8419
+ return intersection / Math.max(wordsA.size, wordsB.size);
8420
+ }
8421
+ function entitiesMatch(a, b) {
8422
+ const na = a.toLowerCase().replace(/[-_]/g, "");
8423
+ const nb = b.toLowerCase().replace(/[-_]/g, "");
8424
+ if (na === nb) return true;
8425
+ if (na.length >= 3 && nb.length >= 3) {
8426
+ if (na.includes(nb) || nb.includes(na)) return true;
8427
+ }
8428
+ return false;
8429
+ }
8430
+ function hasContradiction(oldText, newText) {
8431
+ const negationPatterns = [
8432
+ /\bnot\s+(\w+)/gi,
8433
+ /\bno longer\b/i,
8434
+ /\binstead of\b/i,
8435
+ /\breplaced\b.*\bwith\b/i,
8436
+ /\bremoved\b/i,
8437
+ /\bdeprecated\b/i,
8438
+ /\bobsolete\b/i,
8439
+ /不再/,
8440
+ /已弃用/,
8441
+ /替换为/,
8442
+ /改为/
8443
+ ];
8444
+ return negationPatterns.some((p) => p.test(newText));
8445
+ }
8446
+ function mergeNarratives(oldNarrative, newNarrative) {
8447
+ if (newNarrative.length > oldNarrative.length * 1.5) return newNarrative;
8448
+ if (oldNarrative.length > newNarrative.length * 1.5) return oldNarrative;
8449
+ return `${newNarrative}
8450
+
8451
+ [Previous context]: ${oldNarrative}`;
8452
+ }
8453
+ function mergeFacts2(oldFacts, newFacts) {
8454
+ const seen = /* @__PURE__ */ new Set();
8455
+ const merged = [];
8456
+ for (const f of newFacts) {
8457
+ const norm = f.toLowerCase().trim();
8458
+ if (!seen.has(norm) && f.trim().length > 0) {
8459
+ seen.add(norm);
8460
+ merged.push(f);
8461
+ }
8462
+ }
8463
+ for (const f of oldFacts) {
8464
+ const norm = f.toLowerCase().trim();
8465
+ if (!seen.has(norm) && f.trim().length > 0) {
8466
+ seen.add(norm);
8467
+ merged.push(f);
8468
+ }
8469
+ }
8470
+ return merged;
8471
+ }
8472
+ function scoreCandidate(extracted, candidate) {
8473
+ const entityMatch = entitiesMatch(extracted.entityName, candidate.entityName);
8474
+ const contentOverlap = wordOverlap(
8475
+ `${extracted.title} ${extracted.narrative}`,
8476
+ `${candidate.title} ${candidate.narrative}`
8477
+ );
8478
+ const score = candidate.score * 0.6 + (entityMatch ? 0.2 : 0) + contentOverlap * 0.2;
8479
+ const newLength = extracted.narrative.length + extracted.facts.join(" ").length;
8480
+ const oldLength = candidate.narrative.length + candidate.facts.length;
8481
+ const richer = newLength > oldLength * 1.15;
8482
+ const contradiction = hasContradiction(candidate.narrative, extracted.narrative);
8483
+ return { score, entityMatch, richer, contradiction };
8484
+ }
8485
+ async function runResolve(extracted, projectId, searchMemories, getObservation2) {
8486
+ const query = `${extracted.title} ${extracted.narrative.substring(0, 200)}`;
8487
+ let hits;
8488
+ try {
8489
+ hits = await searchMemories(query, 5, projectId);
8490
+ } catch {
8491
+ return { action: "new", reason: "Search unavailable, defaulting to new" };
8492
+ }
8493
+ if (hits.length === 0) {
8494
+ return { action: "new", reason: "No similar existing memories found" };
8495
+ }
8496
+ const scored = hits.map((hit) => ({
8497
+ hit,
8498
+ ...scoreCandidate(extracted, hit)
8499
+ }));
8500
+ scored.sort((a, b) => b.score - a.score);
8501
+ const best = scored[0];
8502
+ if (best.hit.score >= SIMILARITY_DUPLICATE) {
8503
+ if (best.richer) {
8504
+ const existing = getObservation2(best.hit.observationId);
8505
+ const oldFacts = existing?.facts ?? best.hit.facts.split("\n").filter(Boolean);
8506
+ return {
8507
+ action: "evolve",
8508
+ targetId: best.hit.observationId,
8509
+ reason: `Near-duplicate of #${best.hit.observationId} but richer content (score: ${best.score.toFixed(2)})`,
8510
+ mergedNarrative: mergeNarratives(best.hit.narrative, extracted.narrative),
8511
+ mergedFacts: mergeFacts2(oldFacts, extracted.facts)
8512
+ };
8513
+ }
8514
+ return {
8515
+ action: "discard",
8516
+ targetId: best.hit.observationId,
8517
+ reason: `Duplicate of #${best.hit.observationId} (score: ${best.score.toFixed(2)})`
8518
+ };
8519
+ }
8520
+ if (best.score >= SIMILARITY_HIGH2) {
8521
+ if (best.contradiction) {
8522
+ const existing = getObservation2(best.hit.observationId);
8523
+ const oldFacts = existing?.facts ?? best.hit.facts.split("\n").filter(Boolean);
8524
+ return {
8525
+ action: "evolve",
8526
+ targetId: best.hit.observationId,
8527
+ reason: `Supersedes #${best.hit.observationId}: contradiction detected (score: ${best.score.toFixed(2)})`,
8528
+ mergedNarrative: extracted.narrative,
8529
+ mergedFacts: mergeFacts2(oldFacts, extracted.facts)
8530
+ };
8531
+ }
8532
+ if (best.richer) {
8533
+ const existing = getObservation2(best.hit.observationId);
8534
+ const oldFacts = existing?.facts ?? best.hit.facts.split("\n").filter(Boolean);
8535
+ return {
8536
+ action: "merge",
8537
+ targetId: best.hit.observationId,
8538
+ reason: `Merging with #${best.hit.observationId}: same topic, new content is richer (score: ${best.score.toFixed(2)})`,
8539
+ mergedNarrative: mergeNarratives(best.hit.narrative, extracted.narrative),
8540
+ mergedFacts: mergeFacts2(oldFacts, extracted.facts)
8541
+ };
8542
+ }
8543
+ return {
8544
+ action: "discard",
8545
+ targetId: best.hit.observationId,
8546
+ reason: `Already covered by #${best.hit.observationId} (score: ${best.score.toFixed(2)})`
8547
+ };
8548
+ }
8549
+ if (best.score >= SIMILARITY_MEDIUM2 && best.entityMatch) {
8550
+ const existing = getObservation2(best.hit.observationId);
8551
+ const oldFacts = existing?.facts ?? best.hit.facts.split("\n").filter(Boolean);
8552
+ const newFactCount = extracted.facts.length;
8553
+ const oldFactCount = oldFacts.length;
8554
+ if (newFactCount > oldFactCount) {
8555
+ return {
8556
+ action: "merge",
8557
+ targetId: best.hit.observationId,
8558
+ reason: `Same entity "${extracted.entityName}", new memory has more facts (${newFactCount} > ${oldFactCount})`,
8559
+ mergedNarrative: mergeNarratives(best.hit.narrative, extracted.narrative),
8560
+ mergedFacts: mergeFacts2(oldFacts, extracted.facts)
8561
+ };
8562
+ }
8563
+ }
8564
+ return { action: "new", reason: `Different from existing memories (best score: ${best.score.toFixed(2)})` };
8565
+ }
8566
+
8567
+ // src/memory/formation/evaluate.ts
8568
+ init_esm_shims();
8569
+ var TYPE_WEIGHTS = {
8570
+ "gotcha": 0.85,
8571
+ "decision": 0.8,
8572
+ "problem-solution": 0.75,
8573
+ "trade-off": 0.7,
8574
+ "why-it-exists": 0.65,
8575
+ "how-it-works": 0.6,
8576
+ "discovery": 0.55,
8577
+ "what-changed": 0.45,
8578
+ "session-request": 0.4
8579
+ };
8580
+ var SPECIFICITY_PATTERNS = [
8581
+ /\b\d+\.\d+\.\d+\b/,
8582
+ // Semantic version numbers
8583
+ /\b(ERR_|ENOENT|ECONNREFUSED|E[A-Z]{3,})\b/,
8584
+ // Error codes
8585
+ /\b(port|PORT)\s*[:=]?\s*\d{2,5}\b/i,
8586
+ // Port numbers
8587
+ /\bhttps?:\/\/\S+/,
8588
+ // URLs
8589
+ /`[^`]{3,60}`/,
8590
+ // Inline code references
8591
+ /\b[A-Z][A-Z0-9_]{3,}\b/,
8592
+ // Constants (e.g., MAX_RETRIES)
8593
+ /\b\d+\s*(ms|s|sec|min|MB|GB|KB)\b/i
8594
+ // Measurements with units
8595
+ ];
8596
+ var CAUSAL_PATTERNS = [
8597
+ /\b(because|therefore|due to|caused by|as a result|fixed by|resolved by)\b/i,
8598
+ /\b(so that|in order to|leads to|results in|prevents)\b/i,
8599
+ /(?:因为|所以|由于|导致|造成|因此|为了|解决)/
8600
+ ];
8601
+ var NOISE_PATTERNS = [
8602
+ /^Session activity/i,
8603
+ /^Updated \S+\.\w+$/i,
8604
+ /^Created \S+\.\w+$/i,
8605
+ /^Deleted \S+\.\w+$/i,
8606
+ /^File written successfully/i,
8607
+ /^Command executed/i,
8608
+ /^Tool: (read_file|list_dir|find_by_name)/i,
8609
+ /^\s*$/
8610
+ ];
8611
+ var TOOL_OUTPUT_PATTERNS = [
8612
+ /^(file|directory|folder)\s+(created|deleted|moved|copied)/i,
8613
+ /^Successfully\s+(installed|updated|removed)/i,
8614
+ /^\d+ files? changed/i,
8615
+ /^npm (WARN|notice)/i,
8616
+ /^\s*at\s+\S+\s+\(/
8617
+ // Stack trace lines
8618
+ ];
8619
+ function factDensity(facts, narrativeLength) {
8620
+ if (narrativeLength === 0) return 0;
8621
+ const structuredChars = facts.reduce((sum, f) => sum + f.length, 0);
8622
+ return Math.min(1, structuredChars / Math.max(narrativeLength, 100));
8623
+ }
8624
+ function specificityScore(content) {
8625
+ let count2 = 0;
8626
+ for (const p of SPECIFICITY_PATTERNS) {
8627
+ p.lastIndex = 0;
8628
+ if (p.test(content)) count2++;
8629
+ }
8630
+ return Math.min(1, count2 / 3);
8631
+ }
8632
+ function causalScore(content) {
8633
+ let count2 = 0;
8634
+ for (const p of CAUSAL_PATTERNS) {
8635
+ p.lastIndex = 0;
8636
+ if (p.test(content)) count2++;
8637
+ }
8638
+ return Math.min(1, count2 / 2);
8639
+ }
8640
+ function noiseScore(title, narrative) {
8641
+ let noisiness = 0;
8642
+ for (const p of NOISE_PATTERNS) {
8643
+ if (p.test(title)) {
8644
+ noisiness += 0.3;
8645
+ break;
8646
+ }
8647
+ }
8648
+ const lines = narrative.split("\n").filter((l) => l.trim().length > 0);
8649
+ let toolOutputLines = 0;
8650
+ for (const line of lines) {
8651
+ for (const p of TOOL_OUTPUT_PATTERNS) {
8652
+ if (p.test(line)) {
8653
+ toolOutputLines++;
8654
+ break;
8655
+ }
8656
+ }
8657
+ }
8658
+ if (lines.length > 0) {
8659
+ noisiness += toolOutputLines / lines.length * 0.5;
8660
+ }
8661
+ if (narrative.length < 50) noisiness += 0.2;
8662
+ return Math.min(1, noisiness);
8663
+ }
8664
+ function categorize(score) {
8665
+ if (score >= 0.6) return "core";
8666
+ if (score >= 0.35) return "contextual";
8667
+ return "ephemeral";
8668
+ }
8669
+ function buildReason(typeWeight, factDens, specificity, causal, noise, category) {
8670
+ const parts = [];
8671
+ if (typeWeight >= 0.7) parts.push("high-value type");
8672
+ else if (typeWeight <= 0.45) parts.push("low-value type");
8673
+ if (factDens > 0.3) parts.push("fact-dense");
8674
+ if (specificity > 0.3) parts.push("specific (versions/codes/paths)");
8675
+ if (causal > 0.3) parts.push("causal reasoning");
8676
+ if (noise > 0.3) parts.push("noisy content");
8677
+ const detail = parts.length > 0 ? parts.join(", ") : "average content";
8678
+ return `${category}: ${detail}`;
8679
+ }
8680
+ function runEvaluate(extracted) {
8681
+ const content = `${extracted.title} ${extracted.narrative} ${extracted.facts.join(" ")}`;
8682
+ const typeWeight = TYPE_WEIGHTS[extracted.type] ?? 0.5;
8683
+ const factDens = factDensity(extracted.facts, extracted.narrative.length);
8684
+ const specificity = specificityScore(content);
8685
+ const causal = causalScore(content);
8686
+ const noise = noiseScore(extracted.title, extracted.narrative);
8687
+ const rawScore = typeWeight * 0.5 + factDens * 0.12 + specificity * 0.12 + causal * 0.12 - noise * 0.14;
8688
+ const extractionBonus = extracted.extractedFacts.length > 0 ? 0.05 : 0;
8689
+ const titlePenalty = extracted.titleImproved ? -0.03 : 0;
8690
+ const correctionBonus = extracted.typeCorrected ? 0.03 : 0;
8691
+ const score = Math.max(0, Math.min(1, rawScore + extractionBonus + titlePenalty + correctionBonus));
8692
+ const category = categorize(score);
8693
+ const reason = buildReason(typeWeight, factDens, specificity, causal, noise, category);
8694
+ return { score, category, reason };
8695
+ }
8696
+
8697
+ // src/memory/formation/index.ts
8698
+ var metricsBuffer = [];
8699
+ var MAX_METRICS_BUFFER = 500;
8700
+ function getMetricsSummary() {
8701
+ const total = metricsBuffer.length;
8702
+ if (total === 0) {
8703
+ return {
8704
+ total: 0,
8705
+ avgValueScore: 0,
8706
+ avgExtractedFacts: 0,
8707
+ titleImprovedRate: 0,
8708
+ entityResolvedRate: 0,
8709
+ typeCorectedRate: 0,
8710
+ resolutionBreakdown: {},
8711
+ categoryBreakdown: {},
8712
+ avgDurationMs: 0
8713
+ };
8714
+ }
8715
+ const sum = (fn) => metricsBuffer.reduce((s, m) => s + fn(m), 0);
8716
+ const resolutionBreakdown = {};
8717
+ const categoryBreakdown = {};
8718
+ for (const m of metricsBuffer) {
8719
+ resolutionBreakdown[m.resolutionAction] = (resolutionBreakdown[m.resolutionAction] ?? 0) + 1;
8720
+ categoryBreakdown[m.valueCategory] = (categoryBreakdown[m.valueCategory] ?? 0) + 1;
8721
+ }
8722
+ return {
8723
+ total,
8724
+ avgValueScore: sum((m) => m.valueScore) / total,
8725
+ avgExtractedFacts: sum((m) => m.systemExtractedFacts) / total,
8726
+ titleImprovedRate: sum((m) => m.titleImproved ? 1 : 0) / total,
8727
+ entityResolvedRate: sum((m) => m.entityResolved ? 1 : 0) / total,
8728
+ typeCorectedRate: sum((m) => m.typeCorrected ? 1 : 0) / total,
8729
+ resolutionBreakdown,
8730
+ categoryBreakdown,
8731
+ avgDurationMs: sum((m) => m.durationMs) / total
8732
+ };
8733
+ }
8734
+ async function runFormation(input, config) {
8735
+ const startTime = Date.now();
8736
+ let stagesCompleted = 0;
8737
+ const existingEntities = config.getEntityNames();
8738
+ const extraction = runExtract(input, existingEntities);
8739
+ stagesCompleted = 1;
8740
+ let resolution;
8741
+ if (input.topicKey) {
8742
+ resolution = {
8743
+ action: "new",
8744
+ reason: "TopicKey upsert \u2014 bypasses resolve stage"
8745
+ };
8746
+ } else {
8747
+ resolution = await runResolve(
8748
+ extraction,
8749
+ input.projectId,
8750
+ config.searchMemories,
8751
+ config.getObservation
8752
+ );
8753
+ }
8754
+ stagesCompleted = 2;
8755
+ const evaluation = runEvaluate(extraction);
8756
+ stagesCompleted = 3;
8757
+ const durationMs = Date.now() - startTime;
8758
+ const formed = {
8759
+ // Final enriched data
8760
+ entityName: extraction.entityName,
8761
+ type: extraction.type,
8762
+ title: extraction.title,
8763
+ narrative: resolution.mergedNarrative ?? extraction.narrative,
8764
+ facts: resolution.mergedFacts ?? extraction.facts,
8765
+ // Stage results
8766
+ extraction,
8767
+ resolution,
8768
+ evaluation,
8769
+ // Pipeline metadata
8770
+ pipeline: {
8771
+ mode: "rules",
8772
+ durationMs,
8773
+ stagesCompleted,
8774
+ shadow: config.shadow
8775
+ }
8776
+ };
8777
+ const metrics = {
8778
+ systemExtractedFacts: extraction.extractedFacts.length,
8779
+ titleImproved: extraction.titleImproved,
8780
+ entityResolved: extraction.entityResolved,
8781
+ typeCorrected: extraction.typeCorrected,
8782
+ resolutionAction: resolution.action,
8783
+ valueScore: evaluation.score,
8784
+ valueCategory: evaluation.category,
8785
+ durationMs,
8786
+ mode: "rules"
8787
+ };
8788
+ if (metricsBuffer.length >= MAX_METRICS_BUFFER) {
8789
+ metricsBuffer.shift();
8790
+ }
8791
+ metricsBuffer.push(metrics);
8792
+ return formed;
8793
+ }
8794
+
8795
+ // src/server.ts
8210
8796
  var lastInternalWriteMs = 0;
8211
8797
  var markInternalWrite = () => {
8212
8798
  lastInternalWriteMs = Date.now();
@@ -8338,7 +8924,7 @@ async function createMemorixServer(cwd, existingServer, sharedTeam) {
8338
8924
  let syncAdvisory = null;
8339
8925
  const server = existingServer ?? new McpServer({
8340
8926
  name: "memorix",
8341
- version: true ? "1.0.2" : "1.0.1"
8927
+ version: true ? "1.0.3" : "1.0.1"
8342
8928
  });
8343
8929
  server.registerTool(
8344
8930
  "memorix_store",
@@ -8494,12 +9080,68 @@ Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
8494
9080
  const enrichment = enrichmentParts.length > 0 ? `
8495
9081
  Auto-enriched: ${enrichmentParts.join(", ")}` : "";
8496
9082
  const action = upserted ? "\u{1F504} Updated" : "\u2705 Stored";
9083
+ let formationNote = "";
9084
+ try {
9085
+ const formationConfig = {
9086
+ shadow: true,
9087
+ useLLM: false,
9088
+ minValueScore: 0.3,
9089
+ searchMemories: async (q, limit, pid) => {
9090
+ const result = await compactSearch({ query: q, limit, projectId: pid, status: "active" });
9091
+ if (result.entries.length === 0) return [];
9092
+ const details = await compactDetail(result.entries.map((e) => e.id));
9093
+ return details.documents.map((d, i) => ({
9094
+ id: Number(d.id.replace("obs-", "")),
9095
+ observationId: d.observationId,
9096
+ title: d.title,
9097
+ narrative: d.narrative,
9098
+ facts: d.facts,
9099
+ entityName: d.entityName,
9100
+ type: d.type,
9101
+ score: result.entries[i]?.score ?? 0
9102
+ }));
9103
+ },
9104
+ getObservation: (id) => {
9105
+ const o = getObservation(id);
9106
+ if (!o) return null;
9107
+ return {
9108
+ id: o.id,
9109
+ entityName: o.entityName,
9110
+ type: o.type,
9111
+ title: o.title,
9112
+ narrative: o.narrative,
9113
+ facts: o.facts,
9114
+ topicKey: o.topicKey
9115
+ };
9116
+ },
9117
+ getEntityNames: () => graphManager.getEntityNames()
9118
+ };
9119
+ const formed = await runFormation({
9120
+ entityName,
9121
+ type,
9122
+ title,
9123
+ narrative,
9124
+ facts: safeFacts,
9125
+ projectId: project.id,
9126
+ source: "explicit",
9127
+ topicKey
9128
+ }, formationConfig);
9129
+ formationNote = `
9130
+ \u{1F52C} Formation[shadow]: ${formed.evaluation.category} (${formed.evaluation.score.toFixed(2)}) | ${formed.resolution.action} | ${formed.pipeline.durationMs}ms`;
9131
+ if (formed.extraction.extractedFacts.length > 0) {
9132
+ formationNote += ` | +${formed.extraction.extractedFacts.length} facts`;
9133
+ }
9134
+ if (formed.extraction.titleImproved) formationNote += " | title\u2191";
9135
+ if (formed.extraction.entityResolved) formationNote += ` | entity\u2192${formed.entityName}`;
9136
+ if (formed.extraction.typeCorrected) formationNote += ` | type\u2192${formed.type}`;
9137
+ } catch {
9138
+ }
8497
9139
  return {
8498
9140
  content: [
8499
9141
  {
8500
9142
  type: "text",
8501
9143
  text: `${action} observation #${obs.id} "${title}" (~${obs.tokens} tokens)
8502
- Entity: ${entityName} | Type: ${type} | Project: ${project.id}${obs.topicKey ? ` | Topic: ${obs.topicKey}` : ""}${compactAction}${compressionNote}${enrichment}`
9144
+ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${obs.topicKey ? ` | Topic: ${obs.topicKey}` : ""}${compactAction}${compressionNote}${enrichment}${formationNote}`
8503
9145
  }
8504
9146
  ]
8505
9147
  };
@@ -8854,6 +9496,53 @@ Archived memories can be restored manually if needed.` }]
8854
9496
  };
8855
9497
  }
8856
9498
  );
9499
+ server.registerTool(
9500
+ "memorix_formation_metrics",
9501
+ {
9502
+ title: "Formation Pipeline Metrics",
9503
+ description: "Show aggregated metrics from the Memory Formation Pipeline running in shadow mode. Reports value scores, resolution actions, fact extraction rates, and processing times.",
9504
+ inputSchema: {}
9505
+ },
9506
+ async () => {
9507
+ const summary = getMetricsSummary();
9508
+ if (summary.total === 0) {
9509
+ return {
9510
+ content: [{
9511
+ type: "text",
9512
+ text: "\u{1F4CA} Formation Pipeline: No metrics collected yet.\nStore some observations to start collecting shadow mode data."
9513
+ }]
9514
+ };
9515
+ }
9516
+ const lines = [
9517
+ "\u{1F4CA} **Formation Pipeline Metrics** (shadow mode)",
9518
+ "",
9519
+ `**Total observations processed:** ${summary.total}`,
9520
+ `**Average value score:** ${summary.avgValueScore.toFixed(3)}`,
9521
+ `**Average processing time:** ${summary.avgDurationMs.toFixed(1)}ms`,
9522
+ "",
9523
+ "### Quality Indicators",
9524
+ `- **Avg system-extracted facts:** ${summary.avgExtractedFacts.toFixed(1)} per observation`,
9525
+ `- **Title improved rate:** ${(summary.titleImprovedRate * 100).toFixed(1)}%`,
9526
+ `- **Entity resolved rate:** ${(summary.entityResolvedRate * 100).toFixed(1)}%`,
9527
+ `- **Type corrected rate:** ${(summary.typeCorectedRate * 100).toFixed(1)}%`,
9528
+ "",
9529
+ "### Value Categories"
9530
+ ];
9531
+ for (const [cat, count2] of Object.entries(summary.categoryBreakdown)) {
9532
+ const pct = (count2 / summary.total * 100).toFixed(1);
9533
+ const icon = cat === "core" ? "\u{1F7E2}" : cat === "contextual" ? "\u{1F7E1}" : "\u{1F534}";
9534
+ lines.push(`- ${icon} **${cat}:** ${count2} (${pct}%)`);
9535
+ }
9536
+ lines.push("", "### Resolution Actions");
9537
+ for (const [action, count2] of Object.entries(summary.resolutionBreakdown)) {
9538
+ const pct = (count2 / summary.total * 100).toFixed(1);
9539
+ lines.push(`- **${action}:** ${count2} (${pct}%)`);
9540
+ }
9541
+ return {
9542
+ content: [{ type: "text", text: lines.join("\n") }]
9543
+ };
9544
+ }
9545
+ );
8857
9546
  let enableKG = false;
8858
9547
  try {
8859
9548
  const { homedir: homedir16 } = await import("os");