@futdevpro/fdp-agent-memory 1.1.166 → 1.1.194

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 (30) hide show
  1. package/build/package.json +8 -2
  2. package/build/src/_cli/_commands/prune-duplicates.command.js +93 -0
  3. package/build/src/_cli/_commands/retire-temporary.command.js +77 -0
  4. package/build/src/_cli/register-commands.js +4 -0
  5. package/build/src/_collections/config-catalog.const.js +75 -0
  6. package/build/src/_models/data-models/fam-entry.data-model.js +6 -0
  7. package/build/src/_models/data-models/fam-memory.data-model.js +3 -0
  8. package/build/src/_modules/embedding/_collections/fam-exact-duplicate-prune.util.js +85 -0
  9. package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +32 -0
  10. package/build/src/_modules/embedding/_services/fam-entry.data-service.js +21 -0
  11. package/build/src/_modules/embedding/_services/fam-exact-duplicate-prune.control-service.js +152 -0
  12. package/build/src/_modules/embedding/index.js +7 -1
  13. package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +21 -1
  14. package/build/src/_modules/ingest/_services/fam-scan-job.control-service.js +55 -1
  15. package/build/src/_modules/ingest/_services/fam-scan-scheduler.control-service.js +26 -0
  16. package/build/src/_modules/mcp/_collections/fam-core-tools.const.js +11 -0
  17. package/build/src/_modules/mcp/_services/fam-read-tool.service.js +2 -0
  18. package/build/src/_modules/mcp/_services/fam-write-tool.service.js +29 -7
  19. package/build/src/_modules/retrieval/_collections/fam-cross-table-note.util.js +136 -0
  20. package/build/src/_modules/retrieval/_collections/fam-lexical-match.util.js +75 -2
  21. package/build/src/_modules/retrieval/_collections/fam-literal-phrase.util.js +74 -0
  22. package/build/src/_modules/retrieval/_collections/fam-memory-retention.util.js +70 -0
  23. package/build/src/_modules/retrieval/_services/fam-memory-reactivation.control-service.js +8 -1
  24. package/build/src/_modules/retrieval/_services/fam-memory-retention.control-service.js +118 -0
  25. package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +33 -0
  26. package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +224 -5
  27. package/build/src/_modules/retrieval/index.js +6 -1
  28. package/build/src/_routes/server/api/api.controller.js +77 -15
  29. package/package.json +8 -2
  30. package/build/src/_integration-tests/_helpers/fam-integration-test-setup.util.js +0 -105
@@ -8,7 +8,7 @@
8
8
  * (BFR-AM-002/008); a bedrock-csere (MP-15) a `FAM_EmbeddingProvider` interfész mögött non-breaking.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.FAM_EmbeddingBootstrap_ControlService = exports.FAM_DupAuditScheduler_ControlService = exports.FAM_DupAudit_DataService = exports.FAM_DupAudit_ControlService = exports.FAM_DupFileRollup_Util = exports.FAM_DupForkPair_Util = exports.FAM_DupCoverage_ControlService = exports.FAM_ExactDuplication_ControlService = exports.FAM_DuplicateScan_ControlService = exports.FAM_EmbeddingCost_ControlService = exports.FAM_EmbeddingPipeline_ControlService = exports.FAM_EmbeddingPreset_ControlService = exports.FAM_StoreRegistry_Util = exports.FAM_STORE_REGISTRY = exports.FAM_DedupWarn_ControlService = exports.FAM_Entry_DataService = exports.FAM_HydrationCoordinator_ControlService = exports.FAM_VectorSearch_ControlService = exports.FAM_MOCK_EMBEDDING_DIMS = exports.FAM_Mock_EmbeddingProvider = exports.FAM_LMStudio_EmbeddingProvider = exports.FAM_OpenAI_EmbeddingProvider = exports.FAM_Embedding_ControlService = void 0;
11
+ exports.FAM_EmbeddingBootstrap_ControlService = exports.FAM_DupAuditScheduler_ControlService = exports.FAM_DupAudit_DataService = exports.FAM_DupAudit_ControlService = exports.FAM_DupFileRollup_Util = exports.FAM_DupForkPair_Util = exports.FAM_DupCoverage_ControlService = exports.FAM_ExactDuplication_ControlService = exports.FAM_DuplicateScan_ControlService = exports.FAM_ExactDuplicatePrune_Util = exports.FAM_ExactDuplicatePrune_ControlService = exports.FAM_EmbeddingCost_ControlService = exports.FAM_EmbeddingPipeline_ControlService = exports.FAM_EmbeddingPreset_ControlService = exports.FAM_StoreRegistry_Util = exports.FAM_STORE_REGISTRY = exports.FAM_DedupWarn_ControlService = exports.FAM_Entry_DataService = exports.FAM_HydrationCoordinator_ControlService = exports.FAM_VectorSearch_ControlService = exports.FAM_MOCK_EMBEDDING_DIMS = exports.FAM_Mock_EmbeddingProvider = exports.FAM_LMStudio_EmbeddingProvider = exports.FAM_OpenAI_EmbeddingProvider = exports.FAM_Embedding_ControlService = void 0;
12
12
  // SP-2.1 — provider-dispatch kapu + providerek
13
13
  var fam_embedding_control_service_1 = require("./_services/fam-embedding.control-service");
14
14
  Object.defineProperty(exports, "FAM_Embedding_ControlService", { enumerable: true, get: function () { return fam_embedding_control_service_1.FAM_Embedding_ControlService; } });
@@ -39,6 +39,12 @@ var fam_embedding_pipeline_control_service_1 = require("./_services/fam-embeddin
39
39
  Object.defineProperty(exports, "FAM_EmbeddingPipeline_ControlService", { enumerable: true, get: function () { return fam_embedding_pipeline_control_service_1.FAM_EmbeddingPipeline_ControlService; } });
40
40
  var fam_embedding_cost_control_service_1 = require("./_services/fam-embedding-cost.control-service");
41
41
  Object.defineProperty(exports, "FAM_EmbeddingCost_ControlService", { enumerable: true, get: function () { return fam_embedding_cost_control_service_1.FAM_EmbeddingCost_ControlService; } });
42
+ // BÁJTAZONOS ismétlés-NYESÉS (2026-09-01) — a szemantikus felderítővel ellentétben EZ töröl, de csak
43
+ // bizonyítottan azonos tartalmat, és `dryRun` az alapértelmezés.
44
+ var fam_exact_duplicate_prune_control_service_1 = require("./_services/fam-exact-duplicate-prune.control-service");
45
+ Object.defineProperty(exports, "FAM_ExactDuplicatePrune_ControlService", { enumerable: true, get: function () { return fam_exact_duplicate_prune_control_service_1.FAM_ExactDuplicatePrune_ControlService; } });
46
+ var fam_exact_duplicate_prune_util_1 = require("./_collections/fam-exact-duplicate-prune.util");
47
+ Object.defineProperty(exports, "FAM_ExactDuplicatePrune_Util", { enumerable: true, get: function () { return fam_exact_duplicate_prune_util_1.FAM_ExactDuplicatePrune_Util; } });
42
48
  // near-duplikátum FELDERÍTŐ (read-only; user-direktíva 2026-06-21)
43
49
  var fam_duplicate_scan_control_service_1 = require("./_services/fam-duplicate-scan.control-service");
44
50
  Object.defineProperty(exports, "FAM_DuplicateScan_ControlService", { enumerable: true, get: function () { return fam_duplicate_scan_control_service_1.FAM_DuplicateScan_ControlService; } });
@@ -415,6 +415,10 @@ class FAM_Ingest_ControlService {
415
415
  // A default display-relatív út a friss scan-gyökérhez igazodjon (FAM-REV-050) —
416
416
  // egy korábban más root-reprezentációval (pl. scan-file basename) ingestelt chunk konzisztenssé.
417
417
  await set.dataService.backfillSourceFilePath(item.existingId, set.file.relativePath);
418
+ // SCOPE-backfill (2026-09-01): a scope-vak fallback MÁS scope alatt álló chunkot is
419
+ // adoptál — enélkül az adoptált chunk a RÉGI projekt-név alatt ragadna, és a
420
+ // scope-szűrt keresés az ÚJ néven nem találná meg (egy hibát cserélnénk másikra).
421
+ await set.dataService.backfillScopePath(item.existingId, set.scopePath);
418
422
  if (item.chunk) {
419
423
  await set.dataService.backfillReferenceCodes(item.existingId, fam_reference_code_util_1.FAM_ReferenceCode_Util.extract(item.chunk.content));
420
424
  // Pozíció-backfill (FAM-REV-061): a chunker pozíció-fix a régi equal-chunkokra is,
@@ -743,7 +747,23 @@ class FAM_Ingest_ControlService {
743
747
  const leaf = scopePath[scopePath.length - 1];
744
748
  filter['scopePath.scopeId'] = leaf.scopeId;
745
749
  }
746
- return dataService.findHydratableList(filter);
750
+ const scoped = await dataService.findHydratableList(filter);
751
+ if (scoped.length || !scopePath.length) {
752
+ return scoped;
753
+ }
754
+ // SCOPE-VAK FALLBACK (2026-09-01, mert gyoker-ok). A fajl IDENTITASA az abszolut utvonal — a scope csak
755
+ // besorolas. Ha ugyanaz a mappa MAS projekt-nev alatt kerul ujra-szkennelesre (atnevezes utan a heti
756
+ // re-scan cel-unioja MINDKET nevet megorzi, es ugyanazt a mappat KETSZER szkenneli), akkor a scope-ra
757
+ // szurt lekerdezes URESET ad -> minden chunk `new`-nak latszik -> TELJES DUPLIKALAS, minden heten.
758
+ //
759
+ // MERVE: 51 578 dupla-csoport, mind KULONBOZO scopeId alatt; a legfrissebb eset 2026-08-31 09:32, amikor
760
+ // a `fdp-e2e-full/package.json` egyetlen scan-futasban ketszer kerult be (`futdevpro-e2e` 08:39 es
761
+ // `fdp-e2e-full` 09:32). Osszesen ~59 000 folosleges chunk.
762
+ //
763
+ // Ezert: ha a scope-olt talalat URES, meg egyszer keresunk KIZAROLAG az utvonalra. Ami igy elokerul, az
764
+ // UGYANANNAK a fajlnak a korabbi feldolgozasa — a delta-compare igy `equal`/`modified`-nak latja
765
+ // (adoptalja), nem duplikal. Ha a scope tenyleg valtozott, a `backfill`/update agak vezetik at.
766
+ return dataService.findHydratableList({ 'source.absolutePath': absolutePath });
747
767
  }
748
768
  /**
749
769
  * Törölt-fájl reconciliation (whole-file orphan, dsgn-004 §4.3 kiterjesztés). A scan-folder/scan-project
@@ -480,6 +480,59 @@ class FAM_ScanJob_ControlService {
480
480
  * útvonalak a HÍVÓ `sourceLocation`-jéhez oldódnak fel (a szerver cwd-je eltér a CLI-étől — user-FR 2026-06-22):
481
481
  * a `configPath`/`root` ÉS a végső per-projekt `path` is.
482
482
  */
483
+ /**
484
+ * UTVONAL-SZERINTI DEDUP a cel-listan (2026-09-01, mert gyoker-ok). Ha UGYANAZ a mappa TOBB projekt-nev alatt
485
+ * szerepel, a scan ugyanazt a fajlt tobbszor olvassa be — es mivel a delta-osszevetes scope-ra szurt, a ket
486
+ * menet VAK egymasra, igy TELJES DUPLIKALAS keletkezik.
487
+ *
488
+ * MERVE: az utolso heti scan (2026-08-31) a `fdp-e2e-full/package.json`-t KETSZER vitte be, ket nev alatt
489
+ * (`futdevpro-e2e` 08:39 es `fdp-e2e-full` 09:32). Flotta-szinten 51 578 dupla-csoport, MIND kulonbozo
490
+ * scopeId alatt, osszesen ~59 000 folosleges chunk. Az ok: a heti re-scan a KORABBAN szkennelt celok
491
+ * UNIOJAT hasznalja, igy egy projekt-atnevezes utan a REGI es az UJ nev is bent marad — orokre.
492
+ *
493
+ * Az ELSO elofordulas nyer (a hivo altal megadott sorrend/az ujabb felderites elol all). A kihagyast
494
+ * LOGOLJUK — nema szures nem lehet (`core-rule-integrity` szelleme: ami kimarad, az latszodjon).
495
+ */
496
+ /** A projekt-név megegyezik-e a mappa nevével (a dedup döntőbírója — a lemez az igazság). */
497
+ static matchesFolderName(spec) {
498
+ const folder = String(spec.path ?? '')
499
+ .replace(/[\\/]+/g, '/')
500
+ .replace(/\/+$/, '')
501
+ .split('/')
502
+ .pop() ?? '';
503
+ return folder.length > 0 && folder.toLowerCase() === String(spec.project ?? '').toLowerCase();
504
+ }
505
+ static dedupeByPath(specs) {
506
+ const seen = new Map();
507
+ const dropped = [];
508
+ for (const spec of specs) {
509
+ // Az elvalasztot NORMALIZALJUK (a `\\` es a `/` ugyanaz a mappa Windowson), a zaro elvalasztot
510
+ // levagjuk, es kisbetusitunk — kulonben ugyanaz a cel ket irasmodban ketszer futna le.
511
+ const key = String(spec.path ?? '')
512
+ .replace(/[\\/]+/g, '/')
513
+ .replace(/\/+$/, '')
514
+ .toLowerCase();
515
+ const kept = seen.get(key);
516
+ if (kept) {
517
+ // DÖNTŐBÍRÓ: az a projekt-név nyer, ami a MAPPA NEVÉVEL egyezik. Átnevezés után a heti
518
+ // re-scan cél-uniója a RÉGI nevet is viszi, és az jellemzően előrébb áll — „első nyer"
519
+ // alapon a leváltott név maradna örökre. A mappa neve viszont a lemezen álló IGAZSÁG.
520
+ const winner = FAM_ScanJob_ControlService.matchesFolderName(spec)
521
+ && !FAM_ScanJob_ControlService.matchesFolderName(kept) ? spec : kept;
522
+ const loser = winner === spec ? kept : spec;
523
+ dropped.push({ project: loser.project, keptAs: winner.project, path: spec.path });
524
+ seen.set(key, winner);
525
+ continue;
526
+ }
527
+ seen.set(key, spec);
528
+ }
529
+ if (dropped.length) {
530
+ fsm_dynamo_1.DyFM_Log.warn(`[scanJob] UTVONAL-DEDUP: ${dropped.length} cel kihagyva, mert ugyanarra a mappara mutat, `
531
+ + `mint egy masik projekt-nev (ez okozta a heti dupla-beolvasast): `
532
+ + dropped.map((d) => `${d.project} -> ${d.keptAs}`).join(', '));
533
+ }
534
+ return [...seen.values()];
535
+ }
483
536
  static resolveSpecs(input) {
484
537
  const base = (target) => (target && input.sourceLocation && !(0, path_1.isAbsolute)(target)) ? (0, path_1.resolve)(input.sourceLocation, target) : target;
485
538
  let specs = [];
@@ -497,7 +550,8 @@ class FAM_ScanJob_ControlService {
497
550
  specs = [...specs, ...input.extraTargets];
498
551
  }
499
552
  // A per-projekt `path` relatív → a sourceLocation-höz feloldva (a discover már abszolútat ad; a config relatív lehet).
500
- return specs.map((spec) => ({ ...spec, path: base(spec.path) ?? spec.path }));
553
+ const resolved = specs.map((spec) => ({ ...spec, path: base(spec.path) ?? spec.path }));
554
+ return FAM_ScanJob_ControlService.dedupeByPath(resolved);
501
555
  }
502
556
  /** Egy szám-config feloldása (a hiba/hiány a fallback-ra esik — a cooldown sosem buktatja a job-ot). */
503
557
  static async resolveConfigNumber(key, fallback) {
@@ -4,6 +4,7 @@ exports.FAM_ScanScheduler_ControlService = void 0;
4
4
  const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
5
  const fam_config_level_type_enum_1 = require("../../../_enums/fam-config-level.type-enum");
6
6
  const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
7
+ const retrieval_1 = require("../../retrieval");
7
8
  const fam_scan_job_control_service_1 = require("./fam-scan-job.control-service");
8
9
  const fam_scan_job_data_service_1 = require("./fam-scan-job.data-service");
9
10
  const embedding_1 = require("../../embedding");
@@ -120,6 +121,10 @@ class FAM_ScanScheduler_ControlService {
120
121
  + `(slot: ${FAM_ScanScheduler_ControlService.DAY_NAMES[day] ?? day} ${hour}:00). Indítás…`);
121
122
  await this.markRan(config, nowMs);
122
123
  await this.triggerRescan();
124
+ // RETENCIÓ (2026-09-02): a heti ütemben az alvó törmelék nyugdíjazása is fut — a lényege az
125
+ // automatizmus (különben a törmelék sosem vonul nyugdíjba). A scan-tól FÜGGETLEN munka, nem
126
+ // várja meg (a scan async, waitMs:0); best-effort: a hibája nem buktatja a ticket.
127
+ await this.runWeeklyRetention();
123
128
  }
124
129
  catch (error) {
125
130
  fsm_dynamo_1.DyFM_Log.warn(`[FAM scanScheduler] heti re-scan tick sikertelen (best-effort): ${error?.message}`);
@@ -128,6 +133,27 @@ class FAM_ScanScheduler_ControlService {
128
133
  this.ticking = false;
129
134
  }
130
135
  }
136
+ /**
137
+ * A heti retenció: az alvó törmelék (`user_prompt`/`temporary_context`, sosem felidézett, 30+ napos)
138
+ * nyugdíjazása. Kapu: `memory.retireTemporaryEnabled` (default true). A szabály + indoklás:
139
+ * `retrieval/_collections/fam-memory-retention.util.ts`. Best-effort — a hiba WARN, nem tick-bukás.
140
+ */
141
+ async runWeeklyRetention() {
142
+ try {
143
+ const retention = retrieval_1.FAM_MemoryRetention_ControlService.getInstance();
144
+ if (!(await retention.isAutoEnabled())) {
145
+ fsm_dynamo_1.DyFM_Log.log('[FAM scanScheduler] heti retenció KIKAPCSOLVA (`memory.retireTemporaryEnabled=false`) — kihagyva.');
146
+ return;
147
+ }
148
+ const report = await retention.retire({ dryRun: false });
149
+ fsm_dynamo_1.DyFM_Log.log(`[FAM scanScheduler] heti retenció kész: ${report.retired} nyugdíjazva `
150
+ + `(${report.scanned} jelöltből; ${report.keptTooYoung} még fiatal, `
151
+ + `${report.alreadySuperseded} már nyugdíjas).`);
152
+ }
153
+ catch (error) {
154
+ fsm_dynamo_1.DyFM_Log.warn(`[FAM scanScheduler] heti retenció sikertelen (best-effort): ${error?.message}`);
155
+ }
156
+ }
131
157
  /** A TELJES re-scan indítása: `scan.weeklyRescanRoot` (ha van) VAGY a legutóbbi durable scan-job cél-specjei. */
132
158
  async triggerRescan() {
133
159
  const origin = { agent: 'fam-scheduler', label: 'weekly-rescan' };
@@ -66,6 +66,17 @@ const READ_INPUT_SCHEMA = {
66
66
  description: 'Feltárás-mélység: `deep` → mély feltárás (sokkal több chunk; a `read.deepTopK` '
67
67
  + 'az alap topK helyett). Default `normal`. Az explicit `topK` mindig felülír.',
68
68
  },
69
+ skipReactivation: {
70
+ type: 'boolean',
71
+ description: 'DIAGNOSZTIKA: a visszaadott memory-hitek NE kapjanak felidézés-megerősítést '
72
+ + '(recallCount++). Mérő-/kalibrációs forgalomhoz — a mérés nem terhelheti a mért rendszert.',
73
+ },
74
+ includeCrossTableProbe: {
75
+ type: 'boolean',
76
+ description: 'DIAGNOSZTIKA (nem termék-funkció): a kereszt-tár szonda nyers döntési adata '
77
+ + '(`crossTableProbe`) akkor is kerüljön a válaszba, ha a küszöb alatt maradt — a '
78
+ + 'küszöb-kalibráció csak így mérhető a rendszer VALÓS viselkedésén. A `hits`-hez nem nyúl.',
79
+ },
69
80
  },
70
81
  required: ['tables', 'query'],
71
82
  additionalProperties: false,
@@ -319,6 +319,8 @@ class FAM_ReadTool_Service {
319
319
  kindFilter: query.kindFilter,
320
320
  excludeIds: query.excludeIds,
321
321
  depth: query.depth,
322
+ includeCrossTableProbe: query.includeCrossTableProbe,
323
+ skipReactivation: query.skipReactivation,
322
324
  // Per-query tartalom-kapcsolo atvezetese (2026-08-14 lelet: eddig itt VESZETT EL).
323
325
  includeContent: query.includeContent,
324
326
  };
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_WriteTool_Service = void 0;
4
4
  const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
+ const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
5
6
  const error_codes_const_1 = require("../../../_collections/error-codes.const");
6
7
  const fam_error_factory_util_1 = require("../../../_collections/fam-error-factory.util");
7
8
  const fam_entry_data_model_1 = require("../../../_models/data-models/fam-entry.data-model");
@@ -333,19 +334,40 @@ class FAM_WriteTool_Service {
333
334
  // FAM-REV-051 bulk: `sourceFilePath`/`absolutePath` → egy fájl ÖSSZES chunkja; `id`/`query` → egyetlen.
334
335
  const targets = await this.resolveDeleteTargets(input, dataService, 'write-delete');
335
336
  const deletedIds = [];
337
+ // RETIRE-NOT-DELETE a `memory` táron (2026-09-02, owner-elv: „nem baj, ha fennmarad olyan memória,
338
+ // amit a Claude már elfelejtett — sőt!"). A memória-törlés NYUGDÍJAZÁS: `superseded + retiredAt`,
339
+ // a tartalom és a vektor marad (a penalty süllyeszti, a cold-search megtalálja). Más táron a
340
+ // viselkedés VÁLTOZATLAN — ott a fájl az igazság, a scan a törölt chunkot úgyis újra behozná.
341
+ // Valódi hard-delete-re a scope-maintenance reset az admin-út.
342
+ const retireInsteadOfDelete = input.table === fam_table_type_enum_1.FAM_Table.memory;
343
+ const now = Date.now();
336
344
  for (const entry of targets) {
337
345
  if (!entry._id) {
338
346
  continue;
339
347
  }
340
- // absolute=false → soft-delete (Dynamo-konvenció: `_deleted` marker, archív collection).
341
- await dataService.deleteData(entry._id, false);
342
- // Pool-coherence (FAM-REV-046): a törölt vektort a hívó-process in-memory LVS-pool-jából is kivesszük.
343
- embedding_1.FAM_VectorSearch_ControlService.getInstance().removeVector(input.table, entry._id);
348
+ if (retireInsteadOfDelete) {
349
+ await dataService.updateData({
350
+ filterBy: { _id: entry._id },
351
+ update: { $set: { superseded: true, retiredAt: now } },
352
+ });
353
+ // A vektor a poolban MARAD — a superseded-szemantika része (a rangsor-büntetés süllyeszti).
354
+ }
355
+ else {
356
+ // absolute=false → soft-delete (Dynamo-konvenció: `_deleted` marker, archív collection).
357
+ await dataService.deleteData(entry._id, false);
358
+ // Pool-coherence (FAM-REV-046): a törölt vektort a hívó-process in-memory LVS-pool-jából is kivesszük.
359
+ embedding_1.FAM_VectorSearch_ControlService.getInstance().removeVector(input.table, entry._id);
360
+ }
344
361
  deletedIds.push(entry._id);
345
362
  }
346
- const warnings = deletedIds.length > 1
347
- ? [`Bulk file-delete: ${deletedIds.length} chunk soft-delete-elve (cél: ${input.target?.by}='${input.target?.value}').`]
348
- : [];
363
+ const warnings = [];
364
+ if (retireInsteadOfDelete && deletedIds.length) {
365
+ warnings.push(`A memory táron a törlés NYUGDÍJAZÁS: ${deletedIds.length} bejegyzés superseded+retiredAt `
366
+ + 'jelölést kapott — hidegen kereshető marad, nem törlődött (owner-elv: a felejtett memória is érték).');
367
+ }
368
+ else if (deletedIds.length > 1) {
369
+ warnings.push(`Bulk file-delete: ${deletedIds.length} chunk soft-delete-elve (cél: ${input.target?.by}='${input.target?.value}').`);
370
+ }
349
371
  return this.baseOutput({
350
372
  operation: input.operation, table: input.table,
351
373
  scopePath: (targets[0]?.scopePath ?? []),
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_CrossTableNote_Util = void 0;
4
+ const fam_memory_retention_util_1 = require("./fam-memory-retention.util");
5
+ /**
6
+ * NEM-TUDÁS jelöltek: nyers beszélgetés-törmelék, amivel SOHA nem szabad „mellesleg"-et adni.
7
+ *
8
+ * **Mért indok (2026-09-01):** az aktív `memory` tár **20,2%-a (8 717 bejegyzés) `kind: user_prompt` /
9
+ * `tags: [temporary_context]`** — szó szerinti user-input, amit a session tárolt el kontextusnak. Ezek
10
+ * rövidek és tematikusak, ezért a nyers koszinuszuk MAGASRA fut, és a 44 esetes benchmarkban a 12
11
+ * legerősebb jelöltből **10 ilyen volt**. Egy nyers prompt-idézet nem válasz a „láttuk már ezt?"
12
+ * kérdésre — a javaslat tőle nem hasznos, hanem félrevezető.
13
+ *
14
+ * Ugyanezen az alapon esik ki a **`fs-summary` / `scan-summary`** is: az egy mappa-listázás gépi
15
+ * összefoglalója. A benchmarkban pont a döntési zónában ült (+0,022), és semmilyen kérdésre nem válasz.
16
+ * (A scan-summary zaja külön is mért, ismert probléma — `reference_fam_scan_summary_weight_cap`.)
17
+ */
18
+ // A MEMORIA-tormelek definicioja a KOZOS utilbol jon (fam-memory-retention.util) — egy definicio,
19
+ // harom felhasznalas (retencio + reaktivacio-szuro + kereszt-tar szonda). Itt CSAK a knowledge-tar
20
+ // sajat zaja (fs-summary/scan-summary) egeszul ki.
21
+ const NON_KNOWLEDGE_KINDS = ['fs-summary'];
22
+ const NON_KNOWLEDGE_TAGS = ['scan-summary'];
23
+ class FAM_CrossTableNote_Util {
24
+ /**
25
+ * A döntés. `undefined` = **csend**: nincs mező a válaszban. Szándékosan NEM adunk „nem találtam
26
+ * erősebbet" üzenetet — az minden válaszra ráterhelne egy sort azért, hogy semmit ne közöljön.
27
+ */
28
+ static decide(input) {
29
+ const usable = input.candidates.filter((candidate) => candidate.superseded !== true
30
+ && !FAM_CrossTableNote_Util.isNonKnowledge(candidate)
31
+ && Number.isFinite(candidate.score)
32
+ && candidate.score >= input.minScore);
33
+ if (!usable.length || !(input.minDelta > 0)) {
34
+ return undefined;
35
+ }
36
+ // LEGFELJEBB EGY tétel: a mérés szerint úgyis 0-1 van, és egy 3-elemű mellék-lista már hígítás
37
+ // — pontosan az a zaj, ami miatt nem a `hits`-be tesszük.
38
+ const best = [...usable].sort((a, b) => b.score - a.score)[0];
39
+ const delta = best.score - input.primaryTopScore;
40
+ if (delta < input.minDelta) {
41
+ return undefined;
42
+ }
43
+ const heading = best.headingPath?.length ? best.headingPath.join(' › ') : undefined;
44
+ return {
45
+ table: best.table,
46
+ delta: Number(delta.toFixed(4)),
47
+ hit: {
48
+ id: best.id,
49
+ table: best.table,
50
+ score: best.score,
51
+ sourceFilePath: best.sourceFilePath,
52
+ heading: heading,
53
+ label: FAM_CrossTableNote_Util.label(best),
54
+ },
55
+ message: FAM_CrossTableNote_Util.message({
56
+ table: best.table,
57
+ candidateScore: best.score,
58
+ primaryTopScore: input.primaryTopScore,
59
+ location: FAM_CrossTableNote_Util.label(best),
60
+ }),
61
+ };
62
+ }
63
+ /** Beszélgetés-törmelék / gépi zaj-e a jelölt. A memória-törmelék részt a közös util dönti el. */
64
+ static isNonKnowledge(candidate) {
65
+ if (fam_memory_retention_util_1.FAM_MemoryRetention_Util.isDebris({ kind: candidate.kind, tags: candidate.tags })) {
66
+ return true;
67
+ }
68
+ if (candidate.kind && NON_KNOWLEDGE_KINDS.includes(candidate.kind)) {
69
+ return true;
70
+ }
71
+ return (candidate.tags ?? []).some((tag) => NON_KNOWLEDGE_TAGS.includes(tag));
72
+ }
73
+ /**
74
+ * A jelölt AZONOSÍTHATÓ megnevezése. A fájl-út a legjobb, de az MCP-n ÍRT memória-bejegyzésnek nincs
75
+ * (mérve: a 21 szonda-jelöltből 16-nak). Ott a nyers ObjectId semmit nem mond a hívónak — a `kind` +
76
+ * `tags` + `scope` viszont pontosan megmondja, MIRŐL szól a jegyzet, és hogy érdemes-e megnyitni.
77
+ */
78
+ static label(candidate) {
79
+ if (candidate.sourceFilePath) {
80
+ return candidate.sourceFilePath;
81
+ }
82
+ if (candidate.headingPath?.length) {
83
+ return candidate.headingPath.join(' › ');
84
+ }
85
+ const parts = [];
86
+ if (candidate.kind) {
87
+ parts.push(candidate.kind);
88
+ }
89
+ if (candidate.tags?.length) {
90
+ parts.push(`[${candidate.tags.slice(0, 4).join(', ')}]`);
91
+ }
92
+ if (candidate.scope) {
93
+ parts.push(`(${candidate.scope})`);
94
+ }
95
+ // Az `id` MINDIG ott van a `hit.id`-ben — a szövegbe csak akkor kerül, ha tényleg nincs jobb.
96
+ return parts.length ? `${parts.join(' ')} — id: ${candidate.id}` : `id: ${candidate.id}`;
97
+ }
98
+ /**
99
+ * A szonda nyers döntési adata (diagnosztika). A `superseded` jelölt SZÁNDÉKOSAN benne marad a
100
+ * legjobb-választásban itt — a mérésnek látnia kell, ha a szonda amúgy egy leváltott generációt
101
+ * hozott volna; a `decide` ettől függetlenül kizárja.
102
+ */
103
+ static describeProbe(primaryTopScore, candidates, fired) {
104
+ // A diagnosztika a KÜSZÖB-független képet adja, de a TARTALMI kizárásokat (leváltott generáció,
105
+ // beszélgetés-törmelék) ALKALMAZZA — különben a kalibráció olyan jelöltre számolna deltát, amit a
106
+ // rendszer sosem ajánlana, és megint nem azt mérnénk, ami ténylegesen történik.
107
+ const eligible = candidates.filter((candidate) => candidate.superseded !== true
108
+ && !FAM_CrossTableNote_Util.isNonKnowledge(candidate)
109
+ && Number.isFinite(candidate.score));
110
+ const best = [...eligible]
111
+ .sort((a, b) => b.score - a.score)[0];
112
+ return {
113
+ primaryTopScore: primaryTopScore,
114
+ candidateCount: candidates.length,
115
+ eligibleCount: eligible.length,
116
+ best: best ? {
117
+ table: best.table,
118
+ score: best.score,
119
+ delta: Number((best.score - primaryTopScore).toFixed(4)),
120
+ label: FAM_CrossTableNote_Util.label(best),
121
+ superseded: best.superseded,
122
+ } : undefined,
123
+ fired: fired,
124
+ };
125
+ }
126
+ /** Az ember-olvasható mondat. Megmondja, MI van ott és MIÉRT szólunk — a hívó dönt, mi lesz vele. */
127
+ static message(set) {
128
+ const comparison = set.primaryTopScore > 0
129
+ ? `${set.candidateScore.toFixed(3)} vs ${set.primaryTopScore.toFixed(3)}`
130
+ : `${set.candidateScore.toFixed(3)}, miközben a kért tárban nem volt érdemi találat`;
131
+ return `Mellesleg: a(z) \`${set.table}\` tárban ÉRDEMBEN erősebb találat van erre (${comparison}) — `
132
+ + `${set.location}. A fenti listát ez NEM módosította; ha a kérdés inkább „láttuk-e már ezt", `
133
+ + 'érdemes lehet oda is belenézni.';
134
+ }
135
+ }
136
+ exports.FAM_CrossTableNote_Util = FAM_CrossTableNote_Util;
@@ -31,6 +31,25 @@ class FAM_LexicalMatch_Util {
31
31
  ];
32
32
  /** A tő MINIMÁLIS hossza tövezés után (a túl-tövezés hamis egyezéseket hozna). */
33
33
  static MIN_STEM_LEN = 4;
34
+ /**
35
+ * SZÓ SZERINTI IDÉZET-EGYEZÉS (2026-09-02, black-box futás lelete). A term-átfedés egy HALMAZ-arány:
36
+ * nem tudja megkülönböztetni a „ugyanazok a szavak szétszórva" esetet a „pontosan ez a mondat van benne"
37
+ * esettől — pedig az utóbbi nagyságrenddel erősebb relevancia-jel.
38
+ *
39
+ * **Mért eset:** a `402 Payment Required - You must sign up for private packages` hibaüzenet szó szerinti
40
+ * bemásolása (a legtermészetesebb fejlesztői reflex) a `fdp-never-public-packages` szabályt a **#7**
41
+ * helyre tette 60 találatból — holott a szabály-fájl **literálisan tartalmazza ezt a mondatot**, és övé
42
+ * volt a mezőny LEGMAGASABB term-átfedése (0,325). A nyers koszinusza viszont csak 0,480 volt (egy
43
+ * kontextus nélküli hibaüzenet gyenge szemantikus jel), és a ×1,195-ös boost nem hozta fel.
44
+ *
45
+ * A phrase-bónusz ezt a hibaosztályt fedi — bemásolt hibaüzenet, idézett mondat, pontos azonosító —, és
46
+ * a PARAFRAZEÁLT kérdéseket NEM érinti: ott nincs hosszú, szó szerinti egyezés, tehát nincs bónusz sem.
47
+ */
48
+ static MIN_PHRASE_LEN = 3;
49
+ /** A phrase-bónusz maximuma (a teljes query-hossznyi szó szerinti egyezésre jár). */
50
+ static PHRASE_BONUS = 0.6;
51
+ /** A vizsgált query-tokenek plafonja — egy hosszú prompt ne csináljon O(n²) munkát hit-enként. */
52
+ static MAX_PHRASE_TOKENS = 24;
34
53
  /** Tokenizálás: kisbetűsít, nem-alfanumerikuson tör, stopword + rövid token kihagyva, DEDUP (Set). */
35
54
  static tokenize(text) {
36
55
  const terms = new Set();
@@ -62,12 +81,53 @@ class FAM_LexicalMatch_Util {
62
81
  }
63
82
  return undefined;
64
83
  }
84
+ /**
85
+ * NYERS, SORRENDHELYES token-sorozat (a phrase-egyezéshez). A `tokenize`-zal ellentétben **NEM** szűr
86
+ * stopwordöt és **NEM** dedupál: a szó szerinti idézetben a kötőszavak is a mondat részei — pont azok
87
+ * teszik idézetté. A rövid tokenek (`MIN_TOKEN_LEN` alatt) itt is kimaradnak, mert a normalizált
88
+ * szövegben sem szerepelnek.
89
+ */
90
+ static tokenizeSequence(text) {
91
+ if (!text) {
92
+ return [];
93
+ }
94
+ return text.toLowerCase()
95
+ .split(/[^a-z0-9áéíóöőúüű]+/)
96
+ .filter((token) => token.length >= FAM_LexicalMatch_Util.MIN_TOKEN_LEN);
97
+ }
98
+ /** Az entry kereshető szövegének normalizált alakja (kisbetűs, egy-szóközös) — a phrase-kereséshez. */
99
+ static normalizeForPhrase(text) {
100
+ if (!text) {
101
+ return '';
102
+ }
103
+ return ` ${text.toLowerCase().split(/[^a-z0-9áéíóöőúüű]+/).filter(Boolean).join(' ')} `;
104
+ }
105
+ /**
106
+ * A leghosszabb SZÓ SZERINTI, összefüggő query-részlet hossza, ami az entry szövegében is megvan
107
+ * (0, ha nincs `MIN_PHRASE_LEN`-nél hosszabb egyezés). A leghosszabb ablaktól indul és az ELSŐ
108
+ * egyezésnél visszatér — így a tipikus „nincs idézet" eset is korlátos munkával zárul.
109
+ */
110
+ static longestPhraseRun(queryTokens, normalizedEntryText) {
111
+ const tokens = queryTokens.slice(0, FAM_LexicalMatch_Util.MAX_PHRASE_TOKENS);
112
+ if (tokens.length < FAM_LexicalMatch_Util.MIN_PHRASE_LEN || !normalizedEntryText) {
113
+ return 0;
114
+ }
115
+ for (let length = tokens.length; length >= FAM_LexicalMatch_Util.MIN_PHRASE_LEN; length--) {
116
+ for (let start = 0; start + length <= tokens.length; start++) {
117
+ const phrase = ` ${tokens.slice(start, start + length).join(' ')} `;
118
+ if (normalizedEntryText.includes(phrase)) {
119
+ return length;
120
+ }
121
+ }
122
+ }
123
+ return 0;
124
+ }
65
125
  /**
66
126
  * Lexikai átfedés-score (0..1): a `queryTerms` hányad része van jelen az entry kereshető szövegében.
67
127
  * `content`-egyezés = base-átfedés; `headingPath`/`sourceFilePath`-egyezés = + `TITLE_PATH_BONUS` súly (1-re vágva).
68
128
  * Üres query-term-halmaz → 0 (nincs jel → nincs boost). PURE.
69
129
  */
70
- static score(queryTerms, entry) {
130
+ static score(queryTerms, entry, queryPhraseTokens) {
71
131
  if (!queryTerms.size) {
72
132
  return 0;
73
133
  }
@@ -94,7 +154,20 @@ class FAM_LexicalMatch_Util {
94
154
  const total = queryTerms.size;
95
155
  const base = baseMatched / total;
96
156
  const titleBonus = (titleMatched / total) * FAM_LexicalMatch_Util.TITLE_PATH_BONUS;
97
- return Math.min(1, base + titleBonus);
157
+ // SZÓ SZERINTI IDÉZET-BÓNUSZ: a query hány százaléka szerepel ÖSSZEFÜGGŐEN, szó szerint az entry
158
+ // szövegében. A halmaz-átfedés ezt nem látja (neki a szórt egyezés ugyanannyi) — pedig egy bemásolt
159
+ // hibaüzenet vagy idézett mondat majdnem biztos relevancia-jel. Az indoklás + a mért eset a
160
+ // `MIN_PHRASE_LEN` konstans doc-commentjében.
161
+ let phraseBonus = 0;
162
+ if (queryPhraseTokens && queryPhraseTokens.length) {
163
+ const searchable = FAM_LexicalMatch_Util.normalizeForPhrase(`${entry.content ?? ''} ${(entry.headingPath ?? []).join(' ')} ${entry.sourceFilePath ?? ''}`);
164
+ const run = FAM_LexicalMatch_Util.longestPhraseRun(queryPhraseTokens, searchable);
165
+ if (run > 0) {
166
+ const considered = Math.min(queryPhraseTokens.length, FAM_LexicalMatch_Util.MAX_PHRASE_TOKENS);
167
+ phraseBonus = FAM_LexicalMatch_Util.PHRASE_BONUS * (run / considered);
168
+ }
169
+ }
170
+ return Math.min(1, base + titleBonus + phraseBonus);
98
171
  }
99
172
  }
100
173
  exports.FAM_LexicalMatch_Util = FAM_LexicalMatch_Util;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ /**
3
+ * SZÓ SZERINTI IDÉZET-MENTÉS — a döntés, hogy egy query megéri-e a pontos szöveg-keresést (2026-09-02).
4
+ *
5
+ * **A mért hiba.** A black-box futás leletét külön ellenőrizve: a
6
+ * `402 Payment Required - You must sign up for private packages` hibaüzenet **szó szerinti bemásolása** —
7
+ * a legtermészetesebb fejlesztői reflex — a szabályt nem hozta be. Az első hipotézisem (rangsor-hiba) a
8
+ * mérésen **MEGBUKOTT**: a 60 visszaadott találatból **egy sem tartalmazta a „402"-t**, vagyis a mondatot
9
+ * hordozó chunk **be sem került a jelöltek közé**. Ez **recall-hiba**: egy kontextus nélküli hibaüzenet
10
+ * embeddingje távol esik a szabály-próza embeddingjétől, és amit a vektor-réteg nem hoz be, azt semmilyen
11
+ * rangsor-javítás nem érheti el.
12
+ *
13
+ * **A megoldás mintája MÁR LÉTEZIK:** a FEAT-003 kód-expanzió (`applyCodeExpansion`) pontosan ezt csinálja
14
+ * azonosító-kódokra — ha a query kódot tartalmaz, a citáló elemek determinisztikusan bekerülnek, a
15
+ * vektor-rangsortól függetlenül. Ez a util ugyanezt terjeszti ki a **szó szerinti idézetre**.
16
+ *
17
+ * **Miért kell szigorú kapu.** A mentés Mongo-oldali szöveg-keresés, ami a nagy tárakon collection-scan.
18
+ * Mérve (2026-09-02): `rules` 296 ms · `coding_patterns` 504 ms · `knowledge` 2,1 s · `memory` 4,9 s ·
19
+ * `documents` 13,1 s · `codebase` 45,2 s. Ezért a mentés (1) csak IDÉZET-SZERŰ query-re fut, és (2) csak
20
+ * a `read.literalRescueMaxDocs` alatti méretű tárakon.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.FAM_LiteralPhrase_Util = void 0;
24
+ /** A minimális token-szám: ennél rövidebb részlet nem idézet, hanem véletlen szó-egybeesés. */
25
+ const MIN_TOKENS = 5;
26
+ /** A maximális token-szám: a hosszú promptot levágjuk (a regex-költség és a találhatóság miatt is). */
27
+ const MAX_TOKENS = 16;
28
+ /**
29
+ * A „disztinktív" token minimális hossza. Egy csupa rövid, köznapi szóból álló mondat (`mi a teendo ha a
30
+ * build elszall`) nem idézet — az a normál szemantikus keresés dolga. Számjegyet tartalmazó token (hibakód,
31
+ * verzió, azonosító) MINDIG disztinktívnek számít.
32
+ */
33
+ const DISTINCTIVE_LEN = 7;
34
+ class FAM_LiteralPhrase_Util {
35
+ /**
36
+ * IDÉZET-SZERŰ-e a query, és ha igen, mit keressünk. `undefined` = nem éri meg a szöveg-keresés.
37
+ *
38
+ * **Ismert korlát, nyíltan:** a keresett szöveg a query ELEJÉTŐL vett, `MAX_TOKENS`-ig tartó összefüggő
39
+ * részlet — vagyis a TISZTÁN bemásolt idézetre működik. Ha a felhasználó saját mondatba ágyazza az
40
+ * idézetet („npm publish failed with 402 … what should I do"), a szó szerinti egyezés elmarad, és a
41
+ * mentés nem talál. Az a hibaosztály viszont **mérve nem is szorul mentésre**: ott a normál vektor-út
42
+ * behozza a szabályt (#2 helyezés). A mentés a lyukat foltozza, nem a működő utat írja át.
43
+ */
44
+ static extract(query) {
45
+ const tokens = String(query ?? '')
46
+ .toLowerCase()
47
+ .split(/[^a-z0-9áéíóöőúüű]+/)
48
+ .filter((token) => token.length >= 2);
49
+ if (tokens.length < MIN_TOKENS) {
50
+ return undefined;
51
+ }
52
+ const window = tokens.slice(0, MAX_TOKENS);
53
+ if (!window.some((token) => FAM_LiteralPhrase_Util.isDistinctive(token))) {
54
+ return undefined;
55
+ }
56
+ return { phrase: window.join(' '), tokenCount: window.length };
57
+ }
58
+ /** Disztinktív-e a token: számjegyet tartalmaz (hibakód/verzió/azonosító) VAGY elég hosszú. */
59
+ static isDistinctive(token) {
60
+ return /[0-9]/.test(token) || token.length >= DISTINCTIVE_LEN;
61
+ }
62
+ /**
63
+ * A szó szerinti kereséshez használt regex: a tokenek között TETSZŐLEGES nem-alfanumerikus elválasztó
64
+ * állhat, mert a korpuszban a mondat írásjelekkel, sortöréssel, backtickkel tagolt (`402 Payment
65
+ * Required — You must…`). A tokenek escape-elve — a query SOHA nem tud regexet injektálni.
66
+ */
67
+ static toRegexSource(phrase) {
68
+ return phrase
69
+ .split(' ')
70
+ .map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
71
+ .join('[^a-z0-9áéíóöőúüű]+');
72
+ }
73
+ }
74
+ exports.FAM_LiteralPhrase_Util = FAM_LiteralPhrase_Util;