@futdevpro/fdp-agent-memory 1.1.14 → 1.1.30

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 (28) hide show
  1. package/build/package.json +2 -2
  2. package/build/src/_cli/_commands/config.command.js +4 -1
  3. package/build/src/_cli/_commands/scan-projects.command.js +27 -2
  4. package/build/src/_collections/config-catalog.const.js +34 -1
  5. package/build/src/_collections/fam-config-value-coerce.util.js +63 -0
  6. package/build/src/_collections/fam-cooldown.util.js +35 -0
  7. package/build/src/_collections/title.const.js +31 -0
  8. package/build/src/_modules/capture/_services/fam-auto-capture.control-service.js +1 -1
  9. package/build/src/_modules/embedding/_services/fam-embedding-pipeline.control-service.js +1 -1
  10. package/build/src/_modules/embedding/_services/fam-entry.data-service.js +23 -1
  11. package/build/src/_modules/ingest/_collections/fam-famignore.util.js +57 -1
  12. package/build/src/_modules/ingest/_collections/fam-scan-reconcile.util.js +40 -0
  13. package/build/src/_modules/ingest/_collections/fam-secret-content.util.js +49 -0
  14. package/build/src/_modules/ingest/_collections/fam-secret-exclude.util.js +12 -0
  15. package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +40 -9
  16. package/build/src/_modules/ingest/_services/fam-scan.control-service.js +77 -11
  17. package/build/src/_modules/mcp/_collections/fam-core-tools.const.js +1 -1
  18. package/build/src/_modules/mcp/_services/fam-capability-registry.service.js +113 -33
  19. package/build/src/_modules/mcp/_services/fam-read-tool.service.js +1 -1
  20. package/build/src/_modules/migration/_services/fam-claude-mem-import.control-service.js +1 -1
  21. package/build/src/_modules/scope-reference/_collections/fam-scope-maintenance.util.js +58 -0
  22. package/build/src/_modules/scope-reference/_services/fam-scope-maintenance.control-service.js +190 -0
  23. package/build/src/_modules/scope-reference/_services/fam-scope-resolver.control-service.js +2 -2
  24. package/build/src/_modules/scope-reference/index.js +4 -1
  25. package/build/src/_routes/server/api/api.controller.js +85 -18
  26. package/build/src/_routes/server/config/config.control-service.js +2 -2
  27. package/build/src/app.server.js +30 -1
  28. package/package.json +2 -2
@@ -14,8 +14,11 @@ const fam_entry_data_model_1 = require("../../../_models/data-models/fam-entry.d
14
14
  const fam_reference_code_util_1 = require("../../../_collections/fam-reference-code.util");
15
15
  const embedding_1 = require("../../embedding");
16
16
  const scope_reference_1 = require("../../scope-reference");
17
+ const fam_cooldown_util_1 = require("../../../_collections/fam-cooldown.util");
17
18
  const fam_content_hash_util_1 = require("../_collections/fam-content-hash.util");
18
19
  const fam_git_repo_util_1 = require("../_collections/fam-git-repo.util");
20
+ const fam_scan_reconcile_util_1 = require("../_collections/fam-scan-reconcile.util");
21
+ const fam_secret_content_util_1 = require("../_collections/fam-secret-content.util");
19
22
  const fam_project_identity_util_1 = require("../_collections/fam-project-identity.util");
20
23
  const fam_scan_path_util_1 = require("../_collections/fam-scan-path.util");
21
24
  const fam_scan_progress_util_1 = require("../_collections/fam-scan-progress.util");
@@ -186,6 +189,11 @@ class FAM_Ingest_ControlService {
186
189
  // Futás-közbeni progress (descriptive user feedback): a NAGY (workspace-szintű) scan haladását
187
190
  // throttle-olt `[famIngest]` sorokkal jelezzük a konzolon (a `dryRun` NEM embeddel → ott nincs progress).
188
191
  const progressIntervalMs = await this.resolveScanNumber('scan.progressIntervalMs', canonicalScope, 2000);
192
+ // Kikapcsolható szekció-cooldown (user-FR): a NAGY scan ne terhelje túl a gépet — a batch-ek KÖZÉ
193
+ // opcionális szünet (default 0 = ki; dryRun-on sosem várunk, mert ott nincs valódi embed-munka).
194
+ const sectionCooldownMs = isDryRun
195
+ ? 0
196
+ : fam_cooldown_util_1.FAM_Cooldown_Util.resolveMs((await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.sectionCooldownMs', { scopePath: canonicalScope })).value);
189
197
  let lastProgressAt = startedAt;
190
198
  for (let i = 0; i < total; i += flowLimit) {
191
199
  const batch = discovery.routedFiles.slice(i, i + flowLimit);
@@ -212,13 +220,19 @@ class FAM_Ingest_ControlService {
212
220
  verdicts: verdicts, elapsedSeconds: (now - startedAt) / 1000,
213
221
  }));
214
222
  }
223
+ // Szekció-cooldown: a batch-ek KÖZÉ (NEM az utolsó után) — opcionális gép-tehermentesítő szünet.
224
+ if (sectionCooldownMs > 0 && processed < total) {
225
+ await fam_cooldown_util_1.FAM_Cooldown_Util.sleep(sectionCooldownMs);
226
+ }
215
227
  }
216
228
  // (4b) Törölt-fájl reconciliation (whole-file orphan) — CSAK folder/project scan (a `scan-file`
217
229
  // egyetlen fájlt céloz, nem reconciliál). A lemezről eltűnt fájlok chunkjai soft-delete-elődnek; a
218
230
  // számuk a `verdicts.deleted`-be (a per-fájl, fájlon-belüli `deleted`-ek mellé).
219
231
  if (request.operation !== 'scan-file') {
232
+ const reconcileIgnored = await this.resolveReconcileIgnored(canonicalScope);
220
233
  verdicts.deleted += await this.reconcileDeletedFiles({
221
234
  discovery: discovery, scopePath: canonicalScope, issuer: issuer, dryRun: request.dryRun ?? false,
235
+ reconcileIgnored: reconcileIgnored,
222
236
  });
223
237
  }
224
238
  // (4c) File-rendszer összefoglaló (knowledge) — generált „mi van a projektben" entry a mappastruktúrából
@@ -272,7 +286,15 @@ class FAM_Ingest_ControlService {
272
286
  async processFile(set) {
273
287
  const table = set.file.route;
274
288
  try {
275
- const content = fs.readFileSync(set.file.absolutePath, 'utf-8');
289
+ const rawContent = fs.readFileSync(set.file.absolutePath, 'utf-8');
290
+ // Tartalmi secret-redakció (user-FR): a forrásba hardcode-olt API-kulcs (sk-/AKIA/ghp_/PEM/JWT) a tárolt
291
+ // szöveg/embedding ELŐTT redaktálódik → a titok sose kerül a DB-be (a contentHash is a redaktáltból → egy
292
+ // korábban-szivárgott fájl RE-SCANKOR `modified` → auto-gyógyul). A redaktálatlan kód-kontextus megmarad.
293
+ const redaction = fam_secret_content_util_1.FAM_SecretContent_Util.redact(rawContent);
294
+ if (redaction.count > 0) {
295
+ fsm_dynamo_1.DyFM_Log.warn(`[famIngest] secret-redakció: ${redaction.count} token redaktálva (${set.file.relativePath})`);
296
+ }
297
+ const content = redaction.text;
276
298
  // FAM-REV-064 bináris-tartalom guard: a denylist-en átcsúszott, ismeretlen-kiterjesztésű bináris
277
299
  // (null-byte az első ~8KB-ban) NE kerüljön a generic chunkerbe (szemét-chunk ellen). Skip → 0 chunk.
278
300
  if (FAM_Ingest_ControlService.isLikelyBinary(content)) {
@@ -351,6 +373,10 @@ class FAM_Ingest_ControlService {
351
373
  // re-embed nélkül (a friss chunk charStart/charEnd/sor-pozíciója felülír, ha eltér).
352
374
  await set.dataService.backfillPosition(item.existingId, item.chunk.position);
353
375
  }
376
+ // Git-provenance backfill (user-FR 2026-06-22): a CSAK-metaadat frissítés — a git-provenance
377
+ // feature ELŐTT ingestelt korpusz `equal`-chunkjai is megkapják a repoUrl/repoRelativePath/
378
+ // repoBranch-et RE-EMBED NÉLKÜL (a control-service számolja, így nincs data-service↔ingest ciklus).
379
+ await set.dataService.backfillGitProvenance(item.existingId, fam_git_repo_util_1.FAM_GitRepo_Util.sourceFieldsFor(set.file.absolutePath));
354
380
  }
355
381
  catch {
356
382
  // best-effort metaadat-frissítés — a content már naprakész (equal), nem eszkaláljuk.
@@ -457,7 +483,7 @@ class FAM_Ingest_ControlService {
457
483
  const existing = await dataService.findHydratableList(filter);
458
484
  const existingEntry = existing[0];
459
485
  // Változatlan fájl-lista → NO-OP (nincs fölös re-embed).
460
- if (existingEntry && existingEntry.contentHash === contentHash) {
486
+ if (existingEntry?.contentHash === contentHash) {
461
487
  return;
462
488
  }
463
489
  const entry = new fam_entry_data_model_1.FAM_Entry({
@@ -594,15 +620,15 @@ class FAM_Ingest_ControlService {
594
620
  * így egy al-mappa scan a tágabb gyökérről ingestelt, de a saját subtree-jébe eső fájlokat is helyesen
595
621
  * reconciliálja (és kívülre sosem nyúl). Az `absolutePath` nélküli (régi, migrálatlan) entry-ket kihagyja
596
622
  * (no-migration safe). `dryRun` → csak számol. Visszaadja a reconciliált (soft-deleted) chunkok számát.
623
+ *
624
+ * **`reconcileIgnored` (user-FR 2026-06-21, default true):** a most ignore-mintával (hard/config/`.fdpfamignore`)
625
+ * kizárttá vált, de KORÁBBAN beolvasott fájlok is orphanná válnak → a régi chunkjaik kitakarítódnak (ne ragadjon
626
+ * be, amit ignore-olni akarunk). A present-set számítása a `FAM_ScanReconcile_Util.computePresentPaths`-ban (pure).
597
627
  */
598
628
  async reconcileDeletedFiles(set) {
599
- const presentPaths = new Set();
600
- for (const routed of set.discovery.routedFiles) {
601
- presentPaths.add(routed.absolutePath);
602
- }
603
- for (const skipped of set.discovery.skippedFiles) {
604
- presentPaths.add(skipped.absolutePath);
605
- }
629
+ // A present-set (lemezen megtalált fájlok). `reconcileIgnored` → a MOST ignore-mintával kizárt fájl NEM present
630
+ // (a régi chunkja orphan → kitakarítható); a többi skip (secret/méret/unrouted) present marad (FAM_ScanReconcile_Util).
631
+ const presentPaths = fam_scan_reconcile_util_1.FAM_ScanReconcile_Util.computePresentPaths(set.discovery.routedFiles, set.discovery.skippedFiles, set.reconcileIgnored);
606
632
  const leafScopeId = set.scopePath.length ? set.scopePath[set.scopePath.length - 1].scopeId : undefined;
607
633
  let reconciled = 0;
608
634
  for (const table of set.discovery.tables) {
@@ -651,6 +677,11 @@ class FAM_Ingest_ControlService {
651
677
  const value = typeof resolved.value === 'number' ? resolved.value : 20;
652
678
  return Math.min(Math.max(value, 1), 200);
653
679
  }
680
+ /** Az ignore-add reconciliation (most-kizárt fájlok kitakarítása) a config-ból (`scan.reconcileIgnored`, default true). */
681
+ async resolveReconcileIgnored(scopePath) {
682
+ const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.reconcileIgnored', { scopePath: scopePath });
683
+ return typeof resolved.value === 'boolean' ? resolved.value : true;
684
+ }
654
685
  /** Egy scan-szám-config feloldása a scope-kontextussal + fallback-kel (a `scan.estimateMsPerChunk` / `scan.progressIntervalMs`-hez). */
655
686
  async resolveScanNumber(key, scopePath, fallback) {
656
687
  const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve(key, { scopePath: scopePath });
@@ -51,16 +51,35 @@ class FAM_Scan_ControlService {
51
51
  if (famIgnore.length) {
52
52
  fsm_dynamo_1.DyFM_Log.info(`[famIngest] .fdpfamignore betöltve (${resolvedRoot}): ${famIgnore.length / 2} minta-sor`);
53
53
  }
54
- const ignorePatterns = [...baseIgnore, ...famIgnore];
54
+ // KÉT külön ignore-réteg: a `hardIgnore` (config-default `node_modules`/`.git`/`build`/`dist`/… + config +
55
+ // input-exclude) — ezt a `!`-re-include SOHA nem éleszti fel (különben a `!client/` a `client/node_modules`-t
56
+ // is visszahozná → katasztrofális walk). A `softIgnore` (a .fdpfamignore `*`/saját mintái) — EZT írhatja felül
57
+ // a negáció. Így a „minden-kivéve-X" a hard-defaultokat tiszteletben tartja (eleve-tisztán-szkennelés).
58
+ const hardIgnore = baseIgnore;
59
+ const softIgnore = famIgnore;
60
+ // Re-include (`!`) szabályok a .fdpfamignore-ból (negáció — user-FR): a fájl-szintű re-include-match (`globs`)
61
+ // + a könyvtár-prune lineage-bázisok (`bases`). Üres a scan-file-nál / ha nincs negáció.
62
+ const reinclude = set.operation === 'scan-file'
63
+ ? { globs: [], bases: [] }
64
+ : fam_famignore_util_1.FAM_FamIgnore_Util.loadReincludeRules(resolvedRoot);
55
65
  const maxFileSizeBytes = await this.resolveMaxFileSize(set.scopePath);
56
66
  const followSymlinks = await this.resolveFollowSymlinks(set.scopePath);
57
67
  const testFileWeight = await this.resolveTestFileWeight(set.scopePath);
68
+ // NESTED `.fdpfamignore` (config-kapcsolós, default ON): a bejárás közben minden al-mappa saját famignore-ja
69
+ // is érvényesül (a saját al-útjához horgonyozva). A `collected` a walk-közben FELFEDEZETT nested-mintákat
70
+ // gyűjti, hogy a MÁSODIK (classify) kör is lássa őket (a minták al-út-horgonyzottak → globálisan egyértelműek).
71
+ const nestedEnabled = set.operation === 'scan-file' ? false : await this.resolveNestedFamignore(set.scopePath);
72
+ const collected = { soft: [], reGlobs: [], reBases: [] };
58
73
  const routedFiles = [];
59
74
  const skippedFiles = [];
60
75
  // A felderített abszolút fájlok (file = 1 db; folder/project = rekurzív bejárás).
61
76
  const absoluteFiles = set.operation === 'scan-file'
62
77
  ? [resolvedRoot]
63
- : this.walkDirectory(resolvedRoot, resolvedRoot, ignorePatterns, followSymlinks);
78
+ : this.walkDirectory(resolvedRoot, resolvedRoot, hardIgnore, softIgnore, reinclude, followSymlinks, { enabled: nestedEnabled, collected: collected });
79
+ // A classify-kör effektív soft-ignore + re-include halmaza: a gyökér-szintű + MINDEN walk-közben felfedezett
80
+ // nested minta (a nested fájl-szintű skip-ekhez — a dir-prune-t a walk már elvégezte). Üres collector → no-op.
81
+ const classifySoft = collected.soft.length ? [...softIgnore, ...collected.soft] : softIgnore;
82
+ const classifyReGlobs = collected.reGlobs.length ? [...reinclude.globs, ...collected.reGlobs] : reinclude.globs;
64
83
  for (const absolutePath of absoluteFiles) {
65
84
  const relativePath = set.operation === 'scan-file'
66
85
  ? path.basename(resolvedRoot)
@@ -68,7 +87,9 @@ class FAM_Scan_ControlService {
68
87
  const routed = this.classifyFile({
69
88
  absolutePath: absolutePath,
70
89
  relativePath: relativePath,
71
- ignorePatterns: ignorePatterns,
90
+ hardIgnorePatterns: hardIgnore,
91
+ softIgnorePatterns: classifySoft,
92
+ reincludePatterns: classifyReGlobs,
72
93
  includePatterns: set.include,
73
94
  maxFileSizeBytes: maxFileSizeBytes,
74
95
  tableOverride: set.tableOverride,
@@ -103,9 +124,16 @@ class FAM_Scan_ControlService {
103
124
  if (fam_secret_exclude_util_1.FAM_SecretExclude_Util.isSecret(set.relativePath)) {
104
125
  return { ...base, route: 'skip', skipReason: 'secret-exclude (filename/path-based; content-PII scan = BACKLOG)' };
105
126
  }
106
- // (2) Ignore-patterns (dsgn-004 §6.2) — node_modules/.git/build/dist/lock/... + a config + input-exclude.
107
- if (fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.ignorePatterns)) {
108
- return { ...base, route: 'skip', skipReason: 'ignore-pattern' };
127
+ // (2a) HARD ignore (dsgn-004 §6.2) — node_modules/.git/build/dist/lock/… + a config + input-exclude.
128
+ // Ezt a `!`-re-include SOHA nem éleszti fel (a `!client/` ne hozza vissza a `client/node_modules`-t).
129
+ if (fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.hardIgnorePatterns)) {
130
+ return { ...base, route: 'skip', skipReason: 'ignore-pattern', excludedByIgnore: true };
131
+ }
132
+ // (2b) SOFT ignore (.fdpfamignore saját mintái) — EZT a `!`-RE-INCLUDE felülírja: soft-ignore-match SKIP,
133
+ // KIVÉVE ha re-include-match (negáció). Így a „minden-kivéve-X" csak a famignore-szintet vonja vissza.
134
+ if (set.softIgnorePatterns && fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.softIgnorePatterns)
135
+ && !(set.reincludePatterns && fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.reincludePatterns))) {
136
+ return { ...base, route: 'skip', skipReason: 'ignore-pattern (famignore)', excludedByIgnore: true };
109
137
  }
110
138
  // (3) Include-szűrés (ha megadva: csak az illeszkedő fájl megy tovább, dsgn-003 §3.1).
111
139
  if (set.includePatterns && set.includePatterns.length && !fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.includePatterns)) {
@@ -133,8 +161,13 @@ class FAM_Scan_ControlService {
133
161
  * KÖNYVTÁRAKAT bejárás KÖZBEN kihagyja (ne lépjen `node_modules`-ba — teljesítmény + biztonság).
134
162
  * A symlinkeket alapból NEM követi (`followSymlinks` config); a path-traversal guardot minden
135
163
  * fájlra futtatja (a feloldott valódi útvonal a gyökér alatt kell maradjon).
164
+ *
165
+ * **NESTED `.fdpfamignore` (`nested.enabled`):** minden NEM-gyökér mappánál betölti a mappa saját famignore-ját
166
+ * (a mappa al-útjához horgonyozva), MERGE-eli az örökölt soft-ignore/re-include-ba (a leszármazottak öröklik), és
167
+ * a `nested.collected`-be is gyűjti (a discover második, classify-köre fájl-szinten is alkalmazza). A gyökér
168
+ * famignore-ját a hívó (`discover`) tölti — itt csak az al-mappákét.
136
169
  */
137
- walkDirectory(currentDir, resolvedRoot, ignorePatterns, followSymlinks) {
170
+ walkDirectory(currentDir, resolvedRoot, hardIgnore, softIgnore, reinclude, followSymlinks, nested) {
138
171
  const result = [];
139
172
  let entries;
140
173
  try {
@@ -144,6 +177,24 @@ class FAM_Scan_ControlService {
144
177
  // Olvashatatlan könyvtár → kihagyjuk (a per-fájl olvasás-hiba a chunk-fázisban kezelt).
145
178
  return result;
146
179
  }
180
+ // NESTED famignore: a NEM-gyökér mappa saját `.fdpfamignore`-ja az al-útjához horgonyozva → MERGE az örököltbe
181
+ // (a leszármazottak ezt öröklik) + gyűjtés a classify-körhöz. A gyökér famignore-ját már a `discover` betöltötte.
182
+ let effSoftIgnore = softIgnore;
183
+ let effReinclude = reinclude;
184
+ if (nested.enabled && currentDir !== resolvedRoot) {
185
+ const baseDir = fam_scan_path_util_1.FAM_ScanPath_Util.toRelative(resolvedRoot, currentDir);
186
+ const nestedIgnore = fam_famignore_util_1.FAM_FamIgnore_Util.loadFromDir(currentDir, baseDir);
187
+ const nestedReinc = fam_famignore_util_1.FAM_FamIgnore_Util.loadReincludeRules(currentDir, baseDir);
188
+ if (nestedIgnore.length) {
189
+ effSoftIgnore = [...softIgnore, ...nestedIgnore];
190
+ nested.collected.soft.push(...nestedIgnore);
191
+ }
192
+ if (nestedReinc.globs.length) {
193
+ effReinclude = { globs: [...reinclude.globs, ...nestedReinc.globs], bases: [...reinclude.bases, ...nestedReinc.bases] };
194
+ nested.collected.reGlobs.push(...nestedReinc.globs);
195
+ nested.collected.reBases.push(...nestedReinc.bases);
196
+ }
197
+ }
147
198
  for (const entry of entries) {
148
199
  const entryPath = path.join(currentDir, entry.name);
149
200
  const relativePath = fam_scan_path_util_1.FAM_ScanPath_Util.toRelative(resolvedRoot, entryPath);
@@ -152,16 +203,26 @@ class FAM_Scan_ControlService {
152
203
  continue;
153
204
  }
154
205
  if (entry.isDirectory()) {
155
- // Az ignore-patterns a könyvtárat bejárás közben kihagyja (ne lépjen be).
156
- if (fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath, ignorePatterns)
157
- || fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath + '/', ignorePatterns)) {
206
+ // HARD ignore (node_modules/.git/build/…): a könyvtárat bejárás közben FELTÉTEL NÉLKÜL kihagyja
207
+ // a `!`-re-include NEM élesztheti fel (a `!client/` ne lépjen `client/node_modules`-ba). Ez tartja a
208
+ // walkot eleve-tisztán (a katasztrofális node_modules-bejárás kizárva még re-included dir alatt is).
209
+ if (fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath, hardIgnore)
210
+ || fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath + '/', hardIgnore)) {
211
+ continue;
212
+ }
213
+ // SOFT ignore (.fdpfamignore saját mintái): a mappát kihagyja — DE re-include-tudatosan: egy soft-ignore-olt
214
+ // mappát NEM vágunk le, ha re-included elem van rajta/alatta/fölötte (különben a `*` + `!client/` esetén a
215
+ // `client` szülőjét/magát levágnánk → a re-included client soha nem érhető el).
216
+ const softIgnored = fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath, effSoftIgnore)
217
+ || fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath + '/', effSoftIgnore);
218
+ if (softIgnored && !fam_famignore_util_1.FAM_FamIgnore_Util.dirOnReincludeLineage(relativePath, effReinclude.globs, effReinclude.bases)) {
158
219
  continue;
159
220
  }
160
221
  // Path-traversal guard (a feloldott valódi útvonal a gyökér alatt kell maradjon).
161
222
  if (!this.isInsideRootSafe(resolvedRoot, entryPath)) {
162
223
  continue;
163
224
  }
164
- result.push(...this.walkDirectory(entryPath, resolvedRoot, ignorePatterns, followSymlinks));
225
+ result.push(...this.walkDirectory(entryPath, resolvedRoot, hardIgnore, effSoftIgnore, effReinclude, followSymlinks, nested));
165
226
  continue;
166
227
  }
167
228
  if (entry.isFile()) {
@@ -225,6 +286,11 @@ class FAM_Scan_ControlService {
225
286
  const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.followSymlinks', { scopePath: scopePath });
226
287
  return typeof resolved.value === 'boolean' ? resolved.value : false;
227
288
  }
289
+ /** A nested (per-alkönyvtár) `.fdpfamignore` figyelembevétele a config-ból (`scan.nestedFamignore`, default true). */
290
+ async resolveNestedFamignore(scopePath) {
291
+ const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.nestedFamignore', { scopePath: scopePath });
292
+ return typeof resolved.value === 'boolean' ? resolved.value : true;
293
+ }
228
294
  /** A teszt-/spec-fájlok default súlya a config-ból (`scan.testFileWeight`, default 0.5 — FAM-REV-058). */
229
295
  async resolveTestFileWeight(scopePath) {
230
296
  const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.testFileWeight', { scopePath: scopePath });
@@ -25,7 +25,7 @@ const MAIN_TABLE_ENUM = Object.values(fam_table_type_enum_1.FAM_Table).filter((t
25
25
  /** A nyers scope-lánc array-schema (`{ layer, rawName }[]`). */
26
26
  const SCOPE_LAYER_ARRAY = {
27
27
  type: 'array',
28
- description: 'Nyers scope-lánc (root→leaf): [{ layer, rawName }] párok (pl. organization=FutDevPro, project=Adventor).',
28
+ description: 'Nyers scope-lánc (root→leaf): [{ layer: layer, rawName: rawName }] párok (pl. organization=FutDevPro, project=Adventor).',
29
29
  items: {
30
30
  type: 'object',
31
31
  properties: {
@@ -347,7 +347,7 @@ class FAM_CapabilityRegistry_Service {
347
347
  });
348
348
  this.register({
349
349
  name: 'export_authored', category: category,
350
- description: 'FEAT-007: a non-file-derived (kézzel/agent-írt, `source.type ∈ {manual, agent}`) bejegyzések '
350
+ description: 'FEAT-007: a non-file-derived (kézzel/agent-írt, `source.type ∈ {manual: manual, agent: agent}`) bejegyzések '
351
351
  + 'exportja MINDEN tárból — a re-scan-nel NEM regenerálható, pótolhatatlan kurált érték biztonsági '
352
352
  + 'mentése (a `contentVector` nélkül; a hívó/CLI JSON-fájlba írja).',
353
353
  inputSchema: this.objectSchema({}),
@@ -393,6 +393,56 @@ class FAM_CapabilityRegistry_Service {
393
393
  }),
394
394
  handler: async (args) => this.ok(await this.manageReference(this.asObject(args))),
395
395
  });
396
+ this.register({
397
+ name: 'rename_scope', category: category,
398
+ description: 'Egy scope ÁTNEVEZÉSE (user-FR): a `canonicalName`-t a denormalizált scopePath-okon (MINDEN '
399
+ + 'RAG-entry) + a reference-en + a scope-doc-on bulk-ban átírja (a scopeId VÁLTOZATLAN). A scope '
400
+ + 'megadható `scopeId`-val VAGY `canonicalName`(+`layer`)-rel. **`dryRun:true` (DEFAULT) csak az érintett-'
401
+ + 'számot adja, NEM ír**; a tényleges íráshoz `dryRun:false` explicit. Nagy átírás után szerver-restart '
402
+ + 'ajánlott a LVS-pool friss scope-metaadatához.',
403
+ inputSchema: this.objectSchema({
404
+ scopeId: { type: 'string', description: 'A scope _id-ja (egyértelmű azonosítás).' },
405
+ layer: { type: 'string', description: 'Opcionális layer a canonicalName egyértelműsítéséhez.' },
406
+ canonicalName: { type: 'string', description: 'A scope JELENLEGI neve (scopeId helyett).' },
407
+ newName: { type: 'string', description: 'Az ÚJ canonicalName — KÖTELEZŐ.' },
408
+ dryRun: { type: 'boolean', description: 'true (default): csak preview; false: ír.' },
409
+ }),
410
+ handler: async (args) => {
411
+ const input = this.asObject(args);
412
+ return this.ok(await scope_reference_1.FAM_ScopeMaintenance_ControlService.getInstance().renameScope({
413
+ scopeId: this.asString(input.scopeId),
414
+ layer: this.asString(input.layer),
415
+ canonicalName: this.asString(input.canonicalName),
416
+ newName: this.asString(input.newName) ?? '',
417
+ dryRun: input.dryRun !== false,
418
+ }));
419
+ },
420
+ });
421
+ this.register({
422
+ name: 'merge_scopes', category: category,
423
+ description: 'Két scope ÖSSZEOLVASZTÁSA (user-FR): a `from` scope MINDEN entry-je az `into` scope-ra mutat '
424
+ + '(scopePath + reference átírva), majd a `from` scope soft-delete (az `into` MARAD). from/into megadható '
425
+ + '`...ScopeId`-val VAGY `...Name`(+`...Layer`)-rel. **`dryRun:true` (DEFAULT) csak az érintett-számot, NEM '
426
+ + 'ír**; `dryRun:false` ír. NEM dedup-ol (a content-hash-összevonás a `deduplicate_entries`). Nagy merge '
427
+ + 'után szerver-restart ajánlott.',
428
+ inputSchema: this.objectSchema({
429
+ fromScopeId: { type: 'string', description: 'A FORRÁS scope _id-ja.' },
430
+ fromName: { type: 'string', description: 'A forrás scope neve (scopeId helyett).' },
431
+ fromLayer: { type: 'string', description: 'Opcionális forrás-layer az egyértelműsítéshez.' },
432
+ intoScopeId: { type: 'string', description: 'A CÉL scope _id-ja (ebbe olvad).' },
433
+ intoName: { type: 'string', description: 'A cél scope neve (scopeId helyett).' },
434
+ intoLayer: { type: 'string', description: 'Opcionális cél-layer az egyértelműsítéshez.' },
435
+ dryRun: { type: 'boolean', description: 'true (default): csak preview; false: ír.' },
436
+ }),
437
+ handler: async (args) => {
438
+ const input = this.asObject(args);
439
+ return this.ok(await scope_reference_1.FAM_ScopeMaintenance_ControlService.getInstance().mergeScopes({
440
+ from: { scopeId: this.asString(input.fromScopeId), canonicalName: this.asString(input.fromName), layer: this.asString(input.fromLayer) },
441
+ into: { scopeId: this.asString(input.intoScopeId), canonicalName: this.asString(input.intoName), layer: this.asString(input.intoLayer) },
442
+ dryRun: input.dryRun !== false,
443
+ }));
444
+ },
445
+ });
396
446
  // repair_scope_links / deduplicate_entries (MP-12) — DRY-RUN-ONLY MVP1 (safe). A destruktív
397
447
  // write-ág (entry-mutáció / soft-delete-összevonás) BACKLOG: vak destruktív bulk-op TILOS
398
448
  // (dsgn-003 §4.2), ezért a `dryRun:false` kérés clear "BACKLOG" hibát ad — a `dryRun:true` (default)
@@ -457,7 +507,7 @@ class FAM_CapabilityRegistry_Service {
457
507
  this.register({
458
508
  name: 'import_claude_mem', category: category,
459
509
  description: 'A claude-mem migráció tényleges beírása egy import-batch (batchId) alatt: dedup-skip + '
460
- + 'map + persist + embed. Output: { ok, batchId, ingested, skipped, failed, byTable, status } (dsgn-009 §5.3).',
510
+ + 'map + persist + embed. Output: { ok: ok, batchId: batchId, ingested: ingested, skipped: skipped, failed: failed, byTable: byTable, status: status } (dsgn-009 §5.3).',
461
511
  inputSchema: this.importMapSchema(), handler: runHandler,
462
512
  });
463
513
  this.register({
@@ -556,7 +606,10 @@ class FAM_CapabilityRegistry_Service {
556
606
  // =========================================================================
557
607
  // Wired handler-helpers (delegálás az engine-ekre)
558
608
  // =========================================================================
559
- /** Per-tár statisztika (aktív elem-szám + embedding-státusz bontás) — MP-1. */
609
+ /**
610
+ * Per-tár statisztika (aktív elem-szám + embedding-státusz bontás) — MP-1.
611
+ * FIX (OOM): `countDocuments` + `$group`-aggregáció — NEM tölti a doc-okat memóriába (a 126k-vektoros load OOM-ozott).
612
+ */
560
613
  async tableStats(table) {
561
614
  const tables = table ? [table] : MAIN_TABLES;
562
615
  const result = [];
@@ -565,14 +618,22 @@ class FAM_CapabilityRegistry_Service {
565
618
  if (!registryEntry) {
566
619
  continue;
567
620
  }
568
- const dataService = new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
569
- const entries = await dataService.findHydratableList({});
621
+ const collection = this.entryCollection(registryEntry);
622
+ const count = await collection.countDocuments({});
570
623
  const byStatus = { pending: 0, completed: 0, error: 0 };
571
- for (const entry of entries) {
572
- const status = entry.embeddingStatus ?? 'pending';
573
- byStatus[status] = (byStatus[status] ?? 0) + 1;
624
+ const grouped = await collection
625
+ .aggregate([{ $group: { _id: '$embeddingStatus', n: { $sum: 1 } } }])
626
+ .toArray();
627
+ for (const group of grouped) {
628
+ const status = group._id ?? 'pending';
629
+ if (status in byStatus) {
630
+ byStatus[status] += group.n;
631
+ }
632
+ else {
633
+ byStatus.pending += group.n;
634
+ }
574
635
  }
575
- result.push({ table: target, count: entries.length, embeddingStatus: byStatus });
636
+ result.push({ table: target, count: count, embeddingStatus: byStatus });
576
637
  }
577
638
  return table ? result[0] ?? { table: table, count: 0 } : result;
578
639
  }
@@ -618,7 +679,7 @@ class FAM_CapabilityRegistry_Service {
618
679
  /** Egy env-változó jelenlét-ellenőrzése (`treatDefaultAsPresent`: van builtin default, ezért sosem hiányzik). */
619
680
  checkEnv(name, hint, present, missing, treatDefaultAsPresent = false) {
620
681
  const value = process.env[name];
621
- if ((value && value.trim().length) || treatDefaultAsPresent) {
682
+ if ((value?.trim().length) || treatDefaultAsPresent) {
622
683
  present.push(name);
623
684
  return;
624
685
  }
@@ -750,11 +811,10 @@ class FAM_CapabilityRegistry_Service {
750
811
  */
751
812
  async storageUsage() {
752
813
  const collections = [];
753
- // Fő RAG-tárak (a store-registry-n át, a per-tár dataParams-szal).
814
+ // Fő RAG-tárak — FIX (OOM): `countDocuments` (NEM a doc-ok memóriába töltése; a 126k-vektoros load OOM-ozott).
754
815
  for (const registryEntry of embedding_1.FAM_STORE_REGISTRY) {
755
- const dataService = new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
756
- const entries = await dataService.findHydratableList({});
757
- collections.push({ collection: this.realCollectionName(registryEntry.dataParams.dataName), activeDocs: entries.length });
816
+ const activeDocs = await this.entryCollection(registryEntry).countDocuments({});
817
+ collections.push({ collection: this.realCollectionName(registryEntry.dataParams.dataName), activeDocs: activeDocs });
758
818
  }
759
819
  // Helper collection-ök (reference / scope / ingest-run / config) — a DS-példányosítás regisztrálja a modellt.
760
820
  const reference_DS = new scope_reference_1.FAM_Reference_DataService({ issuer: this.issuer });
@@ -865,21 +925,24 @@ class FAM_CapabilityRegistry_Service {
865
925
  const tables = table ? [table] : MAIN_TABLES;
866
926
  const result = [];
867
927
  for (const target of tables) {
868
- const entries = await this.loadTableEntries(target);
869
- const kindCounts = new Map();
870
- const tagCounts = new Map();
871
- for (const entry of entries) {
872
- if (entry.kind) {
873
- kindCounts.set(entry.kind, (kindCounts.get(entry.kind) ?? 0) + 1);
874
- }
875
- for (const tag of entry.tags ?? []) {
876
- tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
877
- }
928
+ const registryEntry = embedding_1.FAM_STORE_REGISTRY.find((entry) => entry.table === target);
929
+ if (!registryEntry) {
930
+ continue;
878
931
  }
932
+ // FIX (OOM): Mongo-aggregáció ($group kind / $unwind+$group tag) — NEM tölti a doc-okat memóriába.
933
+ const collection = this.entryCollection(registryEntry);
934
+ const kindAgg = await collection
935
+ .aggregate([
936
+ { $match: { kind: { $ne: null } } }, { $group: { _id: '$kind', n: { $sum: 1 } } }, { $sort: { n: -1 } },
937
+ ]).toArray();
938
+ const tagAgg = await collection
939
+ .aggregate([
940
+ { $unwind: '$tags' }, { $group: { _id: '$tags', n: { $sum: 1 } } }, { $sort: { n: -1 } },
941
+ ]).toArray();
879
942
  result.push({
880
943
  table: target,
881
- kinds: this.sortedCounts(kindCounts).map((entry) => ({ kind: entry.label, count: entry.count })),
882
- tags: this.sortedCounts(tagCounts).map((entry) => ({ tag: entry.label, count: entry.count })),
944
+ kinds: kindAgg.filter((group) => group._id).map((group) => ({ kind: group._id, count: group.n })),
945
+ tags: tagAgg.filter((group) => group._id).map((group) => ({ tag: group._id, count: group.n })),
883
946
  });
884
947
  }
885
948
  return result;
@@ -890,8 +953,17 @@ class FAM_CapabilityRegistry_Service {
890
953
  const tables = table ? [table] : MAIN_TABLES;
891
954
  const collected = [];
892
955
  for (const target of tables) {
893
- const entries = await this.loadTableEntries(target);
894
- for (const entry of entries) {
956
+ const registryEntry = embedding_1.FAM_STORE_REGISTRY.find((entry) => entry.table === target);
957
+ if (!registryEntry) {
958
+ continue;
959
+ }
960
+ // FIX (OOM): per-tár BOUNDED query (`sort __created` + `limit`), NEM a teljes tár betöltése N elemért.
961
+ const docs = await this.entryCollection(registryEntry)
962
+ .find({}, { projection: { kind: 1, tags: 1, __created: 1, content: 1 } })
963
+ .sort({ __created: -1 })
964
+ .limit(cappedLimit)
965
+ .toArray();
966
+ for (const entry of docs) {
895
967
  if (!entry._id) {
896
968
  continue;
897
969
  }
@@ -1083,14 +1155,22 @@ class FAM_CapabilityRegistry_Service {
1083
1155
  // =========================================================================
1084
1156
  // MP-12 közös entry-helperek (lazy DataService — eager-resolve elkerülés)
1085
1157
  // =========================================================================
1086
- /** Egy fő tár aktív entry-i (a per-tár dataParams-szal). Nem-fő/ismeretlen tár → üres. */
1158
+ /**
1159
+ * Egy fő tár aktív entry-i (a per-tár dataParams-szal). Nem-fő/ismeretlen tár → üres.
1160
+ * FIX (OOM): a nagy `contentVector` (2560 float/doc) PROJEKCIÓVAL KIHAGYVA — a fogyasztók (kinds/recent/repair/
1161
+ * dedup) NEM használják; e nélkül a teljes-tár betöltés ~20×-kal kisebb (a 126k vektor a heap-OOM forrása volt).
1162
+ */
1087
1163
  async loadTableEntries(table) {
1088
1164
  const registryEntry = embedding_1.FAM_STORE_REGISTRY.find((entry) => entry.table === table);
1089
1165
  if (!registryEntry) {
1090
1166
  return [];
1091
1167
  }
1092
- const dataService = new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
1093
- return dataService.findHydratableList({});
1168
+ return this.entryCollection(registryEntry).find({}, { projection: { contentVector: 0 } }).toArray();
1169
+ }
1170
+ /** Egy fő RAG-tár mongoose-collection-je (a DS-példányosítás regisztrálja a modellt; onnan a valós collection). */
1171
+ entryCollection(registryEntry) {
1172
+ new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
1173
+ return mongoose_1.default.models[registryEntry.dataParams.dataName].collection;
1094
1174
  }
1095
1175
  /** Egy entry feloldása `id`-ből (a megadott tárból, vagy minden fő tárból). Nincs → `FAM-MCP-DISPATCH-002`. */
1096
1176
  async requireEntryById(id, table, capability) {
@@ -1110,7 +1190,7 @@ class FAM_CapabilityRegistry_Service {
1110
1190
  }
1111
1191
  const dataService = new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
1112
1192
  const entry = await dataService.findData({ _id: id }, true);
1113
- if (entry && entry._id) {
1193
+ if (entry?._id) {
1114
1194
  return entry;
1115
1195
  }
1116
1196
  }
@@ -1190,7 +1270,7 @@ class FAM_CapabilityRegistry_Service {
1190
1270
  scopeLayerArraySchema() {
1191
1271
  return {
1192
1272
  type: 'array',
1193
- description: 'Nyers scope-lánc (root→leaf): [{ layer, rawName }] párok.',
1273
+ description: 'Nyers scope-lánc (root→leaf): [{ layer: layer, rawName: rawName }] párok.',
1194
1274
  items: this.objectSchema({
1195
1275
  layer: { type: 'string', description: 'A dinamikus layer-név (pl. organization/project).' },
1196
1276
  rawName: { type: 'string', description: 'A nyers (feloldatlan) scope-név.' },
@@ -98,7 +98,7 @@ class FAM_ReadTool_Service {
98
98
  throw fam_error_factory_util_1.FAM_Error_Util.create({
99
99
  errorCode: error_codes_const_1.FAM_ERROR_CODES.valReadEmptyOrReference,
100
100
  message: 'A `read` legalább egy query-t igényel (`queries[]`). Adj meg legalább egy '
101
- + '{ tables, query } blokkot.',
101
+ + '{ tables: tables, query: query } blokkot.',
102
102
  issuer: this.issuer,
103
103
  context: { operation: 'read-validate' },
104
104
  });
@@ -417,7 +417,7 @@ class FAM_ClaudeMemImport_ControlService {
417
417
  }
418
418
  const overrideKey = `project:${record.project}`;
419
419
  const override = request.overrides?.scope?.[overrideKey];
420
- if (override && override.scopePath.length) {
420
+ if (override?.scopePath.length) {
421
421
  return override.scopePath.map((layer) => ({ layer: layer.layer, rawName: layer.rawName }));
422
422
  }
423
423
  return [{ layer: PROJECT_SCOPE_LAYER, rawName: record.project }];
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_ScopeMaintenance_Util = void 0;
4
+ /**
5
+ * `FAM_ScopeMaintenance_Util` (user-FR 2026-06-22) — a **scope rename/merge** Mongo-update-specjeinek
6
+ * TISZTA (pure) felépítője. A scope-azonosító a denormalizált `scopePath[]`-en (minden RAG-entry) ÉS a
7
+ * reference `canonicalScopeRef`-jén (egyetlen objektum) szerepel; a rename/merge ezeket írja át bulk-ban.
8
+ *
9
+ * **Rename** = a `scopeId`-hez tartozó `canonicalName` átírása (a scopeId marad). **Merge** = a `from`
10
+ * scopeId minden előfordulása az `into` scope `{scopeId, layer, canonicalName}`-jére cserélve (majd a
11
+ * hívó soft-delete-eli a `from` scope-ot). A spec-építés determinisztikus + külön tesztelhető; az
12
+ * alkalmazás (mongoose `updateMany`) a control-service-ben.
13
+ */
14
+ class FAM_ScopeMaintenance_Util {
15
+ /** RENAME — entry-collection (`scopePath[]`): a `scopeId`-elem `canonicalName`-jét `newName`-re. */
16
+ static renameEntryUpdate(scopeId, newName) {
17
+ return {
18
+ filter: { 'scopePath.scopeId': scopeId },
19
+ update: { $set: { 'scopePath.$[el].canonicalName': newName } },
20
+ arrayFilters: [{ 'el.scopeId': scopeId }],
21
+ };
22
+ }
23
+ /** MERGE — entry-collection (`scopePath[]`): a `fromId`-elemet az `into` ref `{scopeId,layer,canonicalName}`-jére. */
24
+ static mergeEntryUpdate(fromId, into) {
25
+ return {
26
+ filter: { 'scopePath.scopeId': fromId },
27
+ update: {
28
+ $set: {
29
+ 'scopePath.$[el].scopeId': into.scopeId,
30
+ 'scopePath.$[el].layer': into.layer,
31
+ 'scopePath.$[el].canonicalName': into.canonicalName,
32
+ },
33
+ },
34
+ arrayFilters: [{ 'el.scopeId': fromId }],
35
+ };
36
+ }
37
+ /** RENAME — reference (`canonicalScopeRef`, egyetlen objektum): a `canonicalName` átírása. */
38
+ static renameReferenceUpdate(scopeId, newName) {
39
+ return {
40
+ filter: { 'canonicalScopeRef.scopeId': scopeId },
41
+ update: { $set: { 'canonicalScopeRef.canonicalName': newName } },
42
+ };
43
+ }
44
+ /** MERGE — reference (`canonicalScopeRef`): a `from` ref cseréje az `into`-ra. */
45
+ static mergeReferenceUpdate(fromId, into) {
46
+ return {
47
+ filter: { 'canonicalScopeRef.scopeId': fromId },
48
+ update: {
49
+ $set: {
50
+ 'canonicalScopeRef.scopeId': into.scopeId,
51
+ 'canonicalScopeRef.layer': into.layer,
52
+ 'canonicalScopeRef.canonicalName': into.canonicalName,
53
+ },
54
+ },
55
+ };
56
+ }
57
+ }
58
+ exports.FAM_ScopeMaintenance_Util = FAM_ScopeMaintenance_Util;