@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.
- package/build/package.json +1 -1
- package/build/src/_cli/_commands/find-duplicates.command.js +41 -16
- package/build/src/_cli/_commands/scan-projects.command.js +113 -113
- package/build/src/_cli/_commands/scan.command.js +2 -0
- package/build/src/_cli/_commands/serve.command.js +110 -10
- package/build/src/_cli/_services/fam-client.service.js +25 -1
- package/build/src/_cli/register-commands.js +28 -0
- package/build/src/_collections/config-catalog.const.js +160 -6
- package/build/src/_collections/error-codes.const.js +2 -0
- package/build/src/_collections/fam-console.util.js +38 -260
- package/build/src/_collections/fam-db-models.const.js +2 -0
- package/build/src/_collections/fam-entry-bootstrap.util.js +20 -6
- package/build/src/_collections/fam-error-context.util.js +1 -0
- package/build/src/_collections/fam-mcp-bridge.util.js +15 -1
- package/build/src/_collections/fam-operation-queue.service.js +85 -0
- package/build/src/_collections/fam-project-discovery.util.js +148 -0
- package/build/src/_collections/fam-request-origin.util.js +41 -0
- package/build/src/_collections/fam-retry.util.js +48 -0
- package/build/src/_collections/fam-scan-progress-sink.service.js +26 -0
- package/build/src/_enums/fam-rule-scope.type-enum.js +25 -0
- package/build/src/_models/data-models/fam-entry.data-model.js +29 -0
- package/build/src/_models/data-models/fam-memory.data-model.js +5 -0
- package/build/src/_models/data-models/fam-rules.data-model.js +24 -0
- package/build/src/_models/data-models/fam-scan-job.data-model.js +73 -0
- package/build/src/_models/data-models/fam-scope.data-model.js +9 -0
- package/build/src/_modules/embedding/_services/fam-dedup-warn.control-service.js +40 -0
- package/build/src/_modules/embedding/_services/fam-duplicate-scan.control-service.js +74 -16
- package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +50 -0
- package/build/src/_modules/embedding/_services/fam-embedding-pipeline.control-service.js +31 -5
- package/build/src/_modules/embedding/_services/fam-embedding.control-service.js +142 -9
- package/build/src/_modules/embedding/_services/fam-entry.data-service.js +9 -0
- package/build/src/_modules/embedding/_services/fam-hydration-coordinator.control-service.js +159 -0
- package/build/src/_modules/embedding/_services/fam-lmstudio-embedding.provider.js +41 -4
- package/build/src/_modules/embedding/_services/fam-vector-search.control-service.js +67 -12
- package/build/src/_modules/embedding/index.js +5 -1
- package/build/src/_modules/ingest/_collections/fam-file-routing.util.js +10 -0
- package/build/src/_modules/ingest/_collections/fam-frontmatter.util.js +134 -0
- package/build/src/_modules/ingest/_collections/fam-project-identity.util.js +30 -0
- package/build/src/_modules/ingest/_collections/fam-scan-progress.util.js +5 -2
- package/build/src/_modules/ingest/_collections/fam-scan-summary.util.js +55 -29
- package/build/src/_modules/ingest/_collections/fam-split-chunker.util.js +139 -0
- package/build/src/_modules/ingest/_models/interfaces/fam-scan-job.interface.js +2 -0
- package/build/src/_modules/ingest/_services/fam-chunker.control-service.js +10 -0
- package/build/src/_modules/ingest/_services/fam-delta-compare.util.js +68 -1
- package/build/src/_modules/ingest/_services/fam-ingest-run.data-service.js +1 -1
- package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +173 -37
- package/build/src/_modules/ingest/_services/fam-scan-job.control-service.js +429 -0
- package/build/src/_modules/ingest/_services/fam-scan-job.data-service.js +34 -0
- package/build/src/_modules/ingest/_services/fam-scan.control-service.js +6 -4
- package/build/src/_modules/ingest/index.js +6 -1
- package/build/src/_modules/mcp/_services/fam-capability-registry.service.js +151 -8
- package/build/src/_modules/mcp/_services/fam-read-tool.service.js +70 -0
- package/build/src/_modules/mcp/_services/fam-write-tool.service.js +36 -0
- package/build/src/_modules/retrieval/_collections/fam-lexical-match.util.js +69 -0
- package/build/src/_modules/retrieval/_collections/fam-memory-activation.util.js +66 -0
- package/build/src/_modules/retrieval/_collections/fam-rule-injection.util.js +125 -0
- package/build/src/_modules/retrieval/_collections/fam-rule-propagation.util.js +95 -0
- package/build/src/_modules/retrieval/_collections/fam-rule-scope-context.util.js +74 -0
- package/build/src/_modules/retrieval/_services/fam-global-rule.control-service.js +82 -0
- package/build/src/_modules/retrieval/_services/fam-memory-cold-search.control-service.js +121 -0
- package/build/src/_modules/retrieval/_services/fam-memory-dormancy.control-service.js +99 -0
- package/build/src/_modules/retrieval/_services/fam-memory-reactivation.control-service.js +68 -0
- package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +15 -2
- package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +216 -5
- package/build/src/_modules/retrieval/_services/fam-rule-injection.control-service.js +102 -0
- package/build/src/_modules/retrieval/index.js +12 -1
- package/build/src/_modules/scope-reference/_collections/fam-scope-maintenance.util.js +76 -0
- package/build/src/_modules/scope-reference/_services/fam-scope-maintenance.control-service.js +233 -0
- package/build/src/_modules/scope-reference/_services/fam-scope.data-service.js +25 -0
- package/build/src/_routes/server/api/api.controller.js +270 -0
- package/build/src/_routes/server/api/fam-doctor.control-service.js +156 -0
- package/build/src/app.server.js +13 -10
- package/client-dist/{chunk-YXHWCJ5O.js → chunk-GF3FJP7E.js} +71 -71
- package/client-dist/chunk-NMUCMQY6.js +1 -0
- package/client-dist/favicon.ico +0 -0
- package/client-dist/index.html +3 -3
- package/client-dist/main-YK4YIVAQ.js +2 -0
- package/package.json +1 -1
- package/build/src/_cli/_collections/fam-project-discovery.util.js +0 -98
- package/client-dist/chunk-I77GXVAQ.js +0 -1
- 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
|
-
* **
|
|
10
|
-
*
|
|
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
|
-
/**
|
|
18
|
-
static
|
|
19
|
-
/**
|
|
20
|
-
static
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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;
|
|
@@ -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
|
});
|