@futdevpro/fdp-agent-memory 1.1.21 → 1.1.112

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 (89) hide show
  1. package/build/package.json +2 -2
  2. package/build/src/_cli/_commands/find-duplicates.command.js +41 -16
  3. package/build/src/_cli/_commands/scan-projects.command.js +113 -113
  4. package/build/src/_cli/_commands/scan.command.js +2 -0
  5. package/build/src/_cli/_commands/serve.command.js +110 -10
  6. package/build/src/_cli/_services/fam-client.service.js +25 -1
  7. package/build/src/_cli/register-commands.js +28 -0
  8. package/build/src/_collections/config-catalog.const.js +152 -33
  9. package/build/src/_collections/error-codes.const.js +2 -0
  10. package/build/src/_collections/fam-console.util.js +38 -260
  11. package/build/src/_collections/fam-db-models.const.js +2 -0
  12. package/build/src/_collections/fam-entry-bootstrap.util.js +20 -6
  13. package/build/src/_collections/fam-error-context.util.js +1 -0
  14. package/build/src/_collections/fam-mcp-bridge.util.js +15 -1
  15. package/build/src/_collections/fam-operation-queue.service.js +85 -0
  16. package/build/src/_collections/fam-project-discovery.util.js +148 -0
  17. package/build/src/_collections/fam-request-origin.util.js +41 -0
  18. package/build/src/_collections/fam-retry.util.js +48 -0
  19. package/build/src/_collections/fam-scan-progress-sink.service.js +26 -0
  20. package/build/src/_collections/title.const.js +26 -26
  21. package/build/src/_enums/fam-rule-scope.type-enum.js +25 -0
  22. package/build/src/_models/data-models/fam-entry.data-model.js +29 -0
  23. package/build/src/_models/data-models/fam-memory.data-model.js +5 -0
  24. package/build/src/_models/data-models/fam-rules.data-model.js +24 -0
  25. package/build/src/_models/data-models/fam-scan-job.data-model.js +73 -0
  26. package/build/src/_models/data-models/fam-scope.data-model.js +9 -0
  27. package/build/src/_modules/capture/_services/fam-auto-capture.control-service.js +1 -1
  28. package/build/src/_modules/embedding/_services/fam-dedup-warn.control-service.js +40 -0
  29. package/build/src/_modules/embedding/_services/fam-duplicate-scan.control-service.js +74 -16
  30. package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +50 -0
  31. package/build/src/_modules/embedding/_services/fam-embedding-pipeline.control-service.js +32 -6
  32. package/build/src/_modules/embedding/_services/fam-embedding.control-service.js +142 -9
  33. package/build/src/_modules/embedding/_services/fam-entry.data-service.js +32 -1
  34. package/build/src/_modules/embedding/_services/fam-hydration-coordinator.control-service.js +159 -0
  35. package/build/src/_modules/embedding/_services/fam-lmstudio-embedding.provider.js +41 -4
  36. package/build/src/_modules/embedding/_services/fam-vector-search.control-service.js +67 -12
  37. package/build/src/_modules/embedding/index.js +5 -1
  38. package/build/src/_modules/ingest/_collections/fam-file-routing.util.js +10 -0
  39. package/build/src/_modules/ingest/_collections/fam-frontmatter.util.js +134 -0
  40. package/build/src/_modules/ingest/_collections/fam-project-identity.util.js +30 -0
  41. package/build/src/_modules/ingest/_collections/fam-scan-progress.util.js +5 -2
  42. package/build/src/_modules/ingest/_collections/fam-scan-summary.util.js +55 -29
  43. package/build/src/_modules/ingest/_collections/fam-split-chunker.util.js +139 -0
  44. package/build/src/_modules/ingest/_models/interfaces/fam-scan-job.interface.js +2 -0
  45. package/build/src/_modules/ingest/_services/fam-chunker.control-service.js +10 -0
  46. package/build/src/_modules/ingest/_services/fam-delta-compare.util.js +68 -1
  47. package/build/src/_modules/ingest/_services/fam-ingest-run.data-service.js +1 -1
  48. package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +177 -37
  49. package/build/src/_modules/ingest/_services/fam-scan-job.control-service.js +429 -0
  50. package/build/src/_modules/ingest/_services/fam-scan-job.data-service.js +34 -0
  51. package/build/src/_modules/ingest/_services/fam-scan.control-service.js +6 -4
  52. package/build/src/_modules/ingest/index.js +6 -1
  53. package/build/src/_modules/mcp/_collections/fam-core-tools.const.js +1 -1
  54. package/build/src/_modules/mcp/_services/fam-capability-registry.service.js +264 -41
  55. package/build/src/_modules/mcp/_services/fam-read-tool.service.js +71 -1
  56. package/build/src/_modules/mcp/_services/fam-write-tool.service.js +36 -0
  57. package/build/src/_modules/migration/_services/fam-claude-mem-import.control-service.js +1 -1
  58. package/build/src/_modules/retrieval/_collections/fam-lexical-match.util.js +69 -0
  59. package/build/src/_modules/retrieval/_collections/fam-memory-activation.util.js +66 -0
  60. package/build/src/_modules/retrieval/_collections/fam-rule-injection.util.js +125 -0
  61. package/build/src/_modules/retrieval/_collections/fam-rule-propagation.util.js +95 -0
  62. package/build/src/_modules/retrieval/_collections/fam-rule-scope-context.util.js +74 -0
  63. package/build/src/_modules/retrieval/_services/fam-global-rule.control-service.js +82 -0
  64. package/build/src/_modules/retrieval/_services/fam-memory-cold-search.control-service.js +121 -0
  65. package/build/src/_modules/retrieval/_services/fam-memory-dormancy.control-service.js +99 -0
  66. package/build/src/_modules/retrieval/_services/fam-memory-reactivation.control-service.js +68 -0
  67. package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +15 -2
  68. package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +197 -5
  69. package/build/src/_modules/retrieval/_services/fam-rule-injection.control-service.js +102 -0
  70. package/build/src/_modules/retrieval/index.js +12 -1
  71. package/build/src/_modules/scope-reference/_collections/fam-scope-maintenance.util.js +134 -0
  72. package/build/src/_modules/scope-reference/_services/fam-scope-maintenance.control-service.js +423 -0
  73. package/build/src/_modules/scope-reference/_services/fam-scope-resolver.control-service.js +2 -2
  74. package/build/src/_modules/scope-reference/_services/fam-scope.data-service.js +25 -0
  75. package/build/src/_modules/scope-reference/index.js +4 -1
  76. package/build/src/_routes/server/api/api.controller.js +324 -23
  77. package/build/src/_routes/server/api/fam-doctor.control-service.js +156 -0
  78. package/build/src/_routes/server/config/config.control-service.js +2 -2
  79. package/build/src/app.server.js +29 -27
  80. package/client-dist/{chunk-YXHWCJ5O.js → chunk-GF3FJP7E.js} +71 -71
  81. package/client-dist/chunk-NMUCMQY6.js +1 -0
  82. package/client-dist/favicon.ico +0 -0
  83. package/client-dist/index.html +3 -3
  84. package/client-dist/main-YK4YIVAQ.js +2 -0
  85. package/package.json +2 -2
  86. package/build/src/_cli/_collections/fam-project-discovery.util.js +0 -98
  87. package/build/src/_collections/fam-heap-guard.control-service.js +0 -176
  88. package/client-dist/chunk-I77GXVAQ.js +0 -1
  89. package/client-dist/main-PJPEDVJT.js +0 -2
@@ -310,6 +310,51 @@ class FAM_CapabilityRegistry_Service {
310
310
  return this.ok(await this.getActiveRules(this.asScopeLayers(input.scopePath), input.includeContent !== false));
311
311
  },
312
312
  });
313
+ // detect_code_duplication (user-FR 2026-07-15 — fleet-duplikáció → bedrock-konszolidáció): a meglévő
314
+ // near-duplikátum motor (FAM_DuplicateScan_ControlService) CÉLZOTT, cross-project módban. A szűrők
315
+ // Mongo-prefilterként szűkítik a jelölt-halmazt (a 286k-s codebase táron is futtatható célzott scan),
316
+ // a `minProjects` (default 2) a cross-project clusterekre fókuszál. Read-only.
317
+ this.register({
318
+ name: 'detect_code_duplication', category: category,
319
+ description: 'CROSS-PROJECT kód-duplikáció FELDERÍTÉS (read-only; user-FR 2026-07-15): embedding-'
320
+ + 'similarity near-duplikátum clusterek egy tárban (default `codebase`), projekt-lefedettséggel '
321
+ + '(cluster → érintett projektek + fájlok + snippet) — a bedrock-konszolidáció döntés-előkészítője. '
322
+ + 'A szűrők (pathRegex/excludePathRegex/excludeRootRegex/projects/excludeProjects) Mongo-prefilterként '
323
+ + 'szűkítik a jelölt-halmazt MÉG a vektor-számítás előtt → a nagy táron CÉLZOTT scan-t futtass '
324
+ + '(pl. pathRegex egy fájl-mintára + excludeRootRegex a STALE/fork zajra). A `minProjects` (default 2) '
325
+ + 'csak a legalább ennyi KÜLÖNBÖZŐ projektet átfedő clustereket adja. A `capped` jelzi, ha a '
326
+ + 'jelölt-halmaz a `maxEntries`-nél nagyobb (emeld, vagy szűkíts tovább). A `threshold` default 0.93 '
327
+ + '(a drift-elt másolatok elkapásához lazább, mint a general 0.95).',
328
+ inputSchema: this.objectSchema({
329
+ table: this.tableSchema(false),
330
+ threshold: { type: 'number', description: 'Koszinusz-küszöb 0..1 (default 0.93).' },
331
+ neighbors: { type: 'number', description: 'Szomszéd-szám/entry (default 10).' },
332
+ maxEntries: { type: 'number', description: 'Jelölt-cap az O(n²) ellen (default 3000; a capped jelzi, ha több a jelölt).' },
333
+ maxClusters: { type: 'number', description: 'A visszaadott clusterek max száma (default 100).' },
334
+ pathRegex: { type: 'string', description: 'A sourceFilePath include-regexe (case-insensitive; pl. "\\\\.(ts|scss)$").' },
335
+ excludePathRegex: { type: 'string', description: 'A sourceFilePath exclude-regexe (pl. "\\\\.spec\\\\.ts$|node_modules").' },
336
+ excludeRootRegex: { type: 'string', description: 'A source.root exclude-regexe (pl. "STALE-projects").' },
337
+ projects: { type: 'array', items: { type: 'string' }, description: 'Csak ezek a projektek (scopePath project-layer canonicalName).' },
338
+ excludeProjects: { type: 'array', items: { type: 'string' }, description: 'Kizárt projektek (fork-zaj — pl. ccap — elnyomása).' },
339
+ minProjects: { type: 'number', description: 'Csak a legalább ennyi KÜLÖNBÖZŐ projektet átfedő clusterek (default 2).' },
340
+ }),
341
+ handler: async (args) => {
342
+ const input = this.asObject(args);
343
+ return this.ok(await embedding_1.FAM_DuplicateScan_ControlService.getInstance().scan({
344
+ table: (this.optionalTable(input.table) ?? fam_table_type_enum_1.FAM_Table.codebase),
345
+ threshold: this.asNumber(input.threshold) ?? 0.93,
346
+ neighbors: this.asNumber(input.neighbors),
347
+ maxEntries: this.asNumber(input.maxEntries),
348
+ maxClusters: this.asNumber(input.maxClusters),
349
+ pathRegex: this.asString(input.pathRegex),
350
+ excludePathRegex: this.asString(input.excludePathRegex),
351
+ excludeRootRegex: this.asString(input.excludeRootRegex),
352
+ projects: this.asStringArray(input.projects),
353
+ excludeProjects: this.asStringArray(input.excludeProjects),
354
+ minProjects: this.asNumber(input.minProjects) ?? 2,
355
+ }));
356
+ },
357
+ });
313
358
  }
314
359
  /** maintenance → MP-2 (re-embed) + MP-3 (rebuild reference index). */
315
360
  registerMaintenance() {
@@ -347,7 +392,7 @@ class FAM_CapabilityRegistry_Service {
347
392
  });
348
393
  this.register({
349
394
  name: 'export_authored', category: category,
350
- description: 'FEAT-007: a non-file-derived (kézzel/agent-írt, `source.type ∈ {manual, agent}`) bejegyzések '
395
+ description: 'FEAT-007: a non-file-derived (kézzel/agent-írt, `source.type ∈ {manual: manual, agent: agent}`) bejegyzések '
351
396
  + 'exportja MINDEN tárból — a re-scan-nel NEM regenerálható, pótolhatatlan kurált érték biztonsági '
352
397
  + 'mentése (a `contentVector` nélkül; a hívó/CLI JSON-fájlba írja).',
353
398
  inputSchema: this.objectSchema({}),
@@ -393,6 +438,138 @@ class FAM_CapabilityRegistry_Service {
393
438
  }),
394
439
  handler: async (args) => this.ok(await this.manageReference(this.asObject(args))),
395
440
  });
441
+ this.register({
442
+ name: 'rename_scope', category: category,
443
+ description: 'Egy scope ÁTNEVEZÉSE (user-FR): a `canonicalName`-t a denormalizált scopePath-okon (MINDEN '
444
+ + 'RAG-entry) + a reference-en + a scope-doc-on bulk-ban átírja (a scopeId VÁLTOZATLAN). A scope '
445
+ + 'megadható `scopeId`-val VAGY `canonicalName`(+`layer`)-rel. **`dryRun:true` (DEFAULT) csak az érintett-'
446
+ + 'számot adja, NEM ír**; a tényleges íráshoz `dryRun:false` explicit. Nagy átírás után szerver-restart '
447
+ + 'ajánlott a LVS-pool friss scope-metaadatához.',
448
+ inputSchema: this.objectSchema({
449
+ scopeId: { type: 'string', description: 'A scope _id-ja (egyértelmű azonosítás).' },
450
+ layer: { type: 'string', description: 'Opcionális layer a canonicalName egyértelműsítéséhez.' },
451
+ canonicalName: { type: 'string', description: 'A scope JELENLEGI neve (scopeId helyett).' },
452
+ newName: { type: 'string', description: 'Az ÚJ canonicalName — KÖTELEZŐ.' },
453
+ dryRun: { type: 'boolean', description: 'true (default): csak preview; false: ír.' },
454
+ }),
455
+ handler: async (args) => {
456
+ const input = this.asObject(args);
457
+ return this.ok(await scope_reference_1.FAM_ScopeMaintenance_ControlService.getInstance().renameScope({
458
+ scopeId: this.asString(input.scopeId),
459
+ layer: this.asString(input.layer),
460
+ canonicalName: this.asString(input.canonicalName),
461
+ newName: this.asString(input.newName) ?? '',
462
+ dryRun: input.dryRun !== false,
463
+ }));
464
+ },
465
+ });
466
+ this.register({
467
+ name: 'merge_scopes', category: category,
468
+ description: 'Két scope ÖSSZEOLVASZTÁSA (user-FR): a `from` scope MINDEN entry-je az `into` scope-ra mutat '
469
+ + '(scopePath + reference átírva), majd a `from` scope soft-delete (az `into` MARAD). from/into megadható '
470
+ + '`...ScopeId`-val VAGY `...Name`(+`...Layer`)-rel. **`dryRun:true` (DEFAULT) csak az érintett-számot, NEM '
471
+ + 'ír**; `dryRun:false` ír. NEM dedup-ol (a content-hash-összevonás a `deduplicate_entries`). Nagy merge '
472
+ + 'után szerver-restart ajánlott.',
473
+ inputSchema: this.objectSchema({
474
+ fromScopeId: { type: 'string', description: 'A FORRÁS scope _id-ja.' },
475
+ fromName: { type: 'string', description: 'A forrás scope neve (scopeId helyett).' },
476
+ fromLayer: { type: 'string', description: 'Opcionális forrás-layer az egyértelműsítéshez.' },
477
+ intoScopeId: { type: 'string', description: 'A CÉL scope _id-ja (ebbe olvad).' },
478
+ intoName: { type: 'string', description: 'A cél scope neve (scopeId helyett).' },
479
+ intoLayer: { type: 'string', description: 'Opcionális cél-layer az egyértelműsítéshez.' },
480
+ dryRun: { type: 'boolean', description: 'true (default): csak preview; false: ír.' },
481
+ force: { type: 'boolean', description: 'A cross-project merge-guard felülírása (KÜLÖNBÖZŐ projekt-gyökerek '
482
+ + 'esetén is enged) — csak tudatos user-akcióra.' },
483
+ }),
484
+ handler: async (args) => {
485
+ const input = this.asObject(args);
486
+ return this.ok(await scope_reference_1.FAM_ScopeMaintenance_ControlService.getInstance().mergeScopes({
487
+ from: { scopeId: this.asString(input.fromScopeId), canonicalName: this.asString(input.fromName), layer: this.asString(input.fromLayer) },
488
+ into: { scopeId: this.asString(input.intoScopeId), canonicalName: this.asString(input.intoName), layer: this.asString(input.intoLayer) },
489
+ dryRun: input.dryRun !== false,
490
+ force: input.force === true,
491
+ }));
492
+ },
493
+ });
494
+ this.register({
495
+ name: 'reset_scope', category: category,
496
+ description: 'Egy scope SCAN-eredetű entry-jeinek TÖRLÉSE (recovery egy félre-routolt scan után). A '
497
+ + '`sourceType` default `scan` → a manuális/agent/import-SZERZETT (pótolhatatlan) tartalmat SOHA nem '
498
+ + 'törli (csak az újra-szkennelhető scan-chunkokat). Opcionális `sourceRootContains`: a `source.root` '
499
+ + 'substring-szűrője a SEBÉSZI cross-root takarításhoz (pl. a `ccap-revisioned` gyökerű szennyezés a '
500
+ + '`ccap-client` scope-ból, a `ccap` saját entry-jeinek MEGTARTÁSÁVAL). **`dryRun:true` (DEFAULT) csak a '
501
+ + 'per-collection számot + a `rootSamples`-t adja (verifikáció: jó scope/gyökér?), NEM ír**; a tényleges '
502
+ + 'soft-delete (+ LVS-pool-kivétel) `dryRun:false`. A scope `scopeId`-val VAGY `canonicalName`(+`layer`)-rel.',
503
+ inputSchema: this.objectSchema({
504
+ scopeId: { type: 'string', description: 'A scope _id-ja (egyértelmű azonosítás).' },
505
+ canonicalName: { type: 'string', description: 'A scope neve (scopeId helyett).' },
506
+ layer: { type: 'string', description: 'Opcionális layer a canonicalName egyértelműsítéséhez.' },
507
+ sourceType: { type: 'string', description: 'A törlendő entry-k source.type-ja (default `scan`; a manuális/agent/import VÉDETT).' },
508
+ sourceRootContains: { type: 'string', description: 'Opcionális: csak az e substring-et tartalmazó `source.root`-ú entry-ket (sebészi cross-root takarítás).' },
509
+ table: this.tableSchema(false),
510
+ dryRun: { type: 'boolean', description: 'true (default): preview (szám + rootSamples); false: soft-delete + pool-kivétel.' },
511
+ }),
512
+ handler: async (args) => {
513
+ const input = this.asObject(args);
514
+ return this.ok(await scope_reference_1.FAM_ScopeMaintenance_ControlService.getInstance().resetScope({
515
+ scopeId: this.asString(input.scopeId),
516
+ canonicalName: this.asString(input.canonicalName),
517
+ layer: this.asString(input.layer),
518
+ sourceType: this.asString(input.sourceType),
519
+ sourceRootContains: this.asString(input.sourceRootContains),
520
+ table: this.asString(input.table),
521
+ dryRun: input.dryRun !== false,
522
+ }));
523
+ },
524
+ });
525
+ this.register({
526
+ name: 'detect_scope_issues', category: category,
527
+ description: 'Scope-integritás SELF-CHECK (user-FR): felismeri az ÁRVA projekt-scope-okat (layer=project, de '
528
+ + 'nincs org-parent — tipikusan import-eredetű lapos scope) + a DUPLIKÁTUMOKAT (ugyanaz a name+layer '
529
+ + 'többször), entry-számmal. CSAK OLVAS (a takarítás a `merge_scopes`). A nagyobb/parented a javasolt `into`.',
530
+ inputSchema: this.objectSchema({}),
531
+ handler: async () => this.ok(await scope_reference_1.FAM_ScopeMaintenance_ControlService.getInstance().detectIssues()),
532
+ });
533
+ this.register({
534
+ name: 'suggest_scope_merges', category: category,
535
+ description: 'Scope-merge JAVASLATOK (user-FR — auto-felismerés AKCIONÁLHATÓVÁ): a `detect_scope_issues` '
536
+ + 'leleteiből KONKRÉT, futtatható `merge_scopes from→into` direktívák. A REPO-KULCS-csoportok elsőbbséget '
537
+ + 'élveznek (a KÜLÖNBÖZŐ nevű, AZONOS repo eseteket — pl. import-`10.3.3` vs scan-`ccap-revisioned` — is '
538
+ + 'elkapja, amit a puszta név-egyezés kihagy); az `into` a parented/legtöbb-entry-jű scope. CSAK OLVAS — '
539
+ + 'a tényleges összeolvasztáshoz futtasd a javasolt `merge_scopes` hívásokat.',
540
+ inputSchema: this.objectSchema({}),
541
+ handler: async () => this.ok(await scope_reference_1.FAM_ScopeMaintenance_ControlService.getInstance().suggestMerges()),
542
+ });
543
+ this.register({
544
+ name: 'set_global_rule', category: category,
545
+ description: 'GLOBÁLIS HARD-RULE jelölés (user-FR — a FAM fő feature-je): a megadott `rules`-bejegyzéseken '
546
+ + 'a `globalRule` flag BE-/KIkapcsolása. A `globalRule:true` rule-ok MINDEN scope-szűrt `rules`-lekérdezésbe '
547
+ + 'BEKERÜLNEK (scope-tól függetlenül) → a workspace-szintű hard-rules öröklődnek minden al-scope-ba. '
548
+ + 'Szelektor (legalább egy): `ids` (entry-_id-k) VAGY `scopeFilter` (egy scope subtree MINDEN rule-ja, '
549
+ + '`[{layer,rawName}]`) VAGY `tags`. `global` (default true) = jelölés / false = visszavonás. `dryRun` → csak szám.',
550
+ inputSchema: this.objectSchema({
551
+ ids: { type: 'array', items: { type: 'string' } },
552
+ scopeFilter: { type: 'array', items: this.objectSchema({ layer: { type: 'string' }, rawName: { type: 'string' } }) },
553
+ tags: { type: 'array', items: { type: 'string' } },
554
+ global: { type: 'boolean' },
555
+ dryRun: { type: 'boolean' },
556
+ }),
557
+ handler: async (args) => {
558
+ const input = this.asObject(args);
559
+ return this.ok(await retrieval_1.FAM_GlobalRule_ControlService.getInstance().setGlobalRule({
560
+ ids: Array.isArray(input.ids) ? input.ids.map((value) => String(value)) : undefined,
561
+ scopeFilter: Array.isArray(input.scopeFilter)
562
+ ? input.scopeFilter.map((layer) => {
563
+ const obj = this.asObject(layer);
564
+ return { layer: this.asString(obj.layer), rawName: this.asString(obj.rawName) };
565
+ })
566
+ : undefined,
567
+ tags: Array.isArray(input.tags) ? input.tags.map((value) => String(value)) : undefined,
568
+ global: typeof input.global === 'boolean' ? input.global : undefined,
569
+ dryRun: input.dryRun === true,
570
+ }));
571
+ },
572
+ });
396
573
  // repair_scope_links / deduplicate_entries (MP-12) — DRY-RUN-ONLY MVP1 (safe). A destruktív
397
574
  // write-ág (entry-mutáció / soft-delete-összevonás) BACKLOG: vak destruktív bulk-op TILOS
398
575
  // (dsgn-003 §4.2), ezért a `dryRun:false` kérés clear "BACKLOG" hibát ad — a `dryRun:true` (default)
@@ -415,16 +592,18 @@ class FAM_CapabilityRegistry_Service {
415
592
  this.register({
416
593
  name: 'deduplicate_entries', category: category,
417
594
  description: 'Duplikált entry-k AUDITja: contentHash-alapú dup-csoportok feltárása (a megtartandó + a '
418
- + 'beolvasztható jelöltek). DRY-RUN MVP1: riportál, NEM ír. A tényleges összevonás (soft-delete) '
595
+ + 'beolvasztható jelöltek), a LEGNAGYOBB csoportok elöl (+ `totalGroups`/`truncated` nincs néma '
596
+ + 'csonkolás). DRY-RUN MVP1: riportál, NEM ír. A tényleges összevonás (soft-delete) '
419
597
  + 'BACKLOG — `dryRun:false` → clear BACKLOG-hiba (vak destruktív op TILOS).',
420
598
  inputSchema: this.objectSchema({
421
599
  table: this.tableSchema(false),
600
+ maxGroups: { type: 'number', description: 'A visszaadott dup-csoportok max száma (default 500; a legnagyobbak elöl).' },
422
601
  dryRun: { type: 'boolean', description: 'MVP1: mindig true (az összevonás BACKLOG). false → BACKLOG-hiba.' },
423
602
  }),
424
603
  handler: async (args) => {
425
604
  const input = this.asObject(args);
426
605
  this.assertDryRunOnly(input.dryRun, 'deduplicate_entries', 'duplikátum-összevonás (soft-delete)');
427
- return this.ok(await this.deduplicateEntriesDryRun(this.optionalTable(input.table)));
606
+ return this.ok(await this.deduplicateEntriesDryRun(this.optionalTable(input.table), this.asNumber(input.maxGroups)));
428
607
  },
429
608
  });
430
609
  }
@@ -457,7 +636,7 @@ class FAM_CapabilityRegistry_Service {
457
636
  this.register({
458
637
  name: 'import_claude_mem', category: category,
459
638
  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).',
639
+ + 'map + persist + embed. Output: { ok: ok, batchId: batchId, ingested: ingested, skipped: skipped, failed: failed, byTable: byTable, status: status } (dsgn-009 §5.3).',
461
640
  inputSchema: this.importMapSchema(), handler: runHandler,
462
641
  });
463
642
  this.register({
@@ -556,7 +735,10 @@ class FAM_CapabilityRegistry_Service {
556
735
  // =========================================================================
557
736
  // Wired handler-helpers (delegálás az engine-ekre)
558
737
  // =========================================================================
559
- /** Per-tár statisztika (aktív elem-szám + embedding-státusz bontás) — MP-1. */
738
+ /**
739
+ * Per-tár statisztika (aktív elem-szám + embedding-státusz bontás) — MP-1.
740
+ * FIX (OOM): `countDocuments` + `$group`-aggregáció — NEM tölti a doc-okat memóriába (a 126k-vektoros load OOM-ozott).
741
+ */
560
742
  async tableStats(table) {
561
743
  const tables = table ? [table] : MAIN_TABLES;
562
744
  const result = [];
@@ -565,14 +747,22 @@ class FAM_CapabilityRegistry_Service {
565
747
  if (!registryEntry) {
566
748
  continue;
567
749
  }
568
- const dataService = new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
569
- const entries = await dataService.findHydratableList({});
750
+ const collection = this.entryCollection(registryEntry);
751
+ const count = await collection.countDocuments({});
570
752
  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;
753
+ const grouped = await collection
754
+ .aggregate([{ $group: { _id: '$embeddingStatus', n: { $sum: 1 } } }])
755
+ .toArray();
756
+ for (const group of grouped) {
757
+ const status = group._id ?? 'pending';
758
+ if (status in byStatus) {
759
+ byStatus[status] += group.n;
760
+ }
761
+ else {
762
+ byStatus.pending += group.n;
763
+ }
574
764
  }
575
- result.push({ table: target, count: entries.length, embeddingStatus: byStatus });
765
+ result.push({ table: target, count: count, embeddingStatus: byStatus });
576
766
  }
577
767
  return table ? result[0] ?? { table: table, count: 0 } : result;
578
768
  }
@@ -618,7 +808,7 @@ class FAM_CapabilityRegistry_Service {
618
808
  /** Egy env-változó jelenlét-ellenőrzése (`treatDefaultAsPresent`: van builtin default, ezért sosem hiányzik). */
619
809
  checkEnv(name, hint, present, missing, treatDefaultAsPresent = false) {
620
810
  const value = process.env[name];
621
- if ((value && value.trim().length) || treatDefaultAsPresent) {
811
+ if ((value?.trim().length) || treatDefaultAsPresent) {
622
812
  present.push(name);
623
813
  return;
624
814
  }
@@ -750,11 +940,10 @@ class FAM_CapabilityRegistry_Service {
750
940
  */
751
941
  async storageUsage() {
752
942
  const collections = [];
753
- // Fő RAG-tárak (a store-registry-n át, a per-tár dataParams-szal).
943
+ // Fő RAG-tárak — FIX (OOM): `countDocuments` (NEM a doc-ok memóriába töltése; a 126k-vektoros load OOM-ozott).
754
944
  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 });
945
+ const activeDocs = await this.entryCollection(registryEntry).countDocuments({});
946
+ collections.push({ collection: this.realCollectionName(registryEntry.dataParams.dataName), activeDocs: activeDocs });
758
947
  }
759
948
  // Helper collection-ök (reference / scope / ingest-run / config) — a DS-példányosítás regisztrálja a modellt.
760
949
  const reference_DS = new scope_reference_1.FAM_Reference_DataService({ issuer: this.issuer });
@@ -865,21 +1054,24 @@ class FAM_CapabilityRegistry_Service {
865
1054
  const tables = table ? [table] : MAIN_TABLES;
866
1055
  const result = [];
867
1056
  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
- }
1057
+ const registryEntry = embedding_1.FAM_STORE_REGISTRY.find((entry) => entry.table === target);
1058
+ if (!registryEntry) {
1059
+ continue;
878
1060
  }
1061
+ // FIX (OOM): Mongo-aggregáció ($group kind / $unwind+$group tag) — NEM tölti a doc-okat memóriába.
1062
+ const collection = this.entryCollection(registryEntry);
1063
+ const kindAgg = await collection
1064
+ .aggregate([
1065
+ { $match: { kind: { $ne: null } } }, { $group: { _id: '$kind', n: { $sum: 1 } } }, { $sort: { n: -1 } },
1066
+ ]).toArray();
1067
+ const tagAgg = await collection
1068
+ .aggregate([
1069
+ { $unwind: '$tags' }, { $group: { _id: '$tags', n: { $sum: 1 } } }, { $sort: { n: -1 } },
1070
+ ]).toArray();
879
1071
  result.push({
880
1072
  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 })),
1073
+ kinds: kindAgg.filter((group) => group._id).map((group) => ({ kind: group._id, count: group.n })),
1074
+ tags: tagAgg.filter((group) => group._id).map((group) => ({ tag: group._id, count: group.n })),
883
1075
  });
884
1076
  }
885
1077
  return result;
@@ -890,8 +1082,17 @@ class FAM_CapabilityRegistry_Service {
890
1082
  const tables = table ? [table] : MAIN_TABLES;
891
1083
  const collected = [];
892
1084
  for (const target of tables) {
893
- const entries = await this.loadTableEntries(target);
894
- for (const entry of entries) {
1085
+ const registryEntry = embedding_1.FAM_STORE_REGISTRY.find((entry) => entry.table === target);
1086
+ if (!registryEntry) {
1087
+ continue;
1088
+ }
1089
+ // FIX (OOM): per-tár BOUNDED query (`sort __created` + `limit`), NEM a teljes tár betöltése N elemért.
1090
+ const docs = await this.entryCollection(registryEntry)
1091
+ .find({}, { projection: { kind: 1, tags: 1, __created: 1, content: 1 } })
1092
+ .sort({ __created: -1 })
1093
+ .limit(cappedLimit)
1094
+ .toArray();
1095
+ for (const entry of docs) {
895
1096
  if (!entry._id) {
896
1097
  continue;
897
1098
  }
@@ -949,7 +1150,8 @@ class FAM_CapabilityRegistry_Service {
949
1150
  .map((ref) => ref.scopeId)
950
1151
  .filter((scopeId) => scopeId && !activeScopeIds.has(scopeId));
951
1152
  if (missing.length && entry._id) {
952
- brokenLinks.push({ id: entry._id, table: target, missingScopeIds: missing });
1153
+ // String-koerció: a nyers Mongo-driver `_id`-je ObjectId (típus-konzisztencia a riportban).
1154
+ brokenLinks.push({ id: String(entry._id), table: target, missingScopeIds: missing });
953
1155
  }
954
1156
  }
955
1157
  }
@@ -960,7 +1162,8 @@ class FAM_CapabilityRegistry_Service {
960
1162
  * (legrégebbi `__created`) + a beolvasztható jelöltek. NEM ír — csak riportál (mit VONNA ÖSSZE). A
961
1163
  * tényleges összevonás (soft-delete) BACKLOG.
962
1164
  */
963
- async deduplicateEntriesDryRun(table) {
1165
+ async deduplicateEntriesDryRun(table, maxGroups) {
1166
+ const groupCap = maxGroups ?? 500;
964
1167
  const tables = table ? [table] : MAIN_TABLES;
965
1168
  let scannedEntries = 0;
966
1169
  const duplicateGroups = [];
@@ -980,14 +1183,26 @@ class FAM_CapabilityRegistry_Service {
980
1183
  if (group.length < 2) {
981
1184
  continue;
982
1185
  }
983
- // A megtartandó = a legrégebbi (`__created` ASC); a többi a merge-jelölt.
1186
+ // A megtartandó = a legrégebbi (`__created` ASC); a többi a merge-jelölt. KRITIKUS: a nyers
1187
+ // Mongo-driver `_id`-je ObjectId (NEM string) — String-koerció nélkül a `.length`-szűrő MINDEN
1188
+ // merge-jelöltet kidobott (üres `mergeIds` — 2026-07-15 bugfix).
984
1189
  const sorted = group.slice().sort((a, b) => this.createdMs(a) - this.createdMs(b));
985
- const keepId = sorted[0]._id ?? '';
986
- const mergeIds = sorted.slice(1).map((entry) => entry._id ?? '').filter((id) => id.length > 0);
1190
+ const keepId = String(sorted[0]._id ?? '');
1191
+ const mergeIds = sorted.slice(1).map((entry) => String(entry._id ?? '')).filter((id) => id.length > 0);
987
1192
  duplicateGroups.push({ table: target, contentHash: contentHash, keepId: keepId, mergeIds: mergeIds });
988
1193
  }
989
1194
  }
990
- return { dryRun: true, scannedEntries: scannedEntries, duplicateGroups: duplicateGroups };
1195
+ // A LEGNAGYOBB csoportok elöl + output-cap (a full-tár audit 50k+ csoportot adhat — a riport a top-N-re
1196
+ // fókuszál, a `totalGroups`/`truncated` jelzi a teljes terjedelmet; nincs néma csonkolás).
1197
+ duplicateGroups.sort((left, right) => right.mergeIds.length - left.mergeIds.length);
1198
+ const totalGroups = duplicateGroups.length;
1199
+ return {
1200
+ dryRun: true,
1201
+ scannedEntries: scannedEntries,
1202
+ totalGroups: totalGroups,
1203
+ truncated: totalGroups > groupCap,
1204
+ duplicateGroups: duplicateGroups.slice(0, groupCap),
1205
+ };
991
1206
  }
992
1207
  // =========================================================================
993
1208
  // MP-12 debug: explain-search / inspect-chunk / inspect-embedding
@@ -1083,14 +1298,22 @@ class FAM_CapabilityRegistry_Service {
1083
1298
  // =========================================================================
1084
1299
  // MP-12 közös entry-helperek (lazy DataService — eager-resolve elkerülés)
1085
1300
  // =========================================================================
1086
- /** Egy fő tár aktív entry-i (a per-tár dataParams-szal). Nem-fő/ismeretlen tár → üres. */
1301
+ /**
1302
+ * Egy fő tár aktív entry-i (a per-tár dataParams-szal). Nem-fő/ismeretlen tár → üres.
1303
+ * FIX (OOM): a nagy `contentVector` (2560 float/doc) PROJEKCIÓVAL KIHAGYVA — a fogyasztók (kinds/recent/repair/
1304
+ * 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).
1305
+ */
1087
1306
  async loadTableEntries(table) {
1088
1307
  const registryEntry = embedding_1.FAM_STORE_REGISTRY.find((entry) => entry.table === table);
1089
1308
  if (!registryEntry) {
1090
1309
  return [];
1091
1310
  }
1092
- const dataService = new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
1093
- return dataService.findHydratableList({});
1311
+ return this.entryCollection(registryEntry).find({}, { projection: { contentVector: 0 } }).toArray();
1312
+ }
1313
+ /** Egy fő RAG-tár mongoose-collection-je (a DS-példányosítás regisztrálja a modellt; onnan a valós collection). */
1314
+ entryCollection(registryEntry) {
1315
+ new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
1316
+ return mongoose_1.default.models[registryEntry.dataParams.dataName].collection;
1094
1317
  }
1095
1318
  /** Egy entry feloldása `id`-ből (a megadott tárból, vagy minden fő tárból). Nincs → `FAM-MCP-DISPATCH-002`. */
1096
1319
  async requireEntryById(id, table, capability) {
@@ -1110,7 +1333,7 @@ class FAM_CapabilityRegistry_Service {
1110
1333
  }
1111
1334
  const dataService = new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
1112
1335
  const entry = await dataService.findData({ _id: id }, true);
1113
- if (entry && entry._id) {
1336
+ if (entry?._id) {
1114
1337
  return entry;
1115
1338
  }
1116
1339
  }
@@ -1190,7 +1413,7 @@ class FAM_CapabilityRegistry_Service {
1190
1413
  scopeLayerArraySchema() {
1191
1414
  return {
1192
1415
  type: 'array',
1193
- description: 'Nyers scope-lánc (root→leaf): [{ layer, rawName }] párok.',
1416
+ description: 'Nyers scope-lánc (root→leaf): [{ layer: layer, rawName: rawName }] párok.',
1194
1417
  items: this.objectSchema({
1195
1418
  layer: { type: 'string', description: 'A dinamikus layer-név (pl. organization/project).' },
1196
1419
  rawName: { type: 'string', description: 'A nyers (feloldatlan) scope-név.' },
@@ -7,6 +7,7 @@ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
7
7
  const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
8
8
  const error_codes_const_1 = require("../../../_collections/error-codes.const");
9
9
  const fam_error_factory_util_1 = require("../../../_collections/fam-error-factory.util");
10
+ const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
10
11
  const retrieval_1 = require("../../retrieval");
11
12
  /**
12
13
  * `FAM_ReadTool_Service` (SP-6.2, dsgn-003 §2) — a `read` core-tool **transport-agnosztikus**
@@ -41,9 +42,78 @@ class FAM_ReadTool_Service {
41
42
  const request = this.toRetrievalRequest(input);
42
43
  const response = await retrieval_1.FAM_Retrieval_ControlService.getInstance().read(request);
43
44
  this.rebaseToBasePath(response, input.basePath);
45
+ // Token-budget ÉRVÉNYESÍTÉS (FAM-REV 2026-06-22): a read-válasz INLINE marad (különben a nagy találat-tartalom
46
+ // miatt a MCP-host FÁJLBA menti → extra olvasás-lépés). A `read.maxTokens` (default 4000) felett a hit-tartalom
47
+ // levágódik + a túlcsorduló hit-ek elhagyva, `truncated:true`-val. A FAM mindig DIREKT adja vissza a találatokat.
48
+ FAM_ReadTool_Service.applyTokenBudget(response, await FAM_ReadTool_Service.resolveMaxTokens());
44
49
  this.logReadActivity(input, response);
45
50
  return response;
46
51
  }
52
+ /** A `read.maxTokens` config feloldása (default/fallback 4000, min 256). */
53
+ static async resolveMaxTokens() {
54
+ try {
55
+ const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('read.maxTokens', {});
56
+ const value = typeof resolved.value === 'number' ? resolved.value : Number(resolved.value);
57
+ return Number.isFinite(value) && value >= 256 ? Math.floor(value) : 4000;
58
+ }
59
+ catch {
60
+ return 4000;
61
+ }
62
+ }
63
+ /**
64
+ * A read-válasz token-budgetre vágása (pure; ~4 char/token). A hit-eket sorrendben tartja meg; a budget elérésekor
65
+ * az aktuális hit `content`-jét a maradékra vágja (`…[levágva]`), a többi hitet elhagyja, és a query-eredményt
66
+ * `truncated:true`-ra állítja. Üres/`includeContent:false` válasz → no-op. Így a read INLINE marad (nem fájl).
67
+ *
68
+ * **ROUND-ROBIN allokáció (eval-lelet 2026-06-25 — multi-query starvation fix):** a budget KÖZÖS az egész válaszra,
69
+ * de NEM query-sorban fogy (különben az ELSŐ query elfogyasztaná, és a többi query 0 hitet kapna — silent data-loss
70
+ * a multi-query read-en). Helyette KÖRÖNKÉNT haladunk: minden körben MINDEN query EGY következő hitje kerül sorra
71
+ * (#1-ek, majd #2-k, …), így minden query megkapja a TOP-hitjeit, mielőtt a budget elfogy → egy query SEM éhezik.
72
+ */
73
+ static applyTokenBudget(response, maxTokens) {
74
+ const charBudget = Math.max(256, maxTokens) * 4;
75
+ const metaPerHit = 220; // a per-hit metaadat (score/path/heading/…) durva char-költsége
76
+ const results = response.results ?? [];
77
+ if (!results.length) {
78
+ return;
79
+ }
80
+ const kept = results.map(() => []);
81
+ const maxHits = Math.max(0, ...results.map((result) => (result.hits ?? []).length));
82
+ let used = 0;
83
+ let budgetExhausted = false;
84
+ for (let round = 0; round < maxHits && !budgetExhausted; round++) {
85
+ for (let ri = 0; ri < results.length && !budgetExhausted; ri++) {
86
+ const hit = (results[ri].hits ?? [])[round];
87
+ if (!hit) {
88
+ continue;
89
+ }
90
+ if (used + metaPerHit >= charBudget) {
91
+ budgetExhausted = true;
92
+ break;
93
+ }
94
+ used += metaPerHit;
95
+ if (typeof hit.content === 'string' && hit.content.length > 0) {
96
+ const remaining = charBudget - used;
97
+ if (hit.content.length > remaining) {
98
+ hit.content = `${hit.content.slice(0, Math.max(0, remaining - 12))}…[levágva]`;
99
+ used = charBudget;
100
+ kept[ri].push(hit);
101
+ budgetExhausted = true;
102
+ break;
103
+ }
104
+ used += hit.content.length;
105
+ }
106
+ kept[ri].push(hit);
107
+ }
108
+ }
109
+ // A megtartott hitek visszaírása + `truncated:true` ott, ahol elhagytunk hitet (a teljes halmaznál kevesebb maradt).
110
+ for (let ri = 0; ri < results.length; ri++) {
111
+ if ((results[ri].hits ?? []).length > kept[ri].length) {
112
+ results[ri].truncated = true;
113
+ }
114
+ results[ri].hits = kept[ri];
115
+ }
116
+ }
47
117
  /**
48
118
  * Ha a kérés `basePath`-et ad, a hit-ek `sourceFilePath`-ját a KANONIKUS `absolutePath`-ból a `basePath`-hez
49
119
  * RELATÍVAN írjuk át (a hívó saját workspace-kontextusa — FAM-REV-050), `/`-normalizálva. `absolutePath` nélküli
@@ -98,7 +168,7 @@ class FAM_ReadTool_Service {
98
168
  throw fam_error_factory_util_1.FAM_Error_Util.create({
99
169
  errorCode: error_codes_const_1.FAM_ERROR_CODES.valReadEmptyOrReference,
100
170
  message: 'A `read` legalább egy query-t igényel (`queries[]`). Adj meg legalább egy '
101
- + '{ tables, query } blokkot.',
171
+ + '{ tables: tables, query: query } blokkot.',
102
172
  issuer: this.issuer,
103
173
  context: { operation: 'read-validate' },
104
174
  });
@@ -6,6 +6,7 @@ const error_codes_const_1 = require("../../../_collections/error-codes.const");
6
6
  const fam_error_factory_util_1 = require("../../../_collections/fam-error-factory.util");
7
7
  const fam_entry_data_model_1 = require("../../../_models/data-models/fam-entry.data-model");
8
8
  const embedding_1 = require("../../embedding");
9
+ const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
9
10
  const ingest_1 = require("../../ingest");
10
11
  const migration_1 = require("../../migration");
11
12
  const scope_reference_1 = require("../../scope-reference");
@@ -178,11 +179,44 @@ class FAM_WriteTool_Service {
178
179
  const warnings = embedOutcome.status === 'error'
179
180
  ? ['Az entry mentve, de az embedding sikertelen (érdemes ellenőrizni az embedding-providert; `re-embed`).']
180
181
  : [];
182
+ // NEAR-DUPLIKÁTUM-WARNING (user-FR 2026-06-25): a friss vektorral megnézzük, van-e a tárban „majdnem azonos"
183
+ // meglévő entry (koszinusz ≥ küszöb). Ha igen → jelzés (NEM blokkol) → az agent tudja: érdemes lehet a meglévőt
184
+ // UPDATE-elni, nem új entry-t létrehozni. A vektor már kész (nincs extra embed); a check 1 vektor-keresés.
185
+ if (embedOutcome.status === 'completed' && embedOutcome.vector.length) {
186
+ await this.appendDedupWarning(input.table, embedOutcome.vector, createdId, warnings);
187
+ }
181
188
  return this.baseOutput({
182
189
  operation: input.operation, table: input.table, scopePath: resolve.scopePath,
183
190
  uncertaintyNotes: resolve.uncertaintyNotes, created: createdId ? [createdId] : [], warnings: warnings,
184
191
  });
185
192
  }
193
+ /**
194
+ * Near-duplikátum-warning hozzáfűzése (user-FR 2026-06-25): a `write.dedupWarnEnabled`/`write.dedupWarnThreshold`
195
+ * config alapján a friss vektor legközelebbi szomszédait nézi; a küszöb fölötti meglévő entry(ke)t jelzi. Best-effort:
196
+ * a hiba NEM buktatja a write-ot (a jelzés kényelmi, nem kritikus).
197
+ */
198
+ async appendDedupWarning(table, vector, createdId, warnings) {
199
+ try {
200
+ const config = config_control_service_1.FAM_Config_ControlService.getInstance();
201
+ const enabledVal = (await config.resolve('write.dedupWarnEnabled', { table: table })).value;
202
+ if (enabledVal === false) {
203
+ return;
204
+ }
205
+ const thVal = (await config.resolve('write.dedupWarnThreshold', { table: table })).value;
206
+ const threshold = typeof thVal === 'number' ? thVal : 0.93;
207
+ const nearDups = await embedding_1.FAM_DedupWarn_ControlService.getInstance().check({
208
+ table: table, vector: vector, excludeId: createdId, threshold: threshold,
209
+ });
210
+ if (nearDups.length) {
211
+ const list = nearDups.map((dup) => `${dup.id} (${dup.score.toFixed(3)})`).join(', ');
212
+ warnings.push(`⚠ Near-duplikátum: az új tartalom nagyon hasonló a meglévő ${list} bejegyzés(ek)hez `
213
+ + `(koszinusz ≥ ${threshold}). Fontold meg a MEGLÉVŐ frissítését (update) új entry létrehozása helyett.`);
214
+ }
215
+ }
216
+ catch {
217
+ // best-effort — a dedup-warning kényelmi, a hiba nem buktatja a write-ot.
218
+ }
219
+ }
186
220
  /**
187
221
  * `update` (dsgn-003 §3.1) → MP-1 modify + (ha content változott) MP-2 re-embed. A `target`
188
222
  * kötelező (`id`/`sourceFilePath`/`query`). A scope opcionális update-nél (a meglévő entry scope-ja
@@ -277,6 +311,8 @@ class FAM_WriteTool_Service {
277
311
  include: scan.include,
278
312
  exclude: scan.exclude,
279
313
  dryRun: scan.dryRun,
314
+ preserveHistory: scan.preserveHistory,
315
+ tableOverride: scan.tableOverride,
280
316
  weights: scan.weights,
281
317
  issuer: this.issuer,
282
318
  };
@@ -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 }];