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