recallnest 1.4.1 → 2.2.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.
Files changed (166) hide show
  1. package/.dockerignore +11 -0
  2. package/CLAUDE.md +7 -3
  3. package/Dockerfile +20 -0
  4. package/README.md +202 -130
  5. package/README_CN.md +263 -190
  6. package/ROADMAP.md +128 -81
  7. package/SKILL.md +250 -0
  8. package/assets/dashboard.png +0 -0
  9. package/assets/knowledge-graph.png +0 -0
  10. package/assets/screenshots/ui-full.png +0 -0
  11. package/assets/screenshots/ui-overview.png +0 -0
  12. package/assets/screenshots/ui-search.png +0 -0
  13. package/assets/ui/app.js +227 -3
  14. package/assets/ui/index.html +81 -11
  15. package/assets/ui/styles.css +178 -2
  16. package/bin/recallnest.mjs +62 -0
  17. package/bun.lock +337 -0
  18. package/daily-recall.sh +23 -0
  19. package/daily-reflections/2026-04-02.md +35 -0
  20. package/daily-reflections/2026-04-03.md +37 -0
  21. package/daily-reflections/2026-04-04.md +37 -0
  22. package/daily-reflections/2026-04-05.md +35 -0
  23. package/daily-reflections/2026-04-06.md +36 -0
  24. package/daily-reflections/2026-04-07.md +35 -0
  25. package/daily-reflections/2026-04-08.md +35 -0
  26. package/daily-reflections/2026-04-12.md +35 -0
  27. package/data/alias-map.example.json +22 -0
  28. package/docs/plans/2026-04-01-recall-boost.md +100 -0
  29. package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +1195 -0
  30. package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +398 -0
  31. package/glama.json +6 -0
  32. package/integrations/setup-all.sh +31 -0
  33. package/package.json +10 -3
  34. package/plan-philosophy-upgrades.md +363 -0
  35. package/plan-product-upgrades.md +155 -0
  36. package/plan.md +140 -0
  37. package/research-memory-evolution.md +440 -0
  38. package/research-rl-agent-memory.md +330 -0
  39. package/scripts/backfill-anchors.ts +130 -0
  40. package/scripts/diagnose-vector-search.ts +132 -0
  41. package/scripts/migrate-language.ts +75 -0
  42. package/scripts/verify-emotion-decay.ts +148 -0
  43. package/src/__tests__/activity-counter.test.ts +96 -0
  44. package/src/__tests__/admission-control.test.ts +206 -0
  45. package/src/__tests__/anchor-generator.test.ts +67 -0
  46. package/src/__tests__/babel-integration.test.ts +139 -0
  47. package/src/__tests__/capture-engine.test.ts +5 -3
  48. package/src/__tests__/cascade-forget.test.ts +1 -1
  49. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  50. package/src/__tests__/circuit-breaker.test.ts +99 -0
  51. package/src/__tests__/cluster-consolidation.test.ts +712 -0
  52. package/src/__tests__/confidence-metadata.test.ts +125 -0
  53. package/src/__tests__/confidence-tracker.test.ts +53 -0
  54. package/src/__tests__/consolidation-engine.test.ts +1 -1
  55. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  56. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  57. package/src/__tests__/context-reconstructor.test.ts +65 -0
  58. package/src/__tests__/conversation-importer.test.ts +692 -0
  59. package/src/__tests__/data-checkup.test.ts +225 -0
  60. package/src/__tests__/decay-exemption.test.ts +84 -0
  61. package/src/__tests__/dedup-guard.test.ts +39 -0
  62. package/src/__tests__/distill-lock.test.ts +176 -0
  63. package/src/__tests__/dream-pipeline.test.ts +232 -0
  64. package/src/__tests__/emotion-decay.test.ts +46 -0
  65. package/src/__tests__/emotion-detector.test.ts +56 -0
  66. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  67. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  68. package/src/__tests__/ethics-layer.test.ts +476 -0
  69. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  70. package/src/__tests__/frequency-tracker.test.ts +113 -0
  71. package/src/__tests__/graph-export.test.ts +489 -0
  72. package/src/__tests__/interference-detector.test.ts +203 -0
  73. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  74. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +243 -0
  75. package/src/__tests__/memory-evolution.test.ts +271 -0
  76. package/src/__tests__/memory-lint.test.ts +271 -0
  77. package/src/__tests__/narrative-architecture.test.ts +456 -0
  78. package/src/__tests__/pending-review.test.ts +77 -0
  79. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  80. package/src/__tests__/prediction-engine.test.ts +349 -0
  81. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  82. package/src/__tests__/preference-slots.test.ts +99 -0
  83. package/src/__tests__/query-expander-alias.test.ts +89 -0
  84. package/src/__tests__/recall-governor.test.ts +227 -0
  85. package/src/__tests__/resume-context.test.ts +156 -1
  86. package/src/__tests__/salience-scoring.test.ts +106 -0
  87. package/src/__tests__/session-distiller.test.ts +611 -0
  88. package/src/__tests__/skill-engine.test.ts +546 -0
  89. package/src/__tests__/skill-promotion.test.ts +349 -0
  90. package/src/__tests__/supersede-chain.test.ts +154 -0
  91. package/src/__tests__/temporal-validity.test.ts +160 -0
  92. package/src/__tests__/topic-tag.test.ts +113 -0
  93. package/src/__tests__/trusted-memory.test.ts +828 -0
  94. package/src/__tests__/write-verifier.test.ts +122 -0
  95. package/src/access-tracker.ts +3 -2
  96. package/src/activity-counter.ts +109 -0
  97. package/src/adaptive-retrieval.ts +6 -5
  98. package/src/admission-control.ts +163 -0
  99. package/src/anchor-generator.ts +80 -0
  100. package/src/api-server.ts +10 -0
  101. package/src/audit-log.ts +122 -0
  102. package/src/auto-gc.ts +98 -29
  103. package/src/auto-recall.ts +33 -4
  104. package/src/capture-engine.ts +372 -23
  105. package/src/cascade-forget.ts +3 -2
  106. package/src/cli.ts +107 -5
  107. package/src/compat.ts +22 -0
  108. package/src/confidence-tracker.ts +210 -47
  109. package/src/conflict-store.ts +12 -3
  110. package/src/consolidation-engine.ts +296 -8
  111. package/src/context-collapse-renderer.ts +222 -0
  112. package/src/context-composer-stable-selection.ts +5 -1
  113. package/src/context-composer.ts +239 -4
  114. package/src/context-reconstructor.ts +404 -0
  115. package/src/conversation-importer.ts +441 -0
  116. package/src/data-checkup.ts +232 -0
  117. package/src/decay-engine.ts +84 -0
  118. package/src/distill-lock.ts +122 -0
  119. package/src/doctor.ts +8 -10
  120. package/src/dream-pipeline.ts +227 -0
  121. package/src/emotion-detector.ts +90 -0
  122. package/src/eval.ts +3 -2
  123. package/src/forget-engine.ts +269 -0
  124. package/src/frequency-tracker.ts +175 -0
  125. package/src/graph-export.ts +644 -0
  126. package/src/ingest.ts +36 -6
  127. package/src/interference-detector.ts +187 -0
  128. package/src/kg-extractor.ts +15 -24
  129. package/src/kg-store.ts +2 -1
  130. package/src/language-hook.ts +67 -0
  131. package/src/llm-client.ts +289 -6
  132. package/src/mcp-server.ts +707 -80
  133. package/src/memory-assets.ts +22 -5
  134. package/src/memory-evolution.ts +365 -0
  135. package/src/memory-lint.ts +429 -0
  136. package/src/memory-output.ts +69 -0
  137. package/src/memory-schema.ts +74 -2
  138. package/src/narrative-schema.ts +79 -0
  139. package/src/narrative-tagger.ts +164 -0
  140. package/src/pii-detector.ts +98 -0
  141. package/src/post-retrieval-filter.ts +76 -0
  142. package/src/prediction-engine.ts +253 -0
  143. package/src/preference-slots.ts +65 -2
  144. package/src/prospective-memory.ts +215 -0
  145. package/src/query-expander.ts +67 -0
  146. package/src/recall-governor.ts +183 -0
  147. package/src/retention-policy.ts +90 -0
  148. package/src/retriever.ts +828 -55
  149. package/src/rif.ts +34 -8
  150. package/src/runtime-config.ts +23 -8
  151. package/src/scope-inventory.ts +4 -3
  152. package/src/scope-policy.ts +4 -0
  153. package/src/session-distiller.ts +307 -0
  154. package/src/session-output.ts +40 -0
  155. package/src/session-schema.ts +37 -0
  156. package/src/session-store.ts +7 -2
  157. package/src/skill-engine.ts +208 -0
  158. package/src/skill-promotion.ts +293 -0
  159. package/src/skill-schema.ts +39 -0
  160. package/src/store.ts +96 -10
  161. package/src/topic-tag.ts +66 -0
  162. package/src/tracker.ts +3 -1
  163. package/src/ui-server.ts +80 -8
  164. package/src/workflow-observation-scope-review.ts +2 -1
  165. package/src/workflow-observation-store.ts +2 -1
  166. package/src/write-verifier.ts +126 -0
package/src/store.ts CHANGED
@@ -9,6 +9,7 @@ import { dirname } from "node:path";
9
9
  import { logWarn } from "./stderr-log.js";
10
10
  import type { DurableMemoryCategory } from "./memory-schema.js";
11
11
  import { matchesScopeFilter } from "./scope-policy.js";
12
+ import { detectEmotionIfEnabled } from "./emotion-detector.js";
12
13
 
13
14
  // ============================================================================
14
15
  // Types
@@ -42,6 +43,8 @@ export interface MemoryEntry {
42
43
  importance: number;
43
44
  timestamp: number;
44
45
  metadata?: string; // JSON string for extensible metadata — includes l0_abstract/l1_overview/l2_content/tier
46
+ language?: string; // ISO 639-1: "zh"|"ja"|"ko"|"en"
47
+ fts_text?: string; // Pre-tokenized text for FTS indexing
45
48
  }
46
49
 
47
50
  export interface MemorySearchResult {
@@ -258,14 +261,24 @@ export class MemoryStore {
258
261
  try {
259
262
  table = await db.openTable(TABLE_NAME);
260
263
 
261
- // Check if we need to add scope column for backward compatibility
264
+ // Backward compatibility: add missing columns to existing tables
262
265
  try {
263
266
  const sample = await table.query().limit(1).toArray();
264
- if (sample.length > 0 && !("scope" in sample[0])) {
265
- logWarn("Adding scope column for backward compatibility with existing data");
267
+ if (sample.length > 0) {
268
+ if (!("scope" in sample[0])) {
269
+ logWarn("Adding scope column for backward compatibility with existing data");
270
+ }
271
+ if (!("language" in sample[0])) {
272
+ logWarn("Adding language column for backward compatibility");
273
+ await table.addColumns([{ name: "language", valueSql: "'en'" }]);
274
+ }
275
+ if (!("fts_text" in sample[0])) {
276
+ logWarn("Adding fts_text column for backward compatibility");
277
+ await table.addColumns([{ name: "fts_text", valueSql: "text" }]);
278
+ }
266
279
  }
267
280
  } catch (err) {
268
- logWarn("Could not check table schema:", err);
281
+ logWarn("Could not check/migrate table schema:", err);
269
282
  }
270
283
  } catch (_openErr) {
271
284
  // Table doesn't exist yet — create it
@@ -278,6 +291,8 @@ export class MemoryStore {
278
291
  importance: 0,
279
292
  timestamp: 0,
280
293
  metadata: "{}",
294
+ language: "en",
295
+ fts_text: "__schema__",
281
296
  };
282
297
 
283
298
  try {
@@ -325,13 +340,13 @@ export class MemoryStore {
325
340
  // Check if FTS index already exists
326
341
  const indices = await table.listIndices();
327
342
  const hasFtsIndex = indices?.some((idx: any) =>
328
- idx.indexType === "FTS" || idx.columns?.includes("text")
343
+ idx.indexType === "FTS" || idx.columns?.includes("fts_text")
329
344
  );
330
345
 
331
346
  if (!hasFtsIndex) {
332
347
  // LanceDB @lancedb/lancedb >=0.26: use Index.fts() config
333
348
  const lancedb = await loadLanceDB();
334
- await table.createIndex("text", {
349
+ await table.createIndex("fts_text", {
335
350
  config: (lancedb as any).Index.fts(),
336
351
  });
337
352
  }
@@ -348,8 +363,17 @@ export class MemoryStore {
348
363
  id: entry.id || deterministicId(entry.scope, entry.text),
349
364
  timestamp: Date.now(),
350
365
  metadata: entry.metadata || "{}",
366
+ language: entry.language || "en",
367
+ fts_text: entry.fts_text || entry.text,
351
368
  };
352
369
 
370
+ const emotionResult = detectEmotionIfEnabled(fullEntry.text);
371
+ if (emotionResult) {
372
+ const existingMeta = JSON.parse(fullEntry.metadata || "{}");
373
+ existingMeta.emotion = emotionResult;
374
+ fullEntry.metadata = JSON.stringify(existingMeta);
375
+ }
376
+
353
377
  try {
354
378
  await this.table!.add([fullEntry]);
355
379
  } catch (err: any) {
@@ -375,10 +399,22 @@ export class MemoryStore {
375
399
  id: deterministicId(entry.scope, entry.text),
376
400
  timestamp: Date.now(),
377
401
  metadata: entry.metadata || "{}",
402
+ language: entry.language || "en",
403
+ fts_text: entry.fts_text || entry.text,
378
404
  }));
379
405
 
406
+ const enrichedEntries = fullEntries.map(e => {
407
+ const emotionResult = detectEmotionIfEnabled(e.text);
408
+ if (emotionResult) {
409
+ const meta = JSON.parse(e.metadata || "{}");
410
+ meta.emotion = emotionResult;
411
+ return { ...e, metadata: JSON.stringify(meta) };
412
+ }
413
+ return e;
414
+ });
415
+
380
416
  try {
381
- await this.table!.add(fullEntries);
417
+ await this.table!.add(enrichedEntries);
382
418
  } catch (err: any) {
383
419
  const code = err.code || "";
384
420
  const message = err.message || String(err);
@@ -414,8 +450,22 @@ export class MemoryStore {
414
450
  importance: Number.isFinite(entry.importance) ? entry.importance : 0.5,
415
451
  timestamp: Number.isFinite(entry.timestamp) ? entry.timestamp : Date.now(),
416
452
  metadata: entry.metadata || "{}",
453
+ language: entry.language || "en",
454
+ fts_text: entry.fts_text || entry.text,
417
455
  };
418
456
 
457
+ // HP-emo: Backfill emotion if absent during import
458
+ try {
459
+ const metaParsed = JSON.parse(full.metadata || "{}");
460
+ if (!metaParsed.emotion) {
461
+ const emotionResult = detectEmotionIfEnabled(full.text);
462
+ if (emotionResult) {
463
+ metaParsed.emotion = emotionResult;
464
+ full.metadata = JSON.stringify(metaParsed);
465
+ }
466
+ }
467
+ } catch { /* malformed metadata — skip backfill */ }
468
+
419
469
  await this.table!.add([full]);
420
470
  return full;
421
471
  }
@@ -716,6 +766,30 @@ export class MemoryStore {
716
766
  };
717
767
  }
718
768
 
769
+ /**
770
+ * Batch-fetch vectors for a list of entry IDs.
771
+ * Used by graph-export for cross-scope semantic bridge computation.
772
+ * Single LanceDB query — much faster than N individual getById calls.
773
+ */
774
+ async getVectors(ids: string[]): Promise<Map<string, number[]>> {
775
+ await this.ensureInitialized();
776
+ if (ids.length === 0) return new Map();
777
+
778
+ const conditions = ids.map(id => `id = '${escapeSqlLiteral(id)}'`).join(" OR ");
779
+ const rows = await this.table!.query()
780
+ .select(["id", "vector"])
781
+ .where(conditions)
782
+ .limit(ids.length)
783
+ .toArray();
784
+
785
+ const result = new Map<string, number[]>();
786
+ for (const row of rows) {
787
+ const vec = Array.from(row.vector as Iterable<number>);
788
+ if (vec.length > 0) result.set(row.id as string, vec);
789
+ }
790
+ return result;
791
+ }
792
+
719
793
  async stats(scopeFilter?: string[]): Promise<{
720
794
  totalCount: number;
721
795
  scopeCounts: Record<string, number>;
@@ -800,7 +874,7 @@ export class MemoryStore {
800
874
 
801
875
  async update(
802
876
  id: string,
803
- updates: { text?: string; vector?: number[]; importance?: number; category?: MemoryEntry["category"]; metadata?: string; timestamp?: number },
877
+ updates: { text?: string; vector?: number[]; importance?: number; category?: MemoryEntry["category"]; metadata?: string; timestamp?: number; language?: string; fts_text?: string },
804
878
  scopeFilter?: string[]
805
879
  ): Promise<MemoryEntry | null> {
806
880
  await this.ensureInitialized();
@@ -821,7 +895,7 @@ export class MemoryStore {
821
895
  rows = await this.table!.query().where(`id = '${safeId}'`).limit(1).toArray();
822
896
  } else {
823
897
  // Prefix match
824
- const all = await this.table!.query().select(["id", "text", "vector", "category", "scope", "importance", "timestamp", "metadata"]).toArray();
898
+ const all = await this.table!.query().select(["id", "text", "vector", "category", "scope", "importance", "timestamp", "metadata", "language", "fts_text"]).toArray();
825
899
  rows = all.filter((r: any) => (r.id as string).startsWith(id));
826
900
  if (rows.length > 1) {
827
901
  throw new Error(`Ambiguous prefix "${id}" matches ${rows.length} memories. Use a longer prefix or full ID.`);
@@ -838,7 +912,7 @@ export class MemoryStore {
838
912
  throw new Error(`Memory ${id} is outside accessible scopes`);
839
913
  }
840
914
 
841
- // Build updated entry, preserving original timestamp
915
+ // Build updated entry, preserving original timestamp and language fields
842
916
  const updated: MemoryEntry = {
843
917
  id: row.id as string,
844
918
  text: updates.text ?? (row.text as string),
@@ -848,8 +922,20 @@ export class MemoryStore {
848
922
  importance: updates.importance ?? Number(row.importance),
849
923
  timestamp: updates.timestamp ?? Number(row.timestamp),
850
924
  metadata: updates.metadata ?? ((row.metadata as string) || "{}"),
925
+ language: updates.language ?? ((row.language as string) || "en"),
926
+ fts_text: updates.fts_text ?? ((row.fts_text as string) || (updates.text ?? (row.text as string))),
851
927
  };
852
928
 
929
+ // HP-emo: Re-detect emotion when text changes
930
+ if (updates.text) {
931
+ const emotionResult = detectEmotionIfEnabled(updates.text);
932
+ if (emotionResult) {
933
+ const meta = JSON.parse(updated.metadata || "{}");
934
+ meta.emotion = emotionResult;
935
+ updated.metadata = JSON.stringify(meta);
936
+ }
937
+ }
938
+
853
939
  // LanceDB doesn't support in-place update; delete + re-add
854
940
  const resolvedId = escapeSqlLiteral(row.id as string);
855
941
  await this.table!.delete(`id = '${resolvedId}'`);
@@ -0,0 +1,66 @@
1
+ /**
2
+ * MP-1: Topic Tag — scope 内主题分区
3
+ *
4
+ * Keyword-based topic detection for memory entries.
5
+ * Tags are stored in metadata and used as a pre-filter during retrieval.
6
+ */
7
+
8
+ const TOPIC_PATTERNS: Array<{ tag: string; patterns: RegExp[] }> = [
9
+ { tag: "auth", patterns: [/\b(auth|login|oauth|jwt|session.?token|credentials?|password|sso|saml)\b/i] },
10
+ { tag: "deploy", patterns: [/\b(deploy|ci[\s/]?cd|pipeline|release|rollout|staging|production|rollback)\b/i] },
11
+ { tag: "infra", patterns: [/\b(docker|kubernetes|k8s|terraform|nginx|server|cluster|load.?balanc|cdn|dns)\b/i] },
12
+ { tag: "testing", patterns: [/\b(test|spec|jest|vitest|bun.?test|coverage|assertion|mock|fixture|e2e)\b/i] },
13
+ { tag: "database", patterns: [/\b(database|sql|postgres|mysql|sqlite|lancedb|mongodb|migration|schema|index)\b/i] },
14
+ { tag: "api", patterns: [/\b(api|endpoint|rest|graphql|grpc|webhook|http|request|response|route)\b/i] },
15
+ { tag: "ui", patterns: [/\b(ui|frontend|react|vue|svelte|css|tailwind|component|layout|responsive)\b/i] },
16
+ { tag: "perf", patterns: [/\b(performance|latency|throughput|cache|optimization|bottleneck|profil|benchmark)\b/i] },
17
+ { tag: "security", patterns: [/\b(security|vulnerability|xss|csrf|injection|encrypt|certificate|tls|ssl)\b/i] },
18
+ { tag: "docs", patterns: [/\b(documentation|readme|changelog|tutorial|guide|specification|api.?doc)\b/i] },
19
+ { tag: "memory", patterns: [/\b(memory|recall|retriev|embed|vector|checkpoint|distill|ingest|lancedb)\b/i] },
20
+ { tag: "mcp", patterns: [/\b(mcp|model.?context.?protocol|tool.?registr|mcp.?server)\b/i] },
21
+ { tag: "llm", patterns: [/\b(llm|language.?model|prompt|token|context.?window|embedding|openai|anthropic|claude)\b/i] },
22
+ { tag: "config", patterns: [/\b(config|settings?|environment|env.?var|dotenv|feature.?flag)\b/i] },
23
+ { tag: "data", patterns: [/\b(data|dataset|csv|json|parsing|transform|etl|pipeline|ingest)\b/i] },
24
+ ];
25
+
26
+ const MAX_SCAN_LENGTH = 2000;
27
+
28
+ export function detectTopicTag(text: string): string | undefined {
29
+ const sample = text.slice(0, MAX_SCAN_LENGTH).toLowerCase();
30
+ let bestTag: string | undefined;
31
+ let bestCount = 0;
32
+
33
+ for (const { tag, patterns } of TOPIC_PATTERNS) {
34
+ let count = 0;
35
+ for (const pattern of patterns) {
36
+ const matches = sample.match(new RegExp(pattern.source, "gi"));
37
+ if (matches) count += matches.length;
38
+ }
39
+ if (count > bestCount) {
40
+ bestCount = count;
41
+ bestTag = tag;
42
+ }
43
+ }
44
+
45
+ return bestCount >= 1 ? bestTag : undefined;
46
+ }
47
+
48
+ export function extractTopicTag(metadata?: string): string | undefined {
49
+ if (!metadata) return undefined;
50
+ try {
51
+ const parsed = JSON.parse(metadata);
52
+ return typeof parsed.topicTag === "string" ? parsed.topicTag : undefined;
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ }
57
+
58
+ export function injectTopicTag(metadata: string, topicTag: string): string {
59
+ try {
60
+ const parsed = JSON.parse(metadata);
61
+ parsed.topicTag = topicTag;
62
+ return JSON.stringify(parsed);
63
+ } catch {
64
+ return metadata;
65
+ }
66
+ }
package/src/tracker.ts CHANGED
@@ -5,7 +5,9 @@
5
5
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
7
 
8
- const TRACKER_PATH = resolve(import.meta.dir, "../data/ingested-files.json");
8
+ import { metaDir } from "./compat.js";
9
+
10
+ const TRACKER_PATH = resolve(metaDir(import.meta), "../data/ingested-files.json");
9
11
 
10
12
  interface TrackerData {
11
13
  files: Record<string, { ingestedAt: string; size: number; chunks: number; mtimeMs?: number }>;
package/src/ui-server.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import { existsSync, realpathSync } from "node:fs";
4
4
  import { join, resolve, sep } from "node:path";
5
5
 
6
+ import { metaDir } from "./compat.js";
6
7
  import type { RetrievalResult } from "./retriever.js";
7
8
  import type { MemoryStore } from "./store.js";
8
9
  import { distillResults, formatExplainResults, formatSearchResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
@@ -83,8 +84,8 @@ function errorResponse(error: unknown): Response {
83
84
  function isAllowedArtifactPath(targetPath: string): boolean {
84
85
  try {
85
86
  const resolved = realpathSync(targetPath);
86
- const exportsDir = resolve(import.meta.dir, "../data/exports");
87
- const pinsDir = resolve(import.meta.dir, "../data/pins");
87
+ const exportsDir = resolve(metaDir(import.meta), "../data/exports");
88
+ const pinsDir = resolve(metaDir(import.meta), "../data/pins");
88
89
  return (
89
90
  resolved === exportsDir ||
90
91
  resolved.startsWith(`${exportsDir}${sep}`) ||
@@ -141,7 +142,7 @@ function serializeResults(results: RetrievalResult[]) {
141
142
  }
142
143
 
143
144
  function serveStatic(pathname: string): Response | null {
144
- const uiDir = resolve(import.meta.dir, "../assets/ui");
145
+ const uiDir = resolve(metaDir(import.meta), "../assets/ui");
145
146
  const relativePath = pathname === "/"
146
147
  ? join(uiDir, "index.html")
147
148
  : join(uiDir, pathname.replace(/^\/ui\//, ""));
@@ -157,14 +158,17 @@ function serveStatic(pathname: string): Response | null {
157
158
  async function handleSearch(mode: "search" | "explain" | "distill", body: Record<string, any>) {
158
159
  const query = requireString(body.query, "query");
159
160
  const { retriever, profile } = getComponents(readOptionalString(body.profile));
161
+ const explicitScope = readOptionalString(body.scope);
160
162
  const results = await retriever.retrieve(buildRetrievalContext({
161
163
  query,
162
164
  limit: readLimit(body.limit, 5, 1, 20),
163
- scope: readOptionalString(body.scope),
165
+ scope: explicitScope,
164
166
  sessionId: readOptionalString(body.sessionId),
165
- allScopes: body.allScopes === true,
167
+ allScopes: explicitScope ? false : true,
168
+ topicTag: readOptionalString(body.topicTag),
166
169
  }, {
167
170
  operation: `ui:${mode}`,
171
+ allowUnscoped: true,
168
172
  }));
169
173
  const context = { query, profile: profile.name };
170
174
  const output = mode === "explain"
@@ -260,6 +264,28 @@ const server = Bun.serve({
260
264
  });
261
265
  }
262
266
 
267
+ if (request.method === "GET" && url.pathname === "/api/skills") {
268
+ const { store, embedder } = getComponents();
269
+ const { retrieveSkills } = await import("./skill-engine.js");
270
+ const scope = url.searchParams.get("scope") || undefined;
271
+ const skills = await retrieveSkills(store, embedder, "all skills", scope, 20);
272
+ return Response.json({
273
+ items: skills.map(({ skill, score }) => ({
274
+ shortId: skill.id.slice(0, 8),
275
+ name: skill.name,
276
+ description: skill.description,
277
+ trigger: skill.triggerPattern,
278
+ type: skill.implementationType,
279
+ implementation: skill.implementation,
280
+ verification: skill.verification,
281
+ scope: skill.scope,
282
+ tags: skill.tags,
283
+ score: Math.round(score * 100),
284
+ storedAt: skill.storedAt,
285
+ })),
286
+ });
287
+ }
288
+
263
289
  if (request.method === "GET" && url.pathname === "/api/stats") {
264
290
  const { store } = getComponents();
265
291
  const stats = await store.stats();
@@ -296,7 +322,7 @@ const server = Bun.serve({
296
322
  const scopeSelection = resolveScopeSelection({
297
323
  scope: readOptionalString(body.scope),
298
324
  sessionId: readOptionalString(body.sessionId),
299
- allScopes: body.allScopes === true,
325
+ allScopes: readOptionalString(body.scope) ? false : true,
300
326
  operation: "ui:pin",
301
327
  });
302
328
  const entry = await store.get(memoryId, scopeSelection.scopeFilter);
@@ -327,7 +353,7 @@ const server = Bun.serve({
327
353
  limit: readLimit(body.limit, 8, 1, 20),
328
354
  scope: readOptionalString(body.scope),
329
355
  sessionId: readOptionalString(body.sessionId),
330
- allScopes: body.allScopes === true,
356
+ allScopes: readOptionalString(body.scope) ? false : true,
331
357
  }, {
332
358
  operation: "ui:brief",
333
359
  }));
@@ -377,7 +403,7 @@ const server = Bun.serve({
377
403
  limit: readLimit(body.limit, 8, 1, 20),
378
404
  scope: readOptionalString(body.scope),
379
405
  sessionId: readOptionalString(body.sessionId),
380
- allScopes: body.allScopes === true,
406
+ allScopes: readOptionalString(body.scope) ? false : true,
381
407
  }, {
382
408
  operation: "ui:export",
383
409
  }));
@@ -420,6 +446,52 @@ const server = Bun.serve({
420
446
  });
421
447
  }
422
448
 
449
+ // ----- Dashboard API -----
450
+ if (request.method === "GET" && url.pathname === "/api/dashboard-stats") {
451
+ const { store } = getComponents();
452
+ const stats = await store.stats();
453
+ const allEntries = await store.list(undefined, undefined, 10000, 0);
454
+
455
+ const now = Date.now();
456
+ const weekAgo = now - 7 * 24 * 60 * 60 * 1000;
457
+ const monthAgo = now - 30 * 24 * 60 * 60 * 1000;
458
+ const thisWeek = allEntries.filter(e => e.timestamp > weekAgo).length;
459
+ const thisMonth = allEntries.filter(e => e.timestamp > monthAgo).length;
460
+
461
+ return Response.json({
462
+ totalCount: stats.totalCount,
463
+ categoryCounts: stats.categoryCounts,
464
+ scopeCounts: stats.scopeCounts,
465
+ growth: { thisWeek, thisMonth },
466
+ });
467
+ }
468
+
469
+ if (request.method === "GET" && url.pathname === "/api/stale-memories") {
470
+ const { store } = getComponents();
471
+ const { runMemoryLint } = await import("./memory-lint.js");
472
+ const report = await runMemoryLint({ store });
473
+ const staleFindings = report.findings.filter(f => f.check === "stale");
474
+ return Response.json({
475
+ count: staleFindings.length,
476
+ items: staleFindings.slice(0, 20).map(f => ({
477
+ detail: f.detail,
478
+ memoryIds: f.memoryIds,
479
+ })),
480
+ });
481
+ }
482
+
483
+ if (request.method === "GET" && url.pathname === "/api/lint-summary") {
484
+ const { store } = getComponents();
485
+ const { runMemoryLint } = await import("./memory-lint.js");
486
+ const report = await runMemoryLint({ store });
487
+ return Response.json({
488
+ healthScore: report.healthScore,
489
+ totalScanned: report.totalScanned,
490
+ summary: report.summary,
491
+ timestamp: report.timestamp,
492
+ });
493
+ }
494
+
423
495
  return new Response("Not Found", { status: 404 });
424
496
  } catch (error) {
425
497
  return errorResponse(error);
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
2
  import { basename, join, resolve } from "node:path";
3
3
 
4
+ import { metaDir } from "./compat.js";
4
5
  import { buildLegacyScopeKeepReview, suppressesLegacyScopeIssue } from "./legacy-scope-review.js";
5
6
  import { classifyLegacyScope } from "./store.js";
6
7
 
@@ -66,7 +67,7 @@ export interface WorkflowObservationScopeReviewOptions {
66
67
  }
67
68
 
68
69
  function defaultDir(): string {
69
- return resolve(import.meta.dir, "../data/workflow-observations");
70
+ return resolve(metaDir(import.meta), "../data/workflow-observations");
70
71
  }
71
72
 
72
73
  function clip(text: string, maxLen = 72): string {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
 
4
+ import { metaDir } from "./compat.js";
4
5
  import type { WorkflowObservationOutcome, WorkflowObservationRecord } from "./workflow-observation-schema.js";
5
6
  import { WorkflowObservationRecordSchema } from "./workflow-observation-schema.js";
6
7
 
@@ -26,7 +27,7 @@ function sortNewestFirst(records: WorkflowObservationRecord[]): WorkflowObservat
26
27
  }
27
28
 
28
29
  export class WorkflowObservationStore {
29
- constructor(private readonly dir = resolve(import.meta.dir, "../data/workflow-observations")) {}
30
+ constructor(private readonly dir = resolve(metaDir(import.meta), "../data/workflow-observations")) {}
30
31
 
31
32
  get dataDir(): string {
32
33
  return ensureDir(this.dir);
@@ -0,0 +1,126 @@
1
+ /**
2
+ * HP-2: Formation Review Pass — post-write verification.
3
+ * After storing a memory, verify: embedding exists, metadata complete, scope set.
4
+ * Non-blocking: failures are logged but never block the write path.
5
+ */
6
+
7
+ import type { MemoryStore, MemoryEntry } from "./store.js";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Config
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export interface WriteVerifierConfig {
14
+ /** Enable/disable verification (default: true) */
15
+ enabled: boolean;
16
+ /** Maximum time to wait for verification in ms (default: 2000) */
17
+ timeoutMs: number;
18
+ }
19
+
20
+ export const DEFAULT_VERIFIER_CONFIG: WriteVerifierConfig = {
21
+ enabled: true,
22
+ timeoutMs: 2000,
23
+ };
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Verification result
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export type VerificationIssue =
30
+ | "missing_entry"
31
+ | "missing_vector"
32
+ | "missing_scope"
33
+ | "missing_importance"
34
+ | "empty_text";
35
+
36
+ export interface VerificationResult {
37
+ ok: boolean;
38
+ entryId: string;
39
+ issues: VerificationIssue[];
40
+ durationMs: number;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Public API
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Verify a just-written memory entry by reading it back.
49
+ * Returns issues found (empty = all good).
50
+ */
51
+ export async function verifyWrite(
52
+ store: Pick<MemoryStore, "get">,
53
+ entryId: string,
54
+ cfg?: Partial<WriteVerifierConfig>,
55
+ ): Promise<VerificationResult> {
56
+ const config = { ...DEFAULT_VERIFIER_CONFIG, ...cfg };
57
+ const start = Date.now();
58
+
59
+ if (!config.enabled) {
60
+ return { ok: true, entryId, issues: [], durationMs: 0 };
61
+ }
62
+
63
+ try {
64
+ const entry = await Promise.race([
65
+ store.get(entryId),
66
+ new Promise<null>((_, reject) =>
67
+ setTimeout(() => reject(new Error("timeout")), config.timeoutMs),
68
+ ),
69
+ ]) as MemoryEntry | null;
70
+
71
+ const issues = checkEntry(entry, entryId);
72
+ return {
73
+ ok: issues.length === 0,
74
+ entryId,
75
+ issues,
76
+ durationMs: Date.now() - start,
77
+ };
78
+ } catch {
79
+ return {
80
+ ok: false,
81
+ entryId,
82
+ issues: ["missing_entry"],
83
+ durationMs: Date.now() - start,
84
+ };
85
+ }
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Internal checks
90
+ // ---------------------------------------------------------------------------
91
+
92
+ function checkEntry(entry: MemoryEntry | null, id: string): VerificationIssue[] {
93
+ const issues: VerificationIssue[] = [];
94
+
95
+ if (!entry) {
96
+ issues.push("missing_entry");
97
+ return issues;
98
+ }
99
+
100
+ // Vector should exist (embedding succeeded)
101
+ if (!entry.vector || (Array.isArray(entry.vector) && entry.vector.length === 0)) {
102
+ issues.push("missing_vector");
103
+ }
104
+
105
+ // Text should not be empty
106
+ if (!entry.text || entry.text.trim().length === 0) {
107
+ issues.push("empty_text");
108
+ }
109
+
110
+ // Metadata should contain scope
111
+ if (entry.metadata) {
112
+ try {
113
+ const meta = JSON.parse(entry.metadata);
114
+ if (!meta.scope) issues.push("missing_scope");
115
+ if (typeof meta.importance !== "number") issues.push("missing_importance");
116
+ } catch {
117
+ issues.push("missing_scope");
118
+ issues.push("missing_importance");
119
+ }
120
+ } else {
121
+ issues.push("missing_scope");
122
+ issues.push("missing_importance");
123
+ }
124
+
125
+ return issues;
126
+ }