@futdevpro/fdp-agent-memory 1.1.30 → 1.1.113

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 (81) hide show
  1. package/build/package.json +1 -1
  2. package/build/src/_cli/_commands/find-duplicates.command.js +41 -16
  3. package/build/src/_cli/_commands/scan-projects.command.js +113 -113
  4. package/build/src/_cli/_commands/scan.command.js +2 -0
  5. package/build/src/_cli/_commands/serve.command.js +110 -10
  6. package/build/src/_cli/_services/fam-client.service.js +25 -1
  7. package/build/src/_cli/register-commands.js +28 -0
  8. package/build/src/_collections/config-catalog.const.js +160 -6
  9. package/build/src/_collections/error-codes.const.js +2 -0
  10. package/build/src/_collections/fam-console.util.js +38 -260
  11. package/build/src/_collections/fam-db-models.const.js +2 -0
  12. package/build/src/_collections/fam-entry-bootstrap.util.js +20 -6
  13. package/build/src/_collections/fam-error-context.util.js +1 -0
  14. package/build/src/_collections/fam-mcp-bridge.util.js +15 -1
  15. package/build/src/_collections/fam-operation-queue.service.js +85 -0
  16. package/build/src/_collections/fam-project-discovery.util.js +148 -0
  17. package/build/src/_collections/fam-request-origin.util.js +41 -0
  18. package/build/src/_collections/fam-retry.util.js +48 -0
  19. package/build/src/_collections/fam-scan-progress-sink.service.js +26 -0
  20. package/build/src/_enums/fam-rule-scope.type-enum.js +25 -0
  21. package/build/src/_models/data-models/fam-entry.data-model.js +29 -0
  22. package/build/src/_models/data-models/fam-memory.data-model.js +5 -0
  23. package/build/src/_models/data-models/fam-rules.data-model.js +24 -0
  24. package/build/src/_models/data-models/fam-scan-job.data-model.js +73 -0
  25. package/build/src/_models/data-models/fam-scope.data-model.js +9 -0
  26. package/build/src/_modules/embedding/_services/fam-dedup-warn.control-service.js +40 -0
  27. package/build/src/_modules/embedding/_services/fam-duplicate-scan.control-service.js +74 -16
  28. package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +50 -0
  29. package/build/src/_modules/embedding/_services/fam-embedding-pipeline.control-service.js +31 -5
  30. package/build/src/_modules/embedding/_services/fam-embedding.control-service.js +142 -9
  31. package/build/src/_modules/embedding/_services/fam-entry.data-service.js +9 -0
  32. package/build/src/_modules/embedding/_services/fam-hydration-coordinator.control-service.js +159 -0
  33. package/build/src/_modules/embedding/_services/fam-lmstudio-embedding.provider.js +41 -4
  34. package/build/src/_modules/embedding/_services/fam-vector-search.control-service.js +67 -12
  35. package/build/src/_modules/embedding/index.js +5 -1
  36. package/build/src/_modules/ingest/_collections/fam-file-routing.util.js +10 -0
  37. package/build/src/_modules/ingest/_collections/fam-frontmatter.util.js +134 -0
  38. package/build/src/_modules/ingest/_collections/fam-project-identity.util.js +30 -0
  39. package/build/src/_modules/ingest/_collections/fam-scan-progress.util.js +5 -2
  40. package/build/src/_modules/ingest/_collections/fam-scan-summary.util.js +55 -29
  41. package/build/src/_modules/ingest/_collections/fam-split-chunker.util.js +139 -0
  42. package/build/src/_modules/ingest/_models/interfaces/fam-scan-job.interface.js +2 -0
  43. package/build/src/_modules/ingest/_services/fam-chunker.control-service.js +10 -0
  44. package/build/src/_modules/ingest/_services/fam-delta-compare.util.js +68 -1
  45. package/build/src/_modules/ingest/_services/fam-ingest-run.data-service.js +1 -1
  46. package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +173 -37
  47. package/build/src/_modules/ingest/_services/fam-scan-job.control-service.js +429 -0
  48. package/build/src/_modules/ingest/_services/fam-scan-job.data-service.js +34 -0
  49. package/build/src/_modules/ingest/_services/fam-scan.control-service.js +6 -4
  50. package/build/src/_modules/ingest/index.js +6 -1
  51. package/build/src/_modules/mcp/_services/fam-capability-registry.service.js +151 -8
  52. package/build/src/_modules/mcp/_services/fam-read-tool.service.js +70 -0
  53. package/build/src/_modules/mcp/_services/fam-write-tool.service.js +36 -0
  54. package/build/src/_modules/retrieval/_collections/fam-lexical-match.util.js +69 -0
  55. package/build/src/_modules/retrieval/_collections/fam-memory-activation.util.js +66 -0
  56. package/build/src/_modules/retrieval/_collections/fam-rule-injection.util.js +125 -0
  57. package/build/src/_modules/retrieval/_collections/fam-rule-propagation.util.js +95 -0
  58. package/build/src/_modules/retrieval/_collections/fam-rule-scope-context.util.js +74 -0
  59. package/build/src/_modules/retrieval/_services/fam-global-rule.control-service.js +82 -0
  60. package/build/src/_modules/retrieval/_services/fam-memory-cold-search.control-service.js +121 -0
  61. package/build/src/_modules/retrieval/_services/fam-memory-dormancy.control-service.js +99 -0
  62. package/build/src/_modules/retrieval/_services/fam-memory-reactivation.control-service.js +68 -0
  63. package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +15 -2
  64. package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +216 -5
  65. package/build/src/_modules/retrieval/_services/fam-rule-injection.control-service.js +102 -0
  66. package/build/src/_modules/retrieval/index.js +12 -1
  67. package/build/src/_modules/scope-reference/_collections/fam-scope-maintenance.util.js +76 -0
  68. package/build/src/_modules/scope-reference/_services/fam-scope-maintenance.control-service.js +233 -0
  69. package/build/src/_modules/scope-reference/_services/fam-scope.data-service.js +25 -0
  70. package/build/src/_routes/server/api/api.controller.js +270 -0
  71. package/build/src/_routes/server/api/fam-doctor.control-service.js +156 -0
  72. package/build/src/app.server.js +13 -10
  73. package/client-dist/{chunk-YXHWCJ5O.js → chunk-GF3FJP7E.js} +71 -71
  74. package/client-dist/chunk-NMUCMQY6.js +1 -0
  75. package/client-dist/favicon.ico +0 -0
  76. package/client-dist/index.html +3 -3
  77. package/client-dist/main-YK4YIVAQ.js +2 -0
  78. package/package.json +1 -1
  79. package/build/src/_cli/_collections/fam-project-discovery.util.js +0 -98
  80. package/client-dist/chunk-I77GXVAQ.js +0 -1
  81. package/client-dist/main-PJPEDVJT.js +0 -2
@@ -6,25 +6,21 @@ exports.FAM_ScanSummary_Util = void 0;
6
6
  * összefoglaló PURE építője. A scan után egy `knowledge`-entry készül a mappastruktúrából (mappák + fájlok
7
7
  * felsorolása), hogy az agent lekérdezhesse a projekt/mappa tartalmát (NEM kell végigscannelnie). Statikus util.
8
8
  *
9
- * **Méret-korlát:** a per-mappa fájllista a `MAX_FILES_PER_DIR`-nál levágódik (`+N további`), a teljes szöveg a
10
- * `MAX_CHARS`-nál így az összefoglaló egyetlen, embeddelhető chunk marad nagy fáknál is (minden MAPPA így is felsorolt).
9
+ * **NINCS info-vesztés (user-FR 2026-06-22):** minden fájl felsorolva (nincs per-mappa cap), és ha a teljes szöveg
10
+ * nem fér egy embeddelhető chunkba, akkor TÖBB, nagyjából EGYENLŐ RÉSZRE bontjuk a mappa-határon (`buildParts`)
11
+ * SOSE vágunk le / nyelünk el tartalmat (a régi `MAX_FILES_PER_DIR` / `MAX_CHARS` csonkolás MEGSZŰNT).
11
12
  */
12
13
  class FAM_ScanSummary_Util {
13
14
  /** A generált összefoglaló-entry `kind`-ja. */
14
15
  static KIND = 'fs-summary';
15
16
  /** A generált összefoglaló-entry `source.type`-ja (a reconciliation `scan`-t szűr → ezt nem érinti). */
16
17
  static SOURCE_TYPE = 'scan-summary';
17
- /** Per-mappa felsorolt fájlok plafonja (felette `+N további`). */
18
- static MAX_FILES_PER_DIR = 80;
19
- /** A teljes összefoglaló-szöveg plafonja (egyetlen embeddelhető chunk). */
20
- static MAX_CHARS = 40000;
21
- /**
22
- * A mappastruktúra-összefoglaló szövege: fejléc (scope / root / fájl-számok) + a mappánként csoportosított,
23
- * rendezett fájllista. A relatív utak `/`-normalizáltak; a gyökér-szintű fájlok a `(gyökér)` alatt.
24
- */
25
- static build(set) {
18
+ /** EGY rész (chunk) méret-budgetje. A split EZT tartja be — de SOHA nem nyel le infót (több rész lesz). */
19
+ static MAX_CHARS_PER_PART = 40000;
20
+ /** Relatív utak `Map<dir, files[]>` (normalizált `/`, a gyökér-szint kulcsa `''`). */
21
+ static groupByDir(relativePaths) {
26
22
  const byDir = new Map();
27
- for (const rawPath of set.relativePaths) {
23
+ for (const rawPath of relativePaths) {
28
24
  const normalized = rawPath.replace(/\\/g, '/');
29
25
  const slashIndex = normalized.lastIndexOf('/');
30
26
  const dir = slashIndex >= 0 ? normalized.slice(0, slashIndex) : '';
@@ -34,27 +30,57 @@ class FAM_ScanSummary_Util {
34
30
  }
35
31
  byDir.get(dir)?.push(name);
36
32
  }
37
- const lines = [];
38
- lines.push(`File-rendszer összefoglaló — ${set.scopeLabel}`);
39
- lines.push(`Gyökér: ${set.root}`);
40
- lines.push(`${set.relativePaths.length} fájl ingestelve (${set.tables.join(', ') || '—'}), ${set.skippedCount} kihagyva.`);
41
- lines.push('');
42
- lines.push(`Mappák (${byDir.size}) + fájlok:`);
33
+ return byDir;
34
+ }
35
+ /**
36
+ * A fs-összefoglaló embeddelhető RÉSZEI (user-FR 2026-06-22 SOHA nem nyelünk le infót). Ha minden elfér egy
37
+ * `MAX_CHARS_PER_PART`-os chunkban → 1 rész. Egyébként a mappa-entry-ket (név szerint rendezve = tree-lokális)
38
+ * NAGYJÁBÓL EGYENLŐ, budget-tartó részekre csomagoljuk a MAPPA-HATÁRON (sose vágunk ketté egy mappát). Minden
39
+ * rész ugyanazt a fejlécet kapja + `(rész i/M)`. Minden fájl szerepel (nincs per-mappa cap). 1+ elemű tömb.
40
+ */
41
+ static buildParts(set) {
42
+ const byDir = FAM_ScanSummary_Util.groupByDir(set.relativePaths);
43
+ const header = [
44
+ `File-rendszer összefoglaló — ${set.scopeLabel}`,
45
+ `Gyökér: ${set.root}`,
46
+ `${set.relativePaths.length} fájl ingestelve (${set.tables.join(', ') || '—'}), ${set.skippedCount} kihagyva.`,
47
+ '',
48
+ `Mappák (${byDir.size}) + fájlok:`,
49
+ ].join('\n');
50
+ const dirEntries = [];
43
51
  for (const dir of Array.from(byDir.keys()).sort()) {
44
52
  const files = (byDir.get(dir) ?? []).sort();
45
53
  const label = dir === '' ? '(gyökér)' : `${dir}/`;
46
- const shown = files.length > FAM_ScanSummary_Util.MAX_FILES_PER_DIR
47
- ? files.slice(0, FAM_ScanSummary_Util.MAX_FILES_PER_DIR)
48
- : files;
49
- const more = files.length > FAM_ScanSummary_Util.MAX_FILES_PER_DIR
50
- ? ` (+${files.length - FAM_ScanSummary_Util.MAX_FILES_PER_DIR} további)`
51
- : '';
52
- lines.push(` ${label} — ${files.length} fájl: ${shown.join(', ')}${more}`);
54
+ dirEntries.push(` ${label} ${files.length} fájl: ${files.join(', ')}`);
53
55
  }
54
- const content = lines.join('\n');
55
- return content.length > FAM_ScanSummary_Util.MAX_CHARS
56
- ? `${content.slice(0, FAM_ScanSummary_Util.MAX_CHARS)}\n… (összefoglaló levágva a méret-plafonnál)`
57
- : content;
56
+ const single = `${header}\n${dirEntries.join('\n')}`;
57
+ if (single.length <= FAM_ScanSummary_Util.MAX_CHARS_PER_PART) {
58
+ return [single];
59
+ }
60
+ // Split: budget = a part-plafon a fejléc + a `(rész i/M)` overhead levonásával. Greedy pack a mappa-határon.
61
+ const budget = Math.max(1000, FAM_ScanSummary_Util.MAX_CHARS_PER_PART - header.length - 24);
62
+ const groups = [];
63
+ let current = [];
64
+ let currentLen = 0;
65
+ for (const entry of dirEntries) {
66
+ const addLen = entry.length + 1;
67
+ if (current.length > 0 && currentLen + addLen > budget) {
68
+ groups.push(current);
69
+ current = [];
70
+ currentLen = 0;
71
+ }
72
+ // Egy túl-nagy mappa-entry (egyetlen óriás-mappa) saját, esetleg budget-feletti, DE TELJES részbe kerül.
73
+ current.push(entry);
74
+ currentLen += addLen;
75
+ }
76
+ if (current.length > 0) {
77
+ groups.push(current);
78
+ }
79
+ return groups.map((group, index) => `${header} (rész ${index + 1}/${groups.length})\n${group.join('\n')}`);
80
+ }
81
+ /** A teljes összefoglaló EGY szövegként (a részek összefűzve) — debug/teszt. A persist a `buildParts`-ot használja. */
82
+ static build(set) {
83
+ return FAM_ScanSummary_Util.buildParts(set).join('\n\n');
58
84
  }
59
85
  }
60
86
  exports.FAM_ScanSummary_Util = FAM_ScanSummary_Util;
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_SplitChunker_Util = void 0;
4
+ /**
5
+ * `FAM_SplitChunker_Util` (Rules-Management MP-3 / F-B) — **STRUKTURÁLIS** split-chunker: 1 strukturális elem = 1 chunk
6
+ * (= 1 entry), a méret-alapú chunking HELYETT. A „N szabály egy fájlban" minta vesztességmentes szétbontása, hogy
7
+ * minden szabály ÖNÁLLÓ, kereshető/injektálható entry legyen.
8
+ *
9
+ * - **`heading` mód:** minden heading-blokk (a heading-sor + a törzse a KÖVETKEZŐ headingig) = 1 chunk; a `headingPath`
10
+ * az ŐS-láncot is tartja (kontextus). A legelső heading ELŐTTi preambulum külön chunk (semmi nem vész el).
11
+ * - **`list-item` mód:** minden TOP-szintű lista-elem (`- `/`* `/`+ `/`N. `) + a behúzott folytatása = 1 chunk; az első
12
+ * elem előtti bevezető külön chunk.
13
+ *
14
+ * **PURE** (nincs config/fs/DB). A `chunkType = 'rule-split'`. Üres tartalom → üres lista.
15
+ */
16
+ class FAM_SplitChunker_Util {
17
+ static HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
18
+ static LIST_ITEM_REGEX = /^(\s*)([-*+]|\d+\.)\s+\S/;
19
+ /** Strukturális split a megadott mód szerint. */
20
+ static chunkStructural(text, mode) {
21
+ if (!text || !text.trim()) {
22
+ return [];
23
+ }
24
+ const blocks = mode === 'list-item'
25
+ ? FAM_SplitChunker_Util.splitByListItem(text)
26
+ : FAM_SplitChunker_Util.splitByHeading(text);
27
+ return blocks
28
+ .filter((block) => block.content.trim().length > 0)
29
+ .map((block) => ({
30
+ content: block.content.trim(),
31
+ position: {
32
+ charStart: block.charStart, charEnd: block.charEnd,
33
+ lineStart: block.lineStart, lineEnd: block.lineEnd,
34
+ },
35
+ chunkType: 'rule-split',
36
+ headingPath: block.headingPath,
37
+ }));
38
+ }
39
+ /** Heading-blokkonkénti bontás (a heading-stack adja az ős-láncot a `headingPath`-hoz). */
40
+ static splitByHeading(text) {
41
+ const lines = text.split(/\r?\n/);
42
+ const blocks = [];
43
+ const headingStack = [];
44
+ let buffer = [];
45
+ let blockCharStart = 0;
46
+ let blockLineStart = 1;
47
+ let charCursor = 0;
48
+ // A FOLYÓ blokk heading-láncának pillanatfelvétele (a blokk NYITÁSAKOR rögzítve).
49
+ let blockHeadingPath = [];
50
+ const flush = (lineEnd, charEnd) => {
51
+ if (buffer.join('\n').trim().length > 0) {
52
+ blocks.push({
53
+ content: buffer.join('\n'), headingPath: blockHeadingPath,
54
+ charStart: blockCharStart, charEnd: charEnd, lineStart: blockLineStart, lineEnd: lineEnd,
55
+ });
56
+ }
57
+ };
58
+ for (let i = 0; i < lines.length; i++) {
59
+ const line = lines[i];
60
+ const match = line.match(FAM_SplitChunker_Util.HEADING_REGEX);
61
+ if (match) {
62
+ // Lezárjuk a folyó blokkot (a preambulumot VAGY az előző heading-blokkot) a heading ELŐTT.
63
+ flush(i, charCursor);
64
+ // A stack frissítése az új heading szintjére.
65
+ const level = match[1].length;
66
+ while (headingStack.length && headingStack[headingStack.length - 1].level >= level) {
67
+ headingStack.pop();
68
+ }
69
+ headingStack.push({ level: level, text: match[2].trim() });
70
+ // Új blokk: a heading-sorral KEZDŐDIK (önhordó content), a heading-lánc = a teljes stack.
71
+ buffer = [line];
72
+ blockHeadingPath = headingStack.map((heading) => '#'.repeat(heading.level) + ' ' + heading.text);
73
+ blockCharStart = charCursor;
74
+ blockLineStart = i + 1;
75
+ }
76
+ else {
77
+ buffer.push(line);
78
+ }
79
+ charCursor += line.length + 1;
80
+ }
81
+ flush(lines.length, charCursor);
82
+ return blocks;
83
+ }
84
+ /** Top-szintű lista-elemenkénti bontás (az első elem előtti bevezető külön blokk). */
85
+ static splitByListItem(text) {
86
+ const lines = text.split(/\r?\n/);
87
+ const blocks = [];
88
+ // A TOP-szint behúzása = az első lista-elem indentje (a mélyebb behúzás a folytatás).
89
+ const topIndent = FAM_SplitChunker_Util.detectTopIndent(lines);
90
+ let buffer = [];
91
+ let blockCharStart = 0;
92
+ let blockLineStart = 1;
93
+ let charCursor = 0;
94
+ const flush = (lineEnd, charEnd) => {
95
+ if (buffer.join('\n').trim().length > 0) {
96
+ blocks.push({
97
+ content: buffer.join('\n'), headingPath: [],
98
+ charStart: blockCharStart, charEnd: charEnd, lineStart: blockLineStart, lineEnd: lineEnd,
99
+ });
100
+ }
101
+ };
102
+ for (let i = 0; i < lines.length; i++) {
103
+ const line = lines[i];
104
+ const isTopItem = FAM_SplitChunker_Util.isTopLevelItem(line, topIndent);
105
+ if (isTopItem && buffer.length) {
106
+ flush(i, charCursor);
107
+ buffer = [line];
108
+ blockCharStart = charCursor;
109
+ blockLineStart = i + 1;
110
+ }
111
+ else {
112
+ if (!buffer.length) {
113
+ blockCharStart = charCursor;
114
+ blockLineStart = i + 1;
115
+ }
116
+ buffer.push(line);
117
+ }
118
+ charCursor += line.length + 1;
119
+ }
120
+ flush(lines.length, charCursor);
121
+ return blocks;
122
+ }
123
+ /** Az első lista-elem behúzása (top-szint); ha nincs lista, 0. */
124
+ static detectTopIndent(lines) {
125
+ for (const line of lines) {
126
+ const match = line.match(FAM_SplitChunker_Util.LIST_ITEM_REGEX);
127
+ if (match) {
128
+ return match[1].length;
129
+ }
130
+ }
131
+ return 0;
132
+ }
133
+ /** Top-szintű lista-elem-e a sor (lista-elem ÉS a behúzása a top-indenttel egyezik). */
134
+ static isTopLevelItem(line, topIndent) {
135
+ const match = line.match(FAM_SplitChunker_Util.LIST_ITEM_REGEX);
136
+ return !!match && match[1].length === topIndent;
137
+ }
138
+ }
139
+ exports.FAM_SplitChunker_Util = FAM_SplitChunker_Util;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -6,6 +6,7 @@ const fam_file_routing_util_1 = require("../_collections/fam-file-routing.util")
6
6
  const fam_ts_chunker_util_1 = require("../_collections/fam-ts-chunker.util");
7
7
  const fam_md_chunker_util_1 = require("../_collections/fam-md-chunker.util");
8
8
  const fam_sliding_chunker_util_1 = require("../_collections/fam-sliding-chunker.util");
9
+ const fam_split_chunker_util_1 = require("../_collections/fam-split-chunker.util");
9
10
  /**
10
11
  * `FAM_Chunker_ControlService` (SP-4.2, dsgn-004 §3) — a chunker-dispatch. A route + kiterjesztés
11
12
  * alapján stratégiát választ (TS-semantic | markdown | generic-sliding), a méret-konstansokat a
@@ -32,6 +33,15 @@ class FAM_Chunker_ControlService {
32
33
  * teljes batch-re konzisztens (a delta-kulcshoz, SP-4.3).
33
34
  */
34
35
  async chunkFile(set) {
36
+ // F-B (MP-3): explicit split-mód → strukturális bontás (a méret-stratégia + tiny-fallback megkerülésével; az
37
+ // author szándéka, hogy minden strukturális elem ÖNÁLLÓ entry legyen — még ha rövid is). Üres → tiny-fallback.
38
+ if (set.splitMode) {
39
+ const splitChunks = fam_split_chunker_util_1.FAM_SplitChunker_Util.chunkStructural(set.content, set.splitMode);
40
+ if (splitChunks.length) {
41
+ return this.assignIndices(splitChunks);
42
+ }
43
+ return set.content.trim().length ? [this.fallbackChunk(set.content)] : [];
44
+ }
35
45
  const strategy = fam_file_routing_util_1.FAM_FileRouting_Util.chunkStrategyFor(set.relativePath);
36
46
  const fallbackUnder = await this.resolveNumber('chunk.fallbackWholeFileUnder', set.table, set.scopePath, 50);
37
47
  // Tiny-file fallback (a `fallbackWholeFileUnder` alatt → 1 chunk, dsgn-004 §3.4).
@@ -32,7 +32,17 @@ class FAM_DeltaCompare_Util {
32
32
  * A `contentHash`-t a util itt számolja az ÚJ chunkokra (`FAM_ContentHash_Util.hash`,
33
33
  * normalizált) — az `existing.contentHash` a táron tárolt érték.
34
34
  */
35
- static compare(newChunks, existing) {
35
+ static compare(newChunks, existing, accumulateOnly = false) {
36
+ // ACCUMULATE-ONLY (user-FR 2026-06-24, a `memory` tár) — a „felülírás azonos fájl-locationnél" szemantika a
37
+ // kódbázis/dokumentáció/rules-é, de a MEMÓRIÁRA NEM: ott minden szken TISZTÁN ADDITÍV. A dedup ekkor NEM a
38
+ // `(fájl, chunkIndex)` kulcson megy (az a kompaktáláskor téves `modified`/`deleted`-et adna → adatvesztés),
39
+ // hanem a CONTENT-HASH HALMAZON: egy chunk `equal` (skip), ha a tartalma MÁR LÉTEZIK a fájl történetében;
40
+ // különben `new` (insert). SOHA nincs `modified` (felülírás) és `deleted` (törlés) → a kompaktálás-előtti
41
+ // tartalom örökre megmarad, az ismételt szken pedig nem duplikál (a hash-egyezés kiszűri).
42
+ if (accumulateOnly) {
43
+ // egyetlen implementáció: a content-addressed additív összevetés (existingId + status backfillel, within-batch dedup).
44
+ return FAM_DeltaCompare_Util.compareAdditive(newChunks, existing);
45
+ }
36
46
  const existingByIndex = new Map();
37
47
  for (const entry of existing) {
38
48
  if (typeof entry.chunkIndex === 'number') {
@@ -73,5 +83,62 @@ class FAM_DeltaCompare_Util {
73
83
  }
74
84
  return items;
75
85
  }
86
+ /**
87
+ * ADDITÍV (content-addressed) delta — a `preserveHistory` szken igazi SUPERSET-szemantikája (user-FR 2026-06-24,
88
+ * memória-archívum). A chunk-identitás itt a **`contentHash`**, NEM a `(fájl, chunkIndex)` — így amikor egy fájl
89
+ * (pl. a kompaktált MEMORY.md) ÁTÍRÓDIK/RÖVIDÜL, a régi tartalom NEM vész el:
90
+ *
91
+ * - `equal` : a chunk `contentHash`-e MÁR létezik a táron (a fájlra) → skip (nincs re-embed; az `existingId` a
92
+ * meglévő egyező chunk — a metaadat-backfillhez).
93
+ * - `new` : a `contentHash` még nincs → insert új embeddinggel (a régi, eltérő-tartalmú chunkok MELLÉ).
94
+ * - **SOHA `deleted` és SOHA `modified`** — a régi (kompaktálás-előtti) tartalom megmarad a FAM-ban, akkor is, ha
95
+ * a forrásból kikerült. Így a kompaktálás + újra-szken VESZTESÉGMENTES (a FAM a memória minden valaha-látott
96
+ * állapotának uniója).
97
+ */
98
+ static compareAdditive(newChunks, existing) {
99
+ const existingByHash = new Map();
100
+ for (const entry of existing) {
101
+ if (typeof entry.contentHash === 'string' && entry.contentHash) {
102
+ // az ELSŐ előfordulást tartjuk (a metaadat-backfill cél-id-ja); a duplikált hash-ek nem számítanak
103
+ if (!existingByHash.has(entry.contentHash)) {
104
+ existingByHash.set(entry.contentHash, entry);
105
+ }
106
+ }
107
+ }
108
+ const items = [];
109
+ const emittedHashes = new Set();
110
+ for (const chunk of newChunks) {
111
+ const contentHash = fam_content_hash_util_1.FAM_ContentHash_Util.hash(chunk.content);
112
+ // a batch-en BELÜLI duplikátum-tartalmat is csak egyszer (ne ágyazzunk be kétszer ugyanazt)
113
+ if (emittedHashes.has(contentHash)) {
114
+ continue;
115
+ }
116
+ emittedHashes.add(contentHash);
117
+ const existingEntry = existingByHash.get(contentHash);
118
+ if (existingEntry) {
119
+ items.push({
120
+ verdict: 'equal', chunk: chunk, existingId: existingEntry._id, contentHash: contentHash,
121
+ existingEmbeddingStatus: existingEntry.embeddingStatus,
122
+ });
123
+ }
124
+ else {
125
+ items.push({ verdict: 'new', chunk: chunk, contentHash: contentHash });
126
+ }
127
+ }
128
+ // (2) SUPERSESSION (dsgn-013 §6, MAM F4) — a táron maradt, az ÚJ batch-ben NEM szereplő tartalmú chunkok NEM
129
+ // törlődnek (additív invariáns), de LEVÁLTOTTÁ (`superseded`) jelölődnek: ezek a fájl RÉGI, már átírt/kivett
130
+ // tartalma (pl. a kompaktált MEMORY.md korábbi bejegyzései). Megőrizve + cold-kereshetők, de a ranking
131
+ // penalizálja őket (a friss verzió fölébük kerül). CSAK ha a fájlnak VAN új tartalma (`newChunks` nem üres) —
132
+ // különben (üres batch) nem leváltunk, csak nincs mit hozzáadni.
133
+ if (newChunks.length) {
134
+ for (const entry of existing) {
135
+ if (typeof entry.contentHash === 'string' && entry.contentHash
136
+ && !emittedHashes.has(entry.contentHash) && !entry.superseded) {
137
+ items.push({ verdict: 'superseded', chunk: null, existingId: entry._id, contentHash: entry.contentHash });
138
+ }
139
+ }
140
+ }
141
+ return items;
142
+ }
76
143
  }
77
144
  exports.FAM_DeltaCompare_Util = FAM_DeltaCompare_Util;
@@ -48,7 +48,7 @@ class FAM_IngestRun_DataService extends nts_dynamo_1.DyNTS_DataService {
48
48
  filesProcessed: 0,
49
49
  filesSkipped: 0,
50
50
  chunkCount: 0,
51
- verdicts: { new: 0, modified: 0, equal: 0, deleted: 0 },
51
+ verdicts: { new: 0, modified: 0, equal: 0, deleted: 0, superseded: 0 },
52
52
  status: 'ok',
53
53
  errors: [],
54
54
  });