@futdevpro/fdp-agent-memory 1.1.30 → 1.1.113
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 +1 -1
- 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 +160 -6
- 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/_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/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 +31 -5
- package/build/src/_modules/embedding/_services/fam-embedding.control-service.js +142 -9
- package/build/src/_modules/embedding/_services/fam-entry.data-service.js +9 -0
- 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 +173 -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/_services/fam-capability-registry.service.js +151 -8
- package/build/src/_modules/mcp/_services/fam-read-tool.service.js +70 -0
- package/build/src/_modules/mcp/_services/fam-write-tool.service.js +36 -0
- 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 +216 -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 +76 -0
- package/build/src/_modules/scope-reference/_services/fam-scope-maintenance.control-service.js +233 -0
- package/build/src/_modules/scope-reference/_services/fam-scope.data-service.js +25 -0
- package/build/src/_routes/server/api/api.controller.js +270 -0
- package/build/src/_routes/server/api/fam-doctor.control-service.js +156 -0
- package/build/src/app.server.js +13 -10
- 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 +1 -1
- package/build/src/_cli/_collections/fam-project-discovery.util.js +0 -98
- 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() {
|
|
@@ -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)
|
|
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
|
-
|
|
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
|
-
|
|
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;
|