@futdevpro/fdp-agent-memory 1.1.122 → 1.1.133

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/build/package.json +1 -1
  2. package/build/src/_cli/_commands/read.command.js +26 -3
  3. package/build/src/_collections/config-catalog.const.js +12 -0
  4. package/build/src/_collections/error-codes.const.js +2 -0
  5. package/build/src/_collections/fam-db-models.const.js +2 -0
  6. package/build/src/_models/data-models/fam-dup-audit.data-model.js +72 -0
  7. package/build/src/_modules/embedding/_collections/fam-dup-file-rollup.util.js +48 -0
  8. package/build/src/_modules/embedding/_collections/fam-dup-fork-pair.util.js +86 -0
  9. package/build/src/_modules/embedding/_services/fam-dup-audit-scheduler.control-service.js +140 -0
  10. package/build/src/_modules/embedding/_services/fam-dup-audit.control-service.js +141 -0
  11. package/build/src/_modules/embedding/_services/fam-dup-audit.data-service.js +66 -0
  12. package/build/src/_modules/embedding/_services/fam-dup-coverage.control-service.js +123 -0
  13. package/build/src/_modules/embedding/_services/fam-duplicate-scan.control-service.js +63 -10
  14. package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +9 -0
  15. package/build/src/_modules/embedding/_services/fam-exact-duplication.control-service.js +309 -0
  16. package/build/src/_modules/embedding/index.js +16 -1
  17. package/build/src/_modules/mcp/_collections/fam-read-response-shape.util.js +167 -0
  18. package/build/src/_modules/mcp/_services/fam-capability-registry.service.js +67 -0
  19. package/build/src/_modules/mcp/_services/fam-mcp-adapter.service.js +8 -3
  20. package/build/src/_modules/mcp/_services/fam-read-tool.service.js +55 -9
  21. package/build/src/_modules/mcp/_services/fam-write-tool.service.js +35 -0
  22. package/build/src/_modules/retrieval/_collections/fam-rule-propagation.util.js +1 -1
  23. package/client-dist/chunk-UKUX2JAV.js +1 -0
  24. package/client-dist/index.html +3 -3
  25. package/client-dist/{main-YK4YIVAQ.js → main-5TKTMY4X.js} +1 -1
  26. package/package.json +1 -1
  27. package/client-dist/chunk-NMUCMQY6.js +0 -1
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_DupCoverage_ControlService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const mongoose_1 = tslib_1.__importDefault(require("mongoose"));
6
+ const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
7
+ const fam_scope_data_service_1 = require("../../scope-reference/_services/fam-scope.data-service");
8
+ const fam_store_registry_const_1 = require("../_collections/fam-store-registry.const");
9
+ const fam_entry_data_service_1 = require("./fam-entry.data-service");
10
+ /**
11
+ * `FAM_DupCoverage_ControlService` (dup-v2, task #20) — a duplikáció-mérés **lefedettség-preflight**-je. A
12
+ * dup-eredmény csak akkor értelmezhető flotta-szinten, ha tudjuk, MELY projektek korpusza hiányzik (0 entry)
13
+ * vagy elavult (régen szkennelt) — különben a „nincs duplikáció X-szel" néma hamis-negatív. A riport a scope-fa
14
+ * project-layer listáját veti össze a tár per-projekt entry-számával + a legfrissebb entry korával. Read-only.
15
+ */
16
+ class FAM_DupCoverage_ControlService {
17
+ static _instance;
18
+ issuer = 'FAM_DupCoverage_ControlService';
19
+ /** Az elavultság-küszöb napban: a heti re-scan (7 nap) + 1 nap türelem. */
20
+ static STALE_WARN_DAYS = 8;
21
+ static getInstance() {
22
+ if (!FAM_DupCoverage_ControlService._instance) {
23
+ FAM_DupCoverage_ControlService._instance = new FAM_DupCoverage_ControlService();
24
+ }
25
+ return FAM_DupCoverage_ControlService._instance;
26
+ }
27
+ /**
28
+ * A lefedettség-riport egy tárra (default `codebase`). A scope-fa project-layer listája + egy per-projekt
29
+ * `$group` aggregáció (aktív entry-szám + max lastModified), majd a PURE `joinCoverage` join. Üres tár /
30
+ * üres scope-fa → deskriptív riport (nem dob).
31
+ */
32
+ async report(table = fam_table_type_enum_1.FAM_Table.codebase) {
33
+ const registryEntry = fam_store_registry_const_1.FAM_StoreRegistry_Util.getEntry(table);
34
+ // A scope-fa project-layer canonical nevei (a „mit KELLENE lefedni" referencia-lista).
35
+ const scope_DS = new fam_scope_data_service_1.FAM_Scope_DataService({ issuer: this.issuer });
36
+ const scopes = await scope_DS.findAllActive();
37
+ const projectNames = [...new Set(scopes
38
+ .filter((scope) => scope.layer === 'project' && scope.canonicalName)
39
+ .map((scope) => scope.canonicalName))].sort();
40
+ // Per-projekt aggregáció a RAW collection-ön (aktív = `_deleted:null`; a lastModified a `__lastModified`
41
+ // vagy fallback a `__created`). A contentVector SOHA nem kerül a pipeline-ba (memória-védelem).
42
+ let rows = [];
43
+ if (registryEntry) {
44
+ const collection = this.entryCollection(registryEntry);
45
+ rows = await collection.aggregate([
46
+ { $match: { _deleted: null } },
47
+ {
48
+ $project: {
49
+ project: {
50
+ $arrayElemAt: [{
51
+ $map: {
52
+ input: {
53
+ $filter: {
54
+ input: { $ifNull: ['$scopePath', []] },
55
+ as: 'ref',
56
+ cond: { $eq: ['$$ref.layer', 'project'] },
57
+ },
58
+ },
59
+ as: 'ref',
60
+ in: '$$ref.canonicalName',
61
+ },
62
+ }, 0],
63
+ },
64
+ lm: { $ifNull: ['$__lastModified', '$__created'] },
65
+ },
66
+ },
67
+ { $group: { _id: '$project', entryCount: { $sum: 1 }, lastModified: { $max: '$lm' } } },
68
+ ], { allowDiskUse: true }).toArray();
69
+ }
70
+ return FAM_DupCoverage_ControlService.joinCoverage({
71
+ table: table, projectNames: projectNames, rows: rows, nowMs: Date.now(),
72
+ });
73
+ }
74
+ /**
75
+ * **PURE** join: a scope-fa projekt-listája × a per-projekt aggregáció-sorok → lefedettség-riport
76
+ * (zero-entry + stale jelzésekkel + warnings). Determinista → unit-tesztelhető DB nélkül.
77
+ */
78
+ static joinCoverage(set) {
79
+ const staleThresholdMs = FAM_DupCoverage_ControlService.STALE_WARN_DAYS * 24 * 3600000;
80
+ const byProject = new Map();
81
+ for (const row of set.rows) {
82
+ if (row._id) {
83
+ byProject.set(row._id, row);
84
+ }
85
+ }
86
+ const projects = set.projectNames.map((project) => {
87
+ const row = byProject.get(project);
88
+ const lastModifiedMs = row?.lastModified ? new Date(row.lastModified).getTime() : 0;
89
+ return {
90
+ project: project,
91
+ entryCount: row?.entryCount ?? 0,
92
+ lastModifiedMs: lastModifiedMs,
93
+ stale: Boolean(row?.entryCount) && (set.nowMs - lastModifiedMs) > staleThresholdMs,
94
+ };
95
+ }).sort((left, right) => right.entryCount - left.entryCount || left.project.localeCompare(right.project));
96
+ const zeroEntryProjects = projects.filter((row) => !row.entryCount).map((row) => row.project);
97
+ const staleProjects = projects.filter((row) => row.stale).map((row) => row.project);
98
+ const warnings = [];
99
+ if (zeroEntryProjects.length) {
100
+ warnings.push(`${zeroEntryProjects.length} projekt 0 entry-vel a '${set.table}' tárban — ezek a dup-mérésből `
101
+ + `NÉMÁN kimaradnak (futtass scant): ${zeroEntryProjects.join(', ')}`);
102
+ }
103
+ if (staleProjects.length) {
104
+ warnings.push(`${staleProjects.length} projekt korpusza elavult (> ${FAM_DupCoverage_ControlService.STALE_WARN_DAYS} `
105
+ + `nap): ${staleProjects.join(', ')}`);
106
+ }
107
+ return {
108
+ table: set.table,
109
+ generatedAt: set.nowMs,
110
+ projectsTotal: set.projectNames.length,
111
+ zeroEntryProjects: zeroEntryProjects,
112
+ staleProjects: staleProjects,
113
+ projects: projects,
114
+ warnings: warnings,
115
+ };
116
+ }
117
+ /** Egy fő RAG-tár mongoose-collection-je (a DS-példányosítás regisztrálja a modellt; onnan a valós collection). */
118
+ entryCollection(registryEntry) {
119
+ new fam_entry_data_service_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
120
+ return mongoose_1.default.models[registryEntry.dataParams.dataName].collection;
121
+ }
122
+ }
123
+ exports.FAM_DupCoverage_ControlService = FAM_DupCoverage_ControlService;
@@ -2,7 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_DuplicateScan_ControlService = void 0;
4
4
  const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
+ const fam_dup_fork_pair_util_1 = require("../_collections/fam-dup-fork-pair.util");
6
+ const fam_dup_file_rollup_util_1 = require("../_collections/fam-dup-file-rollup.util");
5
7
  const fam_store_registry_const_1 = require("../_collections/fam-store-registry.const");
8
+ const fam_dup_coverage_control_service_1 = require("./fam-dup-coverage.control-service");
6
9
  const fam_entry_data_service_1 = require("./fam-entry.data-service");
7
10
  const fam_vector_search_control_service_1 = require("./fam-vector-search.control-service");
8
11
  /**
@@ -44,12 +47,15 @@ class FAM_DuplicateScan_ControlService {
44
47
  const search = fam_vector_search_control_service_1.FAM_VectorSearch_ControlService.getInstance();
45
48
  const totalInPool = registryEntry ? search.getPoolSize(input.table) : 0;
46
49
  const filtered = FAM_DuplicateScan_ControlService.hasCandidateFilter(input);
47
- const empty = {
48
- table: input.table, threshold: threshold, totalInPool: totalInPool, scanned: 0,
49
- capped: false, filtered: filtered, clusterCount: 0, duplicateEntryCount: 0, clusters: [],
50
- };
51
50
  if (!registryEntry || !totalInPool) {
52
- return empty;
51
+ // Üres pool → üres eredmény, DE a coverage-preflight ITT IS beágyazva (a „miért üres?" — pl. 0-entry
52
+ // projektek — ne legyen néma; dup-v2 edge-case).
53
+ return {
54
+ table: input.table, threshold: threshold, totalInPool: totalInPool, scanned: 0,
55
+ capped: false, filtered: filtered, clusterCount: 0, duplicateEntryCount: 0, clusters: [],
56
+ fileRollup: [], forkPairs: { pairs: [], suspectedForkCount: 0 }, suppressedClusters: 0,
57
+ coverage: registryEntry ? await this.maybeCoverage(input) : undefined,
58
+ };
53
59
  }
54
60
  // 1. A (capolt) (id, vektor) halmaz lean-streamelése (NEM a teljes content — csak a vektorok).
55
61
  // A szűrő-mezők Mongo-prefilterként MÁR ITT szűkítik a jelölt-halmazt (célzott scan a nagy táron).
@@ -95,7 +101,7 @@ class FAM_DuplicateScan_ControlService {
95
101
  }
96
102
  }
97
103
  const metaById = await this.fetchMeta(dataService, clusteredIds);
98
- // 5. Cluster-ek építése (tag-dúsítás + projekt-lefedettség) + a `minProjects` cross-project szűrő +
104
+ // 5. Cluster-ek építése (tag-dúsítás + projekt-/repo-lefedettség) + a `minProjects` cross-project szűrő +
99
105
  // rendezés (legszélesebb projekt-lefedettség, majd méret, majd score elöl) + output-cap.
100
106
  const minProjects = input.minProjects ?? 0;
101
107
  const clusters = components
@@ -106,6 +112,7 @@ class FAM_DuplicateScan_ControlService {
106
112
  snippet: metaById.get(id)?.snippet,
107
113
  sourceFilePath: metaById.get(id)?.sourceFilePath,
108
114
  project: metaById.get(id)?.project,
115
+ repoName: metaById.get(id)?.repoName,
109
116
  }));
110
117
  const projects = FAM_DuplicateScan_ControlService.distinctProjects(members);
111
118
  return {
@@ -114,6 +121,7 @@ class FAM_DuplicateScan_ControlService {
114
121
  minScore: component.minScore,
115
122
  projects: projects,
116
123
  projectCount: projects.length,
124
+ repoNames: FAM_DuplicateScan_ControlService.distinctRepoNames(members),
117
125
  members: members,
118
126
  };
119
127
  })
@@ -121,7 +129,24 @@ class FAM_DuplicateScan_ControlService {
121
129
  .sort((left, right) => right.projectCount - left.projectCount
122
130
  || right.size - left.size
123
131
  || right.maxScore - left.maxScore);
124
- const duplicateEntryCount = clusters.reduce((sum, cluster) => sum + cluster.size, 0);
132
+ // 6. Fork-pár statisztika (dup-v2): a súly itt a cluster-MÉRET (a near-scannek nincs wasted-char fogalma —
133
+ // a `wastedShare` így méret-részarány; a JSDoc-olt szemantika a FAM_DupForkPair_Util-ban). Opt-in
134
+ // szuppresszió: CSAK a kimeneti cluster-listát szűri (a felderítés ténye nem vész el — `suppressedClusters`).
135
+ const forkRows = clusters.map((cluster) => ({
136
+ projects: cluster.projects, wasted: cluster.size, repoNames: cluster.repoNames,
137
+ }));
138
+ const totalWeight = clusters.reduce((sum, cluster) => sum + cluster.size, 0);
139
+ const forkPairs = fam_dup_fork_pair_util_1.FAM_DupForkPair_Util.buildReport(forkRows, totalWeight);
140
+ let finalClusters = clusters;
141
+ let suppressedClusters = 0;
142
+ if (input.suppressForkPairs) {
143
+ const suspected = forkPairs.pairs.filter((pair) => pair.suspectedFork);
144
+ finalClusters = clusters.filter((cluster) => !fam_dup_fork_pair_util_1.FAM_DupForkPair_Util.shouldSuppress({ projects: cluster.projects, repoNames: cluster.repoNames }, suspected));
145
+ suppressedClusters = clusters.length - finalClusters.length;
146
+ }
147
+ const duplicateEntryCount = finalClusters.reduce((sum, cluster) => sum + cluster.size, 0);
148
+ // 7. Output-cap + fájl-rollup a CAP-ELT (visszaadott) clusterekből (a clusterRefs indexei konzisztensek).
149
+ const cappedClusters = finalClusters.slice(0, maxClusters);
125
150
  return {
126
151
  table: input.table,
127
152
  threshold: threshold,
@@ -129,15 +154,33 @@ class FAM_DuplicateScan_ControlService {
129
154
  scanned: scanned,
130
155
  capped: capped,
131
156
  filtered: filtered,
132
- clusterCount: clusters.length,
157
+ clusterCount: finalClusters.length,
133
158
  duplicateEntryCount: duplicateEntryCount,
134
- clusters: clusters.slice(0, maxClusters),
159
+ clusters: cappedClusters,
160
+ fileRollup: fam_dup_file_rollup_util_1.FAM_DupFileRollup_Util.build(cappedClusters),
161
+ forkPairs: forkPairs,
162
+ suppressedClusters: suppressedClusters,
163
+ coverage: await this.maybeCoverage(input),
135
164
  };
136
165
  }
166
+ /** A coverage-preflight beágyazása (`includeCoverage !== false`); a coverage-hiba NEM dönti a scant (best-effort). */
167
+ async maybeCoverage(input) {
168
+ if (input.includeCoverage === false) {
169
+ return undefined;
170
+ }
171
+ try {
172
+ return await fam_dup_coverage_control_service_1.FAM_DupCoverage_ControlService.getInstance().report(input.table);
173
+ }
174
+ catch (error) {
175
+ fsm_dynamo_1.DyFM_Log.warn(`[FAM dup-scan] coverage-preflight sikertelen (best-effort): ${error?.message}`);
176
+ return undefined;
177
+ }
178
+ }
137
179
  /** Van-e AKTÍV jelölt-szűrő az inputban (→ Mongo-prefilter + candidateIds-szűkített keresés)? Tiszta. */
138
180
  static hasCandidateFilter(input) {
139
181
  return Boolean(input.pathRegex || input.excludePathRegex || input.excludeRootRegex
140
- || input.projects?.length || input.excludeProjects?.length);
182
+ || input.projects?.length || input.excludeProjects?.length
183
+ || (input.minContentLength ?? 0) > 0);
141
184
  }
142
185
  /**
143
186
  * A jelölt-halmaz Mongo-prefilterének összeállítása az input szűrő-mezőiből. Tiszta (side-effect-mentes) →
@@ -160,6 +203,11 @@ class FAM_DuplicateScan_ControlService {
160
203
  if (input.excludeProjects?.length) {
161
204
  conditions.push({ scopePath: { $not: { $elemMatch: { layer: 'project', canonicalName: { $in: input.excludeProjects } } } } });
162
205
  }
206
+ if ((input.minContentLength ?? 0) > 0) {
207
+ // Rövid boilerplate-zaj kizárása MÁR a prefilterben (dup-v2): a content karakter-hossza (`$strLenCP`)
208
+ // legalább `minContentLength` legyen (hiányzó content → 0 hossz → kiesik).
209
+ conditions.push({ $expr: { $gte: [{ $strLenCP: { $ifNull: ['$content', ''] } }, input.minContentLength] } });
210
+ }
163
211
  return (conditions.length === 1 ? conditions[0] : { $and: conditions });
164
212
  }
165
213
  /** Egy entry projekt-neve a scopePath project-layer eleméből (nincs → undefined). Tiszta. */
@@ -170,6 +218,10 @@ class FAM_DuplicateScan_ControlService {
170
218
  static distinctProjects(members) {
171
219
  return [...new Set(members.map((member) => member.project).filter((project) => Boolean(project)))].sort();
172
220
  }
221
+ /** A cluster-tagok KÜLÖNBÖZŐ (nem-üres) repo-nevei rendezve (fork-pár evidencia — dup-v2). Tiszta. */
222
+ static distinctRepoNames(members) {
223
+ return [...new Set(members.map((member) => member.repoName).filter((repo) => Boolean(repo)))].sort();
224
+ }
173
225
  /**
174
226
  * **Tiszta union-find**: a hasonlóság-élekből összefüggő komponensek (méret ≥ 2). Minden komponens id-i
175
227
  * rendezve + a komponens score-tartománya (min/max él-score). Determinista (azonos input → azonos output).
@@ -252,6 +304,7 @@ class FAM_DuplicateScan_ControlService {
252
304
  snippet: content.replace(/\s+/g, ' ').trim().slice(0, FAM_DuplicateScan_ControlService.SNIPPET_LENGTH),
253
305
  sourceFilePath: entry.sourceFilePath,
254
306
  project: FAM_DuplicateScan_ControlService.projectOf(entry.scopePath),
307
+ repoName: entry.source?.repoName,
255
308
  });
256
309
  }
257
310
  return metaById;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_EmbeddingBootstrap_ControlService = void 0;
4
4
  const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
5
  const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
6
+ const fam_dup_audit_scheduler_control_service_1 = require("./fam-dup-audit-scheduler.control-service");
6
7
  const fam_hydration_coordinator_control_service_1 = require("./fam-hydration-coordinator.control-service");
7
8
  const fam_vector_search_control_service_1 = require("./fam-vector-search.control-service");
8
9
  const fam_scope_maintenance_control_service_1 = require("../../scope-reference/_services/fam-scope-maintenance.control-service");
@@ -56,6 +57,14 @@ class FAM_EmbeddingBootstrap_ControlService {
56
57
  catch (error) {
57
58
  fsm_dynamo_1.DyFM_Log.warn(`[FAM bootstrap] heti re-scan scheduler telepítése sikertelen: ${error?.message}`);
58
59
  }
60
+ // HETI DUP-ŐRJÁRAT (dup-v2, task #20): a scheduler telepítése UGYANITT, a hydrateAll await ELŐTT (boot-timer
61
+ // TILOS await mögé!) — a tick maga kapuzza magát (a heti re-scan lefutása + single-flight + enabled-flag).
62
+ try {
63
+ fam_dup_audit_scheduler_control_service_1.FAM_DupAuditScheduler_ControlService.getInstance().install();
64
+ }
65
+ catch (error) {
66
+ fsm_dynamo_1.DyFM_Log.warn(`[FAM bootstrap] dup-őrjárat scheduler telepítése sikertelen: ${error?.message}`);
67
+ }
59
68
  // A readiness-coordinator hidratálás-bevárás time-outja a `read.hydrationWaitMs` config-ból (a hidratálás
60
69
  // ELŐTT, hogy a `beginBoot` után érkező read-ek már a helyes időkorláttal várjanak). Best-effort (default 180s).
61
70
  try {
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_ExactDuplication_ControlService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const mongoose_1 = tslib_1.__importDefault(require("mongoose"));
6
+ const error_codes_const_1 = require("../../../_collections/error-codes.const");
7
+ const fam_error_factory_util_1 = require("../../../_collections/fam-error-factory.util");
8
+ const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
9
+ const fam_dup_fork_pair_util_1 = require("../_collections/fam-dup-fork-pair.util");
10
+ const fam_store_registry_const_1 = require("../_collections/fam-store-registry.const");
11
+ const fam_dup_coverage_control_service_1 = require("./fam-dup-coverage.control-service");
12
+ const fam_duplicate_scan_control_service_1 = require("./fam-duplicate-scan.control-service");
13
+ const fam_entry_data_service_1 = require("./fam-entry.data-service");
14
+ /**
15
+ * `FAM_ExactDuplication_ControlService` (dup-v2, task #20) — **EXACT (contentHash-azonos) duplikáció-mérő**
16
+ * capability-motor. A near-dup scannel (`FAM_DuplicateScan_ControlService`) szemben NEM vektor-alapú: egyetlen
17
+ * Mongo-aggregáció (`$group` by contentHash) a TELJES szűrt halmazon → a 300k-s codebase táron is fut, cap
18
+ * nélkül, memória-robbanás nélkül (`allowDiskUse` + korai `$project`; a `contentVector` SOSEM kerül a
19
+ * pipeline-ba). Read-only — csak felderít, nem töröl.
20
+ *
21
+ * **Szűrő-konzisztencia:** a user-szűrők (path/root/projekt) a near-dup `buildScanFilter`-rel AZONOS feltételek
22
+ * (REUSE — a két capability ugyanazt a halmazt érti a szűrő alatt, beleértve az `embeddingStatus:'completed'`
23
+ * feltételt is: a mért korpusz = a retrieval-képes korpusz).
24
+ *
25
+ * **16MB-jegyzet (`$addToSet files`):** egy csoport fájl-listája elvileg nőhet nagyra, de a reális eloszlásnál
26
+ * (egy hash tipikusan < pár száz fájl) a 16MB dokumentum-limit messze van; a top-ágon a `$slice` cap-el, a
27
+ * pairSource/multiProject ág nem viszi a `files`-t.
28
+ */
29
+ class FAM_ExactDuplication_ControlService {
30
+ static _instance;
31
+ issuer = 'FAM_ExactDuplication_ControlService';
32
+ /** Default visszaadott csoport-szám. */
33
+ static DEFAULT_MAX_GROUPS = 100;
34
+ /** A visszaadott csoport-szám hard cap-je (context-védelem). */
35
+ static HARD_MAX_GROUPS = 500;
36
+ /** Default per-csoport fájl-cap. */
37
+ static DEFAULT_MAX_FILES_PER_GROUP = 20;
38
+ /** A pár-forrás facet sor-cap-je (a pár-statisztika bounded marad extrém dup-eloszlásnál is). */
39
+ static PAIR_SOURCE_LIMIT = 50000;
40
+ static getInstance() {
41
+ if (!FAM_ExactDuplication_ControlService._instance) {
42
+ FAM_ExactDuplication_ControlService._instance = new FAM_ExactDuplication_ControlService();
43
+ }
44
+ return FAM_ExactDuplication_ControlService._instance;
45
+ }
46
+ /**
47
+ * Az exact dup-mérés futtatása (read-only). Üres tábla / 0 csoport → 0-default totals (nem dob).
48
+ * Érvénytelen user-regex → deskriptív FAM_Error (nem Mongo-mélyhiba).
49
+ */
50
+ async run(input) {
51
+ const normalized = FAM_ExactDuplication_ControlService.normalize(input);
52
+ this.assertValidRegexes(normalized);
53
+ const registryEntry = fam_store_registry_const_1.FAM_StoreRegistry_Util.getEntry(normalized.table);
54
+ if (!registryEntry) {
55
+ throw fam_error_factory_util_1.FAM_Error_Util.create({
56
+ errorCode: error_codes_const_1.FAM_ERROR_CODES.mcpDispatchUnknownCapability,
57
+ message: `A 'detect_exact_duplication' csak fő RAG-táron fut — a(z) '${normalized.table}' nem az `
58
+ + '(a `reference` helper nem vektorizált fő tár).',
59
+ issuer: this.issuer,
60
+ context: { operation: 'capability-invoke:detect_exact_duplication' },
61
+ });
62
+ }
63
+ // Egyetlen aggregáció a RAW collection-ön (allowDiskUse a nagy $group/$sort miatt).
64
+ const collection = this.entryCollection(registryEntry);
65
+ const pipeline = FAM_ExactDuplication_ControlService.buildPipeline(normalized);
66
+ const facetRows = await collection
67
+ .aggregate(pipeline, { allowDiskUse: true }).toArray();
68
+ // Coverage-preflight beágyazás (a hiányzó/elavult projekt-korpusz NE legyen néma hamis-negatív).
69
+ const coverage = normalized.includeCoverage
70
+ ? await fam_dup_coverage_control_service_1.FAM_DupCoverage_ControlService.getInstance().report(normalized.table)
71
+ : undefined;
72
+ return FAM_ExactDuplication_ControlService.toResult(facetRows[0], normalized, coverage);
73
+ }
74
+ /** A nyers input default-olása + cap-elése. Tiszta. */
75
+ static normalize(input) {
76
+ const maxGroups = Math.min(Math.max(input.maxGroups ?? FAM_ExactDuplication_ControlService.DEFAULT_MAX_GROUPS, 1), FAM_ExactDuplication_ControlService.HARD_MAX_GROUPS);
77
+ return {
78
+ table: input.table ?? fam_table_type_enum_1.FAM_Table.codebase,
79
+ pathRegex: input.pathRegex,
80
+ excludePathRegex: input.excludePathRegex,
81
+ excludeRootRegex: input.excludeRootRegex,
82
+ projects: input.projects?.length ? input.projects : undefined,
83
+ excludeProjects: input.excludeProjects?.length ? input.excludeProjects : undefined,
84
+ minProjects: Math.max(input.minProjects ?? 1, 0),
85
+ minContentLength: Math.max(input.minContentLength ?? 0, 0),
86
+ maxGroups: maxGroups,
87
+ maxFilesPerGroup: Math.max(input.maxFilesPerGroup ?? FAM_ExactDuplication_ControlService.DEFAULT_MAX_FILES_PER_GROUP, 1),
88
+ suppressForkPairs: input.suppressForkPairs === true,
89
+ includeCoverage: input.includeCoverage !== false,
90
+ };
91
+ }
92
+ /**
93
+ * A `$match` stage összeállítása: explicit `_deleted:null` (raw-path aktív-szűrő) + `contentHash` string +
94
+ * a near-dup `buildScanFilter`-rel AZONOS user-szűrő feltételek (REUSE) + opcionális `$strLenCP` hossz-kapu.
95
+ * Tiszta.
96
+ */
97
+ static buildMatchStage(input) {
98
+ const conditions = [
99
+ // RAW collection-t olvasunk (nem a Dynamo DataService-t) → a soft-delete kizárás EXPLICIT kötelező.
100
+ { _deleted: null },
101
+ { contentHash: { $type: 'string', $ne: '' } },
102
+ ];
103
+ // A user-szűrők a near-dup scan-nel AZONOS feltétel-készletből (REUSE — nincs drift a két capability közt).
104
+ const scanFilter = fam_duplicate_scan_control_service_1.FAM_DuplicateScan_ControlService.buildScanFilter({
105
+ table: input.table,
106
+ pathRegex: input.pathRegex,
107
+ excludePathRegex: input.excludePathRegex,
108
+ excludeRootRegex: input.excludeRootRegex,
109
+ projects: input.projects,
110
+ excludeProjects: input.excludeProjects,
111
+ });
112
+ const scanConditions = Array.isArray(scanFilter.$and)
113
+ ? scanFilter.$and
114
+ : [scanFilter];
115
+ conditions.push(...scanConditions);
116
+ if (input.minContentLength > 0) {
117
+ conditions.push({ $expr: { $gte: [{ $strLenCP: { $ifNull: ['$content', ''] } }, input.minContentLength] } });
118
+ }
119
+ return { $and: conditions };
120
+ }
121
+ /**
122
+ * A teljes aggregáció-pipeline (korai `$project` — a `contentVector` és a `content` szöveg NEM utazik a
123
+ * `$group`-ba, csak a hossz) + `$facet` a 4 kimeneti ággal (totals / top / pairSource / multiProject). Tiszta.
124
+ */
125
+ static buildPipeline(input) {
126
+ return [
127
+ { $match: FAM_ExactDuplication_ControlService.buildMatchStage(input) },
128
+ // Korai projekció: csak a csoportosításhoz szükséges skalárok (hossz + projekt + repo + fájl-út).
129
+ {
130
+ $project: {
131
+ contentHash: 1,
132
+ sourceFilePath: 1,
133
+ len: { $strLenCP: { $ifNull: ['$content', ''] } },
134
+ project: {
135
+ $arrayElemAt: [{
136
+ $map: {
137
+ input: {
138
+ $filter: {
139
+ input: { $ifNull: ['$scopePath', []] },
140
+ as: 'ref',
141
+ cond: { $eq: ['$$ref.layer', 'project'] },
142
+ },
143
+ },
144
+ as: 'ref',
145
+ in: '$$ref.canonicalName',
146
+ },
147
+ }, 0],
148
+ },
149
+ repoName: '$source.repoName',
150
+ },
151
+ },
152
+ {
153
+ $group: {
154
+ _id: '$contentHash',
155
+ copies: { $sum: 1 },
156
+ contentLen: { $first: '$len' },
157
+ projects: { $addToSet: '$project' },
158
+ repoNames: { $addToSet: '$repoName' },
159
+ files: { $addToSet: '$sourceFilePath' },
160
+ },
161
+ },
162
+ { $match: { copies: { $gte: 2 } } },
163
+ // A project-layer nélküli entry-k null-ja kikerül a halmazokból (a copies/wasted számít, a lefedettség nem).
164
+ {
165
+ $addFields: {
166
+ projects: { $setDifference: ['$projects', [null]] },
167
+ repoNames: { $setDifference: ['$repoNames', [null]] },
168
+ },
169
+ },
170
+ { $match: { $expr: { $gte: [{ $size: '$projects' }, input.minProjects] } } },
171
+ {
172
+ $addFields: {
173
+ wasted: { $multiply: ['$contentLen', { $subtract: ['$copies', 1] }] },
174
+ projectCount: { $size: '$projects' },
175
+ },
176
+ },
177
+ { $sort: { wasted: -1 } },
178
+ {
179
+ $facet: {
180
+ totals: [{
181
+ $group: {
182
+ _id: null,
183
+ totalGroups: { $sum: 1 },
184
+ redundantCopies: { $sum: { $subtract: ['$copies', 1] } },
185
+ wastedChars: { $sum: '$wasted' },
186
+ },
187
+ }],
188
+ top: [
189
+ { $limit: input.maxGroups },
190
+ {
191
+ $project: {
192
+ _id: 0,
193
+ contentHash: '$_id',
194
+ copies: 1,
195
+ projects: 1,
196
+ projectCount: 1,
197
+ repoNames: 1,
198
+ files: { $slice: ['$files', input.maxFilesPerGroup] },
199
+ filesTruncated: { $gt: [{ $size: '$files' }, input.maxFilesPerGroup] },
200
+ contentLen: 1,
201
+ wasted: 1,
202
+ },
203
+ },
204
+ ],
205
+ pairSource: [
206
+ { $match: { projectCount: { $gte: 2 } } },
207
+ { $project: { _id: 0, projects: 1, wasted: 1, repoNames: 1 } },
208
+ { $limit: FAM_ExactDuplication_ControlService.PAIR_SOURCE_LIMIT },
209
+ ],
210
+ multiProject: [
211
+ { $match: { projectCount: { $gte: 4 } } },
212
+ { $project: { _id: 1 } },
213
+ ],
214
+ },
215
+ },
216
+ ];
217
+ }
218
+ /**
219
+ * A `$facet` nyers kimenete → strukturált eredmény: 0-default totals (üres tábla nem dob), rendezett
220
+ * projekt-/repo-listák, fork-pár riport + (opt-in) csoport-szuppresszió. Tiszta.
221
+ */
222
+ static toResult(facetRow, input, coverage) {
223
+ const totalsRow = facetRow?.totals?.[0];
224
+ const totals = {
225
+ totalGroups: totalsRow?.totalGroups ?? 0,
226
+ redundantCopies: totalsRow?.redundantCopies ?? 0,
227
+ wastedChars: totalsRow?.wastedChars ?? 0,
228
+ };
229
+ // Fork-pár statisztika a pár-forrás ágból (a wastedShare nevezője a TELJES pazarlás).
230
+ const pairSource = (facetRow?.pairSource ?? []).map((row) => ({
231
+ projects: row.projects ?? [], wasted: row.wasted ?? 0, repoNames: row.repoNames ?? [],
232
+ }));
233
+ const forkPairs = fam_dup_fork_pair_util_1.FAM_DupForkPair_Util.buildReport(pairSource, totals.wastedChars);
234
+ // A top-csoportok normalizálása (distinct/null-mentes/rendezett projektek — a $setDifference után a
235
+ // null már kint van, itt csak a determinista rendezés a dolgunk).
236
+ let groups = (facetRow?.top ?? []).map((row) => ({
237
+ contentHash: row.contentHash,
238
+ copies: row.copies,
239
+ projects: (row.projects ?? []).filter((project) => Boolean(project)).sort(),
240
+ projectCount: row.projectCount ?? 0,
241
+ repoNames: (row.repoNames ?? []).filter((repo) => Boolean(repo)).sort(),
242
+ // Az explicit `sourceFilePath:null` entry null-t adhat a halmazba — a riportban nem hordoz információt.
243
+ files: (row.files ?? []).filter((file) => Boolean(file)),
244
+ filesTruncated: Boolean(row.filesTruncated),
245
+ contentLen: row.contentLen ?? 0,
246
+ wasted: row.wasted ?? 0,
247
+ }));
248
+ // Opt-in fork-pár szuppresszió: CSAK a visszaadott csoport-listát szűri (a totals a mért valóság marad —
249
+ // a szuppresszió láthatóság-kontroll, nem mérés-hamisítás).
250
+ let suppressedGroups = 0;
251
+ if (input.suppressForkPairs) {
252
+ const suspected = forkPairs.pairs.filter((pair) => pair.suspectedFork);
253
+ const kept = groups.filter((group) => !fam_dup_fork_pair_util_1.FAM_DupForkPair_Util.shouldSuppress({ projects: group.projects, repoNames: group.repoNames }, suspected));
254
+ suppressedGroups = groups.length - kept.length;
255
+ groups = kept;
256
+ }
257
+ return {
258
+ table: input.table,
259
+ filters: {
260
+ pathRegex: input.pathRegex,
261
+ excludePathRegex: input.excludePathRegex,
262
+ excludeRootRegex: input.excludeRootRegex,
263
+ projects: input.projects,
264
+ excludeProjects: input.excludeProjects,
265
+ minProjects: input.minProjects,
266
+ minContentLength: input.minContentLength,
267
+ },
268
+ totals: totals,
269
+ truncated: totals.totalGroups > input.maxGroups,
270
+ groups: groups,
271
+ suppressedGroups: suppressedGroups,
272
+ forkPairs: forkPairs,
273
+ multiProjectGroupHashes: (facetRow?.multiProject ?? []).map((row) => String(row._id)),
274
+ coverage: coverage,
275
+ };
276
+ }
277
+ /** Az user-regexek elő-validálása (érvénytelen minta → deskriptív hiba, nem Mongo-mélyhiba). */
278
+ assertValidRegexes(input) {
279
+ const candidates = [
280
+ { name: 'pathRegex', pattern: input.pathRegex },
281
+ { name: 'excludePathRegex', pattern: input.excludePathRegex },
282
+ { name: 'excludeRootRegex', pattern: input.excludeRootRegex },
283
+ ];
284
+ for (const candidate of candidates) {
285
+ if (!candidate.pattern) {
286
+ continue;
287
+ }
288
+ try {
289
+ new RegExp(candidate.pattern);
290
+ }
291
+ catch (error) {
292
+ throw fam_error_factory_util_1.FAM_Error_Util.create({
293
+ errorCode: error_codes_const_1.FAM_ERROR_CODES.mcpDispatchUnknownCapability,
294
+ message: `A 'detect_exact_duplication' '${candidate.name}' regexe érvénytelen: `
295
+ + `'${candidate.pattern}' — ${error?.message}`,
296
+ issuer: this.issuer,
297
+ cause: error,
298
+ context: { operation: 'capability-invoke:detect_exact_duplication' },
299
+ });
300
+ }
301
+ }
302
+ }
303
+ /** Egy fő RAG-tár mongoose-collection-je (a DS-példányosítás regisztrálja a modellt; onnan a valós collection). */
304
+ entryCollection(registryEntry) {
305
+ new fam_entry_data_service_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
306
+ return mongoose_1.default.models[registryEntry.dataParams.dataName].collection;
307
+ }
308
+ }
309
+ exports.FAM_ExactDuplication_ControlService = FAM_ExactDuplication_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_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_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; } });
@@ -42,6 +42,21 @@ Object.defineProperty(exports, "FAM_EmbeddingCost_ControlService", { enumerable:
42
42
  // near-duplikátum FELDERÍTŐ (read-only; user-direktíva 2026-06-21)
43
43
  var fam_duplicate_scan_control_service_1 = require("./_services/fam-duplicate-scan.control-service");
44
44
  Object.defineProperty(exports, "FAM_DuplicateScan_ControlService", { enumerable: true, get: function () { return fam_duplicate_scan_control_service_1.FAM_DuplicateScan_ControlService; } });
45
+ // dup-v2 (task #20): exact-duplikáció + coverage-preflight + fork-pár + fájl-rollup + heti dup-őrjárat
46
+ var fam_exact_duplication_control_service_1 = require("./_services/fam-exact-duplication.control-service");
47
+ Object.defineProperty(exports, "FAM_ExactDuplication_ControlService", { enumerable: true, get: function () { return fam_exact_duplication_control_service_1.FAM_ExactDuplication_ControlService; } });
48
+ var fam_dup_coverage_control_service_1 = require("./_services/fam-dup-coverage.control-service");
49
+ Object.defineProperty(exports, "FAM_DupCoverage_ControlService", { enumerable: true, get: function () { return fam_dup_coverage_control_service_1.FAM_DupCoverage_ControlService; } });
50
+ var fam_dup_fork_pair_util_1 = require("./_collections/fam-dup-fork-pair.util");
51
+ Object.defineProperty(exports, "FAM_DupForkPair_Util", { enumerable: true, get: function () { return fam_dup_fork_pair_util_1.FAM_DupForkPair_Util; } });
52
+ var fam_dup_file_rollup_util_1 = require("./_collections/fam-dup-file-rollup.util");
53
+ Object.defineProperty(exports, "FAM_DupFileRollup_Util", { enumerable: true, get: function () { return fam_dup_file_rollup_util_1.FAM_DupFileRollup_Util; } });
54
+ var fam_dup_audit_control_service_1 = require("./_services/fam-dup-audit.control-service");
55
+ Object.defineProperty(exports, "FAM_DupAudit_ControlService", { enumerable: true, get: function () { return fam_dup_audit_control_service_1.FAM_DupAudit_ControlService; } });
56
+ var fam_dup_audit_data_service_1 = require("./_services/fam-dup-audit.data-service");
57
+ Object.defineProperty(exports, "FAM_DupAudit_DataService", { enumerable: true, get: function () { return fam_dup_audit_data_service_1.FAM_DupAudit_DataService; } });
58
+ var fam_dup_audit_scheduler_control_service_1 = require("./_services/fam-dup-audit-scheduler.control-service");
59
+ Object.defineProperty(exports, "FAM_DupAuditScheduler_ControlService", { enumerable: true, get: function () { return fam_dup_audit_scheduler_control_service_1.FAM_DupAuditScheduler_ControlService; } });
45
60
  // boot-hook (App.postProcess)
46
61
  var fam_embedding_bootstrap_control_service_1 = require("./_services/fam-embedding-bootstrap.control-service");
47
62
  Object.defineProperty(exports, "FAM_EmbeddingBootstrap_ControlService", { enumerable: true, get: function () { return fam_embedding_bootstrap_control_service_1.FAM_EmbeddingBootstrap_ControlService; } });