@seanhogg/builderforce-memory-mcp 2026.10.2 → 2026.10.22

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 (82) hide show
  1. package/README.md +22 -5
  2. package/dist/backend.d.ts +26 -0
  3. package/dist/backend.d.ts.map +1 -1
  4. package/dist/backends/memory-store.d.ts +50 -2
  5. package/dist/backends/memory-store.d.ts.map +1 -1
  6. package/dist/backends/memory-store.js +88 -10
  7. package/dist/backends/memory-store.js.map +1 -1
  8. package/dist/bin/http.js +7 -1
  9. package/dist/bin/http.js.map +1 -1
  10. package/dist/bin/install.js +6 -3
  11. package/dist/bin/install.js.map +1 -1
  12. package/dist/bin/stdio.d.ts +9 -4
  13. package/dist/bin/stdio.d.ts.map +1 -1
  14. package/dist/bin/stdio.js +20 -5
  15. package/dist/bin/stdio.js.map +1 -1
  16. package/dist/compaction-tools.d.ts +15 -0
  17. package/dist/compaction-tools.d.ts.map +1 -0
  18. package/dist/compaction-tools.js +192 -0
  19. package/dist/compaction-tools.js.map +1 -0
  20. package/dist/compaction.d.ts +114 -8
  21. package/dist/compaction.d.ts.map +1 -1
  22. package/dist/compaction.js +161 -7
  23. package/dist/compaction.js.map +1 -1
  24. package/dist/experience/file-store.d.ts +2 -1
  25. package/dist/experience/file-store.d.ts.map +1 -1
  26. package/dist/experience/file-store.js +4 -0
  27. package/dist/experience/file-store.js.map +1 -1
  28. package/dist/experience/train.d.ts +13 -4
  29. package/dist/experience/train.d.ts.map +1 -1
  30. package/dist/experience/train.js +14 -5
  31. package/dist/experience/train.js.map +1 -1
  32. package/dist/index.d.ts +13 -6
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +11 -7
  35. package/dist/index.js.map +1 -1
  36. package/dist/install/claude-hooks.d.ts +5 -0
  37. package/dist/install/claude-hooks.d.ts.map +1 -1
  38. package/dist/install/claude-hooks.js +69 -9
  39. package/dist/install/claude-hooks.js.map +1 -1
  40. package/dist/install/server-spec.d.ts +33 -7
  41. package/dist/install/server-spec.d.ts.map +1 -1
  42. package/dist/install/server-spec.js +59 -9
  43. package/dist/install/server-spec.js.map +1 -1
  44. package/dist/model/recall-probe.d.ts +29 -0
  45. package/dist/model/recall-probe.d.ts.map +1 -0
  46. package/dist/model/recall-probe.js +72 -0
  47. package/dist/model/recall-probe.js.map +1 -0
  48. package/dist/persistence/fact-archive.d.ts +47 -0
  49. package/dist/persistence/fact-archive.d.ts.map +1 -0
  50. package/dist/persistence/fact-archive.js +85 -0
  51. package/dist/persistence/fact-archive.js.map +1 -0
  52. package/dist/persistence/shared-json-file.d.ts +24 -1
  53. package/dist/persistence/shared-json-file.d.ts.map +1 -1
  54. package/dist/persistence/shared-json-file.js +46 -1
  55. package/dist/persistence/shared-json-file.js.map +1 -1
  56. package/dist/restore.d.ts +56 -0
  57. package/dist/restore.d.ts.map +1 -0
  58. package/dist/restore.js +118 -0
  59. package/dist/restore.js.map +1 -0
  60. package/dist/tools.d.ts +13 -0
  61. package/dist/tools.d.ts.map +1 -1
  62. package/dist/tools.js +11 -90
  63. package/dist/tools.js.map +1 -1
  64. package/package.json +6 -6
  65. package/server.json +2 -2
  66. package/src/backend.ts +29 -0
  67. package/src/backends/memory-store.ts +141 -11
  68. package/src/bin/http.ts +7 -1
  69. package/src/bin/install.ts +7 -3
  70. package/src/bin/stdio.ts +20 -5
  71. package/src/compaction-tools.ts +206 -0
  72. package/src/compaction.ts +228 -9
  73. package/src/experience/file-store.ts +5 -0
  74. package/src/experience/train.ts +26 -9
  75. package/src/index.ts +55 -10
  76. package/src/install/claude-hooks.ts +72 -9
  77. package/src/install/server-spec.ts +74 -8
  78. package/src/model/recall-probe.ts +81 -0
  79. package/src/persistence/fact-archive.ts +101 -0
  80. package/src/persistence/shared-json-file.ts +54 -1
  81. package/src/restore.ts +144 -0
  82. package/src/tools.ts +24 -93
@@ -1 +1 @@
1
- {"version":3,"file":"server-spec.js","sourceRoot":"","sources":["../../src/install/server-spec.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,MAAM,MAAM,SAAS,CAAC;AAC7B,OAAO,QAAQ,MAAM,WAAW,CAAC;AAQjC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,0BAA0B,CAAC;AAE1D,MAAM,UAAU,iBAAiB,CAAC,UAAkB,MAAM,CAAC,OAAO,EAAE;IAChE,OAAO,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,sBAAsB,EAAE,aAAa,CAAC,CAAC;AACzE,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,iBAAiB,CAC7B,MAA0C,OAAO,CAAC,GAAG,EACrD,OAAgB;IAEhB,OAAO,GAAG,CAAC,eAAe,CAAC,IAAI,iBAAiB,CAAC,OAAO,CAAC,CAAC;AAC9D,CAAC;AAgBD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,WAAW,GAAG,mCAAmC,CAAC;AAC/D,MAAM,CAAC,MAAM,OAAO,GAAG,yBAAyB,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IACzB,+BAA+B;IAC/B,sCAAsC;IACtC,gBAAgB;CACnB,CAAC;AAEF,SAAS,OAAO,CAAC,IAAuB;IACpC,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,IAAI,IAAI,CAAC,UAAU;QAAE,GAAG,CAAC,0BAA0B,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC;IACvE,IAAI,IAAI,CAAC,QAAQ;QAAE,GAAG,CAAC,8BAA8B,CAAC,GAAG,GAAG,CAAC;IAC7D,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AACzD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,OAA0B,EAAE;IACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IACnD,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1B,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAC/E,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAE/F,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACvB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IACxF,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACtE,CAAC"}
1
+ {"version":3,"file":"server-spec.js","sourceRoot":"","sources":["../../src/install/server-spec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The recall probe compaction runs before it stubs a fact: the person's local
3
+ * Evermind model — the same `.evermind` package recall embeds with and experience
4
+ * training adapts (`BUILDERFORCE_MEMORY_MODEL`) — is asked the question generated
5
+ * from the fact (`probeQuestionFor`), and the fact is stubbed only when the answer
6
+ * carries it. No model configured, or one that cannot generate, means no probe —
7
+ * and no probe means nothing is compacted.
8
+ */
9
+ import type { RecallProbe } from "../compaction.js";
10
+ /** Tokens the model may spend answering one probe — a cloze answer is a word or two. */
11
+ export declare const PROBE_MAX_NEW_TOKENS = 16;
12
+ /** Wrap any text generator as a probe. Greedy, short, never throws. */
13
+ export declare function generatorProbe(generate: (prompt: string) => string | Promise<string>): RecallProbe;
14
+ export interface RecallProbeEnv {
15
+ env?: Record<string, string | undefined>;
16
+ }
17
+ /**
18
+ * The probe for this server, or null when no model is configured, the package is
19
+ * not a text LM, or its tokenizer cannot decode (no answer text to check).
20
+ */
21
+ export declare function createRecallProbe(opts?: RecallProbeEnv): Promise<RecallProbe | null>;
22
+ /**
23
+ * {@link createRecallProbe}, deferred to the first compaction — a server should not
24
+ * load a checkpoint at boot for a tool most sessions never call. Null when no model
25
+ * is configured; a model that then fails to load answers nothing (every probe fails,
26
+ * so nothing is stubbed).
27
+ */
28
+ export declare function lazyRecallProbe(opts?: RecallProbeEnv): RecallProbe | null;
29
+ //# sourceMappingURL=recall-probe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recall-probe.d.ts","sourceRoot":"","sources":["../../src/model/recall-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AASpD,wFAAwF;AACxF,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,WAAW,CAWlG;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC5C;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAkB9F;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,GAAE,cAAmB,GAAG,WAAW,GAAG,IAAI,CAW7E"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The recall probe compaction runs before it stubs a fact: the person's local
3
+ * Evermind model — the same `.evermind` package recall embeds with and experience
4
+ * training adapts (`BUILDERFORCE_MEMORY_MODEL`) — is asked the question generated
5
+ * from the fact (`probeQuestionFor`), and the fact is stubbed only when the answer
6
+ * carries it. No model configured, or one that cannot generate, means no probe —
7
+ * and no probe means nothing is compacted.
8
+ */
9
+ import { MODEL_FILE_ENV, TOKENIZER_FILE_ENV } from "../embedding/index.js";
10
+ import { loadEvermindPackage } from "./evermind-package.js";
11
+ /** Tokens the model may spend answering one probe — a cloze answer is a word or two. */
12
+ export const PROBE_MAX_NEW_TOKENS = 16;
13
+ /** Wrap any text generator as a probe. Greedy, short, never throws. */
14
+ export function generatorProbe(generate) {
15
+ return {
16
+ async answer(question) {
17
+ try {
18
+ const out = await generate(question);
19
+ return typeof out === "string" ? out : null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ },
25
+ };
26
+ }
27
+ /**
28
+ * The probe for this server, or null when no model is configured, the package is
29
+ * not a text LM, or its tokenizer cannot decode (no answer text to check).
30
+ */
31
+ export async function createRecallProbe(opts = {}) {
32
+ const env = opts.env ?? process.env;
33
+ const modelFile = env[MODEL_FILE_ENV];
34
+ if (!modelFile)
35
+ return null;
36
+ const loaded = await loadEvermindPackage(modelFile, env[TOKENIZER_FILE_ENV] || undefined);
37
+ if (!loaded)
38
+ return null;
39
+ const codec = loaded.codec;
40
+ if (typeof codec.decode !== "function")
41
+ return null;
42
+ let lm;
43
+ try {
44
+ lm = loaded.pkg.loadLM();
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ if (typeof lm?.generateText !== "function")
50
+ return null;
51
+ return generatorProbe((prompt) => lm.generateText(prompt, loaded.codec, { maxNewTokens: PROBE_MAX_NEW_TOKENS, temperature: 0 }));
52
+ }
53
+ /**
54
+ * {@link createRecallProbe}, deferred to the first compaction — a server should not
55
+ * load a checkpoint at boot for a tool most sessions never call. Null when no model
56
+ * is configured; a model that then fails to load answers nothing (every probe fails,
57
+ * so nothing is stubbed).
58
+ */
59
+ export function lazyRecallProbe(opts = {}) {
60
+ const env = opts.env ?? process.env;
61
+ if (!env[MODEL_FILE_ENV])
62
+ return null;
63
+ let probe = null;
64
+ return {
65
+ async answer(question) {
66
+ probe ??= createRecallProbe({ env });
67
+ const p = await probe;
68
+ return p ? p.answer(question) : null;
69
+ },
70
+ };
71
+ }
72
+ //# sourceMappingURL=recall-probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recall-probe.js","sourceRoot":"","sources":["../../src/model/recall-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAO5D,wFAAwF;AACxF,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEvC,uEAAuE;AACvE,MAAM,UAAU,cAAc,CAAC,QAAsD;IACjF,OAAO;QACH,KAAK,CAAC,MAAM,CAAC,QAAgB;YACzB,IAAI,CAAC;gBACD,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBACrC,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;YAChD,CAAC;YAAC,MAAM,CAAC;gBACL,OAAO,IAAI,CAAC;YAChB,CAAC;QACL,CAAC;KACJ,CAAC;AACN,CAAC;AAMD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,OAAuB,EAAE;IAC7D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACpC,MAAM,SAAS,GAAG,GAAG,CAAC,cAAc,CAAC,CAAC;IACtC,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,SAAS,EAAE,GAAG,CAAC,kBAAkB,CAAC,IAAI,SAAS,CAAC,CAAC;IAC1F,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAwC,CAAC;IAC9D,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IACpD,IAAI,EAAgB,CAAC;IACrB,IAAI,CAAC;QACD,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,EAAkB,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,IAAI,OAAO,EAAE,EAAE,YAAY,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IACxD,OAAO,cAAc,CAAC,CAAC,MAAM,EAAE,EAAE,CAC7B,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,EAAE,YAAY,EAAE,oBAAoB,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,CAChG,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,OAAuB,EAAE;IACrD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACpC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,IAAI,KAAK,GAAuC,IAAI,CAAC;IACrD,OAAO;QACH,KAAK,CAAC,MAAM,CAAC,QAAgB;YACzB,KAAK,KAAK,iBAAiB,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;YACrC,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC;YACtB,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACzC,CAAC;KACJ,CAAC;AACN,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * FactArchive — where a compacted fact's full original lives once its body is a stub.
3
+ *
4
+ * The archive is deliberately NOT a field on the stored entry: the store snapshot is
5
+ * rebuilt from the store on every write, rewritten in place by other processes, and
6
+ * capped/evicted at {@link DEFAULT_MAX_ENTRIES}; an archived original riding inside
7
+ * it would be lost to any one of those. A separate sidecar (`memory.archive.json`,
8
+ * see `defaultArchiveFile`) is only ever merged into, never pruned, so an evicted or
9
+ * forgotten stub still has its original.
10
+ *
11
+ * Two implementations, one contract: in-memory (a backend with no disk mirror) and
12
+ * file-backed (shared across processes through {@link SharedJsonFile}, with its
13
+ * rotating backups and freshness check).
14
+ */
15
+ import { type ArchivedFact } from "../compaction.js";
16
+ import { type SharedFileFs } from "./shared-json-file.js";
17
+ export interface FactArchive {
18
+ /** The archived original for `key`, if one exists. */
19
+ get(key: string): ArchivedFact | undefined;
20
+ /** Every row, read once — for callers resolving many keys in one pass. */
21
+ all(): Readonly<Record<string, ArchivedFact>>;
22
+ /** Add or replace archive rows. Durable before it returns. */
23
+ putMany(facts: ArchivedFact[]): void;
24
+ }
25
+ /** Process-local archive — the default for a backend with no disk mirror. */
26
+ export declare class MemoryFactArchive implements FactArchive {
27
+ private readonly rows;
28
+ get(key: string): ArchivedFact | undefined;
29
+ all(): Readonly<Record<string, ArchivedFact>>;
30
+ putMany(facts: ArchivedFact[]): void;
31
+ }
32
+ /**
33
+ * Archive mirrored to a JSON file several processes share. Re-reads the file when it
34
+ * changed underneath this process; every write merges into the latest on-disk text,
35
+ * so two writers add to one archive rather than overwriting each other's rows.
36
+ */
37
+ export declare class FileFactArchive implements FactArchive {
38
+ private rows;
39
+ private text;
40
+ private readonly file;
41
+ constructor(path: string, fs: SharedFileFs);
42
+ get(key: string): ArchivedFact | undefined;
43
+ all(): Readonly<Record<string, ArchivedFact>>;
44
+ putMany(facts: ArchivedFact[]): void;
45
+ private refresh;
46
+ }
47
+ //# sourceMappingURL=fact-archive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fact-archive.d.ts","sourceRoot":"","sources":["../../src/persistence/fact-archive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAA8B,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACjF,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAW1E,MAAM,WAAW,WAAW;IACxB,sDAAsD;IACtD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAC3C,0EAA0E;IAC1E,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;IAC9C,8DAA8D;IAC9D,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;CACxC;AAED,6EAA6E;AAC7E,qBAAa,iBAAkB,YAAW,WAAW;IACjD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqF;IAE1G,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAI1C,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAI7C,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,IAAI;CAGvC;AAED;;;;GAIG;AACH,qBAAa,eAAgB,YAAW,WAAW;IAC/C,OAAO,CAAC,IAAI,CAAoC;IAChD,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiB;gBAE1B,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,YAAY;IAK1C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAK1C,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAK7C,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,IAAI;IAepC,OAAO,CAAC,OAAO;CAOlB"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * FactArchive — where a compacted fact's full original lives once its body is a stub.
3
+ *
4
+ * The archive is deliberately NOT a field on the stored entry: the store snapshot is
5
+ * rebuilt from the store on every write, rewritten in place by other processes, and
6
+ * capped/evicted at {@link DEFAULT_MAX_ENTRIES}; an archived original riding inside
7
+ * it would be lost to any one of those. A separate sidecar (`memory.archive.json`,
8
+ * see `defaultArchiveFile`) is only ever merged into, never pruned, so an evicted or
9
+ * forgotten stub still has its original.
10
+ *
11
+ * Two implementations, one contract: in-memory (a backend with no disk mirror) and
12
+ * file-backed (shared across processes through {@link SharedJsonFile}, with its
13
+ * rotating backups and freshness check).
14
+ */
15
+ import { mergeArchive, parseArchive } from "../compaction.js";
16
+ import { SharedJsonFile } from "./shared-json-file.js";
17
+ function isJson(text) {
18
+ try {
19
+ JSON.parse(text);
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ /** Process-local archive — the default for a backend with no disk mirror. */
27
+ export class MemoryFactArchive {
28
+ rows = Object.create(null);
29
+ get(key) {
30
+ return this.rows[key];
31
+ }
32
+ all() {
33
+ return this.rows;
34
+ }
35
+ putMany(facts) {
36
+ for (const f of facts)
37
+ this.rows[f.key] = f;
38
+ }
39
+ }
40
+ /**
41
+ * Archive mirrored to a JSON file several processes share. Re-reads the file when it
42
+ * changed underneath this process; every write merges into the latest on-disk text,
43
+ * so two writers add to one archive rather than overwriting each other's rows.
44
+ */
45
+ export class FileFactArchive {
46
+ rows = {};
47
+ text = null;
48
+ file;
49
+ constructor(path, fs) {
50
+ this.file = new SharedJsonFile(path, fs);
51
+ this.refresh();
52
+ }
53
+ get(key) {
54
+ this.refresh();
55
+ return this.rows[key];
56
+ }
57
+ all() {
58
+ this.refresh();
59
+ return this.rows;
60
+ }
61
+ putMany(facts) {
62
+ if (facts.length === 0)
63
+ return;
64
+ this.refresh();
65
+ // An archive that exists but will not parse is refused, never overwritten:
66
+ // merging into "{}" would drop every row it holds. The throw reaches the
67
+ // compactor BEFORE any stub is written, so nothing is compacted either.
68
+ if (this.text && this.text.trim() && !isJson(this.text)) {
69
+ throw new Error(`fact archive ${this.file.path} is unreadable; refusing to overwrite it`);
70
+ }
71
+ const next = mergeArchive(this.text, facts);
72
+ this.file.write(next);
73
+ this.text = next;
74
+ this.rows = parseArchive(next);
75
+ }
76
+ refresh() {
77
+ const changed = this.file.readIfChanged();
78
+ if (!changed)
79
+ return;
80
+ this.text = changed.text;
81
+ this.rows = parseArchive(changed.text);
82
+ changed.accept();
83
+ }
84
+ }
85
+ //# sourceMappingURL=fact-archive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fact-archive.js","sourceRoot":"","sources":["../../src/persistence/fact-archive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAqB,MAAM,kBAAkB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAqB,MAAM,uBAAuB,CAAC;AAE1E,SAAS,MAAM,CAAC,IAAY;IACxB,IAAI,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACjB,OAAO,IAAI,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC;AAWD,6EAA6E;AAC7E,MAAM,OAAO,iBAAiB;IACT,IAAI,GAAiC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAiC,CAAC;IAE1G,GAAG,CAAC,GAAW;QACX,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,GAAG;QACC,OAAO,IAAI,CAAC,IAAI,CAAC;IACrB,CAAC;IAED,OAAO,CAAC,KAAqB;QACzB,KAAK,MAAM,CAAC,IAAI,KAAK;YAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChD,CAAC;CACJ;AAED;;;;GAIG;AACH,MAAM,OAAO,eAAe;IAChB,IAAI,GAAiC,EAAE,CAAC;IACxC,IAAI,GAAkB,IAAI,CAAC;IAClB,IAAI,CAAiB;IAEtC,YAAY,IAAY,EAAE,EAAgB;QACtC,IAAI,CAAC,IAAI,GAAG,IAAI,cAAc,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;IAED,GAAG,CAAC,GAAW;QACX,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,GAAG;QACC,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,IAAI,CAAC;IACrB,CAAC;IAED,OAAO,CAAC,KAAqB;QACzB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC/B,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,2EAA2E;QAC3E,yEAAyE;QACzE,wEAAwE;QACxE,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACtD,MAAM,IAAI,KAAK,CAAC,gBAAgB,IAAI,CAAC,IAAI,CAAC,IAAI,0CAA0C,CAAC,CAAC;QAC9F,CAAC;QACD,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC5C,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAEO,OAAO;QACX,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;QAC1C,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvC,OAAO,CAAC,MAAM,EAAE,CAAC;IACrB,CAAC;CACJ"}
@@ -10,6 +10,11 @@
10
10
  * The loop guard is the stamp recorded immediately AFTER each of our own writes: the
11
11
  * next check stats the file, sees the same mtime+size, and returns without reading a
12
12
  * byte. A stat per call is the entire steady-state cost.
13
+ *
14
+ * Every overwrite first rotates the previous file into `<file>.bak.1` (shifting
15
+ * `.bak.1`→`.bak.2`→`.bak.3`, the oldest dropping off), so the last
16
+ * {@link DEFAULT_BACKUPS} versions of a shared store are always recoverable — a
17
+ * bad rewrite (from any process) is never the only copy.
13
18
  */
14
19
  export type SharedFileFs = {
15
20
  readFileSync(path: string, enc: "utf8"): string;
@@ -17,6 +22,8 @@ export type SharedFileFs = {
17
22
  /** When present, writes land atomically (temp file + rename). */
18
23
  renameSync?(from: string, to: string): void;
19
24
  unlinkSync?(path: string): void;
25
+ /** Used to snapshot the current file into a backup before an overwrite. */
26
+ copyFileSync?(from: string, to: string): void;
20
27
  existsSync(path: string): boolean;
21
28
  statSync(path: string): {
22
29
  mtimeMs: number;
@@ -39,12 +46,21 @@ export interface ChangedFile {
39
46
  */
40
47
  accept(): void;
41
48
  }
49
+ /** Rotating backups kept beside a shared file by default (`.bak.1` newest … `.bak.3`). */
50
+ export declare const DEFAULT_BACKUPS = 3;
51
+ /** The path of the `n`-th backup of `file` (1 = newest). */
52
+ export declare function backupFile(file: string, n: number): string;
53
+ export interface SharedJsonFileOptions {
54
+ /** Rotating backups to keep before each overwrite. Default {@link DEFAULT_BACKUPS}; 0 disables. */
55
+ backups?: number;
56
+ }
42
57
  export declare class SharedJsonFile {
43
58
  readonly path: string;
44
59
  private readonly fs;
45
60
  /** How the file looked when this process last wrote or absorbed it. */
46
61
  private stamp;
47
- constructor(path: string, fs: SharedFileFs);
62
+ private readonly backups;
63
+ constructor(path: string, fs: SharedFileFs, opts?: SharedJsonFileOptions);
48
64
  /**
49
65
  * The file's text when it changed since we last touched it — and on first use,
50
66
  * which IS the boot hydration (one routine, so disk and memory never diverge by
@@ -61,5 +77,12 @@ export declare class SharedJsonFile {
61
77
  * file open), the write falls back to writing in place.
62
78
  */
63
79
  write(text: string): void;
80
+ /**
81
+ * Shift `.bak.(n-1)` → `.bak.n` down to `.bak.1`, then copy the current file to
82
+ * `.bak.1`. Best effort: a backup that cannot be written never blocks the write
83
+ * it protects.
84
+ */
85
+ private rotateBackups;
86
+ private moveFile;
64
87
  }
65
88
  //# sourceMappingURL=shared-json-file.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared-json-file.d.ts","sourceRoot":"","sources":["../../src/persistence/shared-json-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,YAAY,GAAG;IACvB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAChD,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,iEAAiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7D,CAAC;AAUF;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED,qEAAqE;AACrE,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,MAAM,IAAI,IAAI,CAAC;CAClB;AAED,qBAAa,cAAc;IAKnB,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,OAAO,CAAC,QAAQ,CAAC,EAAE;IALvB,uEAAuE;IACvE,OAAO,CAAC,KAAK,CAA0B;gBAG1B,IAAI,EAAE,MAAM,EACJ,EAAE,EAAE,YAAY;IAGrC;;;;OAIG;IACH,aAAa,IAAI,WAAW,GAAG,IAAI;IAyBnC;;;;;;;;OAQG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;CAyB5B"}
1
+ {"version":3,"file":"shared-json-file.d.ts","sourceRoot":"","sources":["../../src/persistence/shared-json-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,YAAY,GAAG;IACvB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAChD,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,iEAAiE;IACjE,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,2EAA2E;IAC3E,YAAY,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7D,CAAC;AAUF;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED,qEAAqE;AACrE,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,MAAM,IAAI,IAAI,CAAC;CAClB;AAED,0FAA0F;AAC1F,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC,4DAA4D;AAC5D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,MAAM,WAAW,qBAAqB;IAClC,mGAAmG;IACnG,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,cAAc;IAMnB,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,OAAO,CAAC,QAAQ,CAAC,EAAE;IANvB,uEAAuE;IACvE,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAGpB,IAAI,EAAE,MAAM,EACJ,EAAE,EAAE,YAAY,EACjC,IAAI,GAAE,qBAA0B;IAKpC;;;;OAIG;IACH,aAAa,IAAI,WAAW,GAAG,IAAI;IAyBnC;;;;;;;;OAQG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IA2BzB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,QAAQ;CAOnB"}
@@ -10,6 +10,11 @@
10
10
  * The loop guard is the stamp recorded immediately AFTER each of our own writes: the
11
11
  * next check stats the file, sees the same mtime+size, and returns without reading a
12
12
  * byte. A stat per call is the entire steady-state cost.
13
+ *
14
+ * Every overwrite first rotates the previous file into `<file>.bak.1` (shifting
15
+ * `.bak.1`→`.bak.2`→`.bak.3`, the oldest dropping off), so the last
16
+ * {@link DEFAULT_BACKUPS} versions of a shared store are always recoverable — a
17
+ * bad rewrite (from any process) is never the only copy.
13
18
  */
14
19
  /**
15
20
  * FNV-1a over the file text. Non-cryptographic on purpose: this only has to separate
@@ -25,14 +30,22 @@ export function contentHash(text) {
25
30
  }
26
31
  return (h >>> 0).toString(16);
27
32
  }
33
+ /** Rotating backups kept beside a shared file by default (`.bak.1` newest … `.bak.3`). */
34
+ export const DEFAULT_BACKUPS = 3;
35
+ /** The path of the `n`-th backup of `file` (1 = newest). */
36
+ export function backupFile(file, n) {
37
+ return `${file}.bak.${n}`;
38
+ }
28
39
  export class SharedJsonFile {
29
40
  path;
30
41
  fs;
31
42
  /** How the file looked when this process last wrote or absorbed it. */
32
43
  stamp = null;
33
- constructor(path, fs) {
44
+ backups;
45
+ constructor(path, fs, opts = {}) {
34
46
  this.path = path;
35
47
  this.fs = fs;
48
+ this.backups = Math.max(0, Math.floor(opts.backups ?? DEFAULT_BACKUPS));
36
49
  }
37
50
  /**
38
51
  * The file's text when it changed since we last touched it — and on first use,
@@ -77,6 +90,7 @@ export class SharedJsonFile {
77
90
  * file open), the write falls back to writing in place.
78
91
  */
79
92
  write(text) {
93
+ this.rotateBackups();
80
94
  const rename = this.fs.renameSync?.bind(this.fs);
81
95
  let written = false;
82
96
  if (rename) {
@@ -106,5 +120,36 @@ export class SharedJsonFile {
106
120
  this.stamp = null;
107
121
  }
108
122
  }
123
+ /**
124
+ * Shift `.bak.(n-1)` → `.bak.n` down to `.bak.1`, then copy the current file to
125
+ * `.bak.1`. Best effort: a backup that cannot be written never blocks the write
126
+ * it protects.
127
+ */
128
+ rotateBackups() {
129
+ if (this.backups === 0 || !this.fs.existsSync(this.path))
130
+ return;
131
+ try {
132
+ for (let i = this.backups; i > 1; i--) {
133
+ const from = backupFile(this.path, i - 1);
134
+ if (this.fs.existsSync(from))
135
+ this.moveFile(from, backupFile(this.path, i));
136
+ }
137
+ const newest = backupFile(this.path, 1);
138
+ if (this.fs.copyFileSync)
139
+ this.fs.copyFileSync(this.path, newest);
140
+ else
141
+ this.fs.writeFileSync(newest, this.fs.readFileSync(this.path, "utf8"));
142
+ }
143
+ catch {
144
+ /* best effort */
145
+ }
146
+ }
147
+ moveFile(from, to) {
148
+ if (this.fs.renameSync) {
149
+ this.fs.renameSync(from, to);
150
+ return;
151
+ }
152
+ this.fs.writeFileSync(to, this.fs.readFileSync(from, "utf8"));
153
+ }
109
154
  }
110
155
  //# sourceMappingURL=shared-json-file.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared-json-file.js","sourceRoot":"","sources":["../../src/persistence/shared-json-file.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"shared-json-file.js","sourceRoot":"","sources":["../../src/persistence/shared-json-file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Restore — bringing the full text of a compaction stub back from an export.
3
+ *
4
+ * A stub written before compaction archived its originals is the ONLY copy left:
5
+ * the text is gone (`contentLost`). The text often still exists elsewhere — a
6
+ * memory export (`[{ key, content }]`), or the Evermind coordinator's `mem:` record
7
+ * dump (`[{ id, text }]`) — and this module decides which record restores which
8
+ * stub. Pure and dependency-free; the `memory_restore` tool does the I/O.
9
+ *
10
+ * Matching, in order:
11
+ * 1. by key — a record's `key`, or its `id` with any `mem:` prefix dropped;
12
+ * 2. by text — the record's first line starts with the stub's pointer line (the
13
+ * original first line the stub kept). Only a UNIQUE match restores; two stubs
14
+ * sharing a pointer are reported ambiguous rather than guessed.
15
+ */
16
+ import { type ArchivedFact } from "./compaction.js";
17
+ /** One record from a restore source, normalised. */
18
+ export interface RestoreRecord {
19
+ /** The memory key the record names, when it names one. */
20
+ key?: string;
21
+ /** The full text. */
22
+ content: string;
23
+ }
24
+ /** A stub that may be restored. */
25
+ export interface RestoreTarget {
26
+ key: string;
27
+ /** The stored stub body. */
28
+ stub: string;
29
+ }
30
+ export interface RestoreMatch {
31
+ key: string;
32
+ content: string;
33
+ via: "key" | "text";
34
+ }
35
+ export interface RestorePlan {
36
+ matches: RestoreMatch[];
37
+ /** Records that matched no target. */
38
+ unmatched: number;
39
+ /** Targets more than one stub could claim by text, left alone. */
40
+ ambiguous: string[];
41
+ }
42
+ /**
43
+ * Parse a restore source: a JSON array, an object wrapping one (`{ entries: [...] }`
44
+ * and similar), or JSON Lines. Rows without text, or whose text is itself a stub,
45
+ * are dropped — restoring a stub with a stub restores nothing.
46
+ */
47
+ export declare function parseRestoreSource(text: string): RestoreRecord[];
48
+ /** Shortest pointer line a text match may rest on — below it, a prefix proves nothing. */
49
+ export declare const MIN_POINTER_CHARS = 12;
50
+ /** Decide which record restores which stub. */
51
+ export declare function planRestore(records: RestoreRecord[], targets: RestoreTarget[]): RestorePlan;
52
+ /** The Evermind version a stub names (`[absorbed→Evermind v7] …` → 7), 0 when unreadable. */
53
+ export declare function stubVersion(stub: string): number;
54
+ /** The archive row that makes a restored stub lossless again. */
55
+ export declare function restoredArchive(match: RestoreMatch, stub: string, now?: number): ArchivedFact;
56
+ //# sourceMappingURL=restore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restore.d.ts","sourceRoot":"","sources":["../src/restore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAA+C,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEjG,oDAAoD;AACpD,MAAM,WAAW,aAAa;IAC1B,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,mCAAmC;AACnC,MAAM,WAAW,aAAa;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,KAAK,GAAG,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,SAAS,EAAE,MAAM,EAAE,CAAC;CACvB;AA6BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CAgBhE;AAID,0FAA0F;AAC1F,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,+CAA+C;AAC/C,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,WAAW,CA0B3F;AAKD,6FAA6F;AAC7F,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGhD;AAED,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,YAAY,CAEjG"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Restore — bringing the full text of a compaction stub back from an export.
3
+ *
4
+ * A stub written before compaction archived its originals is the ONLY copy left:
5
+ * the text is gone (`contentLost`). The text often still exists elsewhere — a
6
+ * memory export (`[{ key, content }]`), or the Evermind coordinator's `mem:` record
7
+ * dump (`[{ id, text }]`) — and this module decides which record restores which
8
+ * stub. Pure and dependency-free; the `memory_restore` tool does the I/O.
9
+ *
10
+ * Matching, in order:
11
+ * 1. by key — a record's `key`, or its `id` with any `mem:` prefix dropped;
12
+ * 2. by text — the record's first line starts with the stub's pointer line (the
13
+ * original first line the stub kept). Only a UNIQUE match restores; two stubs
14
+ * sharing a pointer are reported ambiguous rather than guessed.
15
+ */
16
+ import { STUB_PREFIX, firstLine, isStub, stubPointer } from "./compaction.js";
17
+ /** Array-valued properties a wrapped export may keep its rows under. */
18
+ const WRAPPER_KEYS = ["entries", "memories", "records", "items", "data", "facts"];
19
+ function rowsOf(parsed) {
20
+ if (Array.isArray(parsed))
21
+ return parsed;
22
+ if (parsed && typeof parsed === "object") {
23
+ const obj = parsed;
24
+ for (const k of WRAPPER_KEYS) {
25
+ const v = obj[k];
26
+ if (Array.isArray(v))
27
+ return v;
28
+ }
29
+ // A lone record (a one-line JSON Lines file parses as plain JSON).
30
+ if (typeof obj["content"] === "string" || typeof obj["text"] === "string")
31
+ return [obj];
32
+ }
33
+ return [];
34
+ }
35
+ function toRecord(raw) {
36
+ if (!raw || typeof raw !== "object")
37
+ return null;
38
+ const r = raw;
39
+ const content = typeof r["content"] === "string" ? r["content"] : typeof r["text"] === "string" ? r["text"] : null;
40
+ if (content === null || !content.trim() || isStub(content))
41
+ return null;
42
+ const named = typeof r["key"] === "string" ? r["key"] : typeof r["id"] === "string" ? r["id"] : undefined;
43
+ const key = named?.replace(/^mem:/, "").trim();
44
+ return key ? { key, content } : { content };
45
+ }
46
+ /**
47
+ * Parse a restore source: a JSON array, an object wrapping one (`{ entries: [...] }`
48
+ * and similar), or JSON Lines. Rows without text, or whose text is itself a stub,
49
+ * are dropped — restoring a stub with a stub restores nothing.
50
+ */
51
+ export function parseRestoreSource(text) {
52
+ let rows;
53
+ try {
54
+ rows = rowsOf(JSON.parse(text));
55
+ }
56
+ catch {
57
+ rows = [];
58
+ for (const line of text.split(/\r?\n/)) {
59
+ if (!line.trim())
60
+ continue;
61
+ try {
62
+ rows.push(JSON.parse(line));
63
+ }
64
+ catch {
65
+ /* not a record */
66
+ }
67
+ }
68
+ }
69
+ return rows.map(toRecord).filter((r) => r !== null);
70
+ }
71
+ const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
72
+ /** Shortest pointer line a text match may rest on — below it, a prefix proves nothing. */
73
+ export const MIN_POINTER_CHARS = 12;
74
+ /** Decide which record restores which stub. */
75
+ export function planRestore(records, targets) {
76
+ const byKey = new Map(targets.map((t) => [t.key, t]));
77
+ const pointers = targets.map((t) => ({ key: t.key, pointer: norm(stubPointer(t.stub)) }));
78
+ const matched = new Map();
79
+ const ambiguous = new Set();
80
+ let unmatched = 0;
81
+ for (const r of records) {
82
+ if (r.key && byKey.has(r.key)) {
83
+ if (!matched.has(r.key))
84
+ matched.set(r.key, { key: r.key, content: r.content, via: "key" });
85
+ continue;
86
+ }
87
+ const head = norm(firstLine(r.content, 100_000));
88
+ const hits = pointers.filter((p) => p.pointer.length >= MIN_POINTER_CHARS && head.startsWith(p.pointer));
89
+ if (hits.length === 1) {
90
+ const key = hits[0].key;
91
+ if (!matched.has(key))
92
+ matched.set(key, { key, content: r.content, via: "text" });
93
+ continue;
94
+ }
95
+ if (hits.length > 1)
96
+ for (const h of hits)
97
+ ambiguous.add(h.key);
98
+ else
99
+ unmatched++;
100
+ }
101
+ // A key claimed unambiguously by key-match stays restored even if a text match was ambiguous.
102
+ const matches = [...matched.values()];
103
+ for (const m of matches)
104
+ ambiguous.delete(m.key);
105
+ return { matches, unmatched, ambiguous: [...ambiguous] };
106
+ }
107
+ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
108
+ const STUB_VERSION = new RegExp(`^\\s*${escapeRegExp(STUB_PREFIX)} v(\\d+)\\]`);
109
+ /** The Evermind version a stub names (`[absorbed→Evermind v7] …` → 7), 0 when unreadable. */
110
+ export function stubVersion(stub) {
111
+ const m = STUB_VERSION.exec(stub);
112
+ return m ? Number(m[1]) : 0;
113
+ }
114
+ /** The archive row that makes a restored stub lossless again. */
115
+ export function restoredArchive(match, stub, now = Date.now()) {
116
+ return { key: match.key, content: match.content, stub, version: stubVersion(stub), archivedAt: now };
117
+ }
118
+ //# sourceMappingURL=restore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restore.js","sourceRoot":"","sources":["../src/restore.ts"],"names":[],"mappings":"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"}