@futdevpro/fdp-agent-memory 1.1.30 → 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 (81) hide show
  1. package/build/package.json +1 -1
  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 +148 -6
  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/_enums/fam-rule-scope.type-enum.js +25 -0
  21. package/build/src/_models/data-models/fam-entry.data-model.js +29 -0
  22. package/build/src/_models/data-models/fam-memory.data-model.js +5 -0
  23. package/build/src/_models/data-models/fam-rules.data-model.js +24 -0
  24. package/build/src/_models/data-models/fam-scan-job.data-model.js +73 -0
  25. package/build/src/_models/data-models/fam-scope.data-model.js +9 -0
  26. package/build/src/_modules/embedding/_services/fam-dedup-warn.control-service.js +40 -0
  27. package/build/src/_modules/embedding/_services/fam-duplicate-scan.control-service.js +74 -16
  28. package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +50 -0
  29. package/build/src/_modules/embedding/_services/fam-embedding-pipeline.control-service.js +31 -5
  30. package/build/src/_modules/embedding/_services/fam-embedding.control-service.js +142 -9
  31. package/build/src/_modules/embedding/_services/fam-entry.data-service.js +9 -0
  32. package/build/src/_modules/embedding/_services/fam-hydration-coordinator.control-service.js +159 -0
  33. package/build/src/_modules/embedding/_services/fam-lmstudio-embedding.provider.js +41 -4
  34. package/build/src/_modules/embedding/_services/fam-vector-search.control-service.js +67 -12
  35. package/build/src/_modules/embedding/index.js +5 -1
  36. package/build/src/_modules/ingest/_collections/fam-file-routing.util.js +10 -0
  37. package/build/src/_modules/ingest/_collections/fam-frontmatter.util.js +134 -0
  38. package/build/src/_modules/ingest/_collections/fam-project-identity.util.js +30 -0
  39. package/build/src/_modules/ingest/_collections/fam-scan-progress.util.js +5 -2
  40. package/build/src/_modules/ingest/_collections/fam-scan-summary.util.js +55 -29
  41. package/build/src/_modules/ingest/_collections/fam-split-chunker.util.js +139 -0
  42. package/build/src/_modules/ingest/_models/interfaces/fam-scan-job.interface.js +2 -0
  43. package/build/src/_modules/ingest/_services/fam-chunker.control-service.js +10 -0
  44. package/build/src/_modules/ingest/_services/fam-delta-compare.util.js +68 -1
  45. package/build/src/_modules/ingest/_services/fam-ingest-run.data-service.js +1 -1
  46. package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +173 -37
  47. package/build/src/_modules/ingest/_services/fam-scan-job.control-service.js +429 -0
  48. package/build/src/_modules/ingest/_services/fam-scan-job.data-service.js +34 -0
  49. package/build/src/_modules/ingest/_services/fam-scan.control-service.js +6 -4
  50. package/build/src/_modules/ingest/index.js +6 -1
  51. package/build/src/_modules/mcp/_services/fam-capability-registry.service.js +151 -8
  52. package/build/src/_modules/mcp/_services/fam-read-tool.service.js +70 -0
  53. package/build/src/_modules/mcp/_services/fam-write-tool.service.js +36 -0
  54. package/build/src/_modules/retrieval/_collections/fam-lexical-match.util.js +69 -0
  55. package/build/src/_modules/retrieval/_collections/fam-memory-activation.util.js +66 -0
  56. package/build/src/_modules/retrieval/_collections/fam-rule-injection.util.js +125 -0
  57. package/build/src/_modules/retrieval/_collections/fam-rule-propagation.util.js +95 -0
  58. package/build/src/_modules/retrieval/_collections/fam-rule-scope-context.util.js +74 -0
  59. package/build/src/_modules/retrieval/_services/fam-global-rule.control-service.js +82 -0
  60. package/build/src/_modules/retrieval/_services/fam-memory-cold-search.control-service.js +121 -0
  61. package/build/src/_modules/retrieval/_services/fam-memory-dormancy.control-service.js +99 -0
  62. package/build/src/_modules/retrieval/_services/fam-memory-reactivation.control-service.js +68 -0
  63. package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +15 -2
  64. package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +197 -5
  65. package/build/src/_modules/retrieval/_services/fam-rule-injection.control-service.js +102 -0
  66. package/build/src/_modules/retrieval/index.js +12 -1
  67. package/build/src/_modules/scope-reference/_collections/fam-scope-maintenance.util.js +76 -0
  68. package/build/src/_modules/scope-reference/_services/fam-scope-maintenance.control-service.js +233 -0
  69. package/build/src/_modules/scope-reference/_services/fam-scope.data-service.js +25 -0
  70. package/build/src/_routes/server/api/api.controller.js +270 -0
  71. package/build/src/_routes/server/api/fam-doctor.control-service.js +156 -0
  72. package/build/src/app.server.js +13 -10
  73. package/client-dist/{chunk-YXHWCJ5O.js → chunk-GF3FJP7E.js} +71 -71
  74. package/client-dist/chunk-NMUCMQY6.js +1 -0
  75. package/client-dist/favicon.ico +0 -0
  76. package/client-dist/index.html +3 -3
  77. package/client-dist/main-YK4YIVAQ.js +2 -0
  78. package/package.json +1 -1
  79. package/build/src/_cli/_collections/fam-project-discovery.util.js +0 -98
  80. package/client-dist/chunk-I77GXVAQ.js +0 -1
  81. 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() {
@@ -433,6 +478,8 @@ class FAM_CapabilityRegistry_Service {
433
478
  intoName: { type: 'string', description: 'A cél scope neve (scopeId helyett).' },
434
479
  intoLayer: { type: 'string', description: 'Opcionális cél-layer az egyértelműsítéshez.' },
435
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.' },
436
483
  }),
437
484
  handler: async (args) => {
438
485
  const input = this.asObject(args);
@@ -440,6 +487,86 @@ class FAM_CapabilityRegistry_Service {
440
487
  from: { scopeId: this.asString(input.fromScopeId), canonicalName: this.asString(input.fromName), layer: this.asString(input.fromLayer) },
441
488
  into: { scopeId: this.asString(input.intoScopeId), canonicalName: this.asString(input.intoName), layer: this.asString(input.intoLayer) },
442
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,
443
570
  }));
444
571
  },
445
572
  });
@@ -465,16 +592,18 @@ class FAM_CapabilityRegistry_Service {
465
592
  this.register({
466
593
  name: 'deduplicate_entries', category: category,
467
594
  description: 'Duplikált entry-k AUDITja: contentHash-alapú dup-csoportok feltárása (a megtartandó + a '
468
- + '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) '
469
597
  + 'BACKLOG — `dryRun:false` → clear BACKLOG-hiba (vak destruktív op TILOS).',
470
598
  inputSchema: this.objectSchema({
471
599
  table: this.tableSchema(false),
600
+ maxGroups: { type: 'number', description: 'A visszaadott dup-csoportok max száma (default 500; a legnagyobbak elöl).' },
472
601
  dryRun: { type: 'boolean', description: 'MVP1: mindig true (az összevonás BACKLOG). false → BACKLOG-hiba.' },
473
602
  }),
474
603
  handler: async (args) => {
475
604
  const input = this.asObject(args);
476
605
  this.assertDryRunOnly(input.dryRun, 'deduplicate_entries', 'duplikátum-összevonás (soft-delete)');
477
- 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)));
478
607
  },
479
608
  });
480
609
  }
@@ -1021,7 +1150,8 @@ class FAM_CapabilityRegistry_Service {
1021
1150
  .map((ref) => ref.scopeId)
1022
1151
  .filter((scopeId) => scopeId && !activeScopeIds.has(scopeId));
1023
1152
  if (missing.length && entry._id) {
1024
- 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 });
1025
1155
  }
1026
1156
  }
1027
1157
  }
@@ -1032,7 +1162,8 @@ class FAM_CapabilityRegistry_Service {
1032
1162
  * (legrégebbi `__created`) + a beolvasztható jelöltek. NEM ír — csak riportál (mit VONNA ÖSSZE). A
1033
1163
  * tényleges összevonás (soft-delete) BACKLOG.
1034
1164
  */
1035
- async deduplicateEntriesDryRun(table) {
1165
+ async deduplicateEntriesDryRun(table, maxGroups) {
1166
+ const groupCap = maxGroups ?? 500;
1036
1167
  const tables = table ? [table] : MAIN_TABLES;
1037
1168
  let scannedEntries = 0;
1038
1169
  const duplicateGroups = [];
@@ -1052,14 +1183,26 @@ class FAM_CapabilityRegistry_Service {
1052
1183
  if (group.length < 2) {
1053
1184
  continue;
1054
1185
  }
1055
- // 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).
1056
1189
  const sorted = group.slice().sort((a, b) => this.createdMs(a) - this.createdMs(b));
1057
- const keepId = sorted[0]._id ?? '';
1058
- 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);
1059
1192
  duplicateGroups.push({ table: target, contentHash: contentHash, keepId: keepId, mergeIds: mergeIds });
1060
1193
  }
1061
1194
  }
1062
- 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
+ };
1063
1206
  }
1064
1207
  // =========================================================================
1065
1208
  // MP-12 debug: explain-search / inspect-chunk / inspect-embedding
@@ -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
@@ -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
  };
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_LexicalMatch_Util = void 0;
4
+ /**
5
+ * `FAM_LexicalMatch_Util` (Hybrid Lexical Retrieval, Improvement 2) — **pure** lexikai (keyword/term-overlap) jel a
6
+ * retrieval-rangsorhoz. A tiszta-vektor `read` ELVÉTI a literal-keyword választ (eval-lelet: a „port naming" memória
7
+ * kiesett a top-12-ből, a `grep` azonnal hozta), mert az entry nyers koszinusza alacsony a query-megfogalmazásra.
8
+ * Ez a util a query DISZTINKTÍV term-jeinek átfedését méri az entry kereshető szövegével (content + heading + forrás-út),
9
+ * 0..1 skálán — a `toHit` ezt **multiplikatívan, BOOST-only** fűzi a finalScore-ba (`× (1 + keywordWeight × lexical)`),
10
+ * így a literal-match felszínre kerül, de a tiszta-szemantikus találat SOHA nem süllyed (lexical 0 → ×1).
11
+ */
12
+ class FAM_LexicalMatch_Util {
13
+ /** A heading/forrás-út egyezés extra súlya (erős „erről szól" jel): a base-átfedés MELLÉ adódik, 1-re vágva. */
14
+ static TITLE_PATH_BONUS = 0.3;
15
+ /** Minimális token-hossz (az 1-betűs/számjegyes zaj kihagyva). */
16
+ static MIN_TOKEN_LEN = 2;
17
+ /** Tömör stopword-szett (EN + HU — a korpusz Hunglish) — ezek NEM disztinktívek, kihagyva a query-termekből. */
18
+ static STOPWORDS = new Set([
19
+ // EN
20
+ 'the', 'a', 'an', 'of', 'to', 'in', 'on', 'at', 'by', 'for', 'and', 'or', 'is', 'are', 'be', 'as', 'we',
21
+ 'do', 'how', 'what', 'with', 'from', 'this', 'that', 'it', 'its', 'our', 'your', 'can', 'not', 'no', 'so',
22
+ 'if', 'then', 'else', 'into', 'out', 'up', 'down', 'about', 'which', 'who', 'when', 'where', 'why', 'use',
23
+ // HU
24
+ 'az', 'és', 'es', 'hogy', 'van', 'egy', 'nem', 'igen', 'de', 'mert', 'ami', 'amit', 'ez', 'azt', 'volt',
25
+ 'lesz', 'kell', 'meg', 'el', 'be', 'ki', 'le', 'fel', 'rá', 'ra', 'mi', 'te', 'ő', 'mit', 'hol',
26
+ ]);
27
+ /** Tokenizálás: kisbetűsít, nem-alfanumerikuson tör, stopword + rövid token kihagyva, DEDUP (Set). */
28
+ static tokenize(text) {
29
+ const terms = new Set();
30
+ if (!text) {
31
+ return terms;
32
+ }
33
+ for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
34
+ if (raw.length >= FAM_LexicalMatch_Util.MIN_TOKEN_LEN && !FAM_LexicalMatch_Util.STOPWORDS.has(raw)) {
35
+ terms.add(raw);
36
+ }
37
+ }
38
+ return terms;
39
+ }
40
+ /**
41
+ * Lexikai átfedés-score (0..1): a `queryTerms` hányad része van jelen az entry kereshető szövegében.
42
+ * `content`-egyezés = base-átfedés; `headingPath`/`sourceFilePath`-egyezés = + `TITLE_PATH_BONUS` súly (1-re vágva).
43
+ * Üres query-term-halmaz → 0 (nincs jel → nincs boost). PURE.
44
+ */
45
+ static score(queryTerms, entry) {
46
+ if (!queryTerms.size) {
47
+ return 0;
48
+ }
49
+ const titlePathText = `${(entry.headingPath ?? []).join(' ')} ${entry.sourceFilePath ?? ''}`;
50
+ const titlePathTerms = FAM_LexicalMatch_Util.tokenize(titlePathText);
51
+ const contentTerms = FAM_LexicalMatch_Util.tokenize(entry.content);
52
+ let baseMatched = 0;
53
+ let titleMatched = 0;
54
+ for (const term of queryTerms) {
55
+ const inTitlePath = titlePathTerms.has(term);
56
+ if (contentTerms.has(term) || inTitlePath) {
57
+ baseMatched++;
58
+ }
59
+ if (inTitlePath) {
60
+ titleMatched++;
61
+ }
62
+ }
63
+ const total = queryTerms.size;
64
+ const base = baseMatched / total;
65
+ const titleBonus = (titleMatched / total) * FAM_LexicalMatch_Util.TITLE_PATH_BONUS;
66
+ return Math.min(1, base + titleBonus);
67
+ }
68
+ }
69
+ exports.FAM_LexicalMatch_Util = FAM_LexicalMatch_Util;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_MemoryActivation_Util = void 0;
4
+ /**
5
+ * `FAM_MemoryActivation_Util` (dsgn-013 §2) — a Memory Activation Model **lazy-decay magja**, PURE. A `memory`-tár
6
+ * retrieval-rangsorához számolja a felidézés-alapú aktivációs szorzót: a frissen/gyakran felidézett memória
7
+ * felül-, a régóta érintetlen alul-rangsorol — DE sosem tűnik el (`floorWeight > 0`), csak kisebb a súlya.
8
+ *
9
+ * **Emberi-agy-analógia:** a felidézés megerősít (`frequencyBoost`, testing effect), a nem-felidézés lassan
10
+ * degradál (`recencyFactor`, Ebbinghaus-felejtési görbe). NINCS `Date.now()` a magban — a `nowMs` injektált
11
+ * (workflow-determinizmus + unit-stabilitás).
12
+ */
13
+ class FAM_MemoryActivation_Util {
14
+ /** Egy nap ms-ben. */
15
+ static MS_PER_DAY = 86_400_000;
16
+ /**
17
+ * A nyers aktiváció-összetevők (recency × frequency) egy memória-entryre. A `lastRecalledAt` hiánya → a `fallbackMs`
18
+ * (tipikusan a létrehozás ideje: cold-start kegyelmi idő). A `nowMs − lastRecalledAt` negatívra is védett (→ 0 nap).
19
+ */
20
+ static activation(set) {
21
+ const lastRecalled = typeof set.lastRecalledAt === 'number' ? set.lastRecalledAt : (set.fallbackMs ?? set.nowMs);
22
+ const ageDays = Math.max(0, set.nowMs - lastRecalled) / FAM_MemoryActivation_Util.MS_PER_DAY;
23
+ const halfLife = set.halfLifeDays > 0 ? set.halfLifeDays : 1;
24
+ const recencyFactor = Math.pow(0.5, ageDays / halfLife);
25
+ const recallCount = Math.max(0, set.recallCount ?? 0);
26
+ // CAP (eval-lelet): a nyers `1 + log2(1+recallCount)` korlátlan → plafonozzuk, hogy ne nyomja el a relevanciát.
27
+ const rawFrequencyBoost = 1 + Math.log2(1 + recallCount);
28
+ const frequencyBoost = (typeof set.maxFrequencyBoost === 'number' && set.maxFrequencyBoost > 0)
29
+ ? Math.min(rawFrequencyBoost, set.maxFrequencyBoost)
30
+ : rawFrequencyBoost;
31
+ return { recencyFactor: recencyFactor, frequencyBoost: frequencyBoost, activation: recencyFactor * frequencyBoost };
32
+ }
33
+ /**
34
+ * A retrieval-rankinghoz használt **decay-szorzó** (dsgn-013 §3): `max(floorWeight, activation)`. Ezt szorozza a
35
+ * `finalScore` a `memory`-táron (a `chunkFactor`/`sourceFactor` mellé). A `floorWeight` az alsó korlát — a régi
36
+ * memória lesüllyed, de a vektor-keresésben még megtalálható (a `weight ≤ 0 → kizárt` szabályt NEM lépi át).
37
+ */
38
+ static decayFactor(set) {
39
+ const activation = FAM_MemoryActivation_Util.activation({
40
+ lastRecalledAt: set.lastRecalledAt, recallCount: set.recallCount, nowMs: set.nowMs,
41
+ halfLifeDays: set.config.halfLifeDays, fallbackMs: set.fallbackMs,
42
+ maxFrequencyBoost: set.config.maxFrequencyBoost,
43
+ });
44
+ const raw = Math.max(set.config.floorWeight, activation.activation);
45
+ // TEMPER (eval-lelet 2026-06-25): a nyers szorzót az 1 felé húzzuk (`1 + α·(raw−1)`) → az aktiváció FINOMAN
46
+ // modulál, a koszinusz-relevancia dominál. α≥1 vagy hiányzó → változatlan (régi viselkedés, back-compat).
47
+ const alpha = set.config.activationWeight;
48
+ if (typeof alpha === 'number' && alpha >= 0 && alpha < 1) {
49
+ return 1 + alpha * (raw - 1);
50
+ }
51
+ return raw;
52
+ }
53
+ /**
54
+ * DORMANT-szabály (dsgn-013 §5.1, PURE) — egy memória ALVÓ-jelölt, ha **régóta nem-felidézett** (`ageDays ≥
55
+ * minAgeDays`) ÉS **ritkán-felidézett** (`recallCount < maxRecallCount`). A sokszor-felidézett (gyakran használt)
56
+ * memória SOHA nem dormant (a `recallCount`-gát védi). A `lastRecalledAt` hiányában a `fallbackMs` (létrehozás).
57
+ * Ez a szabály Mongo-kifejezhető (a `sweep` ugyanezt az age+recall kaput használja → konzisztens).
58
+ */
59
+ static isDormant(set) {
60
+ const lastRecalled = typeof set.lastRecalledAt === 'number' ? set.lastRecalledAt : (set.fallbackMs ?? set.nowMs);
61
+ const ageDays = Math.max(0, set.nowMs - lastRecalled) / FAM_MemoryActivation_Util.MS_PER_DAY;
62
+ const recallCount = Math.max(0, set.recallCount ?? 0);
63
+ return ageDays >= set.minAgeDays && recallCount < set.maxRecallCount;
64
+ }
65
+ }
66
+ exports.FAM_MemoryActivation_Util = FAM_MemoryActivation_Util;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_RuleInjection_Util = void 0;
4
+ const fam_rule_scope_type_enum_1 = require("../../../_enums/fam-rule-scope.type-enum");
5
+ /**
6
+ * `FAM_RuleInjection_Util` (Rules-Management MP-2) — a rule-injektálás **pure** (DB-/hálózat-mentes) magja:
7
+ * ① a kontextusból Mongo-prefilter `$or` (mely TIER-ek injektálódnak MOST), ② determinisztikus, `importance`-elsődleges
8
+ * rangsor, ③ char-budget-fit + „(…and N more rules not shown)" záró sor. A control-service végzi a DB-query-t és
9
+ * delegál ide — így a kontraktus (mely tier, milyen sorrend, hol vágunk) önállóan unit-tesztelhető.
10
+ *
11
+ * **Záró sor (user-FR):** valahányszor a budget miatt szabály marad ki, a blokk végén JELEZNI kell, hogy van több
12
+ * (NEM-aggresszív injektálás: a core befér, a többi „létezik" jelzéssel) — így az agent tudja, hogy `read`-elhet továbbiakat.
13
+ */
14
+ class FAM_RuleInjection_Util {
15
+ /** Default char-budget az injektált blokkra (a hook felül tudja írni). Tömör core ~ ennyibe fér. */
16
+ static DEFAULT_BUDGET_CHARS = 6000;
17
+ /** A `globalRule:true` legacy hard-rule effektív fontossága, ha nincs explicit `importance`. */
18
+ static LEGACY_GLOBAL_IMPORTANCE = 90;
19
+ /** Importance-default explicit érték nélkül (közepes — a globalRule-legacy fölött, nem-jelölt alatt). */
20
+ static DEFAULT_IMPORTANCE = 50;
21
+ /**
22
+ * A MOST érvényes TIER-ekhez tartozó Mongo-prefilter `$or` ágai (PURE). A `global` MINDIG; a többi a
23
+ * kontextustól függ. A legacy `globalRule:true` (MP-4 átsorolás ELŐTT) is bekerül — így a meglévő curated-core
24
+ * a migráció alatt is injektálódik. A `trigger`-tier SZÁNDÉKOSAN kimarad az always-on injektálásból (on-demand `read`).
25
+ */
26
+ static scopeOrBranches(context) {
27
+ const ors = [
28
+ { ruleScope: fam_rule_scope_type_enum_1.FAM_RuleScope.global },
29
+ { globalRule: true },
30
+ ];
31
+ if (context.isFdp) {
32
+ ors.push({ ruleScope: fam_rule_scope_type_enum_1.FAM_RuleScope.fdpGlobal });
33
+ }
34
+ if (context.inProject) {
35
+ ors.push({ ruleScope: fam_rule_scope_type_enum_1.FAM_RuleScope.projectInternal });
36
+ if (context.projectType) {
37
+ ors.push({ ruleScope: fam_rule_scope_type_enum_1.FAM_RuleScope.projectType, ruleScopeTopic: context.projectType });
38
+ }
39
+ }
40
+ else {
41
+ ors.push({ ruleScope: fam_rule_scope_type_enum_1.FAM_RuleScope.workspaceRoot });
42
+ }
43
+ for (const topic of context.topics ?? []) {
44
+ ors.push({ ruleScope: fam_rule_scope_type_enum_1.FAM_RuleScope.trigger, ruleScopeTopic: topic });
45
+ }
46
+ return ors;
47
+ }
48
+ /** A teljes injektálás-prefilter (soft-delete + aktív + scope-`$or`). */
49
+ static buildFilter(context) {
50
+ return {
51
+ _deleted: null,
52
+ isActive: { $ne: false },
53
+ $or: FAM_RuleInjection_Util.scopeOrBranches(context),
54
+ };
55
+ }
56
+ /** Effektív fontosság (0–100): explicit `importance`, vagy legacy-globalRule → 90, egyébként 50. Clamp 0..100. */
57
+ static effectiveImportance(rule) {
58
+ const raw = typeof rule.importance === 'number'
59
+ ? rule.importance
60
+ : (rule.globalRule ? FAM_RuleInjection_Util.LEGACY_GLOBAL_IMPORTANCE : FAM_RuleInjection_Util.DEFAULT_IMPORTANCE);
61
+ return Math.max(0, Math.min(100, raw));
62
+ }
63
+ /**
64
+ * Determinisztikus injektálás-komparátor: effektív-importance DESC → weight DESC → category ASC →
65
+ * recency (`lastModifiedMs`) DESC → `id` ASC. A teljes lánc stabil, reprodukálható sorrendet ad.
66
+ */
67
+ static compare(a, b) {
68
+ const impDiff = FAM_RuleInjection_Util.effectiveImportance(b) - FAM_RuleInjection_Util.effectiveImportance(a);
69
+ if (impDiff !== 0) {
70
+ return impDiff;
71
+ }
72
+ const weightDiff = (b.weight ?? 1) - (a.weight ?? 1);
73
+ if (weightDiff !== 0) {
74
+ return weightDiff;
75
+ }
76
+ const catA = a.category ?? '';
77
+ const catB = b.category ?? '';
78
+ if (catA !== catB) {
79
+ return catA < catB ? -1 : 1;
80
+ }
81
+ const recA = a.lastModifiedMs ?? 0;
82
+ const recB = b.lastModifiedMs ?? 0;
83
+ if (recA !== recB) {
84
+ return recB - recA;
85
+ }
86
+ return (a.id ?? '') < (b.id ?? '') ? -1 : 1;
87
+ }
88
+ /** A jelöltek rangsorolva (új tömb; nem mutál). */
89
+ static rank(rules) {
90
+ return [...rules].sort(FAM_RuleInjection_Util.compare);
91
+ }
92
+ /**
93
+ * A rangsorolt szabályok char-budget-fit összefűzése + „(…and N more)" záró sor. Greedy: rangsor-sorrendben
94
+ * addig vesz fel, amíg a következő szabály content-je (a `separator`-ral) belefér. Ha a legelső sem fér, akkor is
95
+ * felveszi (legalább a legfontosabb core menjen). A footer csak akkor van, ha tényleg maradt ki.
96
+ */
97
+ static assemble(input) {
98
+ const budget = input.budgetChars && input.budgetChars > 0 ? input.budgetChars : FAM_RuleInjection_Util.DEFAULT_BUDGET_CHARS;
99
+ const sep = input.separator ?? '\n\n';
100
+ // CSAK a content-tel bíró szabályok a populáció — az üres-content entry NEM „budget miatt kihagyott" (nincs mit injektálni).
101
+ const ranked = FAM_RuleInjection_Util.rank(input.rules)
102
+ .filter((rule) => (rule.content ?? '').trim().length > 0);
103
+ const total = ranked.length;
104
+ const parts = [];
105
+ let used = 0;
106
+ let included = 0;
107
+ for (const rule of ranked) {
108
+ const text = (rule.content ?? '').trim();
109
+ const addLen = text.length + (parts.length ? sep.length : 0);
110
+ if (used + addLen > budget && included > 0) {
111
+ break; // budget-stop (de a legelső MINDIG befér — included>0 guard)
112
+ }
113
+ parts.push(text);
114
+ used += addLen;
115
+ included++;
116
+ }
117
+ const omitted = total - included;
118
+ const footer = omitted > 0
119
+ ? `(…and ${omitted} more rule${omitted === 1 ? '' : 's'} not shown — \`read\` the \`rules\` table for the rest.)`
120
+ : undefined;
121
+ const block = footer ? `${parts.join(sep)}${parts.length ? sep : ''}${footer}` : parts.join(sep);
122
+ return { block: block, includedCount: included, omittedCount: omitted, totalCount: total, footer: footer };
123
+ }
124
+ }
125
+ exports.FAM_RuleInjection_Util = FAM_RuleInjection_Util;