@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
@@ -19,9 +19,11 @@ const fam_content_hash_util_1 = require("../_collections/fam-content-hash.util")
19
19
  const fam_git_repo_util_1 = require("../_collections/fam-git-repo.util");
20
20
  const fam_scan_reconcile_util_1 = require("../_collections/fam-scan-reconcile.util");
21
21
  const fam_secret_content_util_1 = require("../_collections/fam-secret-content.util");
22
+ const fam_frontmatter_util_1 = require("../_collections/fam-frontmatter.util");
22
23
  const fam_project_identity_util_1 = require("../_collections/fam-project-identity.util");
23
24
  const fam_scan_path_util_1 = require("../_collections/fam-scan-path.util");
24
25
  const fam_scan_progress_util_1 = require("../_collections/fam-scan-progress.util");
26
+ const fam_scan_progress_sink_service_1 = require("../../../_collections/fam-scan-progress-sink.service");
25
27
  const fam_scan_summary_util_1 = require("../_collections/fam-scan-summary.util");
26
28
  const scope_reference_2 = require("../../scope-reference");
27
29
  const fam_reference_data_model_1 = require("../../../_models/data-models/fam-reference.data-model");
@@ -59,6 +61,16 @@ class FAM_Ingest_ControlService {
59
61
  const sample = content.length > 8192 ? content.slice(0, 8192) : content;
60
62
  return sample.indexOf(String.fromCharCode(0)) !== -1;
61
63
  }
64
+ /**
65
+ * Tábla-vezérelt ADDITÍV (accumulate-only) szabály (user-FR 2026-06-24): a `memory` tár MINDIG content-addressed
66
+ * additív — a memória-bejegyzések sosem „íródnak felül azonos location-nél", így a kompaktálás-előtti állapot
67
+ * a forrás zsugorodása UTÁN is megmarad a FAM-ban (vesztességmentes superset). A többi tár (codebase/documents/
68
+ * rules/...) a szokásos `(fájl, chunkIndex)` delta-t használja; rájuk a `preserveHistory` request-flag kérhet
69
+ * explicit additívot. Pure + statikus (tesztelhető).
70
+ */
71
+ static isAccumulateOnly(table) {
72
+ return table === fam_table_type_enum_1.FAM_Table.memory;
73
+ }
62
74
  /** Default issuer az ingest-műveletekhez. */
63
75
  issuer = 'FAM_Ingest_ControlService';
64
76
  static getInstance() {
@@ -180,7 +192,7 @@ class FAM_Ingest_ControlService {
180
192
  });
181
193
  runId = openedRun._id ?? runId;
182
194
  // (4) Per-fájl feldolgozás (flow-limit kötegekben; a per-fájl hiba NEM buktatja a run-t).
183
- const verdicts = { new: 0, modified: 0, equal: 0, deleted: 0 };
195
+ const verdicts = { new: 0, modified: 0, equal: 0, deleted: 0, superseded: 0 };
184
196
  const errors = [];
185
197
  let chunkCount = 0;
186
198
  const flowLimit = await this.resolveFlowLimit(canonicalScope);
@@ -195,12 +207,21 @@ class FAM_Ingest_ControlService {
195
207
  ? 0
196
208
  : fam_cooldown_util_1.FAM_Cooldown_Util.resolveMs((await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.sectionCooldownMs', { scopePath: canonicalScope })).value);
197
209
  let lastProgressAt = startedAt;
210
+ // Sub-progress (user-FR 2026-06-22): a felderítés→embedding közti csend kitöltése — mit gyűjtöttünk be +
211
+ // hogyan route-oltuk (mely tárakba), MIELŐTT a throttle-olt feldolgozás-sorok elindulnak.
212
+ if (!isDryRun && total > 0) {
213
+ const scopeLabel = canonicalScope.map((ref) => ref.canonicalName).join(' > ') || '(globális)';
214
+ fsm_dynamo_1.DyFM_Log.info(`[famIngest] [${scopeLabel}] felderítés kész: ${total} fájl begyűjtve `
215
+ + `(${discovery.skippedFiles.length} kihagyva, tárak: ${discovery.tables.map((table) => String(table)).join('+') || '—'}) `
216
+ + '→ embedding indul');
217
+ }
198
218
  for (let i = 0; i < total; i += flowLimit) {
199
219
  const batch = discovery.routedFiles.slice(i, i + flowLimit);
200
220
  const results = await Promise.all(batch.map((file) => this.processFile({
201
221
  file: file, runId: runId, scopePath: canonicalScope,
202
222
  resolvedRoot: discovery.resolvedRoot,
203
223
  dryRun: isDryRun, issuer: issuer, errors: errors,
224
+ preserveHistory: request.preserveHistory === true,
204
225
  })));
205
226
  for (const result of results) {
206
227
  if (!result) {
@@ -215,10 +236,17 @@ class FAM_Ingest_ControlService {
215
236
  const now = Date.now();
216
237
  if (!isDryRun && (processed >= total || now - lastProgressAt >= progressIntervalMs)) {
217
238
  lastProgressAt = now;
239
+ const scopeLeaf = canonicalScope.length ? canonicalScope[canonicalScope.length - 1].canonicalName : '';
218
240
  fsm_dynamo_1.DyFM_Log.info(fam_scan_progress_util_1.FAM_ScanProgress_Util.progressLine({
241
+ scopeLabel: scopeLeaf || undefined,
219
242
  processed: processed, total: total, chunkCount: chunkCount,
220
243
  verdicts: verdicts, elapsedSeconds: (now - startedAt) / 1000,
221
244
  }));
245
+ // Sub-progress sink (user-FR): a within-project haladás a scan-job status-ába → a thin-CLI poll is látja.
246
+ fam_scan_progress_sink_service_1.FAM_ScanProgress_Sink.set({
247
+ scopeLabel: scopeLeaf, processed: processed, total: total, chunkCount: chunkCount,
248
+ pct: total ? Math.round((processed / total) * 100) : 100, updatedAt: now,
249
+ });
222
250
  }
223
251
  // Szekció-cooldown: a batch-ek KÖZÉ (NEM az utolsó után) — opcionális gép-tehermentesítő szünet.
224
252
  if (sectionCooldownMs > 0 && processed < total) {
@@ -228,7 +256,11 @@ class FAM_Ingest_ControlService {
228
256
  // (4b) Törölt-fájl reconciliation (whole-file orphan) — CSAK folder/project scan (a `scan-file`
229
257
  // egyetlen fájlt céloz, nem reconciliál). A lemezről eltűnt fájlok chunkjai soft-delete-elődnek; a
230
258
  // számuk a `verdicts.deleted`-be (a per-fájl, fájlon-belüli `deleted`-ek mellé).
231
- if (request.operation !== 'scan-file') {
259
+ // ADDITÍV mód (user-FR 2026-06-22/24): `preserveHistory` request-flag VAGY a `memory` tár (tábla-vezérelt)
260
+ // a reconcile-törlés KIHAGYVA (a FAM superset-ként megtartja a forrásból eltűnt — pl. kompaktálódó memóriám —
261
+ // bejegyzéseit is; nincs destruktív felülírás). A `memory` ezért SOHA nem reconciliál, flag nélkül sem.
262
+ if (request.operation !== 'scan-file' && !request.preserveHistory
263
+ && !(request.tableOverride !== undefined && FAM_Ingest_ControlService.isAccumulateOnly(request.tableOverride))) {
232
264
  const reconcileIgnored = await this.resolveReconcileIgnored(canonicalScope);
233
265
  verdicts.deleted += await this.reconcileDeletedFiles({
234
266
  discovery: discovery, scopePath: canonicalScope, issuer: issuer, dryRun: request.dryRun ?? false,
@@ -284,7 +316,7 @@ class FAM_Ingest_ControlService {
284
316
  * soft-delete). A per-fájl hiba `errors[]`-be (no-silent-failure) + `null` visszatérés (a run nem bukik).
285
317
  */
286
318
  async processFile(set) {
287
- const table = set.file.route;
319
+ let table = set.file.route;
288
320
  try {
289
321
  const rawContent = fs.readFileSync(set.file.absolutePath, 'utf-8');
290
322
  // Tartalmi secret-redakció (user-FR): a forrásba hardcode-olt API-kulcs (sk-/AKIA/ghp_/PEM/JWT) a tárolt
@@ -294,7 +326,16 @@ class FAM_Ingest_ControlService {
294
326
  if (redaction.count > 0) {
295
327
  fsm_dynamo_1.DyFM_Log.warn(`[famIngest] secret-redakció: ${redaction.count} token redaktálva (${set.file.relativePath})`);
296
328
  }
297
- const content = redaction.text;
329
+ // MP-3 / F-A: fájl-fej `---`-frontmatter → ingest-irányítók + fejléc-mentes TÖRZS. Frontmatter nélkül NO-OP
330
+ // (`body===content`). A `table`-override (pl. `table: rules`) + `splitMode` + a per-entry rule-mezők innen jönnek.
331
+ const front = fam_frontmatter_util_1.FAM_Frontmatter_Util.parse(redaction.text);
332
+ const directives = front.directives;
333
+ const overrideTable = this.resolveDirectiveTable(directives.table);
334
+ if (overrideTable) {
335
+ table = overrideTable;
336
+ }
337
+ const splitMode = directives.splitMode === true ? 'heading' : (directives.splitMode || undefined);
338
+ const content = front.body;
298
339
  // FAM-REV-064 bináris-tartalom guard: a denylist-en átcsúszott, ismeretlen-kiterjesztésű bináris
299
340
  // (null-byte az első ~8KB-ban) NE kerüljön a generic chunkerbe (szemét-chunk ellen). Skip → 0 chunk.
300
341
  if (FAM_Ingest_ControlService.isLikelyBinary(content)) {
@@ -302,6 +343,7 @@ class FAM_Ingest_ControlService {
302
343
  }
303
344
  const chunks = await fam_chunker_control_service_1.FAM_Chunker_ControlService.getInstance().chunkFile({
304
345
  content: content, relativePath: set.file.relativePath, table: table, scopePath: set.scopePath,
346
+ splitMode: splitMode,
305
347
  });
306
348
  const registryEntry = embedding_1.FAM_StoreRegistry_Util.getEntry(table);
307
349
  if (!registryEntry) {
@@ -311,11 +353,17 @@ class FAM_Ingest_ControlService {
311
353
  // Existing-by-key betöltés a KANONIKUS abszolút útvonalon (FAM-REV-050) — a scan-root-független
312
354
  // fájl-identitás, így a delta bármely scan-gyökérről ugyanazt a fájlt matcheli (nincs duplikáció).
313
355
  const existing = await this.loadExistingChunks(dataService, set.file.absolutePath, set.scopePath);
314
- const deltaItems = fam_delta_compare_util_1.FAM_DeltaCompare_Util.compare(chunks, existing);
356
+ // ADDITÍV (content-addressed) delta → a régi (kompaktálás-előtti) tartalom MEGMARAD, sose `deleted`/`modified`
357
+ // (a FAM a memória minden valaha-látott állapotának vesztességmentes uniója). TÁBLA-VEZÉRELT (user-FR
358
+ // 2026-06-24): a `memory` tár MINDIG additív (a „felülírás azonos location-nél" csak kódbázis/dok/rules);
359
+ // a `preserveHistory` request-flag pedig bármely tárra explicit additív szkent kérhet.
360
+ const accumulateOnly = set.preserveHistory === true || FAM_Ingest_ControlService.isAccumulateOnly(table);
361
+ const deltaItems = fam_delta_compare_util_1.FAM_DeltaCompare_Util.compare(chunks, existing, accumulateOnly);
315
362
  if (!set.dryRun) {
316
363
  await this.applyVerdicts({
317
364
  deltaItems: deltaItems, file: set.file, table: table, runId: set.runId,
318
365
  scopePath: set.scopePath, root: set.resolvedRoot, dataService: dataService,
366
+ directives: directives,
319
367
  });
320
368
  }
321
369
  return {
@@ -388,7 +436,7 @@ class FAM_Ingest_ControlService {
388
436
  await this.persistAndEmbed({
389
437
  chunk: item.chunk, existingId: item.existingId, contentHash: item.contentHash,
390
438
  file: set.file, table: set.table, runId: set.runId, scopePath: set.scopePath,
391
- root: set.root, dataService: set.dataService,
439
+ root: set.root, dataService: set.dataService, directives: set.directives,
392
440
  });
393
441
  }
394
442
  }
@@ -402,12 +450,21 @@ class FAM_Ingest_ControlService {
402
450
  }
403
451
  continue;
404
452
  }
453
+ if (item.verdict === 'superseded') {
454
+ // dsgn-013 §6 (MAM F4): a fájl régi, már átírt tartalma — NEM törlés (a content + vektor MEGMARAD a
455
+ // poolban is, cold-kereshető), csak `superseded=true` jelölés → a memory-ranking penalizálja (a friss
456
+ // verzió fölébe kerül). A pool-ból NEM vesszük ki (a penalty süllyeszti; a dormancy idővel de-hidratálja).
457
+ if (item.existingId) {
458
+ await set.dataService.setSuperseded(item.existingId, true);
459
+ }
460
+ continue;
461
+ }
405
462
  // new / modified → upsert + embed (a `chunk` itt sosem null).
406
463
  if (item.chunk) {
407
464
  await this.persistAndEmbed({
408
465
  chunk: item.chunk, existingId: item.existingId, contentHash: item.contentHash,
409
466
  file: set.file, table: set.table, runId: set.runId, scopePath: set.scopePath,
410
- root: set.root, dataService: set.dataService,
467
+ root: set.root, dataService: set.dataService, directives: set.directives,
411
468
  });
412
469
  }
413
470
  }
@@ -441,6 +498,9 @@ class FAM_Ingest_ControlService {
441
498
  addedBy: 'scan',
442
499
  ingestRunId: set.runId,
443
500
  });
501
+ // MP-3 / F-A: a frontmatter per-entry rule-mezőit (importance/userApproved/ruleScope/…) + tageket rákötjük.
502
+ // CSAK definiált értékek (Object.assign-nal) — a nem-rules táron a nem-deklarált mezők úgyis nem persistálnak.
503
+ this.applyDirectiveFields(entry, set.directives);
444
504
  // Persist (saveData = upsert: meglévő `_id` → modify, hiányzó → insert).
445
505
  const saved = await set.dataService.saveData(entry);
446
506
  // Embed CSAK a new/modified-re (MP-2). Az embed-hiba az entry-t `error`-státuszba teszi +
@@ -449,6 +509,51 @@ class FAM_Ingest_ControlService {
449
509
  table: set.table, entry: saved, callType: 'embed-write',
450
510
  });
451
511
  }
512
+ /** A frontmatter `table:` direktíva → érvényes `FAM_Table` (ismeretlen → undefined, a file-routing marad). */
513
+ resolveDirectiveTable(raw) {
514
+ if (!raw) {
515
+ return undefined;
516
+ }
517
+ return Object.values(fam_table_type_enum_1.FAM_Table).find((value) => value === raw);
518
+ }
519
+ /**
520
+ * MP-3 / F-A: a frontmatter per-entry rule-mezőit + tageket az `FAM_Entry`-re köti (CSAK definiált értékek). A
521
+ * nem-`rules` táron a nem-deklarált mezők úgyis nem persistálnak (a dataParams.properties dönt), így ártalmatlan.
522
+ */
523
+ applyDirectiveFields(entry, directives) {
524
+ if (!directives) {
525
+ return;
526
+ }
527
+ const target = entry;
528
+ if (typeof directives.importance === 'number') {
529
+ target.importance = directives.importance;
530
+ }
531
+ if (typeof directives.userApproved === 'boolean') {
532
+ target.userApproved = directives.userApproved;
533
+ }
534
+ if (typeof directives.globalRule === 'boolean') {
535
+ target.globalRule = directives.globalRule;
536
+ }
537
+ if (directives.ruleScope) {
538
+ target.ruleScope = directives.ruleScope;
539
+ }
540
+ if (directives.ruleScopeTopic) {
541
+ target.ruleScopeTopic = directives.ruleScopeTopic;
542
+ }
543
+ if (directives.ruleId) {
544
+ target.ruleId = directives.ruleId;
545
+ }
546
+ if (directives.derivedFrom) {
547
+ target.derivedFrom = directives.derivedFrom;
548
+ }
549
+ if (directives.category) {
550
+ target.category = directives.category;
551
+ }
552
+ if (directives.tags && directives.tags.length) {
553
+ const existing = Array.isArray(entry.tags) ? entry.tags : [];
554
+ target.tags = [...new Set([...existing, ...directives.tags])];
555
+ }
556
+ }
452
557
  /**
453
558
  * A file-rendszer-összefoglaló (`knowledge`, kind `fs-summary`) generálása + idempotens upsert-je a scan után.
454
559
  * A `FAM_ScanSummary_Util` a mappastruktúrából építi a kereshető „mi van a projektben" szöveget. **Identitás:**
@@ -463,15 +568,16 @@ class FAM_Ingest_ControlService {
463
568
  }
464
569
  const dataService = new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: set.issuer });
465
570
  const scopeLabel = set.scopePath.map((ref) => ref.canonicalName).join(' > ') || '(globális)';
466
- const content = fam_scan_summary_util_1.FAM_ScanSummary_Util.build({
571
+ // NO-LOSS split (user-FR 2026-06-22): nagy fánál TÖBB, egyenlő részre bontva — minden fájl szerepel,
572
+ // semmit nem nyelünk le. Részenként EGY-EGY `knowledge`-entry (`chunkIndex` = rész-index).
573
+ const parts = fam_scan_summary_util_1.FAM_ScanSummary_Util.buildParts({
467
574
  scopeLabel: scopeLabel,
468
575
  root: set.discovery.resolvedRoot,
469
576
  relativePaths: set.discovery.routedFiles.map((file) => file.relativePath),
470
577
  skippedCount: set.discovery.skippedFiles.length,
471
578
  tables: set.discovery.tables.map((table) => String(table)),
472
579
  });
473
- const contentHash = fam_content_hash_util_1.FAM_ContentHash_Util.hash(content);
474
- // Meglévő összefoglaló a (source-type + scan-gyökér + levél-scope) identitáson.
580
+ // A meglévő összefoglaló-részek a (source-type + scan-gyökér + levél-scope) identitáson, rész-index szerint.
475
581
  const leafScopeId = set.scopePath.length ? set.scopePath[set.scopePath.length - 1].scopeId : undefined;
476
582
  const filter = {};
477
583
  filter['source.type'] = fam_scan_summary_util_1.FAM_ScanSummary_Util.SOURCE_TYPE;
@@ -481,33 +587,54 @@ class FAM_Ingest_ControlService {
481
587
  filter['scopePath.scopeId'] = leafScopeId;
482
588
  }
483
589
  const existing = await dataService.findHydratableList(filter);
484
- const existingEntry = existing[0];
485
- // Változatlan fájl-lista NO-OP (nincs fölös re-embed).
486
- if (existingEntry?.contentHash === contentHash) {
487
- return;
590
+ const existingByIndex = new Map();
591
+ for (const item of existing) {
592
+ existingByIndex.set(item.chunkIndex ?? 0, item); // régi egy-részes entry: chunkIndex undefined → 0
593
+ }
594
+ let changed = 0;
595
+ for (let index = 0; index < parts.length; index++) {
596
+ const partContent = parts[index];
597
+ const partHash = fam_content_hash_util_1.FAM_ContentHash_Util.hash(partContent);
598
+ const existingPart = existingByIndex.get(index);
599
+ if (existingPart?.contentHash === partHash) {
600
+ continue; // változatlan rész → NO-OP (nincs fölös re-embed)
601
+ }
602
+ const entry = new fam_entry_data_model_1.FAM_Entry({
603
+ _id: existingPart?._id,
604
+ table: fam_table_type_enum_1.FAM_Table.knowledge,
605
+ content: partContent,
606
+ contentHash: partHash,
607
+ embeddingStatus: 'pending',
608
+ scopePath: set.scopePath,
609
+ chunkIndex: index,
610
+ kind: fam_scan_summary_util_1.FAM_ScanSummary_Util.KIND,
611
+ tags: ['fs-summary', 'filesystem', 'scan-summary'],
612
+ weight: 2,
613
+ source: {
614
+ type: 'scan-summary', root: set.discovery.resolvedRoot, absolutePath: set.discovery.resolvedRoot,
615
+ },
616
+ addedBy: 'scan',
617
+ ingestRunId: set.runId,
618
+ });
619
+ const saved = await dataService.saveData(entry);
620
+ await embedding_1.FAM_EmbeddingPipeline_ControlService.getInstance().embedAndPersist({
621
+ table: fam_table_type_enum_1.FAM_Table.knowledge, entry: saved, callType: 'embed-write',
622
+ });
623
+ changed++;
624
+ }
625
+ // Fölös részek (egy korábbi, NAGYOBB összefoglaló most-már-nem-létező részei) → soft-delete (no-orphan).
626
+ let removed = 0;
627
+ for (const item of existing) {
628
+ if ((item.chunkIndex ?? 0) >= parts.length) {
629
+ await dataService.deleteData(item._id);
630
+ removed++;
631
+ }
632
+ }
633
+ if (changed > 0 || removed > 0) {
634
+ fsm_dynamo_1.DyFM_Log.info(`[famIngest] fs-summary (${scopeLabel}): ${parts.length} rész `
635
+ + `(${changed} írva/frissítve, ${removed} fölös törölve), ${set.discovery.routedFiles.length} fájl, `
636
+ + `${set.discovery.tables.length} tár`);
488
637
  }
489
- const entry = new fam_entry_data_model_1.FAM_Entry({
490
- _id: existingEntry?._id,
491
- table: fam_table_type_enum_1.FAM_Table.knowledge,
492
- content: content,
493
- contentHash: contentHash,
494
- embeddingStatus: 'pending',
495
- scopePath: set.scopePath,
496
- kind: fam_scan_summary_util_1.FAM_ScanSummary_Util.KIND,
497
- tags: ['fs-summary', 'filesystem', 'scan-summary'],
498
- weight: 2,
499
- source: {
500
- type: 'scan-summary', root: set.discovery.resolvedRoot, absolutePath: set.discovery.resolvedRoot,
501
- },
502
- addedBy: 'scan',
503
- ingestRunId: set.runId,
504
- });
505
- const saved = await dataService.saveData(entry);
506
- await embedding_1.FAM_EmbeddingPipeline_ControlService.getInstance().embedAndPersist({
507
- table: fam_table_type_enum_1.FAM_Table.knowledge, entry: saved, callType: 'embed-write',
508
- });
509
- fsm_dynamo_1.DyFM_Log.info(`[famIngest] fs-summary ${existingEntry ? 'frissítve' : 'létrehozva'} `
510
- + `(${scopeLabel}): ${set.discovery.routedFiles.length} fájl, ${set.discovery.tables.length} tár`);
511
638
  }
512
639
  /**
513
640
  * A scan-gyökér PROJEKT-IDENTITÁSA (git + `package.json`) → (a) kereshető `knowledge` „project-identity"
@@ -562,6 +689,15 @@ class FAM_Ingest_ControlService {
562
689
  // (b) reference-alias (a package-név + rövid alakok → a projekt scope-ja) — string/exact-match feloldás.
563
690
  if (leaf) {
564
691
  await this.persistProjectReference(identity, leaf, set.discovery.resolvedRoot, set.issuer);
692
+ // (c) kanonikus repo-kulcs stamp a leaf project-scope-ra (scope-prevenció, user-FR 2026-06-22) — a STABIL
693
+ // git/package identitás scope-szintű rögzítése; erre épül a repo-kulcs alapú duplikátum-detekció, ami
694
+ // a KÜLÖNBÖZŐ nevű, AZONOS repo eseteket (import vs scan) is elkapja. Best-effort (nincs kulcs → no-op).
695
+ // path-aware kulcs (2026-06-24 fix): a projekt-mappa neve diszkriminátorként → a monorepo/fork
696
+ // sub-projektek külön repoKey-t kapnak (nem keverednek egy scope-ba egy téves merge-dzsel).
697
+ const repoKey = fam_project_identity_util_1.FAM_ProjectIdentity_Util.canonicalRepoKey(identity, path.basename(set.discovery.resolvedRoot));
698
+ if (repoKey) {
699
+ await new scope_reference_2.FAM_Scope_DataService({ issuer: set.issuer }).setCanonicalRepoKey(leaf.scopeId, repoKey);
700
+ }
565
701
  }
566
702
  fsm_dynamo_1.DyFM_Log.info(`[famIngest] project-identity (${scopeLabel}): `
567
703
  + `${identity.packageName ?? '(nincs package.json)'}${identity.isGit ? ' [git]' : ''}`
@@ -697,7 +833,7 @@ class FAM_Ingest_ControlService {
697
833
  ingestRunId: set.runId, trigger: set.request.operation, table: [],
698
834
  scopePath: set.scopePath, rootPath: set.request.path,
699
835
  filesProcessed: 0, filesSkipped: 0, chunkCount: 0,
700
- verdicts: { new: 0, modified: 0, equal: 0, deleted: 0 },
836
+ verdicts: { new: 0, modified: 0, equal: 0, deleted: 0, superseded: 0 },
701
837
  durationMs: Date.now() - set.startedAt, status: 'failed', errors: [errorEntry],
702
838
  };
703
839
  try {