@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.
- package/build/package.json +8 -2
- package/build/src/_cli/_commands/prune-duplicates.command.js +93 -0
- package/build/src/_cli/register-commands.js +2 -0
- package/build/src/_collections/config-catalog.const.js +31 -0
- package/build/src/_modules/embedding/_collections/fam-exact-duplicate-prune.util.js +85 -0
- package/build/src/_modules/embedding/_services/fam-entry.data-service.js +21 -0
- package/build/src/_modules/embedding/_services/fam-exact-duplicate-prune.control-service.js +152 -0
- package/build/src/_modules/embedding/index.js +7 -1
- package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +21 -1
- package/build/src/_modules/ingest/_services/fam-scan-job.control-service.js +55 -1
- package/build/src/_modules/mcp/_collections/fam-core-tools.const.js +6 -0
- package/build/src/_modules/mcp/_services/fam-read-tool.service.js +1 -0
- package/build/src/_modules/retrieval/_collections/fam-cross-table-note.util.js +129 -0
- package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +99 -0
- package/build/src/_routes/server/api/api.controller.js +31 -0
- package/package.json +8 -2
- package/build/src/_integration-tests/_helpers/fam-integration-test-setup.util.js +0 -105
package/build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@futdevpro/fdp-agent-memory",
|
|
3
|
-
"version": "1.1.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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;
|