@futdevpro/fdp-agent-memory 1.1.30 → 1.1.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/build/package.json +1 -1
  2. package/build/src/_cli/_commands/find-duplicates.command.js +41 -16
  3. package/build/src/_cli/_commands/scan-projects.command.js +113 -113
  4. package/build/src/_cli/_commands/scan.command.js +2 -0
  5. package/build/src/_cli/_commands/serve.command.js +110 -10
  6. package/build/src/_cli/_services/fam-client.service.js +25 -1
  7. package/build/src/_cli/register-commands.js +28 -0
  8. package/build/src/_collections/config-catalog.const.js +148 -6
  9. package/build/src/_collections/error-codes.const.js +2 -0
  10. package/build/src/_collections/fam-console.util.js +38 -260
  11. package/build/src/_collections/fam-db-models.const.js +2 -0
  12. package/build/src/_collections/fam-entry-bootstrap.util.js +20 -6
  13. package/build/src/_collections/fam-error-context.util.js +1 -0
  14. package/build/src/_collections/fam-mcp-bridge.util.js +15 -1
  15. package/build/src/_collections/fam-operation-queue.service.js +85 -0
  16. package/build/src/_collections/fam-project-discovery.util.js +148 -0
  17. package/build/src/_collections/fam-request-origin.util.js +41 -0
  18. package/build/src/_collections/fam-retry.util.js +48 -0
  19. package/build/src/_collections/fam-scan-progress-sink.service.js +26 -0
  20. package/build/src/_enums/fam-rule-scope.type-enum.js +25 -0
  21. package/build/src/_models/data-models/fam-entry.data-model.js +29 -0
  22. package/build/src/_models/data-models/fam-memory.data-model.js +5 -0
  23. package/build/src/_models/data-models/fam-rules.data-model.js +24 -0
  24. package/build/src/_models/data-models/fam-scan-job.data-model.js +73 -0
  25. package/build/src/_models/data-models/fam-scope.data-model.js +9 -0
  26. package/build/src/_modules/embedding/_services/fam-dedup-warn.control-service.js +40 -0
  27. package/build/src/_modules/embedding/_services/fam-duplicate-scan.control-service.js +74 -16
  28. package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +50 -0
  29. package/build/src/_modules/embedding/_services/fam-embedding-pipeline.control-service.js +31 -5
  30. package/build/src/_modules/embedding/_services/fam-embedding.control-service.js +142 -9
  31. package/build/src/_modules/embedding/_services/fam-entry.data-service.js +9 -0
  32. package/build/src/_modules/embedding/_services/fam-hydration-coordinator.control-service.js +159 -0
  33. package/build/src/_modules/embedding/_services/fam-lmstudio-embedding.provider.js +41 -4
  34. package/build/src/_modules/embedding/_services/fam-vector-search.control-service.js +67 -12
  35. package/build/src/_modules/embedding/index.js +5 -1
  36. package/build/src/_modules/ingest/_collections/fam-file-routing.util.js +10 -0
  37. package/build/src/_modules/ingest/_collections/fam-frontmatter.util.js +134 -0
  38. package/build/src/_modules/ingest/_collections/fam-project-identity.util.js +30 -0
  39. package/build/src/_modules/ingest/_collections/fam-scan-progress.util.js +5 -2
  40. package/build/src/_modules/ingest/_collections/fam-scan-summary.util.js +55 -29
  41. package/build/src/_modules/ingest/_collections/fam-split-chunker.util.js +139 -0
  42. package/build/src/_modules/ingest/_models/interfaces/fam-scan-job.interface.js +2 -0
  43. package/build/src/_modules/ingest/_services/fam-chunker.control-service.js +10 -0
  44. package/build/src/_modules/ingest/_services/fam-delta-compare.util.js +68 -1
  45. package/build/src/_modules/ingest/_services/fam-ingest-run.data-service.js +1 -1
  46. package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +173 -37
  47. package/build/src/_modules/ingest/_services/fam-scan-job.control-service.js +429 -0
  48. package/build/src/_modules/ingest/_services/fam-scan-job.data-service.js +34 -0
  49. package/build/src/_modules/ingest/_services/fam-scan.control-service.js +6 -4
  50. package/build/src/_modules/ingest/index.js +6 -1
  51. package/build/src/_modules/mcp/_services/fam-capability-registry.service.js +151 -8
  52. package/build/src/_modules/mcp/_services/fam-read-tool.service.js +70 -0
  53. package/build/src/_modules/mcp/_services/fam-write-tool.service.js +36 -0
  54. package/build/src/_modules/retrieval/_collections/fam-lexical-match.util.js +69 -0
  55. package/build/src/_modules/retrieval/_collections/fam-memory-activation.util.js +66 -0
  56. package/build/src/_modules/retrieval/_collections/fam-rule-injection.util.js +125 -0
  57. package/build/src/_modules/retrieval/_collections/fam-rule-propagation.util.js +95 -0
  58. package/build/src/_modules/retrieval/_collections/fam-rule-scope-context.util.js +74 -0
  59. package/build/src/_modules/retrieval/_services/fam-global-rule.control-service.js +82 -0
  60. package/build/src/_modules/retrieval/_services/fam-memory-cold-search.control-service.js +121 -0
  61. package/build/src/_modules/retrieval/_services/fam-memory-dormancy.control-service.js +99 -0
  62. package/build/src/_modules/retrieval/_services/fam-memory-reactivation.control-service.js +68 -0
  63. package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +15 -2
  64. package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +197 -5
  65. package/build/src/_modules/retrieval/_services/fam-rule-injection.control-service.js +102 -0
  66. package/build/src/_modules/retrieval/index.js +12 -1
  67. package/build/src/_modules/scope-reference/_collections/fam-scope-maintenance.util.js +76 -0
  68. package/build/src/_modules/scope-reference/_services/fam-scope-maintenance.control-service.js +233 -0
  69. package/build/src/_modules/scope-reference/_services/fam-scope.data-service.js +25 -0
  70. package/build/src/_routes/server/api/api.controller.js +270 -0
  71. package/build/src/_routes/server/api/fam-doctor.control-service.js +156 -0
  72. package/build/src/app.server.js +13 -10
  73. package/client-dist/{chunk-YXHWCJ5O.js → chunk-GF3FJP7E.js} +71 -71
  74. package/client-dist/chunk-NMUCMQY6.js +1 -0
  75. package/client-dist/favicon.ico +0 -0
  76. package/client-dist/index.html +3 -3
  77. package/client-dist/main-YK4YIVAQ.js +2 -0
  78. package/package.json +1 -1
  79. package/build/src/_cli/_collections/fam-project-discovery.util.js +0 -98
  80. package/client-dist/chunk-I77GXVAQ.js +0 -1
  81. package/client-dist/main-PJPEDVJT.js +0 -2
@@ -79,6 +79,20 @@ class FAM_ScopeMaintenance_ControlService {
79
79
  const dryRun = set.dryRun !== false;
80
80
  const intoRef = { scopeId: intoId, layer: into.layer ?? '', canonicalName: into.canonicalName ?? '' };
81
81
  const affected = await this.countAffected(fromId);
82
+ // MERGE-GUARD (2026-06-24 fix): külön projekt-gyökerek (`source.root`) → gyaníthatóan KÜLÖN projektek →
83
+ // BLOKK (force nélkül). Ez fogja meg a téves repoKey-duplikátum-merge-et (a `ccap-revisioned`→`ccap-speech-
84
+ // recognition` típusú szennyezést), MIELŐTT 50k fájl átkeveredne. A `force:true` (tudatos user-akció) felülírja.
85
+ const blockedReason = set.force
86
+ ? undefined
87
+ : await this.crossProjectMergeBlock(fromId, intoId, from.canonicalName ?? '', into.canonicalName ?? '');
88
+ if (blockedReason) {
89
+ return {
90
+ operation: 'merge_scopes', dryRun: dryRun, blockedReason: blockedReason,
91
+ from: { scopeId: fromId, layer: from.layer ?? '', canonicalName: from.canonicalName ?? '' },
92
+ into: intoRef, affected: affected, totalAffected: affected.reduce((s, i) => s + i.count, 0),
93
+ fromScopeDeleted: false,
94
+ };
95
+ }
82
96
  if (!dryRun) {
83
97
  const entryUpdate = fam_scope_maintenance_util_1.FAM_ScopeMaintenance_Util.mergeEntryUpdate(fromId, intoRef);
84
98
  await this.applyEntryUpdate(entryUpdate);
@@ -97,13 +111,232 @@ class FAM_ScopeMaintenance_ControlService {
97
111
  fromScopeDeleted: !dryRun,
98
112
  };
99
113
  }
114
+ /**
115
+ * Egy scope SCAN-eredetű entry-jeinek soft-delete-je (RECOVERY egy félre-routolt scan után, user-FR 2026-06-24).
116
+ * A `sourceType` default `'scan'` → a manuális/agent/import-SZERZETT (pótolhatatlan) tartalmat SOHA nem törli.
117
+ * Az opcionális `sourceRootContains` a `source.root`-ot szűri (sebészi cross-root takarítás: pl. a `ccap-revisioned`
118
+ * gyökerű szennyezés a `ccap-client` scope-ból, a `ccap` saját entry-jeinek megtartásával). `dryRun` (DEFAULT true)
119
+ * → csak a per-collection szám + a `rootSamples` (verifikáció: jó scope-ot/gyökeret törlünk-e), NEM ír. A tényleges
120
+ * (soft-delete + LVS-pool-kivétel) az explicit `dryRun:false`. A scan-tartalom ÚJRA-SZKENNELHETŐ → nem destruktív-végleges.
121
+ */
122
+ async resetScope(set) {
123
+ const scope = await this.resolveScope(set);
124
+ const scopeId = scope._id;
125
+ const dryRun = set.dryRun !== false;
126
+ const sourceType = (set.sourceType ?? 'scan').trim();
127
+ const sourceRootContains = (set.sourceRootContains ?? '').trim() || undefined;
128
+ const tableName = (set.table ?? '').trim() || undefined;
129
+ const filter = {
130
+ ...fam_scope_maintenance_util_1.FAM_ScopeMaintenance_Util.buildResetFilter({ scopeId: scopeId, sourceType: sourceType, sourceRootContains: sourceRootContains }),
131
+ _deleted: null, // csak az AKTÍV (nem már soft-deleted) entry-ket (a `_deleted` Date-mező; `null` = aktív/hiányzó).
132
+ };
133
+ const targets = tableName
134
+ ? (embedding_1.FAM_StoreRegistry_Util.getEntry(tableName) ? [embedding_1.FAM_StoreRegistry_Util.getEntry(tableName)] : [])
135
+ : embedding_1.FAM_STORE_REGISTRY;
136
+ const affected = [];
137
+ const rootSamples = new Set();
138
+ let totalAffected = 0;
139
+ for (const registryEntry of targets) {
140
+ // A példányosítás regisztrálja a modellt → a valós collection (a raw find a soft-delete-szemantikát explicit kezeli).
141
+ new embedding_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
142
+ const collection = mongoose_1.default.models[registryEntry.dataParams.dataName]?.collection;
143
+ if (!collection) {
144
+ continue;
145
+ }
146
+ const matched = await collection.find(filter, { projection: { 'source.root': 1 } }).toArray();
147
+ for (const entry of matched) {
148
+ const root = entry.source?.root;
149
+ if (root && rootSamples.size < 8) {
150
+ rootSamples.add(root);
151
+ }
152
+ }
153
+ if (!dryRun && matched.length) {
154
+ // BULK soft-delete (DIREKT `_deleted` — NEM `deleteData`): a FAM entry-tárak `addArchive:true`-ok, így a
155
+ // per-entry `deleteData` minden törölt entry-ből ARCHÍV-másolatot készítene (bloat + lassú 59k-ra). A reset
156
+ // DISCARD (nem archive) → közvetlen `_deleted` (Date), amit a `_deleted: null` aktív-szűrő kizár; a hívás
157
+ // utáni restart tiszta (a soft-deletedek nélküli) poolt hidratál. A live pool-coherence: in-memory removeVector.
158
+ await collection.updateMany(filter, { $set: { _deleted: new Date() } });
159
+ for (const entry of matched) {
160
+ embedding_1.FAM_VectorSearch_ControlService.getInstance().removeVector(registryEntry.table, String(entry._id));
161
+ }
162
+ }
163
+ affected.push({ collection: collection.collectionName, count: matched.length });
164
+ totalAffected += matched.length;
165
+ }
166
+ return {
167
+ operation: 'reset_scope',
168
+ dryRun: dryRun,
169
+ scope: { scopeId: scopeId, layer: scope.layer ?? '', canonicalName: scope.canonicalName ?? '' },
170
+ sourceType: sourceType,
171
+ sourceRootContains: sourceRootContains,
172
+ tableFilter: tableName,
173
+ affected: affected,
174
+ totalAffected: totalAffected,
175
+ rootSamples: [...rootSamples],
176
+ };
177
+ }
100
178
  /** Az `entryUpdate` spec alkalmazása MINDEN fő RAG-tár collection-jén (mongoose `updateMany` + arrayFilters). */
101
179
  async applyEntryUpdate(entryUpdate) {
102
180
  for (const named of this.entryCollections()) {
103
181
  await named.collection.updateMany(entryUpdate.filter, entryUpdate.update, { arrayFilters: entryUpdate.arrayFilters });
104
182
  }
105
183
  }
184
+ /** Egy scope entry-jeinek DISTINCT `source.root` halmaza minden fő tárban (a merge-guard bemenete). Normalizált (lowercase, `/`). */
185
+ async distinctRootsForScope(scopeId) {
186
+ const roots = new Set();
187
+ for (const named of this.entryCollections()) {
188
+ const values = await named.collection.distinct('source.root', { 'scopePath.scopeId': scopeId, _deleted: { $ne: true } });
189
+ for (const value of values) {
190
+ if (typeof value === 'string' && value.trim()) {
191
+ roots.add(value.trim().toLowerCase().replace(/\\/g, '/'));
192
+ }
193
+ }
194
+ }
195
+ return roots;
196
+ }
197
+ /**
198
+ * MERGE-GUARD mag (2026-06-24 fix): blokk-ok-szöveg, ha a `from` és az `into` scope entry-jei KÜLÖNBÖZŐ (disjunkt)
199
+ * projekt-gyökerekből származnak — vagyis a merge KÜLÖN projekteket keverne. `undefined` (engedélyezve), ha bármelyik
200
+ * oldalon NINCS root-infó (manuális/importált entry — nem dönthető el), VAGY ha van KÖZÖS gyökér (ugyanaz a projekt,
201
+ * pl. átnevezés-merge legit). Ez fogja meg a téves repoKey-duplikátum-merge-et a tartalom-átkeveredés ELŐTT.
202
+ */
203
+ async crossProjectMergeBlock(fromId, intoId, fromName, intoName) {
204
+ const [fromRoots, intoRoots] = await Promise.all([this.distinctRootsForScope(fromId), this.distinctRootsForScope(intoId)]);
205
+ if (!fromRoots.size || !intoRoots.size) {
206
+ return undefined; // nincs root-infó → nem blokkolunk (nem dönthető el biztosan)
207
+ }
208
+ const overlap = [...fromRoots].some((root) => intoRoots.has(root));
209
+ if (overlap) {
210
+ return undefined; // van közös projekt-gyökér → ugyanaz a projekt (legit merge)
211
+ }
212
+ const sample = (set) => [...set].slice(0, 3).join(', ');
213
+ return `A(z) '${fromName}' és '${intoName}' scope entry-jei KÜLÖNBÖZŐ projekt-gyökerekből származnak `
214
+ + `(from: ${sample(fromRoots)} | into: ${sample(intoRoots)}) — gyaníthatóan KÜLÖN projektek, a merge `
215
+ + `összekeverné a tartalmukat. Blokkolva (a téves repoKey-duplikátum-szennyezés ellen). `
216
+ + `Ha tényleg össze akarod olvasztani: force:true.`;
217
+ }
106
218
  /** A `scopeId`-t hivatkozó aktív entry-k száma per-collection (a 6 fő tár + a reference). */
219
+ /**
220
+ * Scope-integritás SELF-CHECK (user-FR 2026-06-22 — a rendszer ismerje fel magától a problémás scope-okat):
221
+ * (a) **árva projekt-scope** (`layer='project'`, de nincs `parentScopeId` — tipikusan import-eredetű lapos scope,
222
+ * a scan org-parented párja mellett), (b) **duplikátum** (ugyanaz a `(canonicalName, layer)` többször). Mindkettőhöz
223
+ * az entry-számot is adja (a merge-döntéshez: a parented/nagyobb az `into`). Csak OLVAS — a takarítás `merge_scopes`.
224
+ */
225
+ async detectIssues() {
226
+ const scopes = await this.scopeCollection().collection
227
+ .find({}, { projection: { canonicalName: 1, layer: 1, parentScopeId: 1, canonicalRepoKey: 1 } })
228
+ .toArray();
229
+ // Az entry-számokat EGYSZER számoljuk minden érintett scope-ra (a 3 csoport megosztja — nincs dupla countDocuments).
230
+ const entryCountCache = new Map();
231
+ const countFor = async (scopeId) => {
232
+ if (!entryCountCache.has(scopeId)) {
233
+ entryCountCache.set(scopeId, await this.totalEntryCount(scopeId));
234
+ }
235
+ return entryCountCache.get(scopeId);
236
+ };
237
+ const orphanProjectScopes = [];
238
+ const byNameKey = new Map();
239
+ const byRepoKey = new Map();
240
+ const pushTo = (map, key, scope) => {
241
+ const bucket = map.get(key) ?? [];
242
+ bucket.push(scope);
243
+ map.set(key, bucket);
244
+ };
245
+ for (const scope of scopes) {
246
+ pushTo(byNameKey, `${scope.layer ?? ''}|${scope.canonicalName ?? ''}`, scope);
247
+ const repoKey = (scope.canonicalRepoKey ?? '').trim().toLowerCase();
248
+ if (repoKey) {
249
+ pushTo(byRepoKey, repoKey, scope);
250
+ }
251
+ if (scope.layer === 'project' && !scope.parentScopeId) {
252
+ orphanProjectScopes.push({
253
+ scopeId: String(scope._id), canonicalName: scope.canonicalName ?? '',
254
+ entryCount: await countFor(String(scope._id)),
255
+ });
256
+ }
257
+ }
258
+ const toMember = async (scope) => ({
259
+ scopeId: String(scope._id), parentScopeId: scope.parentScopeId,
260
+ canonicalName: scope.canonicalName ?? '', canonicalRepoKey: scope.canonicalRepoKey,
261
+ entryCount: await countFor(String(scope._id)),
262
+ });
263
+ const duplicateScopes = [];
264
+ for (const [, group] of byNameKey) {
265
+ if (group.length <= 1) {
266
+ continue;
267
+ }
268
+ const members = [];
269
+ for (const scope of group) {
270
+ members.push(await toMember(scope));
271
+ }
272
+ duplicateScopes.push({ canonicalName: group[0].canonicalName ?? '', layer: group[0].layer ?? '', members: members });
273
+ }
274
+ const repoKeyDuplicates = [];
275
+ for (const [repoKey, group] of byRepoKey) {
276
+ // Egyedi scopeId-k (ugyanaz a scope ne számítson kétszer); ≥2 KÜLÖNBÖZŐ scope = repo-kulcs-duplikátum.
277
+ const uniqueIds = new Set(group.map((scope) => String(scope._id)));
278
+ if (uniqueIds.size <= 1) {
279
+ continue;
280
+ }
281
+ const members = [];
282
+ for (const scope of group) {
283
+ members.push(await toMember(scope));
284
+ }
285
+ repoKeyDuplicates.push({ canonicalRepoKey: repoKey, members: members });
286
+ }
287
+ return {
288
+ scopesScanned: scopes.length,
289
+ orphanProjectScopes: orphanProjectScopes,
290
+ duplicateScopes: duplicateScopes,
291
+ repoKeyDuplicates: repoKeyDuplicates,
292
+ hasIssues: orphanProjectScopes.length > 0 || duplicateScopes.length > 0 || repoKeyDuplicates.length > 0,
293
+ };
294
+ }
295
+ /**
296
+ * `suggestMerges()` (scope-prevenció, user-FR 2026-06-22) — a `detectIssues` által felismert duplikátumokból
297
+ * KONKRÉT, futtatható `merge_scopes from→into` direktívák (pure `buildMergeSuggestions`). A repo-kulcs-csoportok
298
+ * elsőbbséget élveznek (a legbiztosabb identitás), az `into` a parented/legtöbb-entry-jű scope. NEM mutál — a
299
+ * tényleges összeolvasztás a `mergeScopes` (dryRun-default). Ez teszi az auto-felismerést AKCIONÁLHATÓVÁ.
300
+ */
301
+ async suggestMerges() {
302
+ const report = await this.detectIssues();
303
+ const groups = [
304
+ ...report.repoKeyDuplicates.map((duplicate) => ({
305
+ groupKind: 'repo-key', key: duplicate.canonicalRepoKey,
306
+ members: duplicate.members.map((member) => this.toMergeMember(member)),
307
+ })),
308
+ ...report.duplicateScopes.map((duplicate) => ({
309
+ groupKind: 'name', key: `${duplicate.layer}|${duplicate.canonicalName}`,
310
+ members: duplicate.members.map((member) => this.toMergeMember(member, duplicate.canonicalName, duplicate.layer)),
311
+ })),
312
+ ];
313
+ return { suggestions: fam_scope_maintenance_util_1.FAM_ScopeMaintenance_Util.buildMergeSuggestions(groups), report: report };
314
+ }
315
+ /** Egy issue-member → merge-member (a name-csoportban a közös név/layer pótlása, ha a member nem hordozza). */
316
+ toMergeMember(member, fallbackName, layer) {
317
+ return {
318
+ scopeId: member.scopeId,
319
+ canonicalName: member.canonicalName ?? fallbackName ?? '',
320
+ layer: layer,
321
+ parentScopeId: member.parentScopeId,
322
+ entryCount: member.entryCount,
323
+ canonicalRepoKey: member.canonicalRepoKey,
324
+ };
325
+ }
326
+ /** Egy scope összes entry-száma a 6 fő tárban (a detekció merge-súlyozásához). */
327
+ async totalEntryCount(scopeId) {
328
+ let total = 0;
329
+ for (const named of this.entryCollections()) {
330
+ total += await named.collection.countDocuments({ 'scopePath.scopeId': scopeId });
331
+ }
332
+ return total;
333
+ }
334
+ /** A `fam_scopes` mongoose-collection-je (a DS-példányosítás regisztrálja a modellt). */
335
+ scopeCollection() {
336
+ new fam_scope_data_service_1.FAM_Scope_DataService({ issuer: this.issuer });
337
+ const model = mongoose_1.default.models['fam_scopes'];
338
+ return { name: model?.collection?.collectionName ?? 'fam_scopes', collection: model.collection };
339
+ }
107
340
  async countAffected(scopeId) {
108
341
  const affected = [];
109
342
  for (const named of this.entryCollections()) {
@@ -211,5 +211,30 @@ class FAM_Scope_DataService extends nts_dynamo_1.DyNTS_DataService {
211
211
  async setAliases(scopeId, aliases) {
212
212
  await this.updateData({ filterBy: { _id: scopeId }, update: { $set: { aliases: aliases } } });
213
213
  }
214
+ /**
215
+ * A scope `canonicalRepoKey` stamp-elése (scope-prevenció, user-FR 2026-06-22; `$set`, atomikus). A SCAN
216
+ * hívja a project-layer scope-ra a felismert git/package identitásból — erre épül a repo-kulcs alapú
217
+ * duplikátum-detekció. Csak nem-üres kulcsot ír; idempotens (ugyanaz a kulcs újraírása no-op-szerű).
218
+ */
219
+ async setCanonicalRepoKey(scopeId, canonicalRepoKey) {
220
+ const key = (canonicalRepoKey ?? '').trim();
221
+ if (!scopeId || !key) {
222
+ return;
223
+ }
224
+ await this.updateData({ filterBy: { _id: scopeId }, update: { $set: { canonicalRepoKey: key } } });
225
+ }
226
+ /**
227
+ * `findByRepoKey(canonicalRepoKey)` (scope-prevenció) — egy kanonikus repo-kulcsra illeszkedő összes aktív
228
+ * scope (case-insensitive trim-egyezés). ≥2 találat = repo-kulcs-duplikátum (a `detectIssues` merge-jelölt).
229
+ * A halmaz kicsi → in-memory szűrés.
230
+ */
231
+ async findByRepoKey(canonicalRepoKey) {
232
+ const target = (canonicalRepoKey ?? '').trim().toLowerCase();
233
+ if (!target) {
234
+ return [];
235
+ }
236
+ const all = await this.findAllActive();
237
+ return all.filter((scope) => (scope.canonicalRepoKey ?? '').trim().toLowerCase() === target);
238
+ }
214
239
  }
215
240
  exports.FAM_Scope_DataService = FAM_Scope_DataService;
@@ -9,10 +9,18 @@ const nts_dynamo_2 = require("@futdevpro/nts-dynamo");
9
9
  const fam_version_const_1 = require("../../../_collections/fam-version.const");
10
10
  const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
11
11
  const fam_config_level_type_enum_1 = require("../../../_enums/fam-config-level.type-enum");
12
+ const fam_operation_queue_service_1 = require("../../../_collections/fam-operation-queue.service");
13
+ const fam_scan_job_control_service_1 = require("../../../_modules/ingest/_services/fam-scan-job.control-service");
14
+ const fam_vector_search_control_service_1 = require("../../../_modules/embedding/_services/fam-vector-search.control-service");
12
15
  const config_control_service_1 = require("../config/config.control-service");
16
+ const errors_control_service_1 = require("../errors/errors.control-service");
17
+ const fam_error_data_model_1 = require("../../../_models/data-models/fam-error.data-model");
13
18
  const embedding_1 = require("../../../_modules/embedding");
14
19
  const ingest_1 = require("../../../_modules/ingest");
15
20
  const mcp_1 = require("../../../_modules/mcp");
21
+ const fam_doctor_control_service_1 = require("./fam-doctor.control-service");
22
+ const retrieval_1 = require("../../../_modules/retrieval");
23
+ const fam_request_origin_util_1 = require("../../../_collections/fam-request-origin.util");
16
24
  /** A UI/CLI lapozás CCAP-örökség konstansai (dsgn-010/dsgn-011 §4.1). */
17
25
  const DEFAULT_PAGE_SIZE = 50;
18
26
  const MAX_PAGE_SIZE = 500;
@@ -53,6 +61,9 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
53
61
  this.writeEndpoint(),
54
62
  this.capabilitiesEndpoint(),
55
63
  this.healthEndpoint(),
64
+ this.doctorEndpoint(),
65
+ this.rulesInjectEndpoint(),
66
+ this.rulesPropagateEndpoint(),
56
67
  this.itemsEndpoint(),
57
68
  this.statsEndpoint(),
58
69
  this.statsTableEndpoint(),
@@ -60,6 +71,12 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
60
71
  this.duplicatesEndpoint(),
61
72
  this.configGetEndpoint(),
62
73
  this.configSetEndpoint(),
74
+ this.scanStartEndpoint(),
75
+ this.scanStatusEndpoint(),
76
+ this.scanJobsEndpoint(),
77
+ this.scanCancelEndpoint(),
78
+ this.errorsSummaryEndpoint(),
79
+ this.errorsEndpoint(),
63
80
  ];
64
81
  }
65
82
  // =========================================================================
@@ -110,6 +127,14 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
110
127
  // Heap-load-shed (OOM-hibakezelés): a NEHÉZ write-eket (scan/import/re-embed) critical
111
128
  // memória-nyomásnál elutasítjuk (503), MIELŐTT egy nagy allokáció átlökné a plafont.
112
129
  this.assertHeapCapacity(String(input.operation));
130
+ // FEAT-008 (user-FR 2026-06-22): a NEHÉZ write-ek SZERVER-OLDALI QUEUE-n mennek át → a szerver
131
+ // szerializálja őket (egyszerre egy), így több agent egyidejű write-jai sorba rakódnak, és nincs
132
+ // párhuzamos scan-feldolgozás (a war-factory/warbots átfedés gyökér-megoldása). A könnyű ops
133
+ // (create/update/delete) bypass-olják a queue-t. Lásd `FAM_OperationQueue_Service`.
134
+ if (Api_Controller.HEAVY_WRITE_OPERATIONS.includes(String(input.operation))) {
135
+ return fam_operation_queue_service_1.FAM_OperationQueue_Service.getInstance()
136
+ .enqueue(() => mcp_1.FAM_WriteTool_Service.getInstance().handle(input));
137
+ }
113
138
  return mcp_1.FAM_WriteTool_Service.getInstance().handle(input);
114
139
  });
115
140
  },
@@ -132,9 +157,235 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
132
157
  ],
133
158
  });
134
159
  }
160
+ /**
161
+ * `POST /scan/start` (FEAT-008) — szerver-oldali multi-projekt scan-job INDÍTÁSA. Body: explicit `projects` VAGY
162
+ * `root` (felderítés a SZERVEREN) + opcionális cooldown-override. AZONNAL visszaad `{ jobId, total }` (a hosszú
163
+ * feldolgozás háttérben fut → nincs Node-requestTimeout). A progress a `GET /scan/status/:jobId`-on.
164
+ */
165
+ scanStartEndpoint() {
166
+ return new nts_dynamo_1.DyNTS_Endpoint_Params({
167
+ name: 'scanStart',
168
+ type: fsm_dynamo_1.DyFM_HttpCallType.post,
169
+ endpoint: '/scan/start',
170
+ tasks: [
171
+ async (req, res) => {
172
+ await this.run(res, async () => {
173
+ const input = (req.body ?? {});
174
+ // Aláírás-feloldás (user-FR 2026-06-25): a `X-FAM-*` header VAGY a body `origin` → KI indította
175
+ // a scant (több egyidejű workspace-session beazonosítható). A job-on + a lista-nézetben látszik.
176
+ input.origin = fam_request_origin_util_1.FAM_RequestOrigin_Util.fromRequest(req, input.origin);
177
+ return fam_scan_job_control_service_1.FAM_ScanJob_ControlService.getInstance().start(input);
178
+ });
179
+ },
180
+ ],
181
+ });
182
+ }
183
+ /** `GET /scan/status/:jobId` (FEAT-008) — egy scan-job állapota (progress + per-projekt eredmények). 404, ha ismeretlen. */
184
+ scanStatusEndpoint() {
185
+ return new nts_dynamo_1.DyNTS_Endpoint_Params({
186
+ name: 'scanStatus',
187
+ type: fsm_dynamo_1.DyFM_HttpCallType.get,
188
+ endpoint: '/scan/status/:jobId',
189
+ tasks: [
190
+ async (req, res) => {
191
+ await this.run(res, async () => {
192
+ const job = fam_scan_job_control_service_1.FAM_ScanJob_ControlService.getInstance().getStatus(String(req.params.jobId));
193
+ if (!job) {
194
+ throw new fsm_dynamo_1.DyFM_Error({
195
+ errorCode: 'FAM-SCAN-JOB-404',
196
+ message: `Ismeretlen scan-job: '${String(req.params.jobId)}' (lehet, hogy a szerver `
197
+ + 'újraindult — a job-ok in-memory tárolódnak).',
198
+ issuer: this.issuer,
199
+ });
200
+ }
201
+ return job;
202
+ });
203
+ },
204
+ ],
205
+ });
206
+ }
207
+ /**
208
+ * `GET /scan/jobs` (FEAT-008 Phase 5 — „jobb job kezelés") — AZ ÖSSZES (futó + befejezett) scan-job KOMPAKT listája,
209
+ * legfrissebb elöl. Egy elárvult/futó job a `jobId` ISMERETE NÉLKÜL is felfedezhető (+ a stall-jelzés is látszik).
210
+ */
211
+ scanJobsEndpoint() {
212
+ return new nts_dynamo_1.DyNTS_Endpoint_Params({
213
+ name: 'scanJobs',
214
+ type: fsm_dynamo_1.DyFM_HttpCallType.get,
215
+ endpoint: '/scan/jobs',
216
+ tasks: [
217
+ async (req, res) => {
218
+ await this.run(res, async () => ({ jobs: fam_scan_job_control_service_1.FAM_ScanJob_ControlService.getInstance().listJobs() }));
219
+ },
220
+ ],
221
+ });
222
+ }
223
+ /**
224
+ * `POST /scan/cancel/:jobId` (FEAT-008 Phase 5) — egy FUTÓ scan-job megszakítás-kérése (a processor a következő projekt
225
+ * ELŐTT ellenőrzi). `{ cancelled: true }`, ha a job létezett + futott; `false`, ha ismeretlen/már terminál.
226
+ */
227
+ scanCancelEndpoint() {
228
+ return new nts_dynamo_1.DyNTS_Endpoint_Params({
229
+ name: 'scanCancel',
230
+ type: fsm_dynamo_1.DyFM_HttpCallType.post,
231
+ endpoint: '/scan/cancel/:jobId',
232
+ tasks: [
233
+ async (req, res) => {
234
+ await this.run(res, async () => ({
235
+ cancelled: fam_scan_job_control_service_1.FAM_ScanJob_ControlService.getInstance().cancel(String(req.params.jobId)),
236
+ }));
237
+ },
238
+ ],
239
+ });
240
+ }
241
+ /**
242
+ * `GET /errors` (→ `/api/errors`, user-FR 2026-06-24: „kéne legyenek végpontok, amivel le tudjuk érni az errorokat")
243
+ * — a `fam_errors` rekordok KOMPAKT, FAM-natív nézete a `/api` felületen (a built-in `/api/server/errors` nyers/bő
244
+ * Dynamo-objektumai helyett). Query: `code` (errorCode-részlet, case-insensitive), `unhandledOnly` (`true` → csak nem
245
+ * kezeltek), `limit` (default 50, max 500). Sorrend: occurrence DESC, majd recency. A `contextSnapshot` (benne a
246
+ * `failedSample` + provider/table/operation) is kijön — így az LM-Studio-omlasztó tartalom-séma diagnosztizálható.
247
+ */
248
+ errorsEndpoint() {
249
+ return new nts_dynamo_1.DyNTS_Endpoint_Params({
250
+ name: 'errorsList',
251
+ type: fsm_dynamo_1.DyFM_HttpCallType.get,
252
+ endpoint: '/errors',
253
+ tasks: [
254
+ async (req, res) => {
255
+ await this.run(res, async () => {
256
+ const unhandledOnly = req.query.unhandledOnly === 'true';
257
+ const codeFilter = req.query.code ? String(req.query.code).toLowerCase() : '';
258
+ const limit = Math.min(500, Math.max(1, Number(req.query.limit) || 50));
259
+ const all = await errors_control_service_1.FAM_Error_ControlService.getInstance()
260
+ .listErrors(unhandledOnly ? { handled: false } : undefined);
261
+ const filtered = all
262
+ .filter((r) => !codeFilter || (r.errorCode ?? '').toLowerCase().includes(codeFilter))
263
+ .sort((a, b) => fam_error_data_model_1.FAM_Error_DataModel.occurrenceCount(b) - fam_error_data_model_1.FAM_Error_DataModel.occurrenceCount(a)
264
+ || String(b._id ?? '').localeCompare(String(a._id ?? '')));
265
+ return {
266
+ total: filtered.length,
267
+ returned: Math.min(limit, filtered.length),
268
+ errors: filtered.slice(0, limit).map((r) => this.mapErrorCompact(r)),
269
+ };
270
+ });
271
+ },
272
+ ],
273
+ });
274
+ }
275
+ /**
276
+ * `GET /errors/summary` (→ `/api/errors/summary`) — errorCode szerint CSOPORTOSÍTOTT triage-nézet: kódonként az
277
+ * occurrence-összeg + distinct rekordszám + utolsó üzenet + egy reprezentatív `failedSample`. Gyors „mi omlik a
278
+ * legtöbbet" áttekintés (pl. melyik `FAM-EMB-PROVIDER-002` séma viszi el az LM Studio-t).
279
+ */
280
+ errorsSummaryEndpoint() {
281
+ return new nts_dynamo_1.DyNTS_Endpoint_Params({
282
+ name: 'errorsSummary',
283
+ type: fsm_dynamo_1.DyFM_HttpCallType.get,
284
+ endpoint: '/errors/summary',
285
+ tasks: [
286
+ async (req, res) => {
287
+ await this.run(res, async () => {
288
+ const all = await errors_control_service_1.FAM_Error_ControlService.getInstance().listErrors();
289
+ const groups = {};
290
+ for (const r of all) {
291
+ const code = r.errorCode ?? '(no-code)';
292
+ const group = groups[code] ?? { errorCode: code, occurrences: 0, records: 0 };
293
+ group.occurrences += fam_error_data_model_1.FAM_Error_DataModel.occurrenceCount(r);
294
+ group.records += 1;
295
+ group.lastMessage = r.message ?? group.lastMessage;
296
+ const sample = r.contextSnapshot?.failedSample;
297
+ if (sample && !group.sampleFailedSample) {
298
+ group.sampleFailedSample = String(sample).slice(0, 400);
299
+ }
300
+ groups[code] = group;
301
+ }
302
+ return {
303
+ groups: Object.values(groups).sort((a, b) => b.occurrences - a.occurrences),
304
+ };
305
+ });
306
+ },
307
+ ],
308
+ });
309
+ }
310
+ /** Egy `fam_errors` rekord → KOMPAKT, FAM-natív hiba-nézet (a nyers Dynamo-mezők/óriás-stack helyett a diagnózishoz kell). */
311
+ mapErrorCompact(record) {
312
+ return {
313
+ id: String(record._id ?? ''),
314
+ errorCode: record.errorCode,
315
+ level: typeof Reflect.get(record, 'level') === 'string' ? String(Reflect.get(record, 'level')) : undefined,
316
+ message: record.message,
317
+ occurrenceCount: fam_error_data_model_1.FAM_Error_DataModel.occurrenceCount(record),
318
+ handled: record.handled ?? false,
319
+ issuer: record.issuer,
320
+ context: record.contextSnapshot,
321
+ };
322
+ }
135
323
  // =========================================================================
136
324
  // /health — CLI remote reachability-check (dsgn-010 §9)
137
325
  // =========================================================================
326
+ /**
327
+ * `GET /doctor` (#2 diagnosztikai végpont, user-FR 2026-06-24) — SZERVER-OLDALI aggregált health-report
328
+ * (env/mongodb/config/embedding/vector/collections + scope-integritás), a `fam doctor` REST-tükre (ADR-015
329
+ * server-centric: a CLI/kliens/UI/külső eszköz EZT hívja, nem 6 külön capability-t). Opcionális `?skip=embedding,vector`.
330
+ */
331
+ doctorEndpoint() {
332
+ return new nts_dynamo_1.DyNTS_Endpoint_Params({
333
+ name: 'doctor',
334
+ type: fsm_dynamo_1.DyFM_HttpCallType.get,
335
+ endpoint: '/doctor',
336
+ tasks: [
337
+ async (req, res) => {
338
+ await this.run(res, async () => {
339
+ const skipRaw = typeof req.query.skip === 'string' ? req.query.skip : '';
340
+ const skip = skipRaw.split(',').map((entry) => entry.trim()).filter((entry) => entry.length > 0);
341
+ return fam_doctor_control_service_1.FAM_Doctor_ControlService.getInstance().runDoctor({ skip: skip });
342
+ });
343
+ },
344
+ ],
345
+ });
346
+ }
347
+ /**
348
+ * `POST /rules/inject` (Rules-Management MP-2) — a SessionStart-hook (+ a `read`-reflex) szerver-oldali rule-assembly-je:
349
+ * a `cwd`-ből (vagy explicit felülírásból) detektált tier-kontextusra `importance`-rangsorolt, char-budgetbe illesztett
350
+ * always-apply rule-blokk a „(…and N more)" záró sorral. A hook DUMB marad (csak `additionalContext`-be teszi a `block`-ot).
351
+ * Body: `{ cwd?, isFdp?, inProject?, projectType?, topics?, budgetChars? }`.
352
+ */
353
+ rulesInjectEndpoint() {
354
+ return new nts_dynamo_1.DyNTS_Endpoint_Params({
355
+ name: 'rulesInject',
356
+ type: fsm_dynamo_1.DyFM_HttpCallType.post,
357
+ endpoint: '/rules/inject',
358
+ tasks: [
359
+ async (req, res) => {
360
+ await this.run(res, async () => {
361
+ const input = (req.body ?? {});
362
+ return retrieval_1.FAM_RuleInjection_ControlService.getInstance().assembleInjection(input);
363
+ });
364
+ },
365
+ ],
366
+ });
367
+ }
368
+ /**
369
+ * `POST /rules/propagate` (Rules-Management MP-5) — a kontextusra érvényes szabályok GENERÁLT, marker-keretezett
370
+ * `CLAUDE.md`-blokkja (propagáció a FAM-NÉLKÜLI projekt-agenteknek). Budget nélkül (minden alkalmazható szabály),
371
+ * dedup `ruleId`/`derivedFrom` szerint. A hívó (sync-tool/CLI) a `block`-ot a projekt-`CLAUDE.md` markerei közé illeszti.
372
+ * Body: `{ cwd?, isFdp?, inProject?, projectType?, topics? }`.
373
+ */
374
+ rulesPropagateEndpoint() {
375
+ return new nts_dynamo_1.DyNTS_Endpoint_Params({
376
+ name: 'rulesPropagate',
377
+ type: fsm_dynamo_1.DyFM_HttpCallType.post,
378
+ endpoint: '/rules/propagate',
379
+ tasks: [
380
+ async (req, res) => {
381
+ await this.run(res, async () => {
382
+ const input = (req.body ?? {});
383
+ return retrieval_1.FAM_RuleInjection_ControlService.getInstance().assemblePropagation(input);
384
+ });
385
+ },
386
+ ],
387
+ });
388
+ }
138
389
  /** `GET /health` — a CLI `--server-url` elérhetőség-próbája (dsgn-010 §9). */
139
390
  healthEndpoint() {
140
391
  return new nts_dynamo_1.DyNTS_Endpoint_Params({
@@ -145,10 +396,17 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
145
396
  async (req, res) => {
146
397
  // A heap-nyomás IS kimegy a health-en (observability) — a BEDROCK DyNTS_MemoryGuard pressure-éből.
147
398
  const memory = nts_dynamo_2.DyNTS_MemoryGuard.getInstance().pressure();
399
+ // Hidratálás-állapot (user-FR 2026-06-22): a boot-hidratálás időbe telik → a hívó lássa, kész-e.
400
+ const hydration = fam_vector_search_control_service_1.FAM_VectorSearch_ControlService.getInstance().hydrationSummary();
401
+ // Readiness-coordinator per-tár állapot (pending/hydrating/ready/failed) — a read-queue diagnosztikája.
402
+ const readiness = embedding_1.FAM_HydrationCoordinator_ControlService.getInstance().snapshot();
148
403
  res.send({
149
404
  ok: true, service: 'fdp-agent-memory', version: fam_version_const_1.FAM_VERSION, uptime: process.uptime(),
405
+ ready: hydration.complete,
150
406
  memory: { level: memory.level, heapUsedMb: memory.heapUsedMb, heapLimitMb: memory.heapLimitMb,
151
407
  heapPct: memory.heapPct, rssMb: memory.rssMb, gcFraction: memory.gcFraction },
408
+ hydration: hydration,
409
+ readiness: readiness,
152
410
  });
153
411
  },
154
412
  ],
@@ -344,12 +602,24 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
344
602
  async (req, res) => {
345
603
  await this.run(res, async () => {
346
604
  const table = this.parseTable(req.params.table);
605
+ // A vessző-szeparált lista-paraméterek (projects/excludeProjects) tömbbé bontása.
606
+ const parseList = (value) => (typeof value === 'string' && value.length
607
+ ? value.split(',').map((item) => item.trim()).filter((item) => item.length > 0)
608
+ : undefined);
347
609
  return embedding_1.FAM_DuplicateScan_ControlService.getInstance().scan({
348
610
  table: table,
349
611
  threshold: req.query.threshold !== undefined ? Number(req.query.threshold) : undefined,
350
612
  neighbors: req.query.neighbors !== undefined ? this.parseNumber(req.query.neighbors, 10) : undefined,
351
613
  maxEntries: req.query.maxEntries !== undefined ? this.parseNumber(req.query.maxEntries, 3000) : undefined,
352
614
  maxClusters: req.query.maxClusters !== undefined ? this.parseNumber(req.query.maxClusters, 100) : undefined,
615
+ pathRegex: typeof req.query.pathRegex === 'string' && req.query.pathRegex.length ? req.query.pathRegex : undefined,
616
+ excludePathRegex: typeof req.query.excludePathRegex === 'string' && req.query.excludePathRegex.length
617
+ ? req.query.excludePathRegex : undefined,
618
+ excludeRootRegex: typeof req.query.excludeRootRegex === 'string' && req.query.excludeRootRegex.length
619
+ ? req.query.excludeRootRegex : undefined,
620
+ projects: parseList(req.query.projects),
621
+ excludeProjects: parseList(req.query.excludeProjects),
622
+ minProjects: req.query.minProjects !== undefined ? this.parseNumber(req.query.minProjects, 0) : undefined,
353
623
  });
354
624
  });
355
625
  },