@futdevpro/fdp-agent-memory 1.1.166 → 1.1.177

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
  {
2
2
  "name": "@futdevpro/fdp-agent-memory",
3
- "version": "1.1.166",
3
+ "version": "1.1.177",
4
4
  "description": "Local-first, vector-backed multi-table agent memory exposed as an MCP server (read/write/capabilities). Public, FDP-Templates-free, no auth.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -19,7 +19,13 @@
19
19
  "build",
20
20
  "client-dist",
21
21
  "README.md",
22
- "LICENSE"
22
+ "LICENSE",
23
+ "!build/**/*.spec.js",
24
+ "!build/**/*.spec.js.map",
25
+ "!build/**/*.spec.d.ts",
26
+ "!build/**/_benchmarks/**",
27
+ "!build/**/_integration-tests/**",
28
+ "!build/**/_spec-support/**"
23
29
  ],
24
30
  "engines": {
25
31
  "node": ">=20"
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_PruneDuplicates_Util = void 0;
4
+ const commander_1 = require("commander");
5
+ const fam_output_util_1 = require("../_collections/fam-output.util");
6
+ const fam_client_service_1 = require("../_services/fam-client.service");
7
+ const fam_cli_const_1 = require("../_collections/fam-cli.const");
8
+ /**
9
+ * `fam prune-duplicates [--table t] [--apply]` — BÁJTAZONOS ismétlés-nyesés.
10
+ *
11
+ * Az azonos `(absolutePath, chunkIndex, chunkTotal, contentHash)` négyesű példányok között NINCS
12
+ * információ-különbség: a legfrissebbet megtartjuk, a többit soft-delete-eljük, és a csoport
13
+ * `recallCount` / `lastRecalledAt` MAXIMUMÁT átvisszük a megtartottra (a felidézés-történet nem veszhet el).
14
+ *
15
+ * **Alapértelmezés a DRY-RUN** — a törléshez explicit `--apply` kell. A `find-duplicates`-szel NEM
16
+ * keverendő: az SZEMANTIKAI hasonlóságot keres (koszinusz-cluster) és szándékosan sosem töröl.
17
+ *
18
+ * `superseded` bejegyzést tartalmazó csoporthoz SOHA nem nyúl (az a `memory` tár szándékos, leváltott
19
+ * generációja) — a kihagyás számláltan látszik a riportban.
20
+ *
21
+ * ⏱️ **FUTÁSIDŐ (mérve 2026-09-01, 555k aktív chunk):** a TELJES korpusz **~123 s**, egyetlen tár (`rules`)
22
+ * **~1,4 s**. Ez egy hosszan futó, szinkron aggregáció — automatizált hívónál (CI, HTTP-kliens) vagy emeld
23
+ * a timeoutot, vagy `--table`-lel bontsd táranként. A `--max-groups` a MUNKÁT nem, csak a feldolgozott
24
+ * csoportok számát korlátozza (az aggregáció akkor is végigmegy).
25
+ */
26
+ class FAM_PruneDuplicates_Util {
27
+ /** A `prune-duplicates` parancs-deszkriptor. */
28
+ static command() {
29
+ const command = new commander_1.Command('prune-duplicates');
30
+ command
31
+ .description('Bájtazonos ismétlés-nyesés (a legfrissebb marad; DRY-RUN az alapértelmezés)')
32
+ .option('--table <table>', 'csak ez a tár (default: MINDEN tár)')
33
+ .option('--apply', 'a nyesés TÉNYLEGES végrehajtása (enélkül csak mér, nem ír)')
34
+ .option('--max-groups <n>', 'csoport-plafon egy futásra (default 50000; a capped jelzi, ha több)')
35
+ .action(async () => {
36
+ fam_output_util_1.FAM_Output_Util.exit(await FAM_PruneDuplicates_Util.run(command));
37
+ });
38
+ return command;
39
+ }
40
+ /** A `prune-duplicates` futtatása a REST `POST /api/duplicates/prune`-on. */
41
+ static async run(command) {
42
+ const globals = command.optsWithGlobals();
43
+ const local = command.opts();
44
+ const client = new fam_client_service_1.FAM_CliClient_Service(globals.serverUrl);
45
+ const maxGroups = local.maxGroups !== undefined ? Number(local.maxGroups) : undefined;
46
+ const result = await client.post('/api/duplicates/prune', {
47
+ table: local.table,
48
+ // A `--apply` HIÁNYA a biztonságos irány: dryRun marad. Elgépelt kapcsoló sosem töröl.
49
+ dryRun: local.apply !== true,
50
+ maxGroups: Number.isFinite(maxGroups) ? maxGroups : undefined,
51
+ });
52
+ if (!result.ok) {
53
+ return fam_output_util_1.FAM_Output_Util.failure({
54
+ options: globals,
55
+ command: 'prune-duplicates',
56
+ error: result.error ?? { errorCode: 'FAM-CLI-PRUNE-001', message: 'Ismeretlen nyesési hiba.' },
57
+ exitCode: result.unreachable ? fam_cli_const_1.FAM_CliExitCode.serverUnreachable : fam_cli_const_1.FAM_CliExitCode.operationError,
58
+ });
59
+ }
60
+ return fam_output_util_1.FAM_Output_Util.success({
61
+ options: globals,
62
+ command: 'prune-duplicates',
63
+ data: result.data,
64
+ humanFormatter: (data) => FAM_PruneDuplicates_Util.format(data),
65
+ });
66
+ }
67
+ /** Ember-olvasható összegzés: per-tár sorok + összesítés + a dry-run explicit jelzése. */
68
+ static format(report) {
69
+ const lines = [];
70
+ lines.push(report.dryRun
71
+ ? 'Bájtazonos ismétlés-nyesés — DRY-RUN (semmi nem változott; a végrehajtáshoz: --apply):'
72
+ : 'Bájtazonos ismétlés-nyesés — VÉGREHAJTVA:');
73
+ for (const row of report.tables) {
74
+ if (!row.groups) {
75
+ continue;
76
+ }
77
+ lines.push(` ${row.table.padEnd(18)} ${String(row.groups).padStart(6)} csoport`
78
+ + ` → ${String(row.prunedChunks).padStart(6)} chunk`
79
+ + `${row.recallCarried ? `, ${row.recallCarried} felidézés-történet átvive` : ''}`
80
+ + `${row.skippedSuperseded ? `, ${row.skippedSuperseded} superseded KIHAGYVA` : ''}`
81
+ + `${row.capped ? ' ⚠ CAPPED (emeld a --max-groups-ot, vagy futtasd újra)' : ''}`);
82
+ }
83
+ if (!report.totalGroups) {
84
+ lines.push(' ✓ nincs bájtazonos ismétlés.');
85
+ return lines.join('\n');
86
+ }
87
+ lines.push(` ÖSSZESEN: ${report.totalGroups} csoport, ${report.totalPrunedChunks} felesleges chunk`
88
+ + `, ${report.totalRecallCarried} felidézés-történet átvive`
89
+ + `, ${report.totalSkippedSuperseded} superseded csoport érintetlen.`);
90
+ return lines.join('\n');
91
+ }
92
+ }
93
+ exports.FAM_PruneDuplicates_Util = FAM_PruneDuplicates_Util;
@@ -8,6 +8,7 @@ const scan_projects_command_1 = require("./_commands/scan-projects.command");
8
8
  const config_command_1 = require("./_commands/config.command");
9
9
  const stats_command_1 = require("./_commands/stats.command");
10
10
  const find_duplicates_command_1 = require("./_commands/find-duplicates.command");
11
+ const prune_duplicates_command_1 = require("./_commands/prune-duplicates.command");
11
12
  const errors_command_1 = require("./_commands/errors.command");
12
13
  const init_command_1 = require("./_commands/init.command");
13
14
  const doctor_command_1 = require("./_commands/doctor.command");
@@ -56,6 +57,7 @@ function registerFAMCommands(program, version) {
56
57
  remote_command_1.FAM_Remote_Util.command(),
57
58
  stats_command_1.FAM_Stats_Util.command(),
58
59
  find_duplicates_command_1.FAM_FindDuplicates_Util.command(),
60
+ prune_duplicates_command_1.FAM_PruneDuplicates_Util.command(),
59
61
  errors_command_1.FAM_Errors_Util.command(),
60
62
  // 7 delegáló alias (README copy-paste-elhetőség, nincs külön logika; dsgn-010 §10).
61
63
  doctor_command_1.FAM_Doctor_Util.aliasCommand('validate-env', ['env']),
@@ -187,6 +187,37 @@ exports.CONFIG_CATALOG = {
187
187
  + 'Mert indok (2026-08-22): kontrollalt teszten a rules tar 0/3 aranyban tunt el a documents mogott, '
188
188
  + 'holott a csak-rules query azonnal hozta a kanonikus szabalyt.',
189
189
  },
190
+ // KERESZT-TAR "mellesleg"-javaslat (2026-09-01). A `memory`/`knowledge` a HIBAKERESES-jellegu kerdeseken
191
+ // eros ("miert tort el", "lattuk mar ezt"), de az agent jellemzoen `codebase`/`documents`-ben keres, mert
192
+ // nem tudja, hogy ott van. Merve 15 realisztikus kerdesen: 13-nal az ELSODLEGES tar volt jobb -> a talalat
193
+ // NEM kerulhet a listaba (13 esetben rontanank, hogy 2-ben javitsunk), csak kulon, egyetlen mellek-mezobe.
194
+ 'read.crossTableNoteEnabled': {
195
+ type: 'boolean', default: true, levels: ALL_LEVELS,
196
+ description: 'Kereszt-tar "mellesleg"-javaslat: ha a memory/knowledge taron ERDEMBEN erosebb talalat van, '
197
+ + 'a valasz kap egy kulon `crossTableNote` mezot. A `hits` VALTOZATLAN marad (sem sorrend, sem tagsag). '
198
+ + 'Nem fut, ha a hivo eleve kerte ezeket a tarakat. Merve: +376 ms (+14%) a szonda koltsege.',
199
+ },
200
+ 'read.crossTableNoteMinDelta': {
201
+ type: 'number', default: 0.025, min: 0, max: 1, levels: ALL_LEVELS,
202
+ description: 'A megszolalashoz szukseges NYERS score-elony. 0 = kikapcsolva. Kalibralva 44 eseten, '
203
+ + 'KONTROLLCSOPORTTAL (24 strukturalis kerdes + 20 diagnosztikai). FONTOS: az elso kalibracio '
204
+ + '0,12-t adott, de kiderult, hogy a magas deltakat szinte kizarolag beszelgetes-tormelek '
205
+ + '(user_prompt / temporary_context, a memory tar 20,2%-a) termelte. A tormelek kizarasa utan '
206
+ + '0,12-nel a jegyzet SOHA nem szolalt volna meg. A valos jel a 0,025-0,06 savban van, es ott '
207
+ + 'pontos: 3/44 megszolalas (6,8%), koztuk a docker-cache-token es a lokal-vs-CI tanulsag. '
208
+ + 'KORLAT: ket kulonbozo tartalomtipus nyers koszinuszat hasonlitja -> korpusz-valtaskor UJRA KELL MERNI '
209
+ + '(`node build/src/_benchmarks/fam-crosstable-bench.js`).',
210
+ },
211
+ 'read.crossTableNoteMinScore': {
212
+ type: 'number', default: 0.45, min: 0, max: 1, levels: ALL_LEVELS,
213
+ description: 'Abszolut also korlat a jelolt sajat score-jara: gyenge talalatot akkor sem ajanlunk, '
214
+ + 'ha az elsodleges tar meg gyengebb (kulonben a "nincs talalat" eset minden alkalommal zajt szulne).',
215
+ },
216
+ 'read.crossTableNoteTopK': {
217
+ type: 'number', default: 3, min: 1, max: 10, integer: true, levels: ALL_LEVELS,
218
+ description: 'A szonda topK-ja taranként. Kicsi: a szonda csak azt donti el, VAN-E erdemben erosebb — '
219
+ + 'nem masodik talalati listat epit.',
220
+ },
190
221
  'read.relevanceFloor': {
191
222
  type: 'number', default: 0.60, min: 0.0, max: 1.0, levels: ALL_LEVELS,
192
223
  // PER-TÁR kalibráció (2026-08-14 MÉRÉS): a tárak score-tartománya ELTÉR — a `codebase`/`documents` szimbólum-
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ /**
3
+ * PONTOS (bájtazonos) ismétlés-nyesés — a döntési logika, tisztán és tesztelhetően.
4
+ *
5
+ * **Miben más ez, mint a `FAM_DuplicateScan_ControlService`?** Az a SZEMANTIKAI hasonlóságot keresi
6
+ * (koszinusz-cluster) és szándékosan sosem töröl — ott emberi ítélet kell. EZ viszont a BIZONYÍTOTTAN
7
+ * AZONOS újra-beolvasást nyesi: azonos `(source.absolutePath, chunkIndex, chunkTotal, contentHash)` →
8
+ * a példányok között NINCS információ-különbség, így az elhagyásuk nem veszteség.
9
+ *
10
+ * **Mért indok (2026-09-01):** egy projekt-átnevezés után a heti re-scan cél-uniója MINDKÉT nevet
11
+ * megtartotta ugyanarra a mappára, a delta-összevetés pedig a levél-scopeId-re szűrt — így a második
12
+ * menet üres `existing`-et kapott, és mindent `new`-ként vitt be. Az aktív korpuszban ez **13 201**
13
+ * felesleges chunk (a 555 102 aktívból 2,38%). A gyökér-okot a scope-vak fallback + az útvonal-dedup
14
+ * javítja; EZ a maradék takarítása.
15
+ *
16
+ * **Amihez SOHA nem nyúl:**
17
+ * - `superseded` bejegyzés — az a `memory` tár szándékos, leváltott generációja (dsgn-013 MAM F4),
18
+ * az owner explicit kérése szerint MEGMARAD („nem baj ha fennmarad olyan memória amit a Claude már
19
+ * elfelejtett… sőt!"). Mérve: a `memory` táron 0 bájtazonos csoport van, mind a 287 eltérő tartalmú.
20
+ * - ELTÉRŐ `contentHash`-ű példányok — azok valódi tartalom-történet, nem ismétlés (mérve 3 046 csoport).
21
+ *
22
+ * **Megőrzött felidézés-történet:** a csoport `recallCount` / `lastRecalledAt` MAXIMUMA a megtartott
23
+ * példányra vándorol — az owner „amit többet használtunk, az tovább él" elve nem sérülhet a nyesésben.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.FAM_ExactDuplicatePrune_Util = void 0;
27
+ class FAM_ExactDuplicatePrune_Util {
28
+ /**
29
+ * A megtartandó példány: a LEGFRISSEBB `lastModified`. Döntetlennél a lexikálisan legnagyobb `id`
30
+ * nyer — nem esztétika, hanem DETERMINIZMUS: a nyesés kétszer futtatva ugyanazt kell adja, különben
31
+ * a második futás megint mozgatna, és a művelet nem lenne idempotens.
32
+ */
33
+ static chooseKeeper(members) {
34
+ if (!members.length) {
35
+ return undefined;
36
+ }
37
+ return [...members].sort((a, b) => {
38
+ const at = typeof a.lastModified === 'number' ? a.lastModified : 0;
39
+ const bt = typeof b.lastModified === 'number' ? b.lastModified : 0;
40
+ return bt !== at ? bt - at : (a.id < b.id ? 1 : -1);
41
+ })[0];
42
+ }
43
+ /** A csoport felidézés-történetének maximuma (a megtartottra vitt érték bemenete). */
44
+ static maxRecall(members) {
45
+ let count = 0;
46
+ let at = 0;
47
+ for (const member of members) {
48
+ if (typeof member.recallCount === 'number' && member.recallCount > count) {
49
+ count = member.recallCount;
50
+ }
51
+ if (typeof member.lastRecalledAt === 'number' && member.lastRecalledAt > at) {
52
+ at = member.lastRecalledAt;
53
+ }
54
+ }
55
+ return { recallCount: count, lastRecalledAt: at };
56
+ }
57
+ /**
58
+ * Egy csoport nyesési terve — vagy a kihagyás INDOKA. A `carry` csak azt a mezőt tartalmazza, ami a
59
+ * megtartotton ténylegesen NŐNE: fölösleges írást nem kérünk, és a keeper saját értékét sem rontjuk le.
60
+ */
61
+ static planGroup(members) {
62
+ if (members.length < 2) {
63
+ return 'single-member';
64
+ }
65
+ if (members.some((member) => member.superseded === true)) {
66
+ return 'superseded-present';
67
+ }
68
+ const keeper = FAM_ExactDuplicatePrune_Util.chooseKeeper(members);
69
+ const max = FAM_ExactDuplicatePrune_Util.maxRecall(members);
70
+ const carry = {};
71
+ if (max.recallCount > (keeper.recallCount ?? 0)) {
72
+ carry.recallCount = max.recallCount;
73
+ }
74
+ if (max.lastRecalledAt > (keeper.lastRecalledAt ?? 0)) {
75
+ carry.lastRecalledAt = max.lastRecalledAt;
76
+ }
77
+ return {
78
+ keepId: keeper.id,
79
+ dropIds: members.filter((member) => member.id !== keeper.id)
80
+ .map((member) => member.id),
81
+ carry: carry,
82
+ };
83
+ }
84
+ }
85
+ exports.FAM_ExactDuplicatePrune_Util = FAM_ExactDuplicatePrune_Util;
@@ -156,6 +156,27 @@ class FAM_Entry_DataService extends nts_dynamo_1.DyNTS_DataService {
156
156
  filterBy['source.root'] = { $ne: root };
157
157
  await this.updateData({ filterBy: filterBy, update: update });
158
158
  }
159
+ /**
160
+ * A `scopePath` FELTÉTELES backfill-je (2026-09-01) — a re-scan `equal` ágán hívva.
161
+ *
162
+ * **Miért kell.** A fájl IDENTITÁSA az abszolút útvonal, a scope csak besorolás — ezért az `equal`
163
+ * összevetés (a scope-vak fallback óta) MÁS scope alatt álló chunkot is adoptál. Enélkül a backfill
164
+ * nélkül az adoptált chunk ÖRÖKRE a RÉGI projekt-név alatt maradna: a duplikáció megszűnne, de a
165
+ * scope-szűrt keresés az ÚJ néven nem találná meg. A régi viselkedés (teljes duplikálás) legalább
166
+ * hagyott egy példányt az új scope alatt — a javítás enélkül CSERÉLNE egy hibát egy másikra.
167
+ *
168
+ * Csak akkor ír, ha a LEVÉL scopeId eltér (no-op, ha már stimmel).
169
+ */
170
+ async backfillScopePath(id, scopePath) {
171
+ if (!scopePath.length) {
172
+ return;
173
+ }
174
+ const leafScopeId = scopePath[scopePath.length - 1].scopeId;
175
+ const update = { $set: { scopePath: scopePath } };
176
+ const filterBy = { _id: id };
177
+ filterBy['scopePath.scopeId'] = { $ne: leafScopeId };
178
+ await this.updateData({ filterBy: filterBy, update: update });
179
+ }
159
180
  /**
160
181
  * A `sourceFilePath` (display-relatív út) FELTÉTELES backfill-je (FAM-REV-050) — a re-scan `equal` ágán
161
182
  * hívva, hogy a default (basePath-nélküli) relatív út KONZISZTENS legyen, miután a fájl-identitás az
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_ExactDuplicatePrune_ControlService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const mongoose_1 = tslib_1.__importDefault(require("mongoose"));
6
+ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
7
+ const fam_entry_data_service_1 = require("./fam-entry.data-service");
8
+ const fam_exact_duplicate_prune_util_1 = require("../_collections/fam-exact-duplicate-prune.util");
9
+ const fam_store_registry_const_1 = require("../_collections/fam-store-registry.const");
10
+ const fam_vector_search_control_service_1 = require("./fam-vector-search.control-service");
11
+ const DEFAULT_MAX_GROUPS = 50000;
12
+ /**
13
+ * BÁJTAZONOS ismétlés-nyesés (2026-09-01). Azonos `(absolutePath, chunkIndex, chunkTotal, contentHash)`
14
+ * → a példányok között NINCS információ-különbség; a legfrissebbet tartjuk, a többit soft-delete-eljük.
15
+ *
16
+ * **Nem keverendő** a `FAM_DuplicateScan_ControlService`-szel: az SZEMANTIKAI hasonlóságot keres és
17
+ * sosem töröl (ott emberi ítélet kell). A kritériumok és a kivételek indoklása a pure utilban áll.
18
+ *
19
+ * A soft-delete DIREKT `_deleted` (nem `deleteData`) — a FAM entry-tárak `addArchive:true`-ok, így a
20
+ * per-entry törlés minden példányról ARCHÍV-másolatot csinálna (bloat + lassú tízezres nagyságrenden).
21
+ * Ugyanez a döntés a scope-reset ágon is (`fam-scope-maintenance.control-service.ts`).
22
+ */
23
+ class FAM_ExactDuplicatePrune_ControlService {
24
+ static _instance;
25
+ static getInstance() {
26
+ if (!FAM_ExactDuplicatePrune_ControlService._instance) {
27
+ FAM_ExactDuplicatePrune_ControlService._instance = new FAM_ExactDuplicatePrune_ControlService();
28
+ }
29
+ return FAM_ExactDuplicatePrune_ControlService._instance;
30
+ }
31
+ /** A nyesés futtatása — `dryRun` esetén CSAK mér, semmit nem ír. */
32
+ async prune(input = {}) {
33
+ const dryRun = input.dryRun !== false;
34
+ const maxGroups = typeof input.maxGroups === 'number' && input.maxGroups > 0
35
+ ? Math.floor(input.maxGroups) : DEFAULT_MAX_GROUPS;
36
+ const targets = input.table
37
+ ? [fam_store_registry_const_1.FAM_StoreRegistry_Util.getEntry(input.table)].filter(Boolean)
38
+ : fam_store_registry_const_1.FAM_STORE_REGISTRY;
39
+ const tables = [];
40
+ for (const registryEntry of targets) {
41
+ const result = await this.pruneTable(registryEntry, dryRun, maxGroups);
42
+ if (result) {
43
+ tables.push(result);
44
+ }
45
+ }
46
+ const sum = (pick) => tables.reduce((acc, row) => acc + pick(row), 0);
47
+ return {
48
+ operation: 'prune_exact_duplicates',
49
+ dryRun: dryRun,
50
+ tables: tables,
51
+ totalGroups: sum((row) => row.groups),
52
+ totalPrunedChunks: sum((row) => row.prunedChunks),
53
+ totalRecallCarried: sum((row) => row.recallCarried),
54
+ totalSkippedSuperseded: sum((row) => row.skippedSuperseded),
55
+ };
56
+ }
57
+ /** Egy tár nyesése. A modell példányosítása regisztrálja a mongoose-modellt (a raw collection eléréséhez). */
58
+ async pruneTable(registryEntry, dryRun, maxGroups) {
59
+ new fam_entry_data_service_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: 'exact-duplicate-prune' });
60
+ const collection = mongoose_1.default.models[registryEntry.dataParams.dataName]?.collection;
61
+ if (!collection) {
62
+ return undefined;
63
+ }
64
+ const groups = await collection.aggregate([
65
+ {
66
+ $match: {
67
+ _deleted: null,
68
+ 'source.absolutePath': { $exists: true, $ne: null },
69
+ chunkTotal: { $gt: 0 },
70
+ contentHash: { $exists: true, $ne: null },
71
+ },
72
+ },
73
+ {
74
+ $group: {
75
+ _id: { path: '$source.absolutePath', index: '$chunkIndex', total: '$chunkTotal', hash: '$contentHash' },
76
+ count: { $sum: 1 },
77
+ members: {
78
+ $push: {
79
+ id: '$_id',
80
+ // A `__lastModified` Date — a tiszta util számot vár, ezért itt konvertáljuk (hiány → null).
81
+ lastModified: { $convert: { input: '$__lastModified', to: 'long', onError: null, onNull: null } },
82
+ recallCount: '$recallCount',
83
+ lastRecalledAt: '$lastRecalledAt',
84
+ superseded: '$superseded',
85
+ },
86
+ },
87
+ },
88
+ },
89
+ { $match: { count: { $gt: 1 } } },
90
+ { $limit: maxGroups + 1 },
91
+ ], { allowDiskUse: true }).toArray();
92
+ const capped = groups.length > maxGroups;
93
+ const work = capped ? groups.slice(0, maxGroups) : groups;
94
+ let prunedChunks = 0;
95
+ let recallCarried = 0;
96
+ let skippedSuperseded = 0;
97
+ for (const group of work) {
98
+ // A nyers `_id`-t megőrizzük (ObjectId VAGY string lehet) — a util string-kulcsot lát, a DB a nyerset kapja.
99
+ const byKey = new Map();
100
+ const members = group.members.map((raw) => {
101
+ const key = String(raw.id);
102
+ byKey.set(key, raw.id);
103
+ return {
104
+ id: key,
105
+ lastModified: typeof raw.lastModified === 'number' ? raw.lastModified : undefined,
106
+ recallCount: raw.recallCount,
107
+ lastRecalledAt: raw.lastRecalledAt,
108
+ superseded: raw.superseded,
109
+ };
110
+ });
111
+ const plan = fam_exact_duplicate_prune_util_1.FAM_ExactDuplicatePrune_Util.planGroup(members);
112
+ if (typeof plan === 'string') {
113
+ if (plan === 'superseded-present') {
114
+ skippedSuperseded++;
115
+ }
116
+ continue;
117
+ }
118
+ prunedChunks += plan.dropIds.length;
119
+ const hasCarry = Object.keys(plan.carry).length > 0;
120
+ if (hasCarry) {
121
+ recallCarried++;
122
+ }
123
+ if (dryRun) {
124
+ continue;
125
+ }
126
+ const dropRaw = plan.dropIds.map((id) => byKey.get(id));
127
+ await collection.updateMany({ _id: { $in: dropRaw } }, { $set: { _deleted: new Date() } });
128
+ if (hasCarry) {
129
+ // A felidézés-történet MAXIMUMA a megtartottra — az owner "amit többet használtunk, az tovább
130
+ // él" elve nem sérülhet attól, hogy épp a ritkábban felidézett példány a legfrissebb.
131
+ await collection.updateOne({ _id: byKey.get(plan.keepId) }, { $set: plan.carry });
132
+ }
133
+ for (const id of plan.dropIds) {
134
+ fam_vector_search_control_service_1.FAM_VectorSearch_ControlService.getInstance().removeVector(registryEntry.table, id);
135
+ }
136
+ }
137
+ if (capped) {
138
+ fsm_dynamo_1.DyFM_Log.warn(`[prune-duplicates] ${registryEntry.table}: a csoport-plafon (${maxGroups}) elerve — `
139
+ + 'a riport `capped: true`-t ad, tovabbi futas szukseges (nincs nema csonkolas).');
140
+ }
141
+ return {
142
+ table: registryEntry.table,
143
+ collection: collection.collectionName,
144
+ groups: work.length,
145
+ prunedChunks: prunedChunks,
146
+ recallCarried: recallCarried,
147
+ skippedSuperseded: skippedSuperseded,
148
+ capped: capped,
149
+ };
150
+ }
151
+ }
152
+ exports.FAM_ExactDuplicatePrune_ControlService = FAM_ExactDuplicatePrune_ControlService;
@@ -8,7 +8,7 @@
8
8
  * (BFR-AM-002/008); a bedrock-csere (MP-15) a `FAM_EmbeddingProvider` interfész mögött non-breaking.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.FAM_EmbeddingBootstrap_ControlService = exports.FAM_DupAuditScheduler_ControlService = exports.FAM_DupAudit_DataService = exports.FAM_DupAudit_ControlService = exports.FAM_DupFileRollup_Util = exports.FAM_DupForkPair_Util = exports.FAM_DupCoverage_ControlService = exports.FAM_ExactDuplication_ControlService = exports.FAM_DuplicateScan_ControlService = exports.FAM_EmbeddingCost_ControlService = exports.FAM_EmbeddingPipeline_ControlService = exports.FAM_EmbeddingPreset_ControlService = exports.FAM_StoreRegistry_Util = exports.FAM_STORE_REGISTRY = exports.FAM_DedupWarn_ControlService = exports.FAM_Entry_DataService = exports.FAM_HydrationCoordinator_ControlService = exports.FAM_VectorSearch_ControlService = exports.FAM_MOCK_EMBEDDING_DIMS = exports.FAM_Mock_EmbeddingProvider = exports.FAM_LMStudio_EmbeddingProvider = exports.FAM_OpenAI_EmbeddingProvider = exports.FAM_Embedding_ControlService = void 0;
11
+ exports.FAM_EmbeddingBootstrap_ControlService = exports.FAM_DupAuditScheduler_ControlService = exports.FAM_DupAudit_DataService = exports.FAM_DupAudit_ControlService = exports.FAM_DupFileRollup_Util = exports.FAM_DupForkPair_Util = exports.FAM_DupCoverage_ControlService = exports.FAM_ExactDuplication_ControlService = exports.FAM_DuplicateScan_ControlService = exports.FAM_ExactDuplicatePrune_Util = exports.FAM_ExactDuplicatePrune_ControlService = exports.FAM_EmbeddingCost_ControlService = exports.FAM_EmbeddingPipeline_ControlService = exports.FAM_EmbeddingPreset_ControlService = exports.FAM_StoreRegistry_Util = exports.FAM_STORE_REGISTRY = exports.FAM_DedupWarn_ControlService = exports.FAM_Entry_DataService = exports.FAM_HydrationCoordinator_ControlService = exports.FAM_VectorSearch_ControlService = exports.FAM_MOCK_EMBEDDING_DIMS = exports.FAM_Mock_EmbeddingProvider = exports.FAM_LMStudio_EmbeddingProvider = exports.FAM_OpenAI_EmbeddingProvider = exports.FAM_Embedding_ControlService = void 0;
12
12
  // SP-2.1 — provider-dispatch kapu + providerek
13
13
  var fam_embedding_control_service_1 = require("./_services/fam-embedding.control-service");
14
14
  Object.defineProperty(exports, "FAM_Embedding_ControlService", { enumerable: true, get: function () { return fam_embedding_control_service_1.FAM_Embedding_ControlService; } });
@@ -39,6 +39,12 @@ var fam_embedding_pipeline_control_service_1 = require("./_services/fam-embeddin
39
39
  Object.defineProperty(exports, "FAM_EmbeddingPipeline_ControlService", { enumerable: true, get: function () { return fam_embedding_pipeline_control_service_1.FAM_EmbeddingPipeline_ControlService; } });
40
40
  var fam_embedding_cost_control_service_1 = require("./_services/fam-embedding-cost.control-service");
41
41
  Object.defineProperty(exports, "FAM_EmbeddingCost_ControlService", { enumerable: true, get: function () { return fam_embedding_cost_control_service_1.FAM_EmbeddingCost_ControlService; } });
42
+ // BÁJTAZONOS ismétlés-NYESÉS (2026-09-01) — a szemantikus felderítővel ellentétben EZ töröl, de csak
43
+ // bizonyítottan azonos tartalmat, és `dryRun` az alapértelmezés.
44
+ var fam_exact_duplicate_prune_control_service_1 = require("./_services/fam-exact-duplicate-prune.control-service");
45
+ Object.defineProperty(exports, "FAM_ExactDuplicatePrune_ControlService", { enumerable: true, get: function () { return fam_exact_duplicate_prune_control_service_1.FAM_ExactDuplicatePrune_ControlService; } });
46
+ var fam_exact_duplicate_prune_util_1 = require("./_collections/fam-exact-duplicate-prune.util");
47
+ Object.defineProperty(exports, "FAM_ExactDuplicatePrune_Util", { enumerable: true, get: function () { return fam_exact_duplicate_prune_util_1.FAM_ExactDuplicatePrune_Util; } });
42
48
  // near-duplikátum FELDERÍTŐ (read-only; user-direktíva 2026-06-21)
43
49
  var fam_duplicate_scan_control_service_1 = require("./_services/fam-duplicate-scan.control-service");
44
50
  Object.defineProperty(exports, "FAM_DuplicateScan_ControlService", { enumerable: true, get: function () { return fam_duplicate_scan_control_service_1.FAM_DuplicateScan_ControlService; } });
@@ -415,6 +415,10 @@ class FAM_Ingest_ControlService {
415
415
  // A default display-relatív út a friss scan-gyökérhez igazodjon (FAM-REV-050) —
416
416
  // egy korábban más root-reprezentációval (pl. scan-file basename) ingestelt chunk konzisztenssé.
417
417
  await set.dataService.backfillSourceFilePath(item.existingId, set.file.relativePath);
418
+ // SCOPE-backfill (2026-09-01): a scope-vak fallback MÁS scope alatt álló chunkot is
419
+ // adoptál — enélkül az adoptált chunk a RÉGI projekt-név alatt ragadna, és a
420
+ // scope-szűrt keresés az ÚJ néven nem találná meg (egy hibát cserélnénk másikra).
421
+ await set.dataService.backfillScopePath(item.existingId, set.scopePath);
418
422
  if (item.chunk) {
419
423
  await set.dataService.backfillReferenceCodes(item.existingId, fam_reference_code_util_1.FAM_ReferenceCode_Util.extract(item.chunk.content));
420
424
  // Pozíció-backfill (FAM-REV-061): a chunker pozíció-fix a régi equal-chunkokra is,
@@ -743,7 +747,23 @@ class FAM_Ingest_ControlService {
743
747
  const leaf = scopePath[scopePath.length - 1];
744
748
  filter['scopePath.scopeId'] = leaf.scopeId;
745
749
  }
746
- return dataService.findHydratableList(filter);
750
+ const scoped = await dataService.findHydratableList(filter);
751
+ if (scoped.length || !scopePath.length) {
752
+ return scoped;
753
+ }
754
+ // SCOPE-VAK FALLBACK (2026-09-01, mert gyoker-ok). A fajl IDENTITASA az abszolut utvonal — a scope csak
755
+ // besorolas. Ha ugyanaz a mappa MAS projekt-nev alatt kerul ujra-szkennelesre (atnevezes utan a heti
756
+ // re-scan cel-unioja MINDKET nevet megorzi, es ugyanazt a mappat KETSZER szkenneli), akkor a scope-ra
757
+ // szurt lekerdezes URESET ad -> minden chunk `new`-nak latszik -> TELJES DUPLIKALAS, minden heten.
758
+ //
759
+ // MERVE: 51 578 dupla-csoport, mind KULONBOZO scopeId alatt; a legfrissebb eset 2026-08-31 09:32, amikor
760
+ // a `fdp-e2e-full/package.json` egyetlen scan-futasban ketszer kerult be (`futdevpro-e2e` 08:39 es
761
+ // `fdp-e2e-full` 09:32). Osszesen ~59 000 folosleges chunk.
762
+ //
763
+ // Ezert: ha a scope-olt talalat URES, meg egyszer keresunk KIZAROLAG az utvonalra. Ami igy elokerul, az
764
+ // UGYANANNAK a fajlnak a korabbi feldolgozasa — a delta-compare igy `equal`/`modified`-nak latja
765
+ // (adoptalja), nem duplikal. Ha a scope tenyleg valtozott, a `backfill`/update agak vezetik at.
766
+ return dataService.findHydratableList({ 'source.absolutePath': absolutePath });
747
767
  }
748
768
  /**
749
769
  * Törölt-fájl reconciliation (whole-file orphan, dsgn-004 §4.3 kiterjesztés). A scan-folder/scan-project
@@ -480,6 +480,59 @@ class FAM_ScanJob_ControlService {
480
480
  * útvonalak a HÍVÓ `sourceLocation`-jéhez oldódnak fel (a szerver cwd-je eltér a CLI-étől — user-FR 2026-06-22):
481
481
  * a `configPath`/`root` ÉS a végső per-projekt `path` is.
482
482
  */
483
+ /**
484
+ * UTVONAL-SZERINTI DEDUP a cel-listan (2026-09-01, mert gyoker-ok). Ha UGYANAZ a mappa TOBB projekt-nev alatt
485
+ * szerepel, a scan ugyanazt a fajlt tobbszor olvassa be — es mivel a delta-osszevetes scope-ra szurt, a ket
486
+ * menet VAK egymasra, igy TELJES DUPLIKALAS keletkezik.
487
+ *
488
+ * MERVE: az utolso heti scan (2026-08-31) a `fdp-e2e-full/package.json`-t KETSZER vitte be, ket nev alatt
489
+ * (`futdevpro-e2e` 08:39 es `fdp-e2e-full` 09:32). Flotta-szinten 51 578 dupla-csoport, MIND kulonbozo
490
+ * scopeId alatt, osszesen ~59 000 folosleges chunk. Az ok: a heti re-scan a KORABBAN szkennelt celok
491
+ * UNIOJAT hasznalja, igy egy projekt-atnevezes utan a REGI es az UJ nev is bent marad — orokre.
492
+ *
493
+ * Az ELSO elofordulas nyer (a hivo altal megadott sorrend/az ujabb felderites elol all). A kihagyast
494
+ * LOGOLJUK — nema szures nem lehet (`core-rule-integrity` szelleme: ami kimarad, az latszodjon).
495
+ */
496
+ /** A projekt-név megegyezik-e a mappa nevével (a dedup döntőbírója — a lemez az igazság). */
497
+ static matchesFolderName(spec) {
498
+ const folder = String(spec.path ?? '')
499
+ .replace(/[\\/]+/g, '/')
500
+ .replace(/\/+$/, '')
501
+ .split('/')
502
+ .pop() ?? '';
503
+ return folder.length > 0 && folder.toLowerCase() === String(spec.project ?? '').toLowerCase();
504
+ }
505
+ static dedupeByPath(specs) {
506
+ const seen = new Map();
507
+ const dropped = [];
508
+ for (const spec of specs) {
509
+ // Az elvalasztot NORMALIZALJUK (a `\\` es a `/` ugyanaz a mappa Windowson), a zaro elvalasztot
510
+ // levagjuk, es kisbetusitunk — kulonben ugyanaz a cel ket irasmodban ketszer futna le.
511
+ const key = String(spec.path ?? '')
512
+ .replace(/[\\/]+/g, '/')
513
+ .replace(/\/+$/, '')
514
+ .toLowerCase();
515
+ const kept = seen.get(key);
516
+ if (kept) {
517
+ // DÖNTŐBÍRÓ: az a projekt-név nyer, ami a MAPPA NEVÉVEL egyezik. Átnevezés után a heti
518
+ // re-scan cél-uniója a RÉGI nevet is viszi, és az jellemzően előrébb áll — „első nyer"
519
+ // alapon a leváltott név maradna örökre. A mappa neve viszont a lemezen álló IGAZSÁG.
520
+ const winner = FAM_ScanJob_ControlService.matchesFolderName(spec)
521
+ && !FAM_ScanJob_ControlService.matchesFolderName(kept) ? spec : kept;
522
+ const loser = winner === spec ? kept : spec;
523
+ dropped.push({ project: loser.project, keptAs: winner.project, path: spec.path });
524
+ seen.set(key, winner);
525
+ continue;
526
+ }
527
+ seen.set(key, spec);
528
+ }
529
+ if (dropped.length) {
530
+ fsm_dynamo_1.DyFM_Log.warn(`[scanJob] UTVONAL-DEDUP: ${dropped.length} cel kihagyva, mert ugyanarra a mappara mutat, `
531
+ + `mint egy masik projekt-nev (ez okozta a heti dupla-beolvasast): `
532
+ + dropped.map((d) => `${d.project} -> ${d.keptAs}`).join(', '));
533
+ }
534
+ return [...seen.values()];
535
+ }
483
536
  static resolveSpecs(input) {
484
537
  const base = (target) => (target && input.sourceLocation && !(0, path_1.isAbsolute)(target)) ? (0, path_1.resolve)(input.sourceLocation, target) : target;
485
538
  let specs = [];
@@ -497,7 +550,8 @@ class FAM_ScanJob_ControlService {
497
550
  specs = [...specs, ...input.extraTargets];
498
551
  }
499
552
  // A per-projekt `path` relatív → a sourceLocation-höz feloldva (a discover már abszolútat ad; a config relatív lehet).
500
- return specs.map((spec) => ({ ...spec, path: base(spec.path) ?? spec.path }));
553
+ const resolved = specs.map((spec) => ({ ...spec, path: base(spec.path) ?? spec.path }));
554
+ return FAM_ScanJob_ControlService.dedupeByPath(resolved);
501
555
  }
502
556
  /** Egy szám-config feloldása (a hiba/hiány a fallback-ra esik — a cooldown sosem buktatja a job-ot). */
503
557
  static async resolveConfigNumber(key, fallback) {
@@ -66,6 +66,12 @@ const READ_INPUT_SCHEMA = {
66
66
  description: 'Feltárás-mélység: `deep` → mély feltárás (sokkal több chunk; a `read.deepTopK` '
67
67
  + 'az alap topK helyett). Default `normal`. Az explicit `topK` mindig felülír.',
68
68
  },
69
+ includeCrossTableProbe: {
70
+ type: 'boolean',
71
+ description: 'DIAGNOSZTIKA (nem termék-funkció): a kereszt-tár szonda nyers döntési adata '
72
+ + '(`crossTableProbe`) akkor is kerüljön a válaszba, ha a küszöb alatt maradt — a '
73
+ + 'küszöb-kalibráció csak így mérhető a rendszer VALÓS viselkedésén. A `hits`-hez nem nyúl.',
74
+ },
69
75
  },
70
76
  required: ['tables', 'query'],
71
77
  additionalProperties: false,
@@ -319,6 +319,7 @@ class FAM_ReadTool_Service {
319
319
  kindFilter: query.kindFilter,
320
320
  excludeIds: query.excludeIds,
321
321
  depth: query.depth,
322
+ includeCrossTableProbe: query.includeCrossTableProbe,
322
323
  // Per-query tartalom-kapcsolo atvezetese (2026-08-14 lelet: eddig itt VESZETT EL).
323
324
  includeContent: query.includeContent,
324
325
  };
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_CrossTableNote_Util = void 0;
4
+ /**
5
+ * NEM-TUDÁS jelöltek: nyers beszélgetés-törmelék, amivel SOHA nem szabad „mellesleg"-et adni.
6
+ *
7
+ * **Mért indok (2026-09-01):** az aktív `memory` tár **20,2%-a (8 717 bejegyzés) `kind: user_prompt` /
8
+ * `tags: [temporary_context]`** — szó szerinti user-input, amit a session tárolt el kontextusnak. Ezek
9
+ * rövidek és tematikusak, ezért a nyers koszinuszuk MAGASRA fut, és a 44 esetes benchmarkban a 12
10
+ * legerősebb jelöltből **10 ilyen volt**. Egy nyers prompt-idézet nem válasz a „láttuk már ezt?"
11
+ * kérdésre — a javaslat tőle nem hasznos, hanem félrevezető.
12
+ *
13
+ * Ugyanezen az alapon esik ki a **`fs-summary` / `scan-summary`** is: az egy mappa-listázás gépi
14
+ * összefoglalója. A benchmarkban pont a döntési zónában ült (+0,022), és semmilyen kérdésre nem válasz.
15
+ * (A scan-summary zaja külön is mért, ismert probléma — `reference_fam_scan_summary_weight_cap`.)
16
+ */
17
+ const NON_KNOWLEDGE_KINDS = ['user_prompt', 'fs-summary'];
18
+ const NON_KNOWLEDGE_TAGS = ['temporary_context', 'scan-summary'];
19
+ class FAM_CrossTableNote_Util {
20
+ /**
21
+ * A döntés. `undefined` = **csend**: nincs mező a válaszban. Szándékosan NEM adunk „nem találtam
22
+ * erősebbet" üzenetet — az minden válaszra ráterhelne egy sort azért, hogy semmit ne közöljön.
23
+ */
24
+ static decide(input) {
25
+ const usable = input.candidates.filter((candidate) => candidate.superseded !== true
26
+ && !FAM_CrossTableNote_Util.isNonKnowledge(candidate)
27
+ && Number.isFinite(candidate.score)
28
+ && candidate.score >= input.minScore);
29
+ if (!usable.length || !(input.minDelta > 0)) {
30
+ return undefined;
31
+ }
32
+ // LEGFELJEBB EGY tétel: a mérés szerint úgyis 0-1 van, és egy 3-elemű mellék-lista már hígítás
33
+ // — pontosan az a zaj, ami miatt nem a `hits`-be tesszük.
34
+ const best = [...usable].sort((a, b) => b.score - a.score)[0];
35
+ const delta = best.score - input.primaryTopScore;
36
+ if (delta < input.minDelta) {
37
+ return undefined;
38
+ }
39
+ const heading = best.headingPath?.length ? best.headingPath.join(' › ') : undefined;
40
+ return {
41
+ table: best.table,
42
+ delta: Number(delta.toFixed(4)),
43
+ hit: {
44
+ id: best.id,
45
+ table: best.table,
46
+ score: best.score,
47
+ sourceFilePath: best.sourceFilePath,
48
+ heading: heading,
49
+ label: FAM_CrossTableNote_Util.label(best),
50
+ },
51
+ message: FAM_CrossTableNote_Util.message({
52
+ table: best.table,
53
+ candidateScore: best.score,
54
+ primaryTopScore: input.primaryTopScore,
55
+ location: FAM_CrossTableNote_Util.label(best),
56
+ }),
57
+ };
58
+ }
59
+ /** Beszélgetés-törmelék-e a jelölt (nyers user-prompt / átmeneti kontextus). Lásd a fenti mérést. */
60
+ static isNonKnowledge(candidate) {
61
+ if (candidate.kind && NON_KNOWLEDGE_KINDS.includes(candidate.kind)) {
62
+ return true;
63
+ }
64
+ return (candidate.tags ?? []).some((tag) => NON_KNOWLEDGE_TAGS.includes(tag));
65
+ }
66
+ /**
67
+ * A jelölt AZONOSÍTHATÓ megnevezése. A fájl-út a legjobb, de az MCP-n ÍRT memória-bejegyzésnek nincs
68
+ * (mérve: a 21 szonda-jelöltből 16-nak). Ott a nyers ObjectId semmit nem mond a hívónak — a `kind` +
69
+ * `tags` + `scope` viszont pontosan megmondja, MIRŐL szól a jegyzet, és hogy érdemes-e megnyitni.
70
+ */
71
+ static label(candidate) {
72
+ if (candidate.sourceFilePath) {
73
+ return candidate.sourceFilePath;
74
+ }
75
+ if (candidate.headingPath?.length) {
76
+ return candidate.headingPath.join(' › ');
77
+ }
78
+ const parts = [];
79
+ if (candidate.kind) {
80
+ parts.push(candidate.kind);
81
+ }
82
+ if (candidate.tags?.length) {
83
+ parts.push(`[${candidate.tags.slice(0, 4).join(', ')}]`);
84
+ }
85
+ if (candidate.scope) {
86
+ parts.push(`(${candidate.scope})`);
87
+ }
88
+ // Az `id` MINDIG ott van a `hit.id`-ben — a szövegbe csak akkor kerül, ha tényleg nincs jobb.
89
+ return parts.length ? `${parts.join(' ')} — id: ${candidate.id}` : `id: ${candidate.id}`;
90
+ }
91
+ /**
92
+ * A szonda nyers döntési adata (diagnosztika). A `superseded` jelölt SZÁNDÉKOSAN benne marad a
93
+ * legjobb-választásban itt — a mérésnek látnia kell, ha a szonda amúgy egy leváltott generációt
94
+ * hozott volna; a `decide` ettől függetlenül kizárja.
95
+ */
96
+ static describeProbe(primaryTopScore, candidates, fired) {
97
+ // A diagnosztika a KÜSZÖB-független képet adja, de a TARTALMI kizárásokat (leváltott generáció,
98
+ // beszélgetés-törmelék) ALKALMAZZA — különben a kalibráció olyan jelöltre számolna deltát, amit a
99
+ // rendszer sosem ajánlana, és megint nem azt mérnénk, ami ténylegesen történik.
100
+ const eligible = candidates.filter((candidate) => candidate.superseded !== true
101
+ && !FAM_CrossTableNote_Util.isNonKnowledge(candidate)
102
+ && Number.isFinite(candidate.score));
103
+ const best = [...eligible]
104
+ .sort((a, b) => b.score - a.score)[0];
105
+ return {
106
+ primaryTopScore: primaryTopScore,
107
+ candidateCount: candidates.length,
108
+ eligibleCount: eligible.length,
109
+ best: best ? {
110
+ table: best.table,
111
+ score: best.score,
112
+ delta: Number((best.score - primaryTopScore).toFixed(4)),
113
+ label: FAM_CrossTableNote_Util.label(best),
114
+ superseded: best.superseded,
115
+ } : undefined,
116
+ fired: fired,
117
+ };
118
+ }
119
+ /** Az ember-olvasható mondat. Megmondja, MI van ott és MIÉRT szólunk — a hívó dönt, mi lesz vele. */
120
+ static message(set) {
121
+ const comparison = set.primaryTopScore > 0
122
+ ? `${set.candidateScore.toFixed(3)} vs ${set.primaryTopScore.toFixed(3)}`
123
+ : `${set.candidateScore.toFixed(3)}, miközben a kért tárban nem volt érdemi találat`;
124
+ return `Mellesleg: a(z) \`${set.table}\` tárban ÉRDEMBEN erősebb találat van erre (${comparison}) — `
125
+ + `${set.location}. A fenti listát ez NEM módosította; ha a kérdés inkább „láttuk-e már ezt", `
126
+ + 'érdemes lehet oda is belenézni.';
127
+ }
128
+ }
129
+ exports.FAM_CrossTableNote_Util = FAM_CrossTableNote_Util;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_Retrieval_ControlService = exports.MAX_PAGE_SIZE = void 0;
4
+ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
+ const fam_cross_table_note_util_1 = require("../_collections/fam-cross-table-note.util");
4
6
  const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
5
7
  const fam_reference_code_util_1 = require("../../../_collections/fam-reference-code.util");
6
8
  const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
@@ -112,11 +114,98 @@ class FAM_Retrieval_ControlService {
112
114
  const dedupedSet = FAM_Retrieval_ControlService.dedupeByContentHash(relevantSet);
113
115
  // ⑧ topK-vágás + dense-detektálás + suggestions.
114
116
  const result = this.buildResult({ query, relevantSet: dedupedSet, config, scopeExpand, detectedCodes });
117
+ // ⑨ KERESZT-TÁR „mellesleg"-javaslat (2026-09-01): a `hits` MÁR KÉSZ, és NEM változik tőle. Csak akkor
118
+ // fut, ha a hívó NEM kérte a `memory`/`knowledge` tárat (különben már benne lenne a listában).
119
+ const probeOutcome = await this.probeCrossTable({ query, queryVector, scopeExpand, config, result });
120
+ if (probeOutcome?.note) {
121
+ result.crossTableNote = probeOutcome.note;
122
+ }
123
+ if (query.includeCrossTableProbe && probeOutcome?.debug) {
124
+ result.crossTableProbe = probeOutcome.debug;
125
+ }
115
126
  // dsgn-013 §4 (MAM F2) — FELIDÉZÉS-megerősítés: a ténylegesen VISSZAADOTT `memory`-hitek „új energiát kapnak"
116
127
  // (lastRecalledAt=now, recallCount++, dormant=false). ASYNC fire-and-forget — NEM blokkolja a read választ.
117
128
  this.reactivateReturnedMemoryHits(result.hits, config);
118
129
  return result;
119
130
  }
131
+ /**
132
+ * A kereszt-tár szonda: EGY olcsó keresés a `memory` + `knowledge` táron, hogy kiderüljön, van-e ott
133
+ * ÉRDEMBEN erősebb találat. A döntés a tiszta `FAM_CrossTableNote_Util`-ban van (ott áll a kalibráció
134
+ * és a korlátok indoklása is); ez a metódus csak a jelölteket szerzi be.
135
+ *
136
+ * **Nem fut**, ha ki van kapcsolva, ha a küszöb 0, vagy ha a hívó ELEVE kérte ezeket a tárakat — akkor
137
+ * a találat már a listában van, és egy „mellesleg ott is van" mondat csak zaj lenne.
138
+ *
139
+ * **Hibatűrő:** a szonda MELLÉK-szolgáltatás. Ha bármi elromlik benne, a read válasza attól még teljes
140
+ * — némán elhagyjuk a jegyzetet, nem buktatjuk a hívást.
141
+ */
142
+ async probeCrossTable(set) {
143
+ // A DIAGNOSZTIKA kérése önmagában is ok a futásra: enélkül a kikapcsolt szonda mérhetetlen lenne.
144
+ if ((!set.config.crossTableNoteEnabled || !(set.config.crossTableNoteMinDelta > 0))
145
+ && !set.query.includeCrossTableProbe) {
146
+ return undefined;
147
+ }
148
+ const asked = new Set(set.query.tables);
149
+ const probeTables = [fam_table_type_enum_1.FAM_Table.memory, fam_table_type_enum_1.FAM_Table.knowledge]
150
+ .filter((table) => !asked.has(table));
151
+ if (!probeTables.length) {
152
+ return undefined;
153
+ }
154
+ try {
155
+ // A tárak szondázása PÁRHUZAMOS. Mérve: sorosan +713 ms (+68,6%) minden read-en, ami egy
156
+ // 6,8%-os megszólalási arányhoz mérve elfogadhatatlan ár; a két keresés független egymástól.
157
+ const perTable = await Promise.all(probeTables.map((table) => this.probeTable(table, set)));
158
+ const candidates = perTable.flat();
159
+ // Az összevetés alapja a NYERS koszinusz — a `finalScore` tár-függő szorzókat hordoz (weight, decay,
160
+ // projekt-súly), így két tár között nem összemérhető.
161
+ const primaryTopScore = set.result.hits.reduce((top, hit) => (hit.score > top ? hit.score : top), 0);
162
+ // A JEGYZET csak akkor keletkezik, ha a funkció TÉNYLEGESEN be van kapcsolva. A diagnosztika
163
+ // (`includeCrossTableProbe`) megengedi a szonda LEFUTÁSÁT kikapcsolt állapotban is — de egy
164
+ // mérő-kapcsoló SOHA nem hozhat vissza egy kikapcsolt funkciót a válaszba.
165
+ const enabled = set.config.crossTableNoteEnabled && set.config.crossTableNoteMinDelta > 0;
166
+ const note = enabled ? fam_cross_table_note_util_1.FAM_CrossTableNote_Util.decide({
167
+ primaryTopScore: primaryTopScore,
168
+ candidates: candidates,
169
+ minDelta: set.config.crossTableNoteMinDelta,
170
+ minScore: set.config.crossTableNoteMinScore,
171
+ }) : undefined;
172
+ return { note: note, debug: fam_cross_table_note_util_1.FAM_CrossTableNote_Util.describeProbe(primaryTopScore, candidates, Boolean(note)) };
173
+ }
174
+ catch (error) {
175
+ fsm_dynamo_1.DyFM_Log.warn(`[read] a kereszt-tar szonda hibara futott, a jegyzet elmarad (a valasz teljes): ${String(error)}`);
176
+ return undefined;
177
+ }
178
+ }
179
+ /**
180
+ * EGY szonda-tár lekérdezése — a jelöltekké alakított találatokkal.
181
+ *
182
+ * A configot a SZONDÁZOTT tárra oldjuk fel, NEM az elsődlegesre. Ez nem finomhangolás: a `memory`
183
+ * rangsorát a saját decay-/superseded-/cold-search-beállításai alakítják, és ezek per-tár defaultokkal
184
+ * bírnak. Az elsődleges tár configjával szondázva MÁS jelölt-halmazt kapunk, mint amit ugyanaz a kérdés
185
+ * egy közvetlen `memory`-kereséssel adna — a mérés és a működés más eredményre jut, és a kalibráció
186
+ * olyan számra épülne, ami sehol nem érvényes. (Mérve: 5/44 vs 2/44 megszólalás ugyanazon a 44 eseten.)
187
+ */
188
+ async probeTable(table, set) {
189
+ const tableConfig = await this.resolveReadConfig(table, set.query);
190
+ // A szonda SAJÁT, kicsi topK-val fut — nem második találati listát épít, csak eldönti, van-e erősebb.
191
+ const probeConfig = { ...tableConfig, topK: set.config.crossTableNoteTopK, minScore: 0 };
192
+ const hits = await this.searchTable({
193
+ table: table, query: set.query, queryVector: set.queryVector,
194
+ scopeExpand: set.scopeExpand, config: probeConfig, includeContent: false,
195
+ });
196
+ return hits.slice(0, set.config.crossTableNoteTopK).map((hit) => {
197
+ // A `kind`/`tags`/`scope` NEM dekoráció: az MCP-n ÍRT memória-bejegyzésnek nincs fájl-útja
198
+ // (mérve: 21 jelöltből 16-nak), és ott a nyers ObjectId semmit nem mond a hívónak.
199
+ const leafScope = hit.scopePath?.[hit.scopePath.length - 1];
200
+ return {
201
+ id: hit.id, table: hit.table, score: hit.score,
202
+ sourceFilePath: hit.sourceFilePath, headingPath: hit.headingPath,
203
+ kind: hit.kind, tags: hit.tags,
204
+ scope: leafScope?.canonicalName,
205
+ superseded: hit.superseded,
206
+ };
207
+ });
208
+ }
120
209
  /**
121
210
  * A visszaadott top-K `memory`-hitek async felidézés-megerősítése (dsgn-013 §4). Fire-and-forget (void): a read
122
211
  * már elkészült, a felidézés-frissítés best-effort. Csak a `memory`-táras, `_id`-vel bíró hitre; üres → no-op.
@@ -727,6 +816,12 @@ class FAM_Retrieval_ControlService {
727
816
  const memoryFloorWeight = await this.resolveNumber(config, 'memory.floorWeight', ctx, 0.05);
728
817
  const memoryMaxFrequencyBoost = await this.resolveNumber(config, 'memory.maxFrequencyBoost', ctx, 2.5);
729
818
  const memoryActivationWeight = await this.resolveNumber(config, 'memory.activationWeight', ctx, 0.35);
819
+ // KERESZT-TÁR „mellesleg"-javaslat (2026-09-01) — a `hits`-hez SOHA nem nyúl, csak külön mezőt ad.
820
+ const crossTableNoteValue = (await config.resolve('read.crossTableNoteEnabled', ctx)).value;
821
+ const crossTableNoteEnabled = crossTableNoteValue !== false;
822
+ const crossTableNoteMinDelta = await this.resolveNumber(config, 'read.crossTableNoteMinDelta', ctx, 0.025);
823
+ const crossTableNoteMinScore = await this.resolveNumber(config, 'read.crossTableNoteMinScore', ctx, 0.45);
824
+ const crossTableNoteTopK = await this.resolveNumber(config, 'read.crossTableNoteTopK', ctx, 3);
730
825
  const memoryReactivateValue = (await config.resolve('memory.reactivateOnRead', ctx)).value;
731
826
  const memoryReactivateOnRead = memoryReactivateValue !== false;
732
827
  const memoryColdSearchValue = (await config.resolve('memory.coldSearchEnabled', ctx)).value;
@@ -736,6 +831,10 @@ class FAM_Retrieval_ControlService {
736
831
  return {
737
832
  topK: Math.min(topKDefault, exports.MAX_PAGE_SIZE),
738
833
  minScore: minScore,
834
+ crossTableNoteEnabled: crossTableNoteEnabled,
835
+ crossTableNoteMinDelta: crossTableNoteMinDelta,
836
+ crossTableNoteMinScore: crossTableNoteMinScore,
837
+ crossTableNoteTopK: crossTableNoteTopK,
739
838
  relevanceFloor: relevanceFloor,
740
839
  importChunkWeight: importChunkWeight,
741
840
  importSourceWeight: importSourceWeight,
@@ -69,6 +69,7 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
69
69
  this.statsTableEndpoint(),
70
70
  this.ingestRunsEndpoint(),
71
71
  this.duplicatesEndpoint(),
72
+ this.duplicatesPruneEndpoint(),
72
73
  this.configGetEndpoint(),
73
74
  this.configSetEndpoint(),
74
75
  this.scanStartEndpoint(),
@@ -627,6 +628,36 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
627
628
  });
628
629
  }
629
630
  // =========================================================================
631
+ // /duplicates/prune — BÁJTAZONOS ismétlés-nyesés (dry-run az alapértelmezés)
632
+ // =========================================================================
633
+ /**
634
+ * `POST /duplicates/prune` — az azonos `(absolutePath, chunkIndex, chunkTotal, contentHash)` négyesű
635
+ * példányokból a legfrissebbet megtartja, a többit soft-delete-eli, és a csoport `recallCount` /
636
+ * `lastRecalledAt` MAXIMUMÁT a megtartottra viszi. Body: `table` (opcionális, különben MINDEN tár),
637
+ * `dryRun` (DEFAULT `true` — törölni csak explicit `false`-szal lehet), `maxGroups`.
638
+ *
639
+ * A `GET /duplicates/:table`-lel NEM keverendő: az szemantikus hasonlóságot keres és sosem töröl.
640
+ */
641
+ duplicatesPruneEndpoint() {
642
+ return new nts_dynamo_1.DyNTS_Endpoint_Params({
643
+ name: 'duplicatesPrune',
644
+ type: fsm_dynamo_1.DyFM_HttpCallType.post,
645
+ endpoint: '/duplicates/prune',
646
+ tasks: [
647
+ async (req, res) => {
648
+ await this.run(res, async () => {
649
+ const body = req.body ?? {};
650
+ return embedding_1.FAM_ExactDuplicatePrune_ControlService.getInstance().prune({
651
+ table: body.table ? this.parseTable(body.table) : undefined,
652
+ dryRun: body.dryRun !== false,
653
+ maxGroups: typeof body.maxGroups === 'number' ? body.maxGroups : undefined,
654
+ });
655
+ });
656
+ },
657
+ ],
658
+ });
659
+ }
660
+ // =========================================================================
630
661
  // /config — scoped config olvasás/írás (dsgn-007; a UI settings + CLI config)
631
662
  // =========================================================================
632
663
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@futdevpro/fdp-agent-memory",
3
- "version": "1.1.166",
3
+ "version": "1.1.177",
4
4
  "description": "Local-first, vector-backed multi-table agent memory exposed as an MCP server (read/write/capabilities). Public, FDP-Templates-free, no auth.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -19,7 +19,13 @@
19
19
  "build",
20
20
  "client-dist",
21
21
  "README.md",
22
- "LICENSE"
22
+ "LICENSE",
23
+ "!build/**/*.spec.js",
24
+ "!build/**/*.spec.js.map",
25
+ "!build/**/*.spec.d.ts",
26
+ "!build/**/_benchmarks/**",
27
+ "!build/**/_integration-tests/**",
28
+ "!build/**/_spec-support/**"
23
29
  ],
24
30
  "engines": {
25
31
  "node": ">=20"
@@ -1,105 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FAM_IntegrationTest_Setup_Util = void 0;
4
- const tslib_1 = require("tslib");
5
- const net = tslib_1.__importStar(require("net"));
6
- const mongoose_1 = tslib_1.__importDefault(require("mongoose"));
7
- const nts_dynamo_1 = require("@futdevpro/nts-dynamo");
8
- const fam_db_models_const_1 = require("../../_collections/fam-db-models.const");
9
- /** Integrációs teszt issuer — a MongoDB-dokumentumok azonosítására. */
10
- const INTTEST_ISSUER = 'integration-test';
11
- /** Teszt-adat prefix — a teszt-dokumentumok megkülönböztetésére (NEM ütközik production-nal). */
12
- const INTTEST_PREFIX = 'inttest-';
13
- /** A dedikált spec-DB neve (a runner/local Mongo-n; NEM a `fdp-agent-memory` prod-DB). */
14
- const INTTEST_DB_NAME = 'fam_inttest';
15
- /** MongoDB elérhetőség-ellenőrzés timeout (ms). */
16
- const MONGO_CHECK_TIMEOUT_MS = 1500;
17
- /**
18
- * `FAM_IntegrationTest_Setup_Util` — az integrációs tesztek közös setup-utility-je (a
19
- * `ccap-revisioned` `IntegrationTest_Setup_Util` mintára). MongoDB-elérhetőség (TCP-probe a
20
- * `MONGO_URL`-re), egyedi `inttest-` ID-generálás, ÉS egy **lightweight DB-setup** (mongoose-connect +
21
- * `DyNTS_GlobalService.setServices`) — **NEM a teljes `new App()` boot** (az HTTP-listen + LVS-hydrate
22
- * postProcess-t indít, ami a spec-eket hangolja); csak a DataService-ek DB-rétegét állítja fel.
23
- */
24
- class FAM_IntegrationTest_Setup_Util {
25
- /** Teszt issuer azonosító — minden integrációs-teszt DataService-híváshoz. */
26
- static getIssuer() {
27
- return INTTEST_ISSUER;
28
- }
29
- /**
30
- * Egyedi, időbélyeges ID/tartalom a teszt-adatokhoz. Pattern: `inttest-{timestamp}-{random}` —
31
- * párhuzamos futtatásban is egyedi, és a prefix jelzi a teszt-adatot.
32
- */
33
- static generateUniqueId() {
34
- const timestamp = Date.now();
35
- const random = Math.random().toString(36).substring(2, 8);
36
- return `${INTTEST_PREFIX}${timestamp}-${random}`;
37
- }
38
- /**
39
- * MongoDB elérhetőség TCP-socket-tel (a `MONGO_URL` host:port-ja; default
40
- * `mongodb://127.0.0.1:27017`). Ha nem elérhető → `false`, és a tesztek `pending()`-re állnak
41
- * (CI-ben Mongo nélkül is zöld a suite — pending, NEM failure).
42
- */
43
- static checkMongoAvailability() {
44
- return new Promise((resolve) => {
45
- const mongoUrl = process.env.MONGO_URL ?? 'mongodb://127.0.0.1:27017';
46
- const urlNoProtocol = mongoUrl.replace(/^mongodb(\+srv)?:\/\//, '');
47
- const hostPort = urlNoProtocol.split('/')[0];
48
- const parts = hostPort.split(':');
49
- const host = parts[0];
50
- const port = parseInt(parts[1] ?? '27017', 10);
51
- const socket = net.createConnection({ host: host, port: port });
52
- let finished = false;
53
- const finish = (available) => {
54
- if (finished) {
55
- return;
56
- }
57
- finished = true;
58
- try {
59
- socket.destroy();
60
- }
61
- catch { /* ignore */ }
62
- resolve(available);
63
- };
64
- socket.on('connect', () => { finish(true); });
65
- socket.on('error', () => { finish(false); });
66
- setTimeout(() => { finish(false); }, MONGO_CHECK_TIMEOUT_MS);
67
- });
68
- }
69
- /**
70
- * Lightweight DB-setup (`beforeAll`-ban, ha a Mongo elérhető): mongoose-connect a dedikált
71
- * `fam_inttest` DB-re + `DyNTS_GlobalService.setServices` a `FAM_DB_MODELS`-szel (SSOT). Így a
72
- * `new FAM_*_DataService(...)` (eager getDBService) feloldódik — App-boot, HTTP-listen és
73
- * embedding/LVS-postProcess NÉLKÜL (azok a teljes boot velejárói, itt nem kellenek).
74
- */
75
- static async setupDb() {
76
- const mongoUrl = process.env.MONGO_URL ?? 'mongodb://127.0.0.1:27017';
77
- const base = mongoUrl.replace(/^(mongodb(\+srv)?:\/\/[^/]+).*$/, '$1');
78
- await mongoose_1.default.connect(`${base}/${INTTEST_DB_NAME}`);
79
- await nts_dynamo_1.DyNTS_GlobalService.setServices({ dbModels: fam_db_models_const_1.FAM_DB_MODELS });
80
- }
81
- /** DB-teardown (`afterAll`-ban): a mongoose-kapcsolat zárása (best-effort). */
82
- static async teardownDb() {
83
- try {
84
- if (mongoose_1.default.connection.readyState !== 0) {
85
- await mongoose_1.default.disconnect();
86
- }
87
- }
88
- catch {
89
- // best-effort — a process-vég úgyis bontja.
90
- }
91
- }
92
- /** Egy collection ürítése teszt KÖZÖTT (a teszt-izolációhoz). No-op ha a kapcsolat nem áll. */
93
- static async clearCollection(collectionName) {
94
- if (mongoose_1.default.connection.readyState !== 1) {
95
- return;
96
- }
97
- try {
98
- await mongoose_1.default.connection.collection(collectionName).deleteMany({});
99
- }
100
- catch {
101
- // a collection lehet hogy még nem létezik — nem hiba.
102
- }
103
- }
104
- }
105
- exports.FAM_IntegrationTest_Setup_Util = FAM_IntegrationTest_Setup_Util;