@futdevpro/fdp-agent-memory 1.1.163 → 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 +56 -0
- package/build/src/_models/data-models/fam-entry-base-properties.const.js +1 -0
- package/build/src/_models/data-models/fam-entry.data-model.js +8 -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/_collections/fam-project-config.util.js +101 -0
- package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +23 -1
- package/build/src/_modules/ingest/_services/fam-scan-job.control-service.js +55 -1
- package/build/src/_modules/ingest/_services/fam-scan.control-service.js +14 -0
- 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 +109 -1
- 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
|
@@ -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,
|
|
@@ -484,6 +488,8 @@ class FAM_Ingest_ControlService {
|
|
|
484
488
|
scopePath: set.scopePath,
|
|
485
489
|
// Súly-kiemelés (FAM-REV-055): a scan-`weights`-ből feloldott fájl-súly (undefined → a tár model-default).
|
|
486
490
|
weight: set.file.weight,
|
|
491
|
+
// A projekt-szintu szorzo a `.famconfig.json`-bol — a retrieval TEMPERALATLANUL alkalmazza.
|
|
492
|
+
projectWeight: set.file.projectWeight,
|
|
487
493
|
sourceFilePath: set.file.relativePath,
|
|
488
494
|
chunkIndex: set.chunk.chunkIndex,
|
|
489
495
|
chunkTotal: set.chunk.chunkTotal,
|
|
@@ -741,7 +747,23 @@ class FAM_Ingest_ControlService {
|
|
|
741
747
|
const leaf = scopePath[scopePath.length - 1];
|
|
742
748
|
filter['scopePath.scopeId'] = leaf.scopeId;
|
|
743
749
|
}
|
|
744
|
-
|
|
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 });
|
|
745
767
|
}
|
|
746
768
|
/**
|
|
747
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) {
|
|
@@ -8,6 +8,7 @@ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
|
|
|
8
8
|
const config_catalog_const_1 = require("../../../_collections/config-catalog.const");
|
|
9
9
|
const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
|
|
10
10
|
const fam_famignore_util_1 = require("../_collections/fam-famignore.util");
|
|
11
|
+
const fam_project_config_util_1 = require("../_collections/fam-project-config.util");
|
|
11
12
|
const fam_file_routing_util_1 = require("../_collections/fam-file-routing.util");
|
|
12
13
|
const fam_glob_match_util_1 = require("../_collections/fam-glob-match.util");
|
|
13
14
|
const fam_scan_path_util_1 = require("../_collections/fam-scan-path.util");
|
|
@@ -48,6 +49,17 @@ class FAM_Scan_ControlService {
|
|
|
48
49
|
// (in-repo, gitignore-szerű; folder/project scan-nél — a scan-file egyetlen explicit fájlt céloz). HOZZÁADÓDIK.
|
|
49
50
|
const baseIgnore = await this.resolveIgnorePatterns(set.scopePath, set.exclude);
|
|
50
51
|
const famIgnore = set.operation === 'scan-file' ? [] : fam_famignore_util_1.FAM_FamIgnore_Util.loadFromDir(resolvedRoot);
|
|
52
|
+
// A projektben LATHATO, verziokezelt FAM-beallitas (`.famconfig.json`) — a `.fdpfamignore` testvere.
|
|
53
|
+
// Owner-igeny 2026-08-22: a beallitasok ne CSAK a FAM-ban eljenek, hanem a projektben is latszodjanak.
|
|
54
|
+
const projectConfig = set.operation === 'scan-file'
|
|
55
|
+
? undefined
|
|
56
|
+
: fam_project_config_util_1.FAM_ProjectConfig_Util.load(resolvedRoot);
|
|
57
|
+
if (projectConfig) {
|
|
58
|
+
fsm_dynamo_1.DyFM_Log.info(`[famIngest] ${fam_project_config_util_1.FAM_ProjectConfig_Util.FILE_NAME} betoltve (${resolvedRoot}): `
|
|
59
|
+
+ `weight=${projectConfig.weight ?? '-'}, per-minta szabaly=${projectConfig.weights?.length ?? 0}`
|
|
60
|
+
+ `${projectConfig.note ? ` | ${projectConfig.note}` : ''}`);
|
|
61
|
+
}
|
|
62
|
+
const projectWeightRules = fam_project_config_util_1.FAM_ProjectConfig_Util.projectWeightRules(projectConfig);
|
|
51
63
|
if (famIgnore.length) {
|
|
52
64
|
fsm_dynamo_1.DyFM_Log.info(`[famIngest] .fdpfamignore betöltve (${resolvedRoot}): ${famIgnore.length / 2} minta-sor`);
|
|
53
65
|
}
|
|
@@ -94,6 +106,7 @@ class FAM_Scan_ControlService {
|
|
|
94
106
|
maxFileSizeBytes: maxFileSizeBytes,
|
|
95
107
|
tableOverride: set.tableOverride,
|
|
96
108
|
weights: set.weights,
|
|
109
|
+
projectWeightRules: projectWeightRules,
|
|
97
110
|
testFileWeight: testFileWeight,
|
|
98
111
|
});
|
|
99
112
|
if (routed.route === 'skip') {
|
|
@@ -154,6 +167,7 @@ class FAM_Scan_ControlService {
|
|
|
154
167
|
return {
|
|
155
168
|
...base, route: route,
|
|
156
169
|
weight: fam_scan_weight_util_1.FAM_ScanWeight_Util.resolveWithDefault(set.relativePath, set.weights, set.testFileWeight),
|
|
170
|
+
projectWeight: fam_scan_weight_util_1.FAM_ScanWeight_Util.resolve(set.relativePath, set.projectWeightRules),
|
|
157
171
|
};
|
|
158
172
|
}
|
|
159
173
|
/**
|
|
@@ -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.
|
|
@@ -469,12 +558,21 @@ class FAM_Retrieval_ControlService {
|
|
|
469
558
|
})
|
|
470
559
|
: 0;
|
|
471
560
|
const keywordBoost = 1 + set.config.keywordWeight * lexicalScore;
|
|
561
|
+
// PROJEKT-SZINTU szorzo a `.famconfig.json`-bol — SZANDEKOSAN a temperalason KIVUL. A `weight` mezo az
|
|
562
|
+
// `effectiveWeight`-en megy at, ahonnan a 0.06-os influence mellett MAXIMUM 6% lehuzas johet (merve
|
|
563
|
+
// 2026-08-22), ezert egy projekt-szintu policy ott gyakorlatilag hatastalan lenne. Itt viszont a szorzo
|
|
564
|
+
// KOZVETLENUL hat. A rangsor RELATIV, ezert az egyenletes projekt-szorzo altalanos keresesben halkit, de
|
|
565
|
+
// scope-olt (celzott) keresesben NEM valtoztat sorrendet — minden jelolt ugyanazt a szorzot kapja.
|
|
566
|
+
const projectFactor = typeof set.entry.projectWeight === 'number'
|
|
567
|
+
&& Number.isFinite(set.entry.projectWeight) && set.entry.projectWeight > 0
|
|
568
|
+
? set.entry.projectWeight
|
|
569
|
+
: 1;
|
|
472
570
|
return {
|
|
473
571
|
id: set.vectorHit.id,
|
|
474
572
|
table: set.table,
|
|
475
573
|
score: set.vectorHit.score,
|
|
476
574
|
weight: set.weight,
|
|
477
|
-
finalScore: set.vectorHit.score * FAM_Retrieval_ControlService.effectiveWeight(set.weight, set.config.weightInfluence) * chunkFactor * sourceFactor * summaryFactor * decayFactor * keywordBoost,
|
|
575
|
+
finalScore: set.vectorHit.score * FAM_Retrieval_ControlService.effectiveWeight(set.weight, set.config.weightInfluence) * chunkFactor * sourceFactor * summaryFactor * decayFactor * keywordBoost * projectFactor,
|
|
478
576
|
lexicalScore: lexicalScore,
|
|
479
577
|
content: set.includeContent ? set.entry.content : undefined,
|
|
480
578
|
contentHash: set.entry.contentHash,
|
|
@@ -718,6 +816,12 @@ class FAM_Retrieval_ControlService {
|
|
|
718
816
|
const memoryFloorWeight = await this.resolveNumber(config, 'memory.floorWeight', ctx, 0.05);
|
|
719
817
|
const memoryMaxFrequencyBoost = await this.resolveNumber(config, 'memory.maxFrequencyBoost', ctx, 2.5);
|
|
720
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);
|
|
721
825
|
const memoryReactivateValue = (await config.resolve('memory.reactivateOnRead', ctx)).value;
|
|
722
826
|
const memoryReactivateOnRead = memoryReactivateValue !== false;
|
|
723
827
|
const memoryColdSearchValue = (await config.resolve('memory.coldSearchEnabled', ctx)).value;
|
|
@@ -727,6 +831,10 @@ class FAM_Retrieval_ControlService {
|
|
|
727
831
|
return {
|
|
728
832
|
topK: Math.min(topKDefault, exports.MAX_PAGE_SIZE),
|
|
729
833
|
minScore: minScore,
|
|
834
|
+
crossTableNoteEnabled: crossTableNoteEnabled,
|
|
835
|
+
crossTableNoteMinDelta: crossTableNoteMinDelta,
|
|
836
|
+
crossTableNoteMinScore: crossTableNoteMinScore,
|
|
837
|
+
crossTableNoteTopK: crossTableNoteTopK,
|
|
730
838
|
relevanceFloor: relevanceFloor,
|
|
731
839
|
importChunkWeight: importChunkWeight,
|
|
732
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;
|