@plur-ai/core 0.8.3 → 0.9.1

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  atomicWrite
3
- } from "./chunk-MY4XVDCE.js";
3
+ } from "./chunk-PRK3B7WR.js";
4
4
 
5
5
  // src/logger.ts
6
6
  var level = process.env.PLUR_LOG_LEVEL || "warning";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  atomicWrite
3
- } from "./chunk-MY4XVDCE.js";
3
+ } from "./chunk-PRK3B7WR.js";
4
4
 
5
5
  // src/fts.ts
6
6
  var STOP_WORDS = /* @__PURE__ */ new Set([
@@ -183,6 +183,38 @@ async function embeddingSearch(engrams, query, limit, storagePath) {
183
183
  similarities.sort((a, b) => b.score - a.score);
184
184
  return similarities.slice(0, limit).map((s) => s.engram);
185
185
  }
186
+ async function embeddingSearchWithScores(engrams, query, limit, storagePath) {
187
+ if (engrams.length === 0) return [];
188
+ const cachePath = storagePath ? join(storagePath, ".embeddings-cache.json") : ".embeddings-cache.json";
189
+ const cache = loadCache(cachePath);
190
+ const queryEmbedding = await embed(query);
191
+ if (!queryEmbedding) {
192
+ return [];
193
+ }
194
+ const similarities = [];
195
+ for (const engram of engrams) {
196
+ const searchText = engramSearchText(engram);
197
+ const hash = hashStatement(searchText);
198
+ let engramEmbedding;
199
+ if (cache[engram.id]?.hash === hash) {
200
+ engramEmbedding = new Float32Array(cache[engram.id].embedding);
201
+ } else {
202
+ const emb = await embed(searchText);
203
+ if (!emb) return [];
204
+ engramEmbedding = emb;
205
+ cache[engram.id] = {
206
+ hash,
207
+ embedding: Array.from(engramEmbedding)
208
+ };
209
+ }
210
+ const rawScore = cosineSimilarity(queryEmbedding, engramEmbedding);
211
+ const score = Math.max(0, Math.min(1, rawScore));
212
+ similarities.push({ engram, score });
213
+ }
214
+ saveCache(cachePath, cache);
215
+ similarities.sort((a, b) => b.score - a.score);
216
+ return similarities.slice(0, limit);
217
+ }
186
218
 
187
219
  export {
188
220
  ftsTokenize,
@@ -192,5 +224,6 @@ export {
192
224
  searchEngrams,
193
225
  embed,
194
226
  cosineSimilarity,
195
- embeddingSearch
227
+ embeddingSearch,
228
+ embeddingSearchWithScores
196
229
  };
@@ -80,10 +80,11 @@ function hasConflictMarkers(root) {
80
80
  return result !== null && result.length > 0;
81
81
  }
82
82
  function pullRebase(root) {
83
- const result = gitSafe(["pull", "--rebase", "origin", "main"], root);
83
+ const branch = gitSafe(["rev-parse", "--abbrev-ref", "HEAD"], root) || "main";
84
+ const result = gitSafe(["pull", "--rebase", "origin", branch], root);
84
85
  if (result !== null) return true;
85
86
  gitSafe(["rebase", "--abort"], root);
86
- const mergeResult = gitSafe(["pull", "origin", "main", "--no-edit"], root);
87
+ const mergeResult = gitSafe(["pull", "origin", branch, "--no-edit"], root);
87
88
  if (mergeResult !== null) return true;
88
89
  if (hasConflictMarkers(root)) {
89
90
  gitSafe(["merge", "--abort"], root);
@@ -0,0 +1,14 @@
1
+ import {
2
+ cosineSimilarity,
3
+ embed,
4
+ embeddingSearch,
5
+ embeddingSearchWithScores
6
+ } from "./chunk-MW5EPL7G.js";
7
+ import "./chunk-PRK3B7WR.js";
8
+ import "./chunk-2ZDO52B4.js";
9
+ export {
10
+ cosineSimilarity,
11
+ embed,
12
+ embeddingSearch,
13
+ embeddingSearchWithScores
14
+ };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  interface HistoryEvent {
4
- event: 'engram_created' | 'engram_updated' | 'engram_merged' | 'feedback_received' | 'engram_retired' | 'engram_promoted' | 'failure_reported' | 'procedure_evolved';
4
+ event: 'engram_created' | 'engram_updated' | 'engram_merged' | 'feedback_received' | 'engram_retired' | 'engram_promoted' | 'failure_reported' | 'procedure_evolved' | 'recurrence_detected' | 'contradiction_detected' | 'scope_promoted' | 'buffer_pruned' | 'weekly_review';
5
5
  engram_id: string;
6
6
  timestamp: string;
7
7
  data: Record<string, unknown>;
@@ -536,6 +536,43 @@ type KnowledgeAnchor = z.infer<typeof KnowledgeAnchorSchema>;
536
536
  type Association = z.infer<typeof AssociationSchema>;
537
537
  type PreviousVersionRef = z.infer<typeof PreviousVersionRefSchema>;
538
538
 
539
+ /** Map retrieval strength to a human-readable status label. */
540
+ declare function strengthToStatus(strength: number): string;
541
+ interface DecayTransition {
542
+ engram_id: string;
543
+ old_strength: number;
544
+ new_strength: number;
545
+ old_status: string;
546
+ new_status: string;
547
+ }
548
+ interface BatchDecayResult {
549
+ total: number;
550
+ decayed: number;
551
+ skipped: number;
552
+ transitions: DecayTransition[];
553
+ }
554
+ interface BatchDecayOptions {
555
+ contextScope?: string;
556
+ lambda?: number;
557
+ now?: Date;
558
+ }
559
+ /**
560
+ * Apply ACT-R decay to a batch of engrams.
561
+ * Scope-matched engrams are skipped (they never decay).
562
+ * Status transitions are logged to history.
563
+ * Returns the result summary and the list of engrams that were modified.
564
+ */
565
+ declare function applyBatchDecay(engrams: Engram[], historyRoot: string, options?: BatchDecayOptions): {
566
+ result: BatchDecayResult;
567
+ modified: Engram[];
568
+ };
569
+
570
+ /** Result with cosine similarity score attached. */
571
+ interface SimilarityResult {
572
+ engram: Engram;
573
+ score: number;
574
+ }
575
+
539
576
  declare const EpisodeSchema: z.ZodObject<{
540
577
  id: z.ZodString;
541
578
  summary: z.ZodString;
@@ -1850,6 +1887,18 @@ declare class Plur {
1850
1887
  private _loadAllEngrams;
1851
1888
  /** Load engrams from a path with mtime-based caching */
1852
1889
  private _loadCached;
1890
+ /**
1891
+ * Write engrams to disk and invalidate the cache for that path.
1892
+ *
1893
+ * Why: `_loadCached` uses mtime-based invalidation, but on CI tmpfs
1894
+ * (ubuntu-latest runners) mtime resolution can be coarse enough that a
1895
+ * stat() taken before and after a write returns the same mtime. When that
1896
+ * happens the cache serves a pre-write snapshot and a subsequent `getById`
1897
+ * returns `undefined` for an engram that `learn()` just created. Explicit
1898
+ * invalidation on write removes the filesystem as a source of cache
1899
+ * freshness and closes the race. See issue #25.
1900
+ */
1901
+ private _writeEngrams;
1853
1902
  /** Find which store owns an engram by ID. For namespaced IDs, strips prefix to find in store. */
1854
1903
  private _findEngramStore;
1855
1904
  /** Content hash fast-path dedup. */
@@ -1886,6 +1935,12 @@ declare class Plur {
1886
1935
  recallSemantic(query: string, options?: Omit<RecallOptions, 'mode' | 'llm'>): Promise<Engram[]>;
1887
1936
  /** Hybrid search: BM25 + embeddings merged via Reciprocal Rank Fusion. Async, no API calls. */
1888
1937
  recallHybrid(query: string, options?: Omit<RecallOptions, 'mode' | 'llm'>): Promise<Engram[]>;
1938
+ /** Embedding search returning {engram, score}[] with cosine similarity scores. Async, no API calls. */
1939
+ similaritySearch(query: string, options?: {
1940
+ limit?: number;
1941
+ scope?: string;
1942
+ domain?: string;
1943
+ }): Promise<SimilarityResult[]>;
1889
1944
  /** Expanded search: LLM query expansion + hybrid search + RRF merge. Opt-in, requires LLM function. */
1890
1945
  recallExpanded(query: string, options: RecallOptions & {
1891
1946
  llm: LlmFunction;
@@ -1924,6 +1979,16 @@ declare class Plur {
1924
1979
  removed: number;
1925
1980
  remaining: number;
1926
1981
  };
1982
+ /**
1983
+ * Apply ACT-R decay to all primary store engrams.
1984
+ * Scope-matched engrams are skipped, status transitions logged to history.
1985
+ * Modified engrams are saved back to the store.
1986
+ */
1987
+ batchDecay(options?: {
1988
+ contextScope?: string;
1989
+ lambda?: number;
1990
+ now?: Date;
1991
+ }): BatchDecayResult;
1927
1992
  /** Rebuild SQLite index from YAML source of truth. Only works when index: true. */
1928
1993
  reindex(): void;
1929
1994
  /** Sync SQLite index after YAML write (no-op if index disabled) */
@@ -1985,6 +2050,15 @@ declare class Plur {
1985
2050
  shared?: boolean;
1986
2051
  readonly?: boolean;
1987
2052
  }): void;
2053
+ /**
2054
+ * Auto-discover .plur/engrams.yaml in CWD and parent dirs (up to git root).
2055
+ * If found and not already registered, auto-register as a project store.
2056
+ * Returns list of newly discovered stores (empty if none found or all already known).
2057
+ */
2058
+ autoDiscoverStores(cwd?: string): Array<{
2059
+ path: string;
2060
+ scope: string;
2061
+ }>;
1988
2062
  /** List all configured stores. */
1989
2063
  listStores(): Array<{
1990
2064
  path: string;
@@ -1995,4 +2069,4 @@ declare class Plur {
1995
2069
  }>;
1996
2070
  }
1997
2071
 
1998
- export { ALL_MIGRATIONS, type AlignmentResult, type Association, type AutoSearchResult, type BoundedRecallResult, COMMITMENT_MULTIPLIER, CURRENT_SCHEMA_VERSION, type CaptureContext, type DedupConfig, type DedupDecision, type DomainCoverage, DomainCoverageSchema, type Engram, type EngramCluster, type EngramStore, type Episode, type EvidenceEntry, EvidenceEntrySchema, type ExtractOptions, type ExtractionResult, type Falsification, FalsificationSchema, type HierarchyPosition, HierarchyPositionSchema, type HistoryEvent, IndexedStorage, type IngestCandidate, type IngestOptions, type InjectOptions, type InjectionLayer, type InjectionResult, type KnowledgeAnchor, type LearnAsyncContext, type LearnAsyncResult, type LearnBatchResult, type LearnContext, type LlmFunction, type LlmTierConfig, type MemberAlignment, type MetaConfidence, MetaConfidenceSchema, type MetaField, MetaFieldSchema, type Migration, type MigrationResult, type ModelTier, PLATITUDE_PATTERNS, type PackManifest, Plur, type PlurConfig, type PlurPaths, type PreviewResult, type PreviousVersionRef, type PrivacyIssue, type PrivacyScanResult, type ProfileCache, type RecallBudget, type RecallOptions, type RegistryEntry, type RelationalAnalysis, type RelationalTriple, type SearchStrategy, SessionBreadcrumbs, SqliteStore, type StatusResult, type StorageBackend, type StorageConfig, type StoreEntry, type StructuralTemplate, StructuralTemplateSchema, type SyncResult, type SyncStatus, type TimelineQuery, type TypedRole, type ValidationResult, type VersionCheckResult, YamlStore, alignCluster, analyzeStructure, appendHistory, assignLayer, asyncAtomicWrite, autoSummary, buildBatchDedupPrompt, buildDedupPrompt, checkForUpdate, classifyPolarity, clearVersionCache, clusterByStructure, computeConfidence, computeContentHash, computeMetaConfidence, confidenceBand, createStore, detectPlurStorage, detectSecrets, engramSearchText, extractMetaEngrams, formatLayer1, formatLayer2, formatLayer3, formatWithLayer, formulateMetaEngram, freshTailBoost, generateEventId, generateGuardrails, generateProfile, generateSummary, getCachedUpdateCheck, getProfileForInjection, getSchemaVersion, isPlatitude, listHistoryMonths, loadProfileCache, markProfileDirty, migrateStore, needsSummary, normalizeStatement, organizeHierarchy, parseDedupResponse, profileNeedsRegeneration, readHistory, readHistoryForEngram, recallAuto, resolveOperationTier, rollbackMigrations, runMigrations, saveProfileCache, selectModel, selectModelForOperation, setSchemaVersion, tokenSimilarity, validateMetaEngram, withAsyncLock };
2072
+ export { ALL_MIGRATIONS, type AlignmentResult, type Association, type AutoSearchResult, type BatchDecayOptions, type BatchDecayResult, type BoundedRecallResult, COMMITMENT_MULTIPLIER, CURRENT_SCHEMA_VERSION, type CaptureContext, type DecayTransition, type DedupConfig, type DedupDecision, type DomainCoverage, DomainCoverageSchema, type Engram, type EngramCluster, type EngramStore, type Episode, type EvidenceEntry, EvidenceEntrySchema, type ExtractOptions, type ExtractionResult, type Falsification, FalsificationSchema, type HierarchyPosition, HierarchyPositionSchema, type HistoryEvent, IndexedStorage, type IngestCandidate, type IngestOptions, type InjectOptions, type InjectionLayer, type InjectionResult, type KnowledgeAnchor, type LearnAsyncContext, type LearnAsyncResult, type LearnBatchResult, type LearnContext, type LlmFunction, type LlmTierConfig, type MemberAlignment, type MetaConfidence, MetaConfidenceSchema, type MetaField, MetaFieldSchema, type Migration, type MigrationResult, type ModelTier, PLATITUDE_PATTERNS, type PackManifest, Plur, type PlurConfig, type PlurPaths, type PreviewResult, type PreviousVersionRef, type PrivacyIssue, type PrivacyScanResult, type ProfileCache, type RecallBudget, type RecallOptions, type RegistryEntry, type RelationalAnalysis, type RelationalTriple, type SearchStrategy, SessionBreadcrumbs, type SimilarityResult, SqliteStore, type StatusResult, type StorageBackend, type StorageConfig, type StoreEntry, type StructuralTemplate, StructuralTemplateSchema, type SyncResult, type SyncStatus, type TimelineQuery, type TypedRole, type ValidationResult, type VersionCheckResult, YamlStore, alignCluster, analyzeStructure, appendHistory, applyBatchDecay, assignLayer, asyncAtomicWrite, autoSummary, buildBatchDedupPrompt, buildDedupPrompt, checkForUpdate, classifyPolarity, clearVersionCache, clusterByStructure, computeConfidence, computeContentHash, computeMetaConfidence, confidenceBand, createStore, detectPlurStorage, detectSecrets, engramSearchText, extractMetaEngrams, formatLayer1, formatLayer2, formatLayer3, formatWithLayer, formulateMetaEngram, freshTailBoost, generateEventId, generateGuardrails, generateProfile, generateSummary, getCachedUpdateCheck, getProfileForInjection, getSchemaVersion, isPlatitude, listHistoryMonths, loadProfileCache, markProfileDirty, migrateStore, needsSummary, normalizeStatement, organizeHierarchy, parseDedupResponse, profileNeedsRegeneration, readHistory, readHistoryForEngram, recallAuto, resolveOperationTier, rollbackMigrations, runMigrations, saveProfileCache, selectModel, selectModelForOperation, setSchemaVersion, strengthToStatus, tokenSimilarity, validateMetaEngram, withAsyncLock };
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  computeIdf,
3
3
  embeddingSearch,
4
+ embeddingSearchWithScores,
4
5
  engramSearchText,
5
6
  ftsScore,
6
7
  ftsTokenize,
7
8
  searchEngrams
8
- } from "./chunk-UETCDULF.js";
9
+ } from "./chunk-MW5EPL7G.js";
9
10
  import {
10
11
  EngramSchemaPassthrough,
11
12
  appendHistory,
@@ -25,14 +26,16 @@ import {
25
26
  readHistoryForEngram,
26
27
  saveEngrams,
27
28
  storePrefix
28
- } from "./chunk-GRDNBUIJ.js";
29
+ } from "./chunk-3ZPTRZE3.js";
29
30
  import {
30
31
  atomicWrite,
31
32
  getSyncStatus,
32
33
  sync,
33
34
  withLock
34
- } from "./chunk-MY4XVDCE.js";
35
- import "./chunk-2ZDO52B4.js";
35
+ } from "./chunk-PRK3B7WR.js";
36
+ import {
37
+ __require
38
+ } from "./chunk-2ZDO52B4.js";
36
39
 
37
40
  // src/index.ts
38
41
  import * as fs4 from "fs";
@@ -316,6 +319,72 @@ function confidenceDecay(retrievalStrength, lastPositiveFeedbackDate, commitment
316
319
  const decayed = retrievalStrength * multiplier;
317
320
  return Math.max(CONFIDENCE_DECAY_FLOOR, decayed);
318
321
  }
322
+ function strengthToStatus(strength) {
323
+ if (strength > 0.5) return "active";
324
+ if (strength > 0.3) return "fading";
325
+ if (strength > 0.1) return "dormant";
326
+ return "retirement_candidate";
327
+ }
328
+ function applyBatchDecay(engrams, historyRoot, options) {
329
+ const now = options?.now ?? /* @__PURE__ */ new Date();
330
+ const lambda = options?.lambda ?? DECAY_RATE;
331
+ const contextScope = options?.contextScope;
332
+ const active = engrams.filter((e) => e.status === "active");
333
+ const transitions = [];
334
+ const modified = [];
335
+ let decayed = 0;
336
+ let skipped = 0;
337
+ for (const engram of active) {
338
+ if (contextScope && isScopeMatched(engram.scope, contextScope)) {
339
+ skipped++;
340
+ continue;
341
+ }
342
+ const days = daysSince(engram.activation.last_accessed, now);
343
+ if (days === 0) continue;
344
+ const emotionalWeight = engram.episodic?.emotional_weight ?? 5;
345
+ const effectiveLambda = lambda * (1 - emotionalWeight / 20);
346
+ const oldStrength = engram.activation.retrieval_strength;
347
+ const newStrength = decayedStrength(oldStrength, days, effectiveLambda);
348
+ if (Math.abs(newStrength - oldStrength) < 1e-10) continue;
349
+ const oldStatus = strengthToStatus(oldStrength);
350
+ const newStatus = strengthToStatus(newStrength);
351
+ engram.activation.retrieval_strength = newStrength;
352
+ modified.push(engram);
353
+ decayed++;
354
+ if (oldStatus !== newStatus) {
355
+ const transition = {
356
+ engram_id: engram.id,
357
+ old_strength: oldStrength,
358
+ new_strength: newStrength,
359
+ old_status: oldStatus,
360
+ new_status: newStatus
361
+ };
362
+ transitions.push(transition);
363
+ const event = {
364
+ event: "engram_updated",
365
+ engram_id: engram.id,
366
+ timestamp: now.toISOString(),
367
+ data: {
368
+ reason: "decay_status_transition",
369
+ old_strength: oldStrength,
370
+ new_strength: newStrength,
371
+ old_status: oldStatus,
372
+ new_status: newStatus
373
+ }
374
+ };
375
+ appendHistory(historyRoot, event);
376
+ }
377
+ }
378
+ return {
379
+ result: { total: active.length, decayed, skipped, transitions },
380
+ modified
381
+ };
382
+ }
383
+ function isScopeMatched(engramScope, contextScope) {
384
+ if (engramScope === contextScope) return true;
385
+ if (engramScope.startsWith(contextScope + "/")) return true;
386
+ return false;
387
+ }
319
388
 
320
389
  // src/polarity.ts
321
390
  var DONT_PATTERNS = [
@@ -1516,7 +1585,7 @@ async function computeSimilarityMatrix(templates) {
1516
1585
  const n = templates.length;
1517
1586
  const matrix = Array.from({ length: n }, () => Array(n).fill(0));
1518
1587
  try {
1519
- const { embed, cosineSimilarity } = await import("./embeddings-EX7QPXJS.js");
1588
+ const { embed, cosineSimilarity } = await import("./embeddings-VXK2E2IF.js");
1520
1589
  const embeddings = [];
1521
1590
  for (const t of templates) {
1522
1591
  embeddings.push(await embed(t));
@@ -2942,9 +3011,9 @@ var Plur = class {
2942
3011
  }
2943
3012
  /** Load engrams from a path with mtime-based caching */
2944
3013
  _loadCached(path3) {
2945
- let mtime = 0;
3014
+ let mtime;
2946
3015
  try {
2947
- mtime = fs4.statSync(path3).mtimeMs;
3016
+ mtime = fs4.statSync(path3, { bigint: true }).mtimeNs;
2948
3017
  } catch {
2949
3018
  return [];
2950
3019
  }
@@ -2954,6 +3023,21 @@ var Plur = class {
2954
3023
  this._engramCache.set(path3, { mtime, engrams });
2955
3024
  return engrams;
2956
3025
  }
3026
+ /**
3027
+ * Write engrams to disk and invalidate the cache for that path.
3028
+ *
3029
+ * Why: `_loadCached` uses mtime-based invalidation, but on CI tmpfs
3030
+ * (ubuntu-latest runners) mtime resolution can be coarse enough that a
3031
+ * stat() taken before and after a write returns the same mtime. When that
3032
+ * happens the cache serves a pre-write snapshot and a subsequent `getById`
3033
+ * returns `undefined` for an engram that `learn()` just created. Explicit
3034
+ * invalidation on write removes the filesystem as a source of cache
3035
+ * freshness and closes the race. See issue #25.
3036
+ */
3037
+ _writeEngrams(path3, engrams) {
3038
+ saveEngrams(path3, engrams);
3039
+ this._engramCache.delete(path3);
3040
+ }
2957
3041
  /** Find which store owns an engram by ID. For namespaced IDs, strips prefix to find in store. */
2958
3042
  _findEngramStore(id) {
2959
3043
  const primaryEngrams = this._loadCached(this.paths.engrams);
@@ -3083,7 +3167,7 @@ var Plur = class {
3083
3167
  } : void 0
3084
3168
  };
3085
3169
  engrams.push(engram);
3086
- saveEngrams(this.paths.engrams, engrams);
3170
+ this._writeEngrams(this.paths.engrams, engrams);
3087
3171
  this._syncIndex();
3088
3172
  appendHistory(this.paths.root, {
3089
3173
  event: "engram_created",
@@ -3113,12 +3197,12 @@ var Plur = class {
3113
3197
  }
3114
3198
  /** Async learn with LLM-driven deduplication (Ideas 1+2+19). */
3115
3199
  async learnAsync(statement, context) {
3116
- const { learnAsync: learnAsyncImpl } = await import("./learn-async-VXBH3TYE.js");
3200
+ const { learnAsync: learnAsyncImpl } = await import("./learn-async-UZAPPQK4.js");
3117
3201
  return learnAsyncImpl(this._learnAsyncDeps(), statement, context);
3118
3202
  }
3119
3203
  /** Batch learn with LLM dedup. */
3120
3204
  async learnBatch(statements, llm) {
3121
- const { learnBatch: learnBatchImpl } = await import("./learn-async-VXBH3TYE.js");
3205
+ const { learnBatch: learnBatchImpl } = await import("./learn-async-UZAPPQK4.js");
3122
3206
  return learnBatchImpl(this._learnAsyncDeps(), statements, llm);
3123
3207
  }
3124
3208
  /**
@@ -3159,6 +3243,12 @@ var Plur = class {
3159
3243
  this._reactivateResults(results);
3160
3244
  return results;
3161
3245
  }
3246
+ /** Embedding search returning {engram, score}[] with cosine similarity scores. Async, no API calls. */
3247
+ async similaritySearch(query, options) {
3248
+ const filtered = this._filterEngrams(options);
3249
+ const limit = options?.limit ?? 20;
3250
+ return embeddingSearchWithScores(filtered, query, limit, this.paths.root);
3251
+ }
3162
3252
  /** Expanded search: LLM query expansion + hybrid search + RRF merge. Opt-in, requires LLM function. */
3163
3253
  async recallExpanded(query, options) {
3164
3254
  const filtered = this._filterEngrams(options);
@@ -3267,7 +3357,7 @@ var Plur = class {
3267
3357
  }
3268
3358
  }
3269
3359
  if (modified) {
3270
- saveEngrams(this.paths.engrams, allEngrams);
3360
+ this._writeEngrams(this.paths.engrams, allEngrams);
3271
3361
  this._syncIndex();
3272
3362
  }
3273
3363
  });
@@ -3347,7 +3437,7 @@ var Plur = class {
3347
3437
  } else if (signal === "negative") {
3348
3438
  engram.activation.retrieval_strength = Math.max(0, engram.activation.retrieval_strength - 0.1);
3349
3439
  }
3350
- saveEngrams(this.paths.engrams, engrams);
3440
+ this._writeEngrams(this.paths.engrams, engrams);
3351
3441
  this._syncIndex();
3352
3442
  appendHistory(this.paths.root, {
3353
3443
  event: "feedback_received",
@@ -3375,8 +3465,7 @@ var Plur = class {
3375
3465
  } else if (signal === "negative") {
3376
3466
  engram.activation.retrieval_strength = Math.max(0, engram.activation.retrieval_strength - 0.1);
3377
3467
  }
3378
- saveEngrams(storeInfo.path, storeEngrams);
3379
- this._engramCache.delete(storeInfo.path);
3468
+ this._writeEngrams(storeInfo.path, storeEngrams);
3380
3469
  this._syncIndex();
3381
3470
  return;
3382
3471
  }
@@ -3399,7 +3488,7 @@ var Plur = class {
3399
3488
  }
3400
3489
  }
3401
3490
  if (saved > 0) {
3402
- saveEngrams(this.paths.engrams, engrams);
3491
+ this._writeEngrams(this.paths.engrams, engrams);
3403
3492
  this._syncIndex();
3404
3493
  }
3405
3494
  return { saved, skipped };
@@ -3412,7 +3501,7 @@ var Plur = class {
3412
3501
  const idx = engrams.findIndex((e) => e.id === updated.id);
3413
3502
  if (idx === -1) return false;
3414
3503
  engrams[idx] = updated;
3415
- saveEngrams(this.paths.engrams, engrams);
3504
+ this._writeEngrams(this.paths.engrams, engrams);
3416
3505
  this._syncIndex();
3417
3506
  return true;
3418
3507
  });
@@ -3427,7 +3516,7 @@ var Plur = class {
3427
3516
  if (reason && !engram.rationale) {
3428
3517
  engram.rationale = `Retired: ${reason}`;
3429
3518
  }
3430
- saveEngrams(this.paths.engrams, engrams);
3519
+ this._writeEngrams(this.paths.engrams, engrams);
3431
3520
  this._syncIndex();
3432
3521
  appendHistory(this.paths.root, {
3433
3522
  event: "engram_retired",
@@ -3450,8 +3539,7 @@ var Plur = class {
3450
3539
  if (reason && !engram.rationale) {
3451
3540
  engram.rationale = `Retired: ${reason}`;
3452
3541
  }
3453
- saveEngrams(storeInfo.path, storeEngrams);
3454
- this._engramCache.delete(storeInfo.path);
3542
+ this._writeEngrams(storeInfo.path, storeEngrams);
3455
3543
  this._syncIndex();
3456
3544
  return;
3457
3545
  }
@@ -3465,12 +3553,28 @@ var Plur = class {
3465
3553
  const active = engrams.filter((e) => e.status !== "retired");
3466
3554
  const removed = engrams.length - active.length;
3467
3555
  if (removed > 0) {
3468
- saveEngrams(this.paths.engrams, active);
3556
+ this._writeEngrams(this.paths.engrams, active);
3469
3557
  this._syncIndex();
3470
3558
  }
3471
3559
  return { removed, remaining: active.length };
3472
3560
  });
3473
3561
  }
3562
+ /**
3563
+ * Apply ACT-R decay to all primary store engrams.
3564
+ * Scope-matched engrams are skipped, status transitions logged to history.
3565
+ * Modified engrams are saved back to the store.
3566
+ */
3567
+ batchDecay(options) {
3568
+ return withLock(this.paths.engrams, () => {
3569
+ const engrams = loadEngrams(this.paths.engrams);
3570
+ const { result, modified } = applyBatchDecay(engrams, this.paths.root, options);
3571
+ if (result.transitions.length > 0) {
3572
+ this._writeEngrams(this.paths.engrams, modified);
3573
+ this._syncIndex();
3574
+ }
3575
+ return result;
3576
+ });
3577
+ }
3474
3578
  /** Rebuild SQLite index from YAML source of truth. Only works when index: true. */
3475
3579
  reindex() {
3476
3580
  if (!this.indexedStorage) {
@@ -3672,7 +3776,7 @@ Generate an improved version of the procedure that prevents this failure. Return
3672
3776
  raw.previous_version_ref = { event_id: eventId, changed_at: now };
3673
3777
  if (!raw.episode_ids) raw.episode_ids = [];
3674
3778
  raw.episode_ids.push(episode.id);
3675
- saveEngrams(this.paths.engrams, engrams);
3779
+ this._writeEngrams(this.paths.engrams, engrams);
3676
3780
  this._syncIndex();
3677
3781
  appendHistory(this.paths.root, {
3678
3782
  event: "procedure_evolved",
@@ -3702,7 +3806,7 @@ Generate an improved version of the procedure that prevents this failure. Return
3702
3806
  const raw = engrams[idx];
3703
3807
  if (!raw.episode_ids) raw.episode_ids = [];
3704
3808
  raw.episode_ids.push(episode.id);
3705
- saveEngrams(this.paths.engrams, engrams);
3809
+ this._writeEngrams(this.paths.engrams, engrams);
3706
3810
  this._syncIndex();
3707
3811
  }
3708
3812
  });
@@ -3759,6 +3863,53 @@ Generate an improved version of the procedure that prevents this failure. Return
3759
3863
  fs4.writeFileSync(this.paths.config, yaml6.dump(configData, { lineWidth: 120, noRefs: true }));
3760
3864
  this.config = loadConfig(this.paths.config);
3761
3865
  }
3866
+ /**
3867
+ * Auto-discover .plur/engrams.yaml in CWD and parent dirs (up to git root).
3868
+ * If found and not already registered, auto-register as a project store.
3869
+ * Returns list of newly discovered stores (empty if none found or all already known).
3870
+ */
3871
+ autoDiscoverStores(cwd) {
3872
+ const startDir = cwd || process.cwd();
3873
+ const discovered = [];
3874
+ const os = __require("os");
3875
+ const tmpDir = os.tmpdir();
3876
+ if (this.paths.root.startsWith(tmpDir) || this.paths.root.startsWith("/tmp/")) {
3877
+ return discovered;
3878
+ }
3879
+ const knownPaths = new Set((this.config.stores ?? []).map((s) => s.path));
3880
+ const primaryDir = __require("path").dirname(this.paths.engrams);
3881
+ let dir = startDir;
3882
+ const { join: join5, dirname: dirname2, basename: basename2 } = __require("path");
3883
+ const visited = /* @__PURE__ */ new Set();
3884
+ while (dir && !visited.has(dir)) {
3885
+ visited.add(dir);
3886
+ const candidate = join5(dir, ".plur", "engrams.yaml");
3887
+ if (join5(dir, ".plur") === primaryDir) {
3888
+ dir = dirname2(dir);
3889
+ continue;
3890
+ }
3891
+ if (fs4.existsSync(candidate) && !knownPaths.has(candidate)) {
3892
+ let scope = `project:${basename2(dir)}`;
3893
+ try {
3894
+ const plurYaml = join5(dir, ".plur.yaml");
3895
+ if (fs4.existsSync(plurYaml)) {
3896
+ const raw = yaml6.load(fs4.readFileSync(plurYaml, "utf8"));
3897
+ if (raw?.scope) scope = raw.scope;
3898
+ }
3899
+ } catch {
3900
+ }
3901
+ this.addStore(candidate, scope, { shared: true, readonly: false });
3902
+ discovered.push({ path: candidate, scope });
3903
+ knownPaths.add(candidate);
3904
+ logger.info(`Auto-discovered project store: ${candidate} (${scope})`);
3905
+ }
3906
+ if (fs4.existsSync(join5(dir, ".git"))) break;
3907
+ const parent = dirname2(dir);
3908
+ if (parent === dir) break;
3909
+ dir = parent;
3910
+ }
3911
+ return discovered;
3912
+ }
3762
3913
  /** List all configured stores. */
3763
3914
  listStores() {
3764
3915
  const stores = this.config.stores ?? [];
@@ -3800,6 +3951,7 @@ export {
3800
3951
  alignCluster,
3801
3952
  analyzeStructure,
3802
3953
  appendHistory,
3954
+ applyBatchDecay,
3803
3955
  assignLayer,
3804
3956
  asyncAtomicWrite,
3805
3957
  autoSummary,
@@ -3851,6 +4003,7 @@ export {
3851
4003
  selectModel,
3852
4004
  selectModelForOperation,
3853
4005
  setSchemaVersion,
4006
+ strengthToStatus,
3854
4007
  tokenSimilarity,
3855
4008
  validateMetaEngram,
3856
4009
  withAsyncLock
@@ -6,10 +6,10 @@ import {
6
6
  logger,
7
7
  parseDedupResponse,
8
8
  saveEngrams
9
- } from "./chunk-GRDNBUIJ.js";
9
+ } from "./chunk-3ZPTRZE3.js";
10
10
  import {
11
11
  withLock
12
- } from "./chunk-MY4XVDCE.js";
12
+ } from "./chunk-PRK3B7WR.js";
13
13
  import "./chunk-2ZDO52B4.js";
14
14
 
15
15
  // src/learn-async.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plur-ai/core",
3
- "version": "0.8.3",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,12 +0,0 @@
1
- import {
2
- cosineSimilarity,
3
- embed,
4
- embeddingSearch
5
- } from "./chunk-UETCDULF.js";
6
- import "./chunk-MY4XVDCE.js";
7
- import "./chunk-2ZDO52B4.js";
8
- export {
9
- cosineSimilarity,
10
- embed,
11
- embeddingSearch
12
- };