@plur-ai/core 0.4.0 → 0.4.2

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.
@@ -62,32 +62,42 @@ function computeIdf(engrams, queryTokens) {
62
62
  }
63
63
  return idf;
64
64
  }
65
- function ftsScore(engram, queryTokens, idfWeights) {
65
+ var BM25_K1 = 1.2;
66
+ var BM25_B = 0.75;
67
+ function ftsScore(engram, queryTokens, idfWeights, avgDocLength) {
66
68
  const allTerms = ftsTokenize(engramSearchText(engram));
67
69
  if (queryTokens.length === 0) return 0;
68
- let weightedHits = 0;
69
- let totalWeight = 0;
70
+ const docLen = allTerms.length;
71
+ const avgdl = avgDocLength && avgDocLength > 0 ? avgDocLength : docLen;
72
+ const hasNonZeroIdf = idfWeights && Array.from(idfWeights.values()).some((v) => v > 0);
73
+ let score = 0;
70
74
  for (const qt of queryTokens) {
71
- const weight = idfWeights?.get(qt) ?? 1;
72
- totalWeight += weight;
73
- if (allTerms.some((t) => t.includes(qt) || qt.includes(t))) {
74
- weightedHits += weight;
75
+ let effectiveIdf;
76
+ if (!idfWeights) {
77
+ effectiveIdf = 1;
78
+ } else if (hasNonZeroIdf) {
79
+ effectiveIdf = idfWeights.get(qt) ?? 0;
80
+ if (effectiveIdf === 0) continue;
81
+ } else {
82
+ effectiveIdf = 1;
75
83
  }
76
- }
77
- if (totalWeight === 0) {
78
- let matches = 0;
79
- for (const qt of queryTokens) {
80
- if (allTerms.some((t) => t.includes(qt) || qt.includes(t))) matches++;
84
+ let tf = 0;
85
+ for (const t of allTerms) {
86
+ if (t.includes(qt) || qt.includes(t)) tf++;
81
87
  }
82
- return matches / queryTokens.length;
88
+ if (tf === 0) continue;
89
+ const numerator = tf * (BM25_K1 + 1);
90
+ const denominator = tf + BM25_K1 * (1 - BM25_B + BM25_B * docLen / avgdl);
91
+ score += effectiveIdf * (numerator / denominator);
83
92
  }
84
- return weightedHits / totalWeight;
93
+ return score;
85
94
  }
86
95
  function searchEngrams(engrams, query, limit = 20) {
87
96
  const queryTokens = ftsTokenize(query);
88
97
  if (queryTokens.length === 0) return [];
89
98
  const idfWeights = computeIdf(engrams, queryTokens);
90
- return engrams.map((e) => ({ engram: e, score: ftsScore(e, queryTokens, idfWeights) })).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((r) => r.engram);
99
+ const avgDocLength = engrams.length > 0 ? engrams.reduce((sum, e) => sum + ftsTokenize(engramSearchText(e)).length, 0) / engrams.length : 0;
100
+ return engrams.map((e) => ({ engram: e, score: ftsScore(e, queryTokens, idfWeights, avgDocLength) })).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((r) => r.engram);
91
101
  }
92
102
 
93
103
  // src/embeddings.ts
@@ -97,7 +107,7 @@ import { createHash } from "crypto";
97
107
 
98
108
  // src/sync.ts
99
109
  import { execFileSync } from "child_process";
100
- import { existsSync, writeFileSync, renameSync, mkdirSync } from "fs";
110
+ import { existsSync, writeFileSync, renameSync, mkdirSync, unlinkSync, statSync } from "fs";
101
111
  import { join, dirname } from "path";
102
112
  var GITIGNORE = `# PLUR \u2014 derived/cache files (regenerated automatically)
103
113
  embeddings/
@@ -249,6 +259,44 @@ function sync(root, remote) {
249
259
  files_changed: filesChanged
250
260
  };
251
261
  }
262
+ function withLock(filePath, fn, options) {
263
+ const lockPath = filePath + ".lock";
264
+ const maxRetries = options?.maxRetries ?? 5;
265
+ const baseDelay = options?.baseDelay ?? 100;
266
+ const staleThreshold = options?.staleThreshold ?? 1e4;
267
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
268
+ try {
269
+ writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
270
+ break;
271
+ } catch (err) {
272
+ if (err.code !== "EEXIST") throw err;
273
+ try {
274
+ const stat = statSync(lockPath);
275
+ if (Date.now() - stat.mtimeMs > staleThreshold) {
276
+ unlinkSync(lockPath);
277
+ continue;
278
+ }
279
+ } catch {
280
+ continue;
281
+ }
282
+ if (attempt === maxRetries) {
283
+ throw new Error(`Failed to acquire lock on ${filePath} after ${maxRetries} retries`);
284
+ }
285
+ const delay = baseDelay * Math.pow(2, attempt);
286
+ const end = Date.now() + delay;
287
+ while (Date.now() < end) {
288
+ }
289
+ }
290
+ }
291
+ try {
292
+ return fn();
293
+ } finally {
294
+ try {
295
+ unlinkSync(lockPath);
296
+ } catch {
297
+ }
298
+ }
299
+ }
252
300
  function atomicWrite(filePath, content) {
253
301
  const dir = dirname(filePath);
254
302
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
@@ -337,6 +385,7 @@ async function embeddingSearch(engrams, query, limit, storagePath) {
337
385
  export {
338
386
  getSyncStatus,
339
387
  sync,
388
+ withLock,
340
389
  atomicWrite,
341
390
  ftsTokenize,
342
391
  engramSearchText,
@@ -2,7 +2,7 @@ import {
2
2
  cosineSimilarity,
3
3
  embed,
4
4
  embeddingSearch
5
- } from "./chunk-WPD4MPTT.js";
5
+ } from "./chunk-KMVQYBNP.js";
6
6
  import "./chunk-2ZDO52B4.js";
7
7
  export {
8
8
  cosineSimilarity,
package/dist/index.d.ts CHANGED
@@ -616,6 +616,7 @@ declare const PlurConfigSchema: z.ZodObject<{
616
616
  co_access?: boolean | undefined;
617
617
  }>>>;
618
618
  allow_secrets: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
619
+ index: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
619
620
  }, "strip", z.ZodTypeAny, {
620
621
  auto_learn?: boolean | undefined;
621
622
  auto_capture?: boolean | undefined;
@@ -629,6 +630,7 @@ declare const PlurConfigSchema: z.ZodObject<{
629
630
  co_access: boolean;
630
631
  } | undefined;
631
632
  allow_secrets?: boolean | undefined;
633
+ index?: boolean | undefined;
632
634
  }, {
633
635
  auto_learn?: boolean | undefined;
634
636
  auto_capture?: boolean | undefined;
@@ -642,6 +644,7 @@ declare const PlurConfigSchema: z.ZodObject<{
642
644
  co_access?: boolean | undefined;
643
645
  } | undefined;
644
646
  allow_secrets?: boolean | undefined;
647
+ index?: boolean | undefined;
645
648
  }>;
646
649
  type PlurConfig = z.infer<typeof PlurConfigSchema>;
647
650
 
@@ -1164,9 +1167,36 @@ interface PlurPaths {
1164
1167
  packs: string;
1165
1168
  exchange: string;
1166
1169
  config: string;
1170
+ db: string;
1167
1171
  }
1168
1172
  declare function detectPlurStorage(explicitPath?: string): PlurPaths;
1169
1173
 
1174
+ declare class IndexedStorage {
1175
+ private dbPath;
1176
+ private engramsPath;
1177
+ private db;
1178
+ constructor(engramsPath: string, dbPath: string);
1179
+ private getDb;
1180
+ /** Load all engrams from SQLite index. Auto-rebuilds if db missing. */
1181
+ loadAll(): Engram[];
1182
+ /** Load engrams with SQL-level filtering. */
1183
+ loadFiltered(filter: {
1184
+ status?: string;
1185
+ scope?: string;
1186
+ domain?: string;
1187
+ }): Engram[];
1188
+ /** Count engrams with optional status filter. */
1189
+ count(filter?: {
1190
+ status?: string;
1191
+ }): number;
1192
+ /** Sync SQLite index from YAML source of truth. */
1193
+ syncFromYaml(): void;
1194
+ /** Drop and rebuild the entire index from YAML. */
1195
+ reindex(): void;
1196
+ /** Close the database connection. */
1197
+ close(): void;
1198
+ }
1199
+
1170
1200
  /**
1171
1201
  * Non-blocking version check against npm registry.
1172
1202
  * Caches result in memory — one fetch per process lifetime.
@@ -1212,6 +1242,7 @@ interface StatusResult {
1212
1242
  declare class Plur {
1213
1243
  private paths;
1214
1244
  private config;
1245
+ private indexedStorage;
1215
1246
  constructor(options?: {
1216
1247
  path?: string;
1217
1248
  });
@@ -1263,6 +1294,15 @@ declare class Plur {
1263
1294
  updateEngram(updated: Engram): boolean;
1264
1295
  /** Set engram status to 'retired'. */
1265
1296
  forget(id: string, reason?: string): void;
1297
+ /** Remove retired engrams from storage. Returns count of removed and remaining. */
1298
+ compact(): {
1299
+ removed: number;
1300
+ remaining: number;
1301
+ };
1302
+ /** Rebuild SQLite index from YAML source of truth. Only works when index: true. */
1303
+ reindex(): void;
1304
+ /** Sync SQLite index after YAML write (no-op if index disabled) */
1305
+ private _syncIndex;
1266
1306
  /** Capture an episodic memory. */
1267
1307
  capture(summary: string, context?: CaptureContext): Episode;
1268
1308
  /** Query the episode timeline. */
@@ -1294,4 +1334,4 @@ declare class Plur {
1294
1334
  status(): StatusResult;
1295
1335
  }
1296
1336
 
1297
- export { type AlignmentResult, type Association, type CaptureContext, type DomainCoverage, DomainCoverageSchema, type Engram, type EngramCluster, type Episode, type EvidenceEntry, EvidenceEntrySchema, type ExtractOptions, type ExtractionResult, type Falsification, FalsificationSchema, type HierarchyPosition, HierarchyPositionSchema, type IngestCandidate, type IngestOptions, type InjectOptions, type InjectionResult, type KnowledgeAnchor, type LearnContext, type LlmFunction, type MemberAlignment, type MetaConfidence, MetaConfidenceSchema, type MetaField, MetaFieldSchema, PLATITUDE_PATTERNS, type PackManifest, Plur, type PlurConfig, type PlurPaths, type RecallOptions, type RelationalAnalysis, type RelationalTriple, SessionBreadcrumbs, type StatusResult, type StructuralTemplate, StructuralTemplateSchema, type SyncResult, type SyncStatus, type TimelineQuery, type TypedRole, type ValidationResult, type VersionCheckResult, alignCluster, analyzeStructure, checkForUpdate, classifyPolarity, clearVersionCache, clusterByStructure, computeConfidence, computeMetaConfidence, confidenceBand, detectPlurStorage, detectSecrets, engramSearchText, extractMetaEngrams, formulateMetaEngram, generateGuardrails, getCachedUpdateCheck, isPlatitude, organizeHierarchy, tokenSimilarity, validateMetaEngram };
1337
+ export { type AlignmentResult, type Association, type CaptureContext, type DomainCoverage, DomainCoverageSchema, type Engram, type EngramCluster, type Episode, type EvidenceEntry, EvidenceEntrySchema, type ExtractOptions, type ExtractionResult, type Falsification, FalsificationSchema, type HierarchyPosition, HierarchyPositionSchema, IndexedStorage, type IngestCandidate, type IngestOptions, type InjectOptions, type InjectionResult, type KnowledgeAnchor, type LearnContext, type LlmFunction, type MemberAlignment, type MetaConfidence, MetaConfidenceSchema, type MetaField, MetaFieldSchema, PLATITUDE_PATTERNS, type PackManifest, Plur, type PlurConfig, type PlurPaths, type RecallOptions, type RelationalAnalysis, type RelationalTriple, SessionBreadcrumbs, type StatusResult, type StructuralTemplate, StructuralTemplateSchema, type SyncResult, type SyncStatus, type TimelineQuery, type TypedRole, type ValidationResult, type VersionCheckResult, alignCluster, analyzeStructure, checkForUpdate, classifyPolarity, clearVersionCache, clusterByStructure, computeConfidence, computeMetaConfidence, confidenceBand, detectPlurStorage, detectSecrets, engramSearchText, extractMetaEngrams, formulateMetaEngram, generateGuardrails, getCachedUpdateCheck, isPlatitude, organizeHierarchy, tokenSimilarity, validateMetaEngram };
package/dist/index.js CHANGED
@@ -6,8 +6,9 @@ import {
6
6
  ftsTokenize,
7
7
  getSyncStatus,
8
8
  searchEngrams,
9
- sync
10
- } from "./chunk-WPD4MPTT.js";
9
+ sync,
10
+ withLock
11
+ } from "./chunk-KMVQYBNP.js";
11
12
  import "./chunk-2ZDO52B4.js";
12
13
 
13
14
  // src/storage.ts
@@ -26,98 +27,71 @@ function detectPlurStorage(explicitPath) {
26
27
  candidates: join(root, "candidates.yaml"),
27
28
  packs: packsDir,
28
29
  exchange: join(root, "exchange"),
29
- config: join(root, "config.yaml")
30
+ config: join(root, "config.yaml"),
31
+ db: join(root, "engrams.db")
30
32
  };
31
33
  }
32
34
 
33
- // src/config.ts
34
- import { existsSync as existsSync2, readFileSync } from "fs";
35
- import yaml from "js-yaml";
36
-
37
- // src/schemas/config.ts
38
- import { z } from "zod";
39
- var PlurConfigSchema = z.object({
40
- auto_learn: z.boolean().default(true),
41
- auto_capture: z.boolean().default(true),
42
- injection_budget: z.number().default(2e3),
43
- decay_enabled: z.boolean().default(true),
44
- decay_threshold: z.number().default(0.15),
45
- packs: z.array(z.string()).default([]),
46
- injection: z.object({
47
- spread_cap: z.number().default(3),
48
- spread_budget: z.number().default(480),
49
- co_access: z.boolean().default(true)
50
- }).default({}),
51
- allow_secrets: z.boolean().default(false)
52
- }).partial();
53
-
54
- // src/config.ts
55
- function loadConfig(configPath) {
56
- if (!existsSync2(configPath)) return PlurConfigSchema.parse({});
57
- try {
58
- const raw = yaml.load(readFileSync(configPath, "utf8"));
59
- return PlurConfigSchema.parse(raw ?? {});
60
- } catch {
61
- return PlurConfigSchema.parse({});
62
- }
63
- }
35
+ // src/storage-indexed.ts
36
+ import { existsSync as existsSync3 } from "fs";
37
+ import { createRequire } from "module";
64
38
 
65
39
  // src/engrams.ts
66
40
  import * as fs from "fs";
67
- import * as yaml2 from "js-yaml";
41
+ import * as yaml from "js-yaml";
68
42
 
69
43
  // src/schemas/engram.ts
70
- import { z as z2 } from "zod";
71
- var ActivationSchema = z2.object({
72
- retrieval_strength: z2.number().min(0).max(1),
73
- storage_strength: z2.number().min(0).max(1),
74
- frequency: z2.number().int().min(0),
75
- last_accessed: z2.string()
44
+ import { z } from "zod";
45
+ var ActivationSchema = z.object({
46
+ retrieval_strength: z.number().min(0).max(1),
47
+ storage_strength: z.number().min(0).max(1),
48
+ frequency: z.number().int().min(0),
49
+ last_accessed: z.string()
76
50
  });
77
- var KnowledgeTypeSchema = z2.object({
78
- memory_class: z2.enum(["semantic", "episodic", "procedural", "metacognitive"]),
79
- cognitive_level: z2.enum(["remember", "understand", "apply", "analyze", "evaluate", "create"])
51
+ var KnowledgeTypeSchema = z.object({
52
+ memory_class: z.enum(["semantic", "episodic", "procedural", "metacognitive"]),
53
+ cognitive_level: z.enum(["remember", "understand", "apply", "analyze", "evaluate", "create"])
80
54
  });
81
- var KnowledgeAnchorSchema = z2.object({
82
- path: z2.string(),
83
- relevance: z2.enum(["primary", "supporting", "example"]).default("supporting"),
84
- snippet: z2.string().max(200).optional(),
85
- snippet_extracted_at: z2.string().optional()
55
+ var KnowledgeAnchorSchema = z.object({
56
+ path: z.string(),
57
+ relevance: z.enum(["primary", "supporting", "example"]).default("supporting"),
58
+ snippet: z.string().max(200).optional(),
59
+ snippet_extracted_at: z.string().optional()
86
60
  });
87
- var AssociationSchema = z2.object({
88
- target_type: z2.enum(["engram", "document"]),
89
- target: z2.string(),
90
- strength: z2.number().min(0).max(0.95),
91
- type: z2.enum(["semantic", "temporal", "causal", "co_accessed"]),
92
- updated_at: z2.string().optional()
61
+ var AssociationSchema = z.object({
62
+ target_type: z.enum(["engram", "document"]),
63
+ target: z.string(),
64
+ strength: z.number().min(0).max(0.95),
65
+ type: z.enum(["semantic", "temporal", "causal", "co_accessed"]),
66
+ updated_at: z.string().optional()
93
67
  });
94
- var DualCodingSchema = z2.object({
95
- example: z2.string().optional(),
96
- analogy: z2.string().optional()
68
+ var DualCodingSchema = z.object({
69
+ example: z.string().optional(),
70
+ analogy: z.string().optional()
97
71
  }).refine(
98
72
  (d) => d.example || d.analogy,
99
73
  "At least one of example or analogy must be provided"
100
74
  );
101
- var RelationsSchema = z2.object({
102
- broader: z2.array(z2.string()).default([]),
103
- narrower: z2.array(z2.string()).default([]),
104
- related: z2.array(z2.string()).default([]),
105
- conflicts: z2.array(z2.string()).default([])
75
+ var RelationsSchema = z.object({
76
+ broader: z.array(z.string()).default([]),
77
+ narrower: z.array(z.string()).default([]),
78
+ related: z.array(z.string()).default([]),
79
+ conflicts: z.array(z.string()).default([])
106
80
  });
107
- var ProvenanceSchema = z2.object({
108
- origin: z2.string(),
109
- chain: z2.array(z2.string()).default([]),
110
- signature: z2.string().nullable().default(null),
111
- license: z2.string().default("cc-by-sa-4.0")
81
+ var ProvenanceSchema = z.object({
82
+ origin: z.string(),
83
+ chain: z.array(z.string()).default([]),
84
+ signature: z.string().nullable().default(null),
85
+ license: z.string().default("cc-by-sa-4.0")
112
86
  });
113
- var FeedbackSignalsSchema = z2.object({
114
- positive: z2.number().int().default(0),
115
- negative: z2.number().int().default(0),
116
- neutral: z2.number().int().default(0)
87
+ var FeedbackSignalsSchema = z.object({
88
+ positive: z.number().int().default(0),
89
+ negative: z.number().int().default(0),
90
+ neutral: z.number().int().default(0)
117
91
  });
118
- var EntityRefSchema = z2.object({
119
- name: z2.string(),
120
- type: z2.enum([
92
+ var EntityRefSchema = z.object({
93
+ name: z.string(),
94
+ type: z.enum([
121
95
  "person",
122
96
  "organization",
123
97
  "technology",
@@ -129,56 +103,56 @@ var EntityRefSchema = z2.object({
129
103
  "standard",
130
104
  "other"
131
105
  ]),
132
- uri: z2.string().url().optional()
106
+ uri: z.string().url().optional()
133
107
  });
134
- var TemporalSchema = z2.object({
135
- learned_at: z2.string(),
136
- valid_from: z2.string().optional(),
137
- valid_until: z2.string().optional(),
138
- ingested_at: z2.string().optional()
108
+ var TemporalSchema = z.object({
109
+ learned_at: z.string(),
110
+ valid_from: z.string().optional(),
111
+ valid_until: z.string().optional(),
112
+ ingested_at: z.string().optional()
139
113
  });
140
- var UsageStatsSchema = z2.object({
141
- injections: z2.number().int().default(0),
142
- hits: z2.number().int().default(0),
143
- misses: z2.number().int().default(0),
144
- last_hit_at: z2.string().optional()
114
+ var UsageStatsSchema = z.object({
115
+ injections: z.number().int().default(0),
116
+ hits: z.number().int().default(0),
117
+ misses: z.number().int().default(0),
118
+ last_hit_at: z.string().optional()
145
119
  });
146
- var EpisodicFieldsSchema = z2.object({
147
- emotional_weight: z2.number().int().min(1).max(10).default(5),
148
- confidence: z2.number().int().min(1).max(10).default(5),
149
- trigger_context: z2.string().optional(),
150
- journal_ref: z2.string().optional()
120
+ var EpisodicFieldsSchema = z.object({
121
+ emotional_weight: z.number().int().min(1).max(10).default(5),
122
+ confidence: z.number().int().min(1).max(10).default(5),
123
+ trigger_context: z.string().optional(),
124
+ journal_ref: z.string().optional()
151
125
  });
152
- var ExchangeMetadataSchema = z2.object({
153
- fitness_score: z2.number().min(0).max(1).optional(),
154
- environmental_diversity: z2.number().int().default(0),
155
- adoption_count: z2.number().int().default(0),
156
- contradiction_rate: z2.number().min(0).max(1).default(0)
126
+ var ExchangeMetadataSchema = z.object({
127
+ fitness_score: z.number().min(0).max(1).optional(),
128
+ environmental_diversity: z.number().int().default(0),
129
+ adoption_count: z.number().int().default(0),
130
+ contradiction_rate: z.number().min(0).max(1).default(0)
157
131
  });
158
- var EngramSchema = z2.object({
132
+ var EngramSchema = z.object({
159
133
  // Identity
160
- id: z2.string().regex(/^(ENG|ABS|META)-[A-Za-z0-9-]+$/),
161
- version: z2.number().int().min(1).default(2),
162
- status: z2.enum(["active", "dormant", "retired", "candidate"]),
163
- consolidated: z2.boolean().default(false),
164
- type: z2.enum(["behavioral", "terminological", "procedural", "architectural"]),
165
- scope: z2.string(),
166
- visibility: z2.enum(["private", "public", "template"]).default("private"),
134
+ id: z.string().regex(/^(ENG|ABS|META)-[A-Za-z0-9-]+$/),
135
+ version: z.number().int().min(1).default(2),
136
+ status: z.enum(["active", "dormant", "retired", "candidate"]),
137
+ consolidated: z.boolean().default(false),
138
+ type: z.enum(["behavioral", "terminological", "procedural", "architectural"]),
139
+ scope: z.string(),
140
+ visibility: z.enum(["private", "public", "template"]).default("private"),
167
141
  // Content
168
- statement: z2.string().min(1),
169
- rationale: z2.string().optional(),
170
- contraindications: z2.array(z2.string()).optional(),
142
+ statement: z.string().min(1),
143
+ rationale: z.string().optional(),
144
+ contraindications: z.array(z.string()).optional(),
171
145
  // Lineage
172
- source: z2.string().optional(),
173
- source_patterns: z2.array(z2.string()).optional(),
174
- derivation_count: z2.number().int().min(0).default(1),
175
- pack: z2.string().nullable().default(null),
176
- abstract: z2.string().nullable().default(null),
177
- derived_from: z2.string().nullable().default(null),
146
+ source: z.string().optional(),
147
+ source_patterns: z.array(z.string()).optional(),
148
+ derivation_count: z.number().int().min(0).default(1),
149
+ pack: z.string().nullable().default(null),
150
+ abstract: z.string().nullable().default(null),
151
+ derived_from: z.string().nullable().default(null),
178
152
  // Classification
179
153
  knowledge_type: KnowledgeTypeSchema.optional(),
180
- domain: z2.string().optional(),
181
- tags: z2.array(z2.string()).default([]),
154
+ domain: z.string().optional(),
155
+ tags: z.array(z.string()).default([]),
182
156
  // Activation (ACT-R model)
183
157
  activation: ActivationSchema.default({
184
158
  retrieval_strength: 0.7,
@@ -188,8 +162,8 @@ var EngramSchema = z2.object({
188
162
  }),
189
163
  // Relations & grounding
190
164
  relations: RelationsSchema.optional(),
191
- associations: z2.array(AssociationSchema).default([]),
192
- knowledge_anchors: z2.array(KnowledgeAnchorSchema).default([]),
165
+ associations: z.array(AssociationSchema).default([]),
166
+ knowledge_anchors: z.array(KnowledgeAnchorSchema).default([]),
193
167
  dual_coding: DualCodingSchema.optional(),
194
168
  // Provenance
195
169
  provenance: ProvenanceSchema.optional(),
@@ -197,7 +171,7 @@ var EngramSchema = z2.object({
197
171
  feedback_signals: FeedbackSignalsSchema.default({ positive: 0, negative: 0, neutral: 0 }),
198
172
  // === NEW OPTIONAL FIELDS (v2.1) ===
199
173
  /** Typed entity references extracted from statement. Enables graph queries. */
200
- entities: z2.array(EntityRefSchema).optional(),
174
+ entities: z.array(EntityRefSchema).optional(),
201
175
  /** Temporal validity window. When is this knowledge true? */
202
176
  temporal: TemporalSchema.optional(),
203
177
  /** Automatic usage tracking. Injections, hits, misses. */
@@ -207,33 +181,33 @@ var EngramSchema = z2.object({
207
181
  /** Exchange marketplace metadata: fitness, adoption, diversity. */
208
182
  exchange: ExchangeMetadataSchema.optional(),
209
183
  /** Extensible key-value data for domain-specific fields. */
210
- structured_data: z2.record(z2.string(), z2.unknown()).optional(),
184
+ structured_data: z.record(z.string(), z.unknown()).optional(),
211
185
  /** Polarity classification: 'do' for directives, 'dont' for prohibitions, null for unclassified. */
212
- polarity: z2.enum(["do", "dont"]).nullable().default(null)
186
+ polarity: z.enum(["do", "dont"]).nullable().default(null)
213
187
  });
214
188
 
215
189
  // src/schemas/pack.ts
216
- import { z as z3 } from "zod";
217
- var PackManifestSchema = z3.object({
218
- name: z3.string(),
219
- version: z3.string(),
220
- description: z3.string().optional(),
221
- creator: z3.string().optional(),
222
- license: z3.string().default("cc-by-sa-4.0"),
223
- tags: z3.array(z3.string()).default([]),
224
- metadata: z3.object({
225
- id: z3.string().optional(),
226
- injection_policy: z3.enum(["on_match", "on_request", "always"]).default("on_match"),
227
- match_terms: z3.array(z3.string()).default([]),
228
- domain: z3.string().optional(),
229
- engram_count: z3.number().optional()
190
+ import { z as z2 } from "zod";
191
+ var PackManifestSchema = z2.object({
192
+ name: z2.string(),
193
+ version: z2.string(),
194
+ description: z2.string().optional(),
195
+ creator: z2.string().optional(),
196
+ license: z2.string().default("cc-by-sa-4.0"),
197
+ tags: z2.array(z2.string()).default([]),
198
+ metadata: z2.object({
199
+ id: z2.string().optional(),
200
+ injection_policy: z2.enum(["on_match", "on_request", "always"]).default("on_match"),
201
+ match_terms: z2.array(z2.string()).default([]),
202
+ domain: z2.string().optional(),
203
+ engram_count: z2.number().optional()
230
204
  }).optional(),
231
- "x-datacore": z3.object({
232
- id: z3.string(),
233
- injection_policy: z3.enum(["on_match", "on_request"]),
234
- match_terms: z3.array(z3.string()).default([]),
235
- domain: z3.string().optional(),
236
- engram_count: z3.number().int().min(0)
205
+ "x-datacore": z2.object({
206
+ id: z2.string(),
207
+ injection_policy: z2.enum(["on_match", "on_request"]),
208
+ match_terms: z2.array(z2.string()).default([]),
209
+ domain: z2.string().optional(),
210
+ engram_count: z2.number().int().min(0)
237
211
  }).optional()
238
212
  });
239
213
 
@@ -260,7 +234,7 @@ var logger = {
260
234
  function loadEngrams(filePath) {
261
235
  if (!fs.existsSync(filePath)) return [];
262
236
  try {
263
- const raw = yaml2.load(fs.readFileSync(filePath, "utf8"));
237
+ const raw = yaml.load(fs.readFileSync(filePath, "utf8"));
264
238
  if (!raw?.engrams || !Array.isArray(raw.engrams)) return [];
265
239
  const valid = [];
266
240
  let skipped = 0;
@@ -277,14 +251,14 @@ function loadEngrams(filePath) {
277
251
  }
278
252
  }
279
253
  function saveEngrams(filePath, engrams) {
280
- const content = yaml2.dump({ engrams }, { lineWidth: 120, noRefs: true, quotingType: '"' });
254
+ const content = yaml.dump({ engrams }, { lineWidth: 120, noRefs: true, quotingType: '"' });
281
255
  atomicWrite(filePath, content);
282
256
  }
283
257
  function parseSkillMdFrontmatter(filePath) {
284
258
  const content = fs.readFileSync(filePath, "utf8");
285
259
  const match = content.match(/^---\n([\s\S]*?)\n---/);
286
260
  if (!match) throw new Error(`No frontmatter found in ${filePath}`);
287
- return yaml2.load(match[1]);
261
+ return yaml.load(match[1]);
288
262
  }
289
263
  function loadPack(packDir) {
290
264
  const skillMdPath = `${packDir}/SKILL.md`;
@@ -294,7 +268,7 @@ function loadPack(packDir) {
294
268
  if (fs.existsSync(skillMdPath)) {
295
269
  rawManifest = parseSkillMdFrontmatter(skillMdPath);
296
270
  } else if (fs.existsSync(manifestYamlPath)) {
297
- rawManifest = yaml2.load(fs.readFileSync(manifestYamlPath, "utf8"));
271
+ rawManifest = yaml.load(fs.readFileSync(manifestYamlPath, "utf8"));
298
272
  } else {
299
273
  throw new Error(`No SKILL.md or manifest.yaml found in ${packDir}`);
300
274
  }
@@ -326,6 +300,166 @@ function generateEngramId(existing) {
326
300
  return `${prefix}${String(next).padStart(3, "0")}`;
327
301
  }
328
302
 
303
+ // src/storage-indexed.ts
304
+ var require2 = createRequire(import.meta.url);
305
+ var Database = null;
306
+ function getDatabase() {
307
+ if (!Database) {
308
+ try {
309
+ Database = require2("better-sqlite3");
310
+ } catch {
311
+ throw new Error(
312
+ "better-sqlite3 is required for index: true. Install it with: npm install better-sqlite3"
313
+ );
314
+ }
315
+ }
316
+ return Database;
317
+ }
318
+ var IndexedStorage = class {
319
+ dbPath;
320
+ engramsPath;
321
+ db = null;
322
+ constructor(engramsPath, dbPath) {
323
+ this.engramsPath = engramsPath;
324
+ this.dbPath = dbPath;
325
+ }
326
+ getDb() {
327
+ if (!this.db) {
328
+ const DB = getDatabase();
329
+ this.db = new DB(this.dbPath);
330
+ this.db.pragma("journal_mode = WAL");
331
+ this.db.exec(`
332
+ CREATE TABLE IF NOT EXISTS engrams (
333
+ id TEXT PRIMARY KEY,
334
+ status TEXT NOT NULL,
335
+ scope TEXT NOT NULL,
336
+ domain TEXT,
337
+ last_accessed TEXT,
338
+ data TEXT NOT NULL
339
+ );
340
+ CREATE INDEX IF NOT EXISTS idx_status ON engrams(status);
341
+ CREATE INDEX IF NOT EXISTS idx_scope ON engrams(scope);
342
+ CREATE INDEX IF NOT EXISTS idx_domain ON engrams(domain);
343
+ `);
344
+ }
345
+ return this.db;
346
+ }
347
+ /** Load all engrams from SQLite index. Auto-rebuilds if db missing. */
348
+ loadAll() {
349
+ if (!existsSync3(this.dbPath)) {
350
+ this.reindex();
351
+ }
352
+ const db = this.getDb();
353
+ const rows = db.prepare("SELECT data FROM engrams").all();
354
+ return rows.map((r) => JSON.parse(r.data));
355
+ }
356
+ /** Load engrams with SQL-level filtering. */
357
+ loadFiltered(filter) {
358
+ if (!existsSync3(this.dbPath)) {
359
+ this.reindex();
360
+ }
361
+ const db = this.getDb();
362
+ const conditions = [];
363
+ const params = [];
364
+ if (filter.status) {
365
+ conditions.push("status = ?");
366
+ params.push(filter.status);
367
+ }
368
+ if (filter.scope) {
369
+ conditions.push("(scope = 'global' OR scope = ? OR scope LIKE ? || '%')");
370
+ params.push(filter.scope, filter.scope);
371
+ }
372
+ if (filter.domain) {
373
+ conditions.push("domain LIKE ? || '%'");
374
+ params.push(filter.domain);
375
+ }
376
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
377
+ const rows = db.prepare(`SELECT data FROM engrams ${where}`).all(...params);
378
+ return rows.map((r) => JSON.parse(r.data));
379
+ }
380
+ /** Count engrams with optional status filter. */
381
+ count(filter) {
382
+ if (!existsSync3(this.dbPath)) {
383
+ this.reindex();
384
+ }
385
+ const db = this.getDb();
386
+ if (filter?.status) {
387
+ return db.prepare("SELECT COUNT(*) as c FROM engrams WHERE status = ?").get(filter.status).c;
388
+ }
389
+ return db.prepare("SELECT COUNT(*) as c FROM engrams").get().c;
390
+ }
391
+ /** Sync SQLite index from YAML source of truth. */
392
+ syncFromYaml() {
393
+ const engrams = loadEngrams(this.engramsPath);
394
+ const db = this.getDb();
395
+ const upsert = db.prepare(`
396
+ INSERT OR REPLACE INTO engrams (id, status, scope, domain, last_accessed, data)
397
+ VALUES (?, ?, ?, ?, ?, ?)
398
+ `);
399
+ const deleteStmt = db.prepare("DELETE FROM engrams WHERE id = ?");
400
+ const yamlIds = new Set(engrams.map((e) => e.id));
401
+ const dbIds = new Set(
402
+ db.prepare("SELECT id FROM engrams").all().map((r) => r.id)
403
+ );
404
+ const tx = db.transaction(() => {
405
+ for (const e of engrams) {
406
+ upsert.run(e.id, e.status, e.scope, e.domain ?? null, e.activation.last_accessed, JSON.stringify(e));
407
+ }
408
+ for (const id of dbIds) {
409
+ if (!yamlIds.has(id)) deleteStmt.run(id);
410
+ }
411
+ });
412
+ tx();
413
+ }
414
+ /** Drop and rebuild the entire index from YAML. */
415
+ reindex() {
416
+ this.close();
417
+ const db = this.getDb();
418
+ db.exec("DELETE FROM engrams");
419
+ this.syncFromYaml();
420
+ }
421
+ /** Close the database connection. */
422
+ close() {
423
+ if (this.db) {
424
+ this.db.close();
425
+ this.db = null;
426
+ }
427
+ }
428
+ };
429
+
430
+ // src/config.ts
431
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
432
+ import yaml2 from "js-yaml";
433
+
434
+ // src/schemas/config.ts
435
+ import { z as z3 } from "zod";
436
+ var PlurConfigSchema = z3.object({
437
+ auto_learn: z3.boolean().default(true),
438
+ auto_capture: z3.boolean().default(true),
439
+ injection_budget: z3.number().default(2e3),
440
+ decay_enabled: z3.boolean().default(true),
441
+ decay_threshold: z3.number().default(0.15),
442
+ packs: z3.array(z3.string()).default([]),
443
+ injection: z3.object({
444
+ spread_cap: z3.number().default(3),
445
+ spread_budget: z3.number().default(480),
446
+ co_access: z3.boolean().default(true)
447
+ }).default({}),
448
+ allow_secrets: z3.boolean().default(false),
449
+ index: z3.boolean().default(false)
450
+ }).partial();
451
+
452
+ // src/config.ts
453
+ function loadConfig(configPath) {
454
+ if (!existsSync4(configPath)) return PlurConfigSchema.parse({});
455
+ try {
456
+ const raw = yaml2.load(readFileSync2(configPath, "utf8"));
457
+ return PlurConfigSchema.parse(raw ?? {});
458
+ } catch {
459
+ return PlurConfigSchema.parse({});
460
+ }
461
+ }
462
+
329
463
  // src/decay.ts
330
464
  var DECAY_RATE = 0.05;
331
465
  var FLOOR = 0.05;
@@ -653,7 +787,7 @@ function selectAndSpread(ctx, personalEngrams, packs, config, embeddingBoosts) {
653
787
  }
654
788
 
655
789
  // src/episodes.ts
656
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
790
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
657
791
  import yaml3 from "js-yaml";
658
792
  function generateEpisodeId() {
659
793
  const ts = Date.now();
@@ -688,7 +822,7 @@ function queryTimeline(path2, query) {
688
822
  return episodes;
689
823
  }
690
824
  function loadEpisodes(path2) {
691
- if (!existsSync4(path2)) return [];
825
+ if (!existsSync5(path2)) return [];
692
826
  try {
693
827
  const raw = yaml3.load(readFileSync3(path2, "utf8"));
694
828
  return Array.isArray(raw) ? raw : [];
@@ -1097,7 +1231,7 @@ async function computeSimilarityMatrix(templates) {
1097
1231
  const n = templates.length;
1098
1232
  const matrix = Array.from({ length: n }, () => Array(n).fill(0));
1099
1233
  try {
1100
- const { embed, cosineSimilarity } = await import("./embeddings-Q76LNQ5B.js");
1234
+ const { embed, cosineSimilarity } = await import("./embeddings-2IODIQAF.js");
1101
1235
  const embeddings = [];
1102
1236
  for (const t of templates) {
1103
1237
  embeddings.push(await embed(t));
@@ -1832,9 +1966,13 @@ var INGEST_PATTERNS = [
1832
1966
  var Plur = class {
1833
1967
  paths;
1834
1968
  config;
1969
+ indexedStorage = null;
1835
1970
  constructor(options) {
1836
1971
  this.paths = detectPlurStorage(options?.path);
1837
1972
  this.config = loadConfig(this.paths.config);
1973
+ if (this.config.index) {
1974
+ this.indexedStorage = new IndexedStorage(this.paths.engrams, this.paths.db);
1975
+ }
1838
1976
  }
1839
1977
  /** Create engram, detect conflicts, save. Returns the created engram. */
1840
1978
  learn(statement, context) {
@@ -1844,48 +1982,51 @@ var Plur = class {
1844
1982
  throw new Error(`Secret detected in statement: ${secrets[0].pattern}. Use config.allow_secrets to override.`);
1845
1983
  }
1846
1984
  }
1847
- const engrams = loadEngrams(this.paths.engrams);
1848
- const id = generateEngramId(engrams);
1849
- const scope = context?.scope ?? "global";
1850
- const now = (/* @__PURE__ */ new Date()).toISOString();
1851
- const conflictingEngrams = detectConflicts({ statement, scope }, engrams);
1852
- const conflictIds = conflictingEngrams.map((e) => e.id);
1853
- const engram = {
1854
- id,
1855
- version: 2,
1856
- status: "active",
1857
- consolidated: false,
1858
- type: context?.type ?? "behavioral",
1859
- scope,
1860
- visibility: "private",
1861
- statement,
1862
- source: context?.source,
1863
- domain: context?.domain,
1864
- activation: {
1865
- retrieval_strength: 0.7,
1866
- storage_strength: 1,
1867
- frequency: 0,
1868
- last_accessed: now.slice(0, 10)
1869
- },
1870
- feedback_signals: { positive: 0, negative: 0, neutral: 0 },
1871
- knowledge_anchors: [],
1872
- associations: [],
1873
- derivation_count: 1,
1874
- tags: [],
1875
- pack: null,
1876
- abstract: null,
1877
- derived_from: null,
1878
- polarity: null,
1879
- relations: conflictIds.length > 0 ? {
1880
- broader: [],
1881
- narrower: [],
1882
- related: [],
1883
- conflicts: conflictIds
1884
- } : void 0
1885
- };
1886
- engrams.push(engram);
1887
- saveEngrams(this.paths.engrams, engrams);
1888
- return engram;
1985
+ return withLock(this.paths.engrams, () => {
1986
+ const engrams = loadEngrams(this.paths.engrams);
1987
+ const id = generateEngramId(engrams);
1988
+ const scope = context?.scope ?? "global";
1989
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1990
+ const conflictingEngrams = detectConflicts({ statement, scope }, engrams);
1991
+ const conflictIds = conflictingEngrams.map((e) => e.id);
1992
+ const engram = {
1993
+ id,
1994
+ version: 2,
1995
+ status: "active",
1996
+ consolidated: false,
1997
+ type: context?.type ?? "behavioral",
1998
+ scope,
1999
+ visibility: "private",
2000
+ statement,
2001
+ source: context?.source,
2002
+ domain: context?.domain,
2003
+ activation: {
2004
+ retrieval_strength: 0.7,
2005
+ storage_strength: 1,
2006
+ frequency: 0,
2007
+ last_accessed: now.slice(0, 10)
2008
+ },
2009
+ feedback_signals: { positive: 0, negative: 0, neutral: 0 },
2010
+ knowledge_anchors: [],
2011
+ associations: [],
2012
+ derivation_count: 1,
2013
+ tags: [],
2014
+ pack: null,
2015
+ abstract: null,
2016
+ derived_from: null,
2017
+ polarity: null,
2018
+ relations: conflictIds.length > 0 ? {
2019
+ broader: [],
2020
+ narrower: [],
2021
+ related: [],
2022
+ conflicts: conflictIds
2023
+ } : void 0
2024
+ };
2025
+ engrams.push(engram);
2026
+ saveEngrams(this.paths.engrams, engrams);
2027
+ this._syncIndex();
2028
+ return engram;
2029
+ });
1889
2030
  }
1890
2031
  /**
1891
2032
  * Search engrams, filter by scope/domain/strength, reactivate accessed.
@@ -1939,75 +2080,89 @@ var Plur = class {
1939
2080
  }
1940
2081
  /** Filter engrams by scope/domain/strength (shared by both modes) */
1941
2082
  _filterEngrams(options) {
1942
- let engrams = loadEngrams(this.paths.engrams);
1943
- engrams = engrams.filter((e) => e.status === "active");
2083
+ let engrams;
2084
+ if (this.indexedStorage) {
2085
+ engrams = this.indexedStorage.loadFiltered({
2086
+ status: "active",
2087
+ scope: options?.scope,
2088
+ domain: options?.domain
2089
+ });
2090
+ } else {
2091
+ engrams = loadEngrams(this.paths.engrams);
2092
+ engrams = engrams.filter((e) => e.status === "active");
2093
+ if (options?.domain) {
2094
+ engrams = engrams.filter((e) => e.domain?.startsWith(options.domain));
2095
+ }
2096
+ if (options?.scope) {
2097
+ const scope = options.scope;
2098
+ engrams = engrams.filter(
2099
+ (e) => e.scope === "global" || e.scope === scope || e.scope.startsWith(scope)
2100
+ );
2101
+ }
2102
+ }
1944
2103
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1945
2104
  engrams = engrams.filter((e) => {
1946
2105
  if (e.temporal?.valid_until && e.temporal.valid_until < today) return false;
1947
2106
  if (e.temporal?.valid_from && e.temporal.valid_from > today) return false;
1948
2107
  return true;
1949
2108
  });
1950
- if (options?.domain) {
1951
- engrams = engrams.filter((e) => e.domain?.startsWith(options.domain));
1952
- }
1953
2109
  if (options?.min_strength !== void 0) {
1954
2110
  engrams = engrams.filter((e) => e.activation.retrieval_strength >= options.min_strength);
1955
2111
  }
1956
- if (options?.scope) {
1957
- const scope = options.scope;
1958
- engrams = engrams.filter(
1959
- (e) => e.scope === "global" || e.scope === scope || e.scope.startsWith(scope)
1960
- );
1961
- }
1962
2112
  return engrams;
1963
2113
  }
1964
2114
  /** Reactivate accessed engrams and update co-access associations */
1965
2115
  _reactivateResults(results) {
1966
2116
  if (results.length === 0) return;
1967
- const allEngrams = loadEngrams(this.paths.engrams);
1968
- const resultIds = new Set(results.map((e) => e.id));
1969
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1970
- let modified = false;
1971
- for (const e of allEngrams) {
1972
- if (resultIds.has(e.id)) {
1973
- e.activation.retrieval_strength = reactivate(e.activation.retrieval_strength);
1974
- e.activation.last_accessed = today;
1975
- e.activation.frequency += 1;
1976
- modified = true;
2117
+ withLock(this.paths.engrams, () => {
2118
+ const allEngrams = loadEngrams(this.paths.engrams);
2119
+ const resultIds = new Set(results.map((e) => e.id));
2120
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2121
+ let modified = false;
2122
+ for (const e of allEngrams) {
2123
+ if (resultIds.has(e.id)) {
2124
+ e.activation.retrieval_strength = reactivate(e.activation.retrieval_strength);
2125
+ e.activation.last_accessed = today;
2126
+ e.activation.frequency += 1;
2127
+ modified = true;
2128
+ }
1977
2129
  }
1978
- }
1979
- if (results.length >= 2 && this.config.injection?.co_access !== false) {
1980
- const topHalf = results.slice(0, Math.max(2, Math.ceil(results.length / 2)));
1981
- const topIds = topHalf.map((e) => e.id);
1982
- for (const sourceId of topIds) {
1983
- const source = allEngrams.find((e) => e.id === sourceId);
1984
- if (!source) continue;
1985
- for (const targetId of topIds) {
1986
- if (targetId === sourceId) continue;
1987
- const existing = source.associations.find(
1988
- (a) => a.type === "co_accessed" && a.target === targetId
1989
- );
1990
- if (existing) {
1991
- existing.strength = Math.min(0.95, existing.strength + 0.05);
1992
- existing.updated_at = today;
1993
- modified = true;
1994
- } else {
1995
- const coAccessCount = source.associations.filter((a) => a.type === "co_accessed").length;
1996
- if (coAccessCount < 5) {
1997
- source.associations.push({
1998
- target_type: "engram",
1999
- target: targetId,
2000
- type: "co_accessed",
2001
- strength: 0.3,
2002
- updated_at: today
2003
- });
2130
+ if (results.length >= 2 && this.config.injection?.co_access !== false) {
2131
+ const topHalf = results.slice(0, Math.max(2, Math.ceil(results.length / 2)));
2132
+ const topIds = topHalf.map((e) => e.id);
2133
+ for (const sourceId of topIds) {
2134
+ const source = allEngrams.find((e) => e.id === sourceId);
2135
+ if (!source) continue;
2136
+ for (const targetId of topIds) {
2137
+ if (targetId === sourceId) continue;
2138
+ const existing = source.associations.find(
2139
+ (a) => a.type === "co_accessed" && a.target === targetId
2140
+ );
2141
+ if (existing) {
2142
+ existing.strength = Math.min(0.95, existing.strength + 0.05);
2143
+ existing.updated_at = today;
2004
2144
  modified = true;
2145
+ } else {
2146
+ const coAccessCount = source.associations.filter((a) => a.type === "co_accessed").length;
2147
+ if (coAccessCount < 5) {
2148
+ source.associations.push({
2149
+ target_type: "engram",
2150
+ target: targetId,
2151
+ type: "co_accessed",
2152
+ strength: 0.3,
2153
+ updated_at: today
2154
+ });
2155
+ modified = true;
2156
+ }
2005
2157
  }
2006
2158
  }
2007
2159
  }
2008
2160
  }
2009
- }
2010
- if (modified) saveEngrams(this.paths.engrams, allEngrams);
2161
+ if (modified) {
2162
+ saveEngrams(this.paths.engrams, allEngrams);
2163
+ this._syncIndex();
2164
+ }
2165
+ });
2011
2166
  }
2012
2167
  /** Scored injection within token budget (BM25 only). Returns formatted strings. */
2013
2168
  inject(task, options) {
@@ -2066,56 +2221,96 @@ var Plur = class {
2066
2221
  }
2067
2222
  /** Update feedback_signals and adjust retrieval_strength. */
2068
2223
  feedback(id, signal) {
2069
- const engrams = loadEngrams(this.paths.engrams);
2070
- const engram = engrams.find((e) => e.id === id);
2071
- if (!engram) throw new Error(`Engram not found: ${id}`);
2072
- if (!engram.feedback_signals) {
2073
- engram.feedback_signals = { positive: 0, negative: 0, neutral: 0 };
2074
- }
2075
- engram.feedback_signals[signal] += 1;
2076
- if (signal === "positive") {
2077
- engram.activation.retrieval_strength = Math.min(1, engram.activation.retrieval_strength + 0.05);
2078
- } else if (signal === "negative") {
2079
- engram.activation.retrieval_strength = Math.max(0, engram.activation.retrieval_strength - 0.1);
2080
- }
2081
- saveEngrams(this.paths.engrams, engrams);
2224
+ withLock(this.paths.engrams, () => {
2225
+ const engrams = loadEngrams(this.paths.engrams);
2226
+ const engram = engrams.find((e) => e.id === id);
2227
+ if (!engram) throw new Error(`Engram not found: ${id}`);
2228
+ if (!engram.feedback_signals) {
2229
+ engram.feedback_signals = { positive: 0, negative: 0, neutral: 0 };
2230
+ }
2231
+ engram.feedback_signals[signal] += 1;
2232
+ if (signal === "positive") {
2233
+ engram.activation.retrieval_strength = Math.min(1, engram.activation.retrieval_strength + 0.05);
2234
+ } else if (signal === "negative") {
2235
+ engram.activation.retrieval_strength = Math.max(0, engram.activation.retrieval_strength - 0.1);
2236
+ }
2237
+ saveEngrams(this.paths.engrams, engrams);
2238
+ this._syncIndex();
2239
+ });
2082
2240
  }
2083
2241
  /** Save extracted meta-engrams to the engram store. Skips IDs that already exist. */
2084
2242
  saveMetaEngrams(metas) {
2085
- const engrams = loadEngrams(this.paths.engrams);
2086
- const existingIds = new Set(engrams.map((e) => e.id));
2087
- let saved = 0;
2088
- let skipped = 0;
2089
- for (const meta of metas) {
2090
- if (existingIds.has(meta.id)) {
2091
- skipped++;
2092
- } else {
2093
- engrams.push(meta);
2094
- saved++;
2243
+ return withLock(this.paths.engrams, () => {
2244
+ const engrams = loadEngrams(this.paths.engrams);
2245
+ const existingIds = new Set(engrams.map((e) => e.id));
2246
+ let saved = 0;
2247
+ let skipped = 0;
2248
+ for (const meta of metas) {
2249
+ if (existingIds.has(meta.id)) {
2250
+ skipped++;
2251
+ } else {
2252
+ engrams.push(meta);
2253
+ saved++;
2254
+ }
2095
2255
  }
2096
- }
2097
- if (saved > 0) saveEngrams(this.paths.engrams, engrams);
2098
- return { saved, skipped };
2256
+ if (saved > 0) {
2257
+ saveEngrams(this.paths.engrams, engrams);
2258
+ this._syncIndex();
2259
+ }
2260
+ return { saved, skipped };
2261
+ });
2099
2262
  }
2100
2263
  /** Update an existing engram in the store by ID. Returns true if found and updated. */
2101
2264
  updateEngram(updated) {
2102
- const engrams = loadEngrams(this.paths.engrams);
2103
- const idx = engrams.findIndex((e) => e.id === updated.id);
2104
- if (idx === -1) return false;
2105
- engrams[idx] = updated;
2106
- saveEngrams(this.paths.engrams, engrams);
2107
- return true;
2265
+ return withLock(this.paths.engrams, () => {
2266
+ const engrams = loadEngrams(this.paths.engrams);
2267
+ const idx = engrams.findIndex((e) => e.id === updated.id);
2268
+ if (idx === -1) return false;
2269
+ engrams[idx] = updated;
2270
+ saveEngrams(this.paths.engrams, engrams);
2271
+ this._syncIndex();
2272
+ return true;
2273
+ });
2108
2274
  }
2109
2275
  /** Set engram status to 'retired'. */
2110
2276
  forget(id, reason) {
2111
- const engrams = loadEngrams(this.paths.engrams);
2112
- const engram = engrams.find((e) => e.id === id);
2113
- if (!engram) throw new Error(`Engram not found: ${id}`);
2114
- engram.status = "retired";
2115
- if (reason && !engram.rationale) {
2116
- engram.rationale = `Retired: ${reason}`;
2277
+ withLock(this.paths.engrams, () => {
2278
+ const engrams = loadEngrams(this.paths.engrams);
2279
+ const engram = engrams.find((e) => e.id === id);
2280
+ if (!engram) throw new Error(`Engram not found: ${id}`);
2281
+ engram.status = "retired";
2282
+ if (reason && !engram.rationale) {
2283
+ engram.rationale = `Retired: ${reason}`;
2284
+ }
2285
+ saveEngrams(this.paths.engrams, engrams);
2286
+ this._syncIndex();
2287
+ });
2288
+ }
2289
+ /** Remove retired engrams from storage. Returns count of removed and remaining. */
2290
+ compact() {
2291
+ return withLock(this.paths.engrams, () => {
2292
+ const engrams = loadEngrams(this.paths.engrams);
2293
+ const active = engrams.filter((e) => e.status !== "retired");
2294
+ const removed = engrams.length - active.length;
2295
+ if (removed > 0) {
2296
+ saveEngrams(this.paths.engrams, active);
2297
+ this._syncIndex();
2298
+ }
2299
+ return { removed, remaining: active.length };
2300
+ });
2301
+ }
2302
+ /** Rebuild SQLite index from YAML source of truth. Only works when index: true. */
2303
+ reindex() {
2304
+ if (!this.indexedStorage) {
2305
+ this.indexedStorage = new IndexedStorage(this.paths.engrams, this.paths.db);
2306
+ }
2307
+ this.indexedStorage.reindex();
2308
+ }
2309
+ /** Sync SQLite index after YAML write (no-op if index disabled) */
2310
+ _syncIndex() {
2311
+ if (this.indexedStorage) {
2312
+ this.indexedStorage.syncFromYaml();
2117
2313
  }
2118
- saveEngrams(this.paths.engrams, engrams);
2119
2314
  }
2120
2315
  /** Capture an episodic memory. */
2121
2316
  capture(summary, context) {
@@ -2196,6 +2391,7 @@ export {
2196
2391
  EvidenceEntrySchema,
2197
2392
  FalsificationSchema,
2198
2393
  HierarchyPositionSchema,
2394
+ IndexedStorage,
2199
2395
  MetaConfidenceSchema,
2200
2396
  MetaFieldSchema,
2201
2397
  PLATITUDE_PATTERNS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plur-ai/core",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,9 +12,11 @@
12
12
  "zod": "^3.23.0"
13
13
  },
14
14
  "optionalDependencies": {
15
- "@huggingface/transformers": "^3.8.1"
15
+ "@huggingface/transformers": "^3.8.1",
16
+ "better-sqlite3": "^11.0.0"
16
17
  },
17
18
  "devDependencies": {
19
+ "@types/better-sqlite3": "^7.6.0",
18
20
  "@types/js-yaml": "^4.0.0",
19
21
  "@types/node": "^25.5.0"
20
22
  },