@futdevpro/fdp-agent-memory 1.1.30 → 1.1.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +148 -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 +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 +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
|
@@ -8,6 +8,10 @@ const embedding_1 = require("../../embedding");
|
|
|
8
8
|
const scope_reference_1 = require("../../scope-reference");
|
|
9
9
|
const fam_retrieval_candidate_data_service_1 = require("./fam-retrieval-candidate.data-service");
|
|
10
10
|
const fam_retrieval_suggestions_util_1 = require("./fam-retrieval-suggestions.util");
|
|
11
|
+
const fam_memory_activation_util_1 = require("../_collections/fam-memory-activation.util");
|
|
12
|
+
const fam_lexical_match_util_1 = require("../_collections/fam-lexical-match.util");
|
|
13
|
+
const fam_memory_reactivation_control_service_1 = require("./fam-memory-reactivation.control-service");
|
|
14
|
+
const fam_memory_cold_search_control_service_1 = require("./fam-memory-cold-search.control-service");
|
|
11
15
|
/** A topK / lapozás abszolút plafonja (CCAP MAX_PAGE_SIZE=500; dsgn-005 §6). */
|
|
12
16
|
const MAX_PAGE_SIZE = 500;
|
|
13
17
|
/**
|
|
@@ -95,8 +99,57 @@ class FAM_Retrieval_ControlService {
|
|
|
95
99
|
const relevantSet = visibleHits.filter((hit) => this.codeExactRank(hit) > 0 || hit.finalScore >= config.minScore);
|
|
96
100
|
// ⑦ rendezés (finalScore desc → recency → _id asc).
|
|
97
101
|
this.sortHits(relevantSet, config.scoreOrder);
|
|
102
|
+
// ⑦b R1 duplikátum-dedup (FAM-REV 2026-06-22, eval-lelet): az AZONOS-tartalmú (kereszt-repo) hitek összevonása
|
|
103
|
+
// a top-K ELŐTT — a kanonikus (legjobb-ranked) marad, a másolat-szám a `duplicateCount`-ban. A közös fájlok
|
|
104
|
+
// (`.cursor/rules` / CLAUDE.md / guideline) ne pazarolják a top-K-t 4-5 azonos másolattal. A `sortHits` UTÁN fut
|
|
105
|
+
// (a megtartott a legjobb-rangsorolt); a contentHash-mentes hitek érintetlenek.
|
|
106
|
+
const dedupedSet = FAM_Retrieval_ControlService.dedupeByContentHash(relevantSet);
|
|
98
107
|
// ⑧ topK-vágás + dense-detektálás + suggestions.
|
|
99
|
-
|
|
108
|
+
const result = this.buildResult({ query, relevantSet: dedupedSet, config, scopeExpand, detectedCodes });
|
|
109
|
+
// dsgn-013 §4 (MAM F2) — FELIDÉZÉS-megerősítés: a ténylegesen VISSZAADOTT `memory`-hitek „új energiát kapnak"
|
|
110
|
+
// (lastRecalledAt=now, recallCount++, dormant=false). ASYNC fire-and-forget — NEM blokkolja a read választ.
|
|
111
|
+
this.reactivateReturnedMemoryHits(result.hits, config);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A visszaadott top-K `memory`-hitek async felidézés-megerősítése (dsgn-013 §4). Fire-and-forget (void): a read
|
|
116
|
+
* már elkészült, a felidézés-frissítés best-effort. Csak a `memory`-táras, `_id`-vel bíró hitre; üres → no-op.
|
|
117
|
+
* `memoryReactivateOnRead=false` → kikapcsolva.
|
|
118
|
+
*/
|
|
119
|
+
reactivateReturnedMemoryHits(hits, config) {
|
|
120
|
+
if (!config.memoryReactivateOnRead) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const memoryIds = fam_memory_reactivation_control_service_1.FAM_MemoryReactivation_ControlService.selectMemoryIds(hits);
|
|
124
|
+
if (!memoryIds.length) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
void fam_memory_reactivation_control_service_1.FAM_MemoryReactivation_ControlService.getInstance().reactivate(memoryIds, Date.now());
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* R1 dedup (pure): az AZONOS `contentHash`-ű hitek összevonása — az ELSŐ (a `sortHits` után legjobb-ranked) marad,
|
|
131
|
+
* a többi azonos-tartalmú másolatot beleszámoljuk a megtartott `duplicateCount`-jába. A `contentHash`-mentes hitek
|
|
132
|
+
* (pl. compact-mode projekció nélkül) NEM dedup-olódnak (mind egyedi). Sorrend-tartó.
|
|
133
|
+
*/
|
|
134
|
+
static dedupeByContentHash(hits) {
|
|
135
|
+
const keptByHash = new Map();
|
|
136
|
+
const result = [];
|
|
137
|
+
for (const hit of hits) {
|
|
138
|
+
const key = hit.contentHash;
|
|
139
|
+
if (!key) {
|
|
140
|
+
result.push(hit);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const kept = keptByHash.get(key);
|
|
144
|
+
if (kept) {
|
|
145
|
+
kept.duplicateCount = (kept.duplicateCount ?? 0) + 1;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
keptByHash.set(key, hit);
|
|
149
|
+
result.push(hit);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
100
153
|
}
|
|
101
154
|
// =========================================================================
|
|
102
155
|
// ④b KÓD-EXPANZIÓ (FEAT-003) — determinisztikus link-retrieval a vektor MELLÉ
|
|
@@ -171,6 +224,7 @@ class FAM_Retrieval_ControlService {
|
|
|
171
224
|
weight: set.weight,
|
|
172
225
|
finalScore: 1.0 * set.weight,
|
|
173
226
|
content: set.includeContent ? set.entry.content : undefined,
|
|
227
|
+
contentHash: set.entry.contentHash,
|
|
174
228
|
kind: set.entry.kind,
|
|
175
229
|
tags: set.entry.tags ?? [],
|
|
176
230
|
...this.ruleMeta(set.entry, set.table),
|
|
@@ -256,6 +310,8 @@ class FAM_Retrieval_ControlService {
|
|
|
256
310
|
});
|
|
257
311
|
}
|
|
258
312
|
// ⑤ weight-szorzó: finalScore = score × (weight ?? 1); weight ≤ 0 → kizárt (nem kerül a hits közé).
|
|
313
|
+
// Improvement 2: a query lexikai term-jeit EGYSZER tokenizáljuk (a hibrid keyword-boost-hoz a toHit-ban).
|
|
314
|
+
const queryTerms = fam_lexical_match_util_1.FAM_LexicalMatch_Util.tokenize(set.query.query);
|
|
259
315
|
const hits = [];
|
|
260
316
|
for (const vectorHit of vectorHits) {
|
|
261
317
|
const entry = byId.get(vectorHit.id);
|
|
@@ -267,12 +323,63 @@ class FAM_Retrieval_ControlService {
|
|
|
267
323
|
continue;
|
|
268
324
|
}
|
|
269
325
|
hits.push(this.toHit({
|
|
270
|
-
entry, vectorHit, table: set.table, weight,
|
|
271
|
-
importChunkWeight: set.config.importChunkWeight,
|
|
326
|
+
entry, vectorHit, table: set.table, weight, queryTerms: queryTerms,
|
|
327
|
+
importChunkWeight: set.config.importChunkWeight, importSourceWeight: set.config.importSourceWeight,
|
|
328
|
+
includeContent: set.includeContent, config: set.config, nowMs: Date.now(),
|
|
272
329
|
}));
|
|
273
330
|
}
|
|
331
|
+
// dsgn-013 §5.3 (MAM F3b) — COLD-search supplement: ha `memory` + engedélyezett + a hot pool kevés ERŐSEN-
|
|
332
|
+
// releváns találatot ad. FONTOS: NEM a candidate-darabszámot nézzük (a vektor-keresés ~MAX_PAGE_SIZE jelöltet ad
|
|
333
|
+
// vissza, így a `hits.length` szinte mindig nagy) — hanem az ERŐS (raw cosine ≥ relevanceFloor) hot hitek számát.
|
|
334
|
+
// Ha ezekből kevesebb mint topK van, a hot pool nem elégítette ki a query-t → a dormant készleten keresünk.
|
|
335
|
+
const strongHotHits = hits.filter((hit) => hit.score >= set.config.relevanceFloor).length;
|
|
336
|
+
if (set.table === fam_table_type_enum_1.FAM_Table.memory && set.config.memoryColdSearchEnabled && strongHotHits < set.config.topK) {
|
|
337
|
+
await this.supplementWithColdSearch(set, hits, set.query);
|
|
338
|
+
}
|
|
274
339
|
return hits;
|
|
275
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* A hot memória-találatok kiegészítése a dormant készletből (dsgn-013 §5.3). A cold-hiteket a `toHit`-en át
|
|
343
|
+
* (decay + faktorok) fűzzük be, a már látott `_id`-ket kiszűrve; a `weight ≤ 0` továbbra is kizárt.
|
|
344
|
+
*/
|
|
345
|
+
async supplementWithColdSearch(set, hits, query) {
|
|
346
|
+
// `topK` cold-jelölt (NEM `topK - hits.length` — a `hits` itt a teljes candidate-halmaz, ~MAX_PAGE_SIZE, ami
|
|
347
|
+
// negatívot adna). A cold-hitek a NORMÁL ranking-pipeline-ba kerülnek; a végső topK-vágás (buildResult) dönt.
|
|
348
|
+
// A tag/kind prefilter UGYANAZ, mint a hot-path-é (különben kiszűrt entry-ket hozna vissza a cold-supplement).
|
|
349
|
+
const coldHits = await fam_memory_cold_search_control_service_1.FAM_MemoryColdSearch_ControlService.getInstance().search({
|
|
350
|
+
queryVector: set.queryVector,
|
|
351
|
+
scopeIdSet: set.scopeExpand.scopeIdSet ?? [],
|
|
352
|
+
topK: set.config.topK,
|
|
353
|
+
maxCandidates: set.config.memoryColdSearchMaxCandidates,
|
|
354
|
+
nowMs: Date.now(),
|
|
355
|
+
tagFilter: query.tagFilter,
|
|
356
|
+
kindFilter: query.kindFilter,
|
|
357
|
+
});
|
|
358
|
+
const seen = new Set(hits.map((hit) => hit.id));
|
|
359
|
+
const queryTerms = fam_lexical_match_util_1.FAM_LexicalMatch_Util.tokenize(query.query);
|
|
360
|
+
for (const cold of coldHits) {
|
|
361
|
+
const id = cold.entry._id;
|
|
362
|
+
const weight = cold.entry.weight ?? 1;
|
|
363
|
+
if (!id || seen.has(id) || weight <= 0) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
hits.push(this.toHit({
|
|
367
|
+
entry: cold.entry, vectorHit: { id: id, score: cold.score }, table: set.table, weight: weight, queryTerms: queryTerms,
|
|
368
|
+
importChunkWeight: set.config.importChunkWeight, importSourceWeight: set.config.importSourceWeight,
|
|
369
|
+
includeContent: set.includeContent, config: set.config, nowMs: Date.now(),
|
|
370
|
+
}));
|
|
371
|
+
seen.add(id);
|
|
372
|
+
// KRITIKUS (dsgn-013 §5.3 promóció-gap fix): a cold-talált entry vektorát AZONNAL a hot poolba is upsert-eljük.
|
|
373
|
+
// Különben — miután a reaktiváció `dormant=false`-ra állítja — az entry SEM a hot poolban (csak boot-kor
|
|
374
|
+
// hidratálódik), SEM a cold-keresésben (a `dormant:true` szűrő kizárja) nem lenne megtalálható a következő
|
|
375
|
+
// re-hidratálásig → „elérhetetlen a promóció után". A pool-upsert idempotens; a (ritka) topK-ból kieső,
|
|
376
|
+
// de még dormant entry a következő bootkor úgyis kiesik a poolból (a hidratálás-szűrő korrigálja).
|
|
377
|
+
const vector = (cold.entry.contentVector ?? []);
|
|
378
|
+
if (vector.length) {
|
|
379
|
+
embedding_1.FAM_VectorSearch_ControlService.getInstance().upsertVector({ table: set.table, id: id, vector: vector });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
276
383
|
/**
|
|
277
384
|
* Az import-only chunkok (`chunkType:"imports"`) read-idejű rangsor-szorzója (FAM-REV-060). Egy import-sor
|
|
278
385
|
* (`import { X } from "./y"`) magas cosine-t kap egy szimbólum-név query-re, de alacsony az ÉRTÉKe (csak
|
|
@@ -282,18 +389,70 @@ class FAM_Retrieval_ControlService {
|
|
|
282
389
|
chunkTypeFactor(chunkType, importChunkWeight) {
|
|
283
390
|
return chunkType === 'imports' ? importChunkWeight : 1;
|
|
284
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* R2 import-source faktor (2026-06-22 eval-lelet): a `source.type:'import'` (claude-mem-import) entry-k finalScore-ja
|
|
394
|
+
* `importSourceWeight`-tel szorzódik (<1), hogy a kurált (`manual`/`agent`/scan) tanulságok a `memory`/`knowledge`
|
|
395
|
+
* táblákon FÖLÉ rangsoroljanak — az import zaja ne temesse őket. Minden NEM-import source érintetlen (×1).
|
|
396
|
+
* READ-time, a nyers `score` (relevancia) változatlan; `importSourceWeight=1` → kikapcsolva (változatlan rangsor).
|
|
397
|
+
*/
|
|
398
|
+
importSourceFactor(source, importSourceWeight) {
|
|
399
|
+
return source?.type === 'import' ? importSourceWeight : 1;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Memory Activation Model decay-szorzó (dsgn-013 §3) — CSAK a `memory` táron, ha engedélyezett. A frissen/gyakran
|
|
403
|
+
* FELIDÉZETT memória felül-, a régóta érintetlen alul-rangsorol, de a `floorWeight > 0` miatt SOSEM esik ki. A
|
|
404
|
+
* `lastRecalledAt` hiányában a `__created` ideje a fallback (cold-start kegyelmi idő). Minden más tár → ×1.
|
|
405
|
+
*/
|
|
406
|
+
memoryDecayFactor(entry, table, config, nowMs) {
|
|
407
|
+
if (table !== fam_table_type_enum_1.FAM_Table.memory) {
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
let factor = 1;
|
|
411
|
+
if (config.memoryActivationEnabled) {
|
|
412
|
+
const createdMs = entry.__created ? new Date(entry.__created).getTime() : undefined;
|
|
413
|
+
factor = fam_memory_activation_util_1.FAM_MemoryActivation_Util.decayFactor({
|
|
414
|
+
lastRecalledAt: entry.lastRecalledAt,
|
|
415
|
+
recallCount: entry.recallCount,
|
|
416
|
+
nowMs: nowMs,
|
|
417
|
+
fallbackMs: createdMs,
|
|
418
|
+
config: {
|
|
419
|
+
halfLifeDays: config.memoryHalfLifeDays, floorWeight: config.memoryFloorWeight,
|
|
420
|
+
maxFrequencyBoost: config.memoryMaxFrequencyBoost, activationWeight: config.memoryActivationWeight,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
// dsgn-013 §6 (MAM F4): a LEVÁLTOTT (superseded) régi tartalom penalizálva — a friss verzió fölébe kerül (de
|
|
425
|
+
// megmarad, cold-kereshető). A penalty az aktiváció-decay MELLÉ szorzódik (akkor is hat, ha a decay ki van kapcsolva).
|
|
426
|
+
if (entry.superseded) {
|
|
427
|
+
factor *= config.memorySupersededPenalty;
|
|
428
|
+
}
|
|
429
|
+
return factor;
|
|
430
|
+
}
|
|
285
431
|
/** Egy candidate-entry + a vektor-hit → `FAM_RetrievalHit` (finalScore + denormalizált mezők). */
|
|
286
432
|
toHit(set) {
|
|
287
433
|
const lastModified = set.entry.__lastModified;
|
|
288
|
-
// ⑤ weight-szorzó + FAM-REV-060 import-chunk read-faktor
|
|
434
|
+
// ⑤ weight-szorzó + FAM-REV-060 import-chunk read-faktor + R2 import-SOURCE faktor (2026-06-22 eval-lelet:
|
|
435
|
+
// a claude-mem-import zaja ne temesse a kurált tanulságokat) + dsgn-013 memory-decay (CSAK a memory táron) +
|
|
436
|
+
// Improvement 2 hibrid LEXIKAI keyword-boost (a literal-match felszínre kerül, BOOST-only) →
|
|
437
|
+
// finalScore = score × weight × chunkF × sourceF × decayF × (1 + keywordWeight × lexicalScore).
|
|
289
438
|
const chunkFactor = this.chunkTypeFactor(set.entry.chunkType, set.importChunkWeight);
|
|
439
|
+
const sourceFactor = this.importSourceFactor(set.entry.source, set.importSourceWeight);
|
|
440
|
+
const decayFactor = this.memoryDecayFactor(set.entry, set.table, set.config, set.nowMs);
|
|
441
|
+
const lexicalScore = set.config.keywordWeight > 0
|
|
442
|
+
? fam_lexical_match_util_1.FAM_LexicalMatch_Util.score(set.queryTerms, {
|
|
443
|
+
content: set.entry.content, headingPath: set.entry.headingPath, sourceFilePath: set.entry.source?.path,
|
|
444
|
+
})
|
|
445
|
+
: 0;
|
|
446
|
+
const keywordBoost = 1 + set.config.keywordWeight * lexicalScore;
|
|
290
447
|
return {
|
|
291
448
|
id: set.vectorHit.id,
|
|
292
449
|
table: set.table,
|
|
293
450
|
score: set.vectorHit.score,
|
|
294
451
|
weight: set.weight,
|
|
295
|
-
finalScore: set.vectorHit.score * set.weight * chunkFactor,
|
|
452
|
+
finalScore: set.vectorHit.score * set.weight * chunkFactor * sourceFactor * decayFactor * keywordBoost,
|
|
453
|
+
lexicalScore: lexicalScore,
|
|
296
454
|
content: set.includeContent ? set.entry.content : undefined,
|
|
455
|
+
contentHash: set.entry.contentHash,
|
|
297
456
|
kind: set.entry.kind,
|
|
298
457
|
tags: set.entry.tags ?? [],
|
|
299
458
|
...this.ruleMeta(set.entry, set.table),
|
|
@@ -324,6 +483,13 @@ class FAM_Retrieval_ControlService {
|
|
|
324
483
|
category: rule.category,
|
|
325
484
|
isActive: rule.isActive,
|
|
326
485
|
applicableScopes: (rule.applicableScopes ?? []),
|
|
486
|
+
// Rules-Management MP-1 mezők a hit-en (read-DTO bővítés) — a hívó/reflex így megkülönbözteti a KURÁLT
|
|
487
|
+
// kanonikus szabályt (ruleScope/importance/userApproved/ruleId) a nyers, scannelt rules-korpusztól.
|
|
488
|
+
ruleScope: rule.ruleScope,
|
|
489
|
+
ruleScopeTopic: rule.ruleScopeTopic,
|
|
490
|
+
importance: rule.importance,
|
|
491
|
+
userApproved: rule.userApproved,
|
|
492
|
+
ruleId: rule.ruleId,
|
|
327
493
|
};
|
|
328
494
|
}
|
|
329
495
|
// =========================================================================
|
|
@@ -488,18 +654,44 @@ class FAM_Retrieval_ControlService {
|
|
|
488
654
|
const minScore = query.minScore ?? await this.resolveNumber(config, 'read.minScore', ctx, 0);
|
|
489
655
|
const relevanceFloor = await this.resolveNumber(config, 'read.relevanceFloor', ctx, 0.5);
|
|
490
656
|
const importChunkWeight = await this.resolveNumber(config, 'read.importChunkWeight', ctx, 0.4);
|
|
657
|
+
const importSourceWeight = await this.resolveNumber(config, 'read.importSourceWeight', ctx, 0.85);
|
|
658
|
+
const keywordWeight = await this.resolveNumber(config, 'read.keywordWeight', ctx, 0.6);
|
|
491
659
|
const denseResultRatio = await this.resolveNumber(config, 'read.denseResultRatio', ctx, 2.0);
|
|
492
660
|
const denseResultAbsMin = await this.resolveNumber(config, 'read.denseResultAbsMin', ctx, 25);
|
|
493
661
|
const scoreOrderValue = (await config.resolve('read.scoreOrder', ctx)).value;
|
|
494
662
|
const scoreOrder = typeof scoreOrderValue === 'string' ? scoreOrderValue : 'weighted';
|
|
663
|
+
// Memory Activation Model (dsgn-013) — a `memory.*` namespace; a decay CSAK a memory táron alkalmazódik (toHit).
|
|
664
|
+
const memoryActivationValue = (await config.resolve('memory.activationEnabled', ctx)).value;
|
|
665
|
+
const memoryActivationEnabled = memoryActivationValue !== false;
|
|
666
|
+
const memoryHalfLifeDays = await this.resolveNumber(config, 'memory.decayHalfLifeDays', ctx, 90);
|
|
667
|
+
const memoryFloorWeight = await this.resolveNumber(config, 'memory.floorWeight', ctx, 0.05);
|
|
668
|
+
const memoryMaxFrequencyBoost = await this.resolveNumber(config, 'memory.maxFrequencyBoost', ctx, 2.5);
|
|
669
|
+
const memoryActivationWeight = await this.resolveNumber(config, 'memory.activationWeight', ctx, 0.35);
|
|
670
|
+
const memoryReactivateValue = (await config.resolve('memory.reactivateOnRead', ctx)).value;
|
|
671
|
+
const memoryReactivateOnRead = memoryReactivateValue !== false;
|
|
672
|
+
const memoryColdSearchValue = (await config.resolve('memory.coldSearchEnabled', ctx)).value;
|
|
673
|
+
const memoryColdSearchEnabled = memoryColdSearchValue !== false;
|
|
674
|
+
const memoryColdSearchMaxCandidates = await this.resolveNumber(config, 'memory.coldSearchMaxCandidates', ctx, 5000);
|
|
675
|
+
const memorySupersededPenalty = await this.resolveNumber(config, 'memory.supersededPenalty', ctx, 0.25);
|
|
495
676
|
return {
|
|
496
677
|
topK: Math.min(topKDefault, MAX_PAGE_SIZE),
|
|
497
678
|
minScore: minScore,
|
|
498
679
|
relevanceFloor: relevanceFloor,
|
|
499
680
|
importChunkWeight: importChunkWeight,
|
|
681
|
+
importSourceWeight: importSourceWeight,
|
|
682
|
+
keywordWeight: keywordWeight,
|
|
500
683
|
denseResultRatio: denseResultRatio,
|
|
501
684
|
denseResultAbsMin: denseResultAbsMin,
|
|
502
685
|
scoreOrder: scoreOrder,
|
|
686
|
+
memoryActivationEnabled: memoryActivationEnabled,
|
|
687
|
+
memoryHalfLifeDays: memoryHalfLifeDays,
|
|
688
|
+
memoryFloorWeight: memoryFloorWeight,
|
|
689
|
+
memoryMaxFrequencyBoost: memoryMaxFrequencyBoost,
|
|
690
|
+
memoryActivationWeight: memoryActivationWeight,
|
|
691
|
+
memoryReactivateOnRead: memoryReactivateOnRead,
|
|
692
|
+
memoryColdSearchEnabled: memoryColdSearchEnabled,
|
|
693
|
+
memoryColdSearchMaxCandidates: memoryColdSearchMaxCandidates,
|
|
694
|
+
memorySupersededPenalty: memorySupersededPenalty,
|
|
503
695
|
};
|
|
504
696
|
}
|
|
505
697
|
/** Egy szám-config feloldása a default-tal (a `resolve.value` lehet más típus → fallback). */
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FAM_RuleInjection_ControlService = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const mongoose_1 = tslib_1.__importDefault(require("mongoose"));
|
|
6
|
+
const error_codes_const_1 = require("../../../_collections/error-codes.const");
|
|
7
|
+
const fam_error_factory_util_1 = require("../../../_collections/fam-error-factory.util");
|
|
8
|
+
const fam_rules_data_model_1 = require("../../../_models/data-models/fam-rules.data-model");
|
|
9
|
+
const embedding_1 = require("../../embedding");
|
|
10
|
+
const fam_rule_injection_util_1 = require("../_collections/fam-rule-injection.util");
|
|
11
|
+
const fam_rule_propagation_util_1 = require("../_collections/fam-rule-propagation.util");
|
|
12
|
+
const fam_rule_scope_context_util_1 = require("../_collections/fam-rule-scope-context.util");
|
|
13
|
+
/**
|
|
14
|
+
* `FAM_RuleInjection_ControlService` (Rules-Management MP-2) — a SessionStart-injektálás (+ a `read`-reflex) szerver-oldali
|
|
15
|
+
* ASSEMBLY-je: a `cwd`-ből (vagy explicit felülírásból) detektálja a tier-kontextust, prefilterezi a `fam_rules`-t a
|
|
16
|
+
* MOST érvényes TIER-ekre (`FAM_RuleInjection_Util.buildFilter`), `importance`-rangsorolja, és char-budgetbe illeszti
|
|
17
|
+
* a „(…and N more)" záró sorral. A hook így DUMB marad (csak hív + az `additionalContext`-be teszi a blokkot) — a teljes
|
|
18
|
+
* ranking/scope/budget logika ITT, szerver-oldalon, unit-tesztelhetően él (mint a `/api/doctor`).
|
|
19
|
+
*/
|
|
20
|
+
class FAM_RuleInjection_ControlService {
|
|
21
|
+
static _instance;
|
|
22
|
+
issuer = 'FAM_RuleInjection_ControlService';
|
|
23
|
+
static getInstance() {
|
|
24
|
+
if (!FAM_RuleInjection_ControlService._instance) {
|
|
25
|
+
FAM_RuleInjection_ControlService._instance = new FAM_RuleInjection_ControlService();
|
|
26
|
+
}
|
|
27
|
+
return FAM_RuleInjection_ControlService._instance;
|
|
28
|
+
}
|
|
29
|
+
/** A MOST érvényes always-apply rule-blokk összeállítása (detektált/felülírt kontextus alapján). */
|
|
30
|
+
async assembleInjection(input) {
|
|
31
|
+
const context = this.resolveContext(input);
|
|
32
|
+
const rules = await this.loadRulesForContext(context);
|
|
33
|
+
const assembled = fam_rule_injection_util_1.FAM_RuleInjection_Util.assemble({
|
|
34
|
+
rules: rules, budgetChars: input.budgetChars,
|
|
35
|
+
});
|
|
36
|
+
return { ...assembled, context: context };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* MP-5 — a kontextusra érvényes szabályok GENERÁLT, marker-keretezett `CLAUDE.md`-blokkja (propagáció a FAM-nélküli
|
|
40
|
+
* projekt-agenteknek). Budget NÉLKÜL (minden alkalmazható szabály bekerül), dedup `ruleId`/`derivedFrom` szerint.
|
|
41
|
+
*/
|
|
42
|
+
async assemblePropagation(input) {
|
|
43
|
+
const context = this.resolveContext(input);
|
|
44
|
+
const rules = await this.loadRulesForContext(context);
|
|
45
|
+
const block = fam_rule_propagation_util_1.FAM_RulePropagation_Util.wrap(fam_rule_propagation_util_1.FAM_RulePropagation_Util.renderBlock(rules));
|
|
46
|
+
return { block: block, ruleCount: fam_rule_propagation_util_1.FAM_RulePropagation_Util.dedup(rules).length, context: context };
|
|
47
|
+
}
|
|
48
|
+
/** A tier-kontextusra érvényes, aktív `rules`-entry-k betöltése injektálható view-ként (közös SSOT a két assembly-hez). */
|
|
49
|
+
async loadRulesForContext(context) {
|
|
50
|
+
const model = this.rulesModel();
|
|
51
|
+
if (!model) {
|
|
52
|
+
throw fam_error_factory_util_1.FAM_Error_Util.create({
|
|
53
|
+
errorCode: error_codes_const_1.FAM_ERROR_CODES.vecSearch,
|
|
54
|
+
message: 'A `fam_rules` model nincs regisztrálva (boot-előtt?).',
|
|
55
|
+
issuer: this.issuer,
|
|
56
|
+
context: { operation: 'load-rules-for-context' },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const filter = fam_rule_injection_util_1.FAM_RuleInjection_Util.buildFilter(context);
|
|
60
|
+
// CSAK a rangsorhoz/megjelenítéshez kellő mezők (a vektor NEM kell → könnyű lean-query).
|
|
61
|
+
const docs = await model.find(filter, {
|
|
62
|
+
content: 1, importance: 1, weight: 1, globalRule: 1, category: 1,
|
|
63
|
+
ruleScope: 1, ruleScopeTopic: 1, ruleId: 1, derivedFrom: 1, __lastModified: 1,
|
|
64
|
+
}).lean();
|
|
65
|
+
return docs.map((doc) => this.toInjectable(doc));
|
|
66
|
+
}
|
|
67
|
+
/** Explicit felülírás > `cwd`-detekció. A `projectType`/`topics`/budget mindig az inputból. */
|
|
68
|
+
resolveContext(input) {
|
|
69
|
+
const detected = fam_rule_scope_context_util_1.FAM_RuleScopeContext_Util.detect(input.cwd);
|
|
70
|
+
return {
|
|
71
|
+
isFdp: input.isFdp ?? detected.isFdp,
|
|
72
|
+
inProject: input.inProject ?? detected.inProject,
|
|
73
|
+
projectName: detected.projectName,
|
|
74
|
+
projectRoot: detected.projectRoot,
|
|
75
|
+
projectType: input.projectType ?? detected.projectType,
|
|
76
|
+
topics: input.topics ?? detected.topics,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Egy lean Mongo-doc → `FAM_InjectableRule` view. */
|
|
80
|
+
toInjectable(doc) {
|
|
81
|
+
const lastModified = doc.__lastModified;
|
|
82
|
+
return {
|
|
83
|
+
id: typeof doc._id === 'string' ? doc._id : (doc._id ? String(doc._id) : undefined),
|
|
84
|
+
content: typeof doc.content === 'string' ? doc.content : undefined,
|
|
85
|
+
importance: typeof doc.importance === 'number' ? doc.importance : undefined,
|
|
86
|
+
weight: typeof doc.weight === 'number' ? doc.weight : undefined,
|
|
87
|
+
globalRule: doc.globalRule === true,
|
|
88
|
+
category: typeof doc.category === 'string' ? doc.category : undefined,
|
|
89
|
+
ruleScope: typeof doc.ruleScope === 'string' ? doc.ruleScope : undefined,
|
|
90
|
+
ruleScopeTopic: typeof doc.ruleScopeTopic === 'string' ? doc.ruleScopeTopic : undefined,
|
|
91
|
+
ruleId: typeof doc.ruleId === 'string' ? doc.ruleId : undefined,
|
|
92
|
+
derivedFrom: typeof doc.derivedFrom === 'string' ? doc.derivedFrom : undefined,
|
|
93
|
+
lastModifiedMs: lastModified ? new Date(lastModified).getTime() : 0,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** A `fam_rules` mongoose-modellje (a DS-példányosítás regisztrálja). */
|
|
97
|
+
rulesModel() {
|
|
98
|
+
new embedding_1.FAM_Entry_DataService({ dataParams: fam_rules_data_model_1.famRules_dataParams, issuer: this.issuer });
|
|
99
|
+
return mongoose_1.default.models[fam_rules_data_model_1.famRules_dataParams.dataName];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.FAM_RuleInjection_ControlService = FAM_RuleInjection_ControlService;
|
|
@@ -10,10 +10,21 @@
|
|
|
10
10
|
* opcionális relevance-filter = BACKLOG (NEM épül).
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.FAM_RetrievalSuggestions_Util = exports.FAM_RetrievalCandidate_DataService = exports.FAM_Retrieval_ControlService = void 0;
|
|
13
|
+
exports.FAM_RetrievalSuggestions_Util = exports.FAM_RetrievalCandidate_DataService = exports.FAM_RuleScopeContext_Util = exports.FAM_RulePropagation_Util = exports.FAM_RuleInjection_Util = exports.FAM_RuleInjection_ControlService = exports.FAM_GlobalRule_ControlService = exports.FAM_Retrieval_ControlService = void 0;
|
|
14
14
|
// SP-5.1/5.2/5.3 — a read-pipeline orchestrátor
|
|
15
15
|
var fam_retrieval_control_service_1 = require("./_services/fam-retrieval.control-service");
|
|
16
16
|
Object.defineProperty(exports, "FAM_Retrieval_ControlService", { enumerable: true, get: function () { return fam_retrieval_control_service_1.FAM_Retrieval_ControlService; } });
|
|
17
|
+
var fam_global_rule_control_service_1 = require("./_services/fam-global-rule.control-service");
|
|
18
|
+
Object.defineProperty(exports, "FAM_GlobalRule_ControlService", { enumerable: true, get: function () { return fam_global_rule_control_service_1.FAM_GlobalRule_ControlService; } });
|
|
19
|
+
// Rules-Management MP-2 — always-apply rule-injektálás (SessionStart inject + read-reflex) szerver-oldali assembly
|
|
20
|
+
var fam_rule_injection_control_service_1 = require("./_services/fam-rule-injection.control-service");
|
|
21
|
+
Object.defineProperty(exports, "FAM_RuleInjection_ControlService", { enumerable: true, get: function () { return fam_rule_injection_control_service_1.FAM_RuleInjection_ControlService; } });
|
|
22
|
+
var fam_rule_injection_util_1 = require("./_collections/fam-rule-injection.util");
|
|
23
|
+
Object.defineProperty(exports, "FAM_RuleInjection_Util", { enumerable: true, get: function () { return fam_rule_injection_util_1.FAM_RuleInjection_Util; } });
|
|
24
|
+
var fam_rule_propagation_util_1 = require("./_collections/fam-rule-propagation.util");
|
|
25
|
+
Object.defineProperty(exports, "FAM_RulePropagation_Util", { enumerable: true, get: function () { return fam_rule_propagation_util_1.FAM_RulePropagation_Util; } });
|
|
26
|
+
var fam_rule_scope_context_util_1 = require("./_collections/fam-rule-scope-context.util");
|
|
27
|
+
Object.defineProperty(exports, "FAM_RuleScopeContext_Util", { enumerable: true, get: function () { return fam_rule_scope_context_util_1.FAM_RuleScopeContext_Util; } });
|
|
17
28
|
// SP-5.1 ① scope + ② tag/kind Mongo-prefilter (a vektor-keresés ELŐTT)
|
|
18
29
|
var fam_retrieval_candidate_data_service_1 = require("./_services/fam-retrieval-candidate.data-service");
|
|
19
30
|
Object.defineProperty(exports, "FAM_RetrievalCandidate_DataService", { enumerable: true, get: function () { return fam_retrieval_candidate_data_service_1.FAM_RetrievalCandidate_DataService; } });
|
|
@@ -54,5 +54,81 @@ class FAM_ScopeMaintenance_Util {
|
|
|
54
54
|
},
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* RESET — egy scope (scan-eredetű) entry-jeinek TÖRLÉS-szűrője (PURE, user-FR 2026-06-24 — recovery
|
|
59
|
+
* egy félre-routolt scan után). A `source.type` default `'scan'` → a manuális/agent/import-SZERZETT
|
|
60
|
+
* (pótolhatatlan) tartalmat SOHA nem törli (csak az újra-szkennelhető scan-chunkokat). Opcionális
|
|
61
|
+
* `sourceRootContains` SUBSTRING-szűrés a `source.root`-on (case-insensitive, literál-escape-elve) — a
|
|
62
|
+
* cross-root szennyezés SEBÉSZI eltávolításához: pl. a `ccap-revisioned` gyökerű entry-k a `ccap-client`
|
|
63
|
+
* scope-ból, a `ccap` saját entry-jeinek MEGTARTÁSÁVAL. `sourceRootContains` nélkül a scope MINDEN
|
|
64
|
+
* scan-entry-je (a teljes reset).
|
|
65
|
+
*/
|
|
66
|
+
static buildResetFilter(set) {
|
|
67
|
+
const filter = { 'scopePath.scopeId': set.scopeId };
|
|
68
|
+
const sourceType = (set.sourceType ?? 'scan').trim();
|
|
69
|
+
if (sourceType) {
|
|
70
|
+
filter['source.type'] = sourceType;
|
|
71
|
+
}
|
|
72
|
+
const needle = (set.sourceRootContains ?? '').trim();
|
|
73
|
+
if (needle) {
|
|
74
|
+
filter['source.root'] = { $regex: FAM_ScopeMaintenance_Util.escapeRegex(needle), $options: 'i' };
|
|
75
|
+
}
|
|
76
|
+
return filter;
|
|
77
|
+
}
|
|
78
|
+
/** Regex-metakarakter escape (PURE): a substring-szűrőt LITERÁLKÉNT illeszti (nincs véletlen regex-tágítás). */
|
|
79
|
+
static escapeRegex(value) {
|
|
80
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `buildMergeSuggestions(groups)` (scope-prevenció, user-FR 2026-06-22) — TISZTA logika: a detektált
|
|
84
|
+
* duplikátum-csoportokból konkrét, futtatható `merge_scopes from→into` direktívák. A csoportonkénti `into`
|
|
85
|
+
* a **kanonikus** (megtartandó) scope: (1) preferáltan a parented (= tipikusan scan-eredetű, org-fán lóg),
|
|
86
|
+
* (2) azon belül/különben a LEGTÖBB entry-jű (a legtöbb tudást hordozó). A többi tag → `from` (átpontozandó).
|
|
87
|
+
*
|
|
88
|
+
* **Prioritás:** a `'repo-key'` csoportok ELŐBB (ez a legbiztosabb identitás — a KÜLÖNBÖZŐ nevű, AZONOS repo
|
|
89
|
+
* eseteket is elkapja), majd a `'name'` csoportok. Egy `from` scope CSAK EGYSZER javasolt (a már elköltött
|
|
90
|
+
* scope-okat — akár from, akár egy korábbi into — kihagyjuk → nincs ütköző/körkörös merge). Determinisztikus
|
|
91
|
+
* (entryCount-, majd scopeId-rendezés). NEM mutál — csak javaslat; az alkalmazás a `mergeScopes` (dryRun).
|
|
92
|
+
*/
|
|
93
|
+
static buildMergeSuggestions(groups) {
|
|
94
|
+
const suggestions = [];
|
|
95
|
+
const consumed = new Set();
|
|
96
|
+
// A 'repo-key' csoportok előbb (biztosabb identitás), majd a 'name'.
|
|
97
|
+
const ordered = groups.slice().sort((a, b) => (a.groupKind === b.groupKind ? 0 : (a.groupKind === 'repo-key' ? -1 : 1)));
|
|
98
|
+
for (const group of ordered) {
|
|
99
|
+
const available = group.members.filter((member) => !consumed.has(member.scopeId));
|
|
100
|
+
if (available.length < 2) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const into = FAM_ScopeMaintenance_Util.chooseMergeInto(available);
|
|
104
|
+
consumed.add(into.scopeId);
|
|
105
|
+
for (const member of available) {
|
|
106
|
+
if (member.scopeId === into.scopeId || consumed.has(member.scopeId)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
consumed.add(member.scopeId);
|
|
110
|
+
suggestions.push({
|
|
111
|
+
from: { scopeId: member.scopeId, canonicalName: member.canonicalName },
|
|
112
|
+
into: { scopeId: into.scopeId, canonicalName: into.canonicalName },
|
|
113
|
+
reason: group.groupKind === 'repo-key'
|
|
114
|
+
? `Azonos kanonikus repo-kulcs ('${group.key}') — különböző nevű scope-ok ugyanarra a repóra mutatnak.`
|
|
115
|
+
: `Azonos (layer|név) duplikátum ('${group.key}') — a parented/nagyobb scope a megtartandó.`,
|
|
116
|
+
fromEntryCount: member.entryCount,
|
|
117
|
+
intoEntryCount: into.entryCount,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return suggestions;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A megtartandó (`into`) scope kiválasztása egy duplikátum-csoportból (PURE): preferáltan a parented (van
|
|
125
|
+
* `parentScopeId` — az org-fán lógó, tipikusan scan-eredetű), azon belül/különben a LEGTÖBB entry-jű; döntetlen
|
|
126
|
+
* → a kisebb `scopeId` (determinizmus). A csoportnak legalább 1 tagja kell legyen.
|
|
127
|
+
*/
|
|
128
|
+
static chooseMergeInto(members) {
|
|
129
|
+
const parented = members.filter((member) => !!member.parentScopeId);
|
|
130
|
+
const pool = parented.length ? parented : members;
|
|
131
|
+
return pool.slice().sort((a, b) => (b.entryCount - a.entryCount) || a.scopeId.localeCompare(b.scopeId))[0];
|
|
132
|
+
}
|
|
57
133
|
}
|
|
58
134
|
exports.FAM_ScopeMaintenance_Util = FAM_ScopeMaintenance_Util;
|