@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.
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 +160 -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 +216 -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
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_HydrationCoordinator_ControlService = void 0;
4
+ const error_codes_const_1 = require("../../../_collections/error-codes.const");
5
+ const fam_error_factory_util_1 = require("../../../_collections/fam-error-factory.util");
6
+ /**
7
+ * `FAM_HydrationCoordinator_ControlService` (Singleton SSOT, user-FR 2026-06-22) — a vektor-pool **hidratálás-
8
+ * készenlét** EGYETLEN koordinátora: a `read`/`search` kérések **MEGVÁRJÁK** egy tár (kezdő VAGY újra-) hidratálását
9
+ * ahelyett, hogy `FAM-VEC-SEARCH-001 pool ÜRES`-t kapnának, amíg a hidratálás folyamatban van.
10
+ *
11
+ * **Probléma (amit megold):** a `/api/health` ELŐBB zöldül, mint ahogy a tárankénti pool-hidratálás (codebase 152k a
12
+ * legnagyobb) befejeződik → közvetlenül boot/auto-start után a `read` `pool ÜRES`-t dobott. A koordinátor **sorba
13
+ * állítja** (queue) a kéréseket: minden várakozó UGYANARRA a per-tár promise-ra vár (nincs polling, nincs thundering-
14
+ * herd) — a hidratálás végén MIND egyszerre továbbenged.
15
+ *
16
+ * **Process-koordináció (SSOT):** a `hydrateAll` boot-kor `beginBoot`-tal ELŐRE `hydrating`-re jelöl MINDEN
17
+ * regisztrált tárat (így a korán beérkező read is vár, akkor is, ha az ő tárának `hydrateTable`-je még nem futott).
18
+ * Minden `hydrateTable` (boot ÉS standalone re-hidratálás egyaránt) `markHydrating`→`markReady`/`markFailed`-et jelez —
19
+ * ezért a **re-hidratálás is fedett**: ha egy `ready` tár újra-hidratál (a pool törlődik + újratölt), a `markHydrating`
20
+ * friss deferred-et nyit → a közben érkező read VÁR, nem lát fél-üres pool-t.
21
+ *
22
+ * **Determinizmus:** a `markReady`/`markFailed` idempotens (ugyanazt a deferred-et nem oldja kétszer); az `awaitReady`
23
+ * a `ready`/`failed` állapotnál azonnal tér vissza/dob, csak a `hydrating` ágon vár (időkorláttal — a time-out tiszta
24
+ * „még hidratál, próbáld újra" hiba, NEM végtelen blokk).
25
+ */
26
+ class FAM_HydrationCoordinator_ControlService {
27
+ static _instance;
28
+ issuer = 'FAM_HydrationCoordinator_ControlService';
29
+ /** Per-tár állapot + deferred (a SSOT). */
30
+ entries = new Map();
31
+ /** A hidratálás-bevárás default felső korlátja ms-ban (env/config felülírható; lásd `configureWaitMs`). */
32
+ waitTimeoutMs = Number(process.env.FAM_HYDRATION_WAIT_MS) || 180000;
33
+ static getInstance() {
34
+ if (!FAM_HydrationCoordinator_ControlService._instance) {
35
+ FAM_HydrationCoordinator_ControlService._instance = new FAM_HydrationCoordinator_ControlService();
36
+ }
37
+ return FAM_HydrationCoordinator_ControlService._instance;
38
+ }
39
+ /** A hidratálás-bevárás default time-outjának beállítása (a bootstrap a `read.hydrationWaitMs` config-ból hívja). */
40
+ configureWaitMs(waitTimeoutMs) {
41
+ if (Number.isFinite(waitTimeoutMs) && waitTimeoutMs >= 0) {
42
+ this.waitTimeoutMs = Math.floor(waitTimeoutMs);
43
+ }
44
+ }
45
+ /** Az aktuális default time-out (diag / teszt). */
46
+ getWaitTimeoutMs() {
47
+ return this.waitTimeoutMs;
48
+ }
49
+ /**
50
+ * Boot-jelzés (`hydrateAll` hívja a tár-iteráció ELŐTT): MINDEN megadott tárat `hydrating`-re jelöl (friss
51
+ * deferred), hogy a korán beérkező read VÁRJON, akkor is, ha az ő tárának `hydrateTable`-je a soros boot-ban
52
+ * még nem futott le. Idempotens: a már `hydrating` tárat nem nyitja újra.
53
+ */
54
+ beginBoot(tables) {
55
+ for (const table of tables) {
56
+ this.markHydrating(table);
57
+ }
58
+ }
59
+ /**
60
+ * Egy tár hidratálásának KEZDETE (a `hydrateTable` hívja a pool-törlés ELŐTT — boot ÉS re-hidratálás). Ha a tár még
61
+ * nem `hydrating`, friss deferred-et nyit (a `ready`→`hydrating` átmenet a re-hidratálás kulcsa: az addigi várók a
62
+ * RÉGI, már-feloldott promise-on már átmentek, az újak az ÚJ deferred-re várnak). Ha már `hydrating`, no-op (a
63
+ * `beginBoot`+`hydrateTable` dupla-hívás nem nyit két deferred-et).
64
+ */
65
+ markHydrating(table) {
66
+ const existing = this.entries.get(table);
67
+ if (existing && existing.state === 'hydrating') {
68
+ return;
69
+ }
70
+ let resolve = () => undefined;
71
+ const promise = new Promise((resolver) => {
72
+ resolve = resolver;
73
+ });
74
+ this.entries.set(table, { state: 'hydrating', promise: promise, resolve: resolve });
75
+ }
76
+ /** Egy tár hidratálása SIKERES (a `hydrateTable` hívja a végén): `ready` + a várók továbbengedése. */
77
+ markReady(table) {
78
+ this.settle(table, 'ready', true);
79
+ }
80
+ /** Egy tár hidratálása MEGBUKOTT (a `hydrateTable` catch-ágában): `failed` + a várók elengedése (hiba felé). */
81
+ markFailed(table) {
82
+ this.settle(table, 'failed', false);
83
+ }
84
+ /**
85
+ * Egy tár készenlétének BEVÁRÁSA a `search`/`read` ELŐTT (a queue-mag). `ready` → azonnal vissza; `failed` →
86
+ * tiszta hidratálás-bukás hiba; `hydrating` → a per-tár promise bevárása time-outtal (a time-out NEM végtelen
87
+ * blokk, hanem „még hidratál, próbáld újra"); `pending`/ismeretlen → azonnal vissza (a `search` poolSize-checkje
88
+ * dönt — genuinely üres tár vs. nem-konfigurált). Az időkorlát a `timeoutMs` VAGY a config-default.
89
+ */
90
+ async awaitReady(table, timeoutMs) {
91
+ const entry = this.entries.get(table);
92
+ if (!entry || entry.state === 'pending') {
93
+ return;
94
+ }
95
+ if (entry.state === 'ready') {
96
+ return;
97
+ }
98
+ if (entry.state === 'failed') {
99
+ throw this.hydrationError(table, 'A tár hidratálása MEGBUKOTT (a pool nem elérhető). '
100
+ + 'Nézd a szerver-logot (`FAM hydrate` sorok) + az embedding-konfigurációt; majd `re_embed_table`/restart.');
101
+ }
102
+ // state === 'hydrating': a per-tár promise bevárása time-outtal.
103
+ const limitMs = timeoutMs ?? this.waitTimeoutMs;
104
+ const ready = await this.raceWithTimeout(entry.promise, limitMs);
105
+ if (!ready) {
106
+ throw this.hydrationError(table, `A tár hidratálása ${Math.round(limitMs / 1000)}s alatt nem fejeződött be `
107
+ + '(a pool-hidratálás folyamatban — boot-kor a nagy tárak percekig tarthatnak). Próbáld újra pár '
108
+ + 'másodperc múlva; a következő hívás már a kész poolt találja.');
109
+ }
110
+ }
111
+ /** Egy tár aktuális állapota (diag / health / teszt). */
112
+ getState(table) {
113
+ return this.entries.get(table)?.state ?? 'pending';
114
+ }
115
+ /** Pillanatkép minden ismert tár állapotáról (a `/health` + diag bővítéséhez). */
116
+ snapshot() {
117
+ return Array.from(this.entries.entries()).map(([table, entry]) => ({ table: String(table), state: entry.state }));
118
+ }
119
+ // =========================================================================
120
+ // helpers
121
+ // =========================================================================
122
+ /** Az állapot véglegesítése (`ready`/`failed`) + a deferred egyszeri feloldása (idempotens). */
123
+ settle(table, state, ready) {
124
+ const entry = this.entries.get(table);
125
+ if (!entry) {
126
+ // Nem volt `markHydrating` (pl. közvetlen markReady) — rögzítjük a végállapotot egy már-feloldott deferred-del.
127
+ this.entries.set(table, { state: state, promise: Promise.resolve(ready), resolve: () => undefined });
128
+ return;
129
+ }
130
+ if (entry.state === state) {
131
+ return;
132
+ }
133
+ entry.state = state;
134
+ entry.resolve(ready);
135
+ }
136
+ /** A promise bevárása felső időkorláttal: a time-out `false`-t ad (NEM dob) — a hívó dönt a hibáról. */
137
+ raceWithTimeout(promise, timeoutMs) {
138
+ if (timeoutMs <= 0) {
139
+ return Promise.resolve(false);
140
+ }
141
+ return new Promise((resolve) => {
142
+ const timer = setTimeout(() => resolve(false), timeoutMs);
143
+ void promise.then((ready) => {
144
+ clearTimeout(timer);
145
+ resolve(ready);
146
+ });
147
+ });
148
+ }
149
+ /** A hidratálás-bevárás strukturált hibája (`FAM-VEC-HYDRATE-002`). */
150
+ hydrationError(table, detail) {
151
+ return fam_error_factory_util_1.FAM_Error_Util.create({
152
+ errorCode: error_codes_const_1.FAM_ERROR_CODES.vecHydrateWait,
153
+ message: `A(z) '${table}' tár nem áll készen a kereséshez. ${detail}`,
154
+ issuer: this.issuer,
155
+ context: { operation: 'await-hydration', table: table },
156
+ });
157
+ }
158
+ }
159
+ exports.FAM_HydrationCoordinator_ControlService = FAM_HydrationCoordinator_ControlService;
@@ -14,18 +14,26 @@ exports.FAM_LMStudio_EmbeddingProvider = void 0;
14
14
  class FAM_LMStudio_EmbeddingProvider {
15
15
  baseUrl;
16
16
  apiKey;
17
+ timeoutMs;
17
18
  providerId = 'lmstudio';
19
+ /** A request-timeout default (ms), ha a hívó nem ad explicitet — 2 perc (a nagy batch-eknek elég, de BOUNDED). */
20
+ static DEFAULT_TIMEOUT_MS = 120000;
18
21
  /**
19
22
  * @param baseUrl OpenAI-kompatibilis lokális endpoint (pl. `http://localhost:1234/v1`).
20
23
  * @param apiKey opcionális Bearer token (LM Studio default-ban nem kér).
24
+ * @param timeoutMs request-timeout (ms) — egy NEM-VÁLASZOLÓ LM Studio (a modell crash-elt a kérés közben:
25
+ * „bekap egy feladatot, aztán nem csinál semmit") ne fagyassza be a teljes scant. `0`/negatív → nincs timeout
26
+ * (a régi viselkedés). A timeout AbortError-rá fordul → a `FAM_Embedding_ControlService` retry-zza, majd
27
+ * `FAM-EMB-PROVIDER-002`-ként tárolja (user-FR 2026-06-24).
21
28
  */
22
- constructor(baseUrl, apiKey) {
29
+ constructor(baseUrl, apiKey, timeoutMs = FAM_LMStudio_EmbeddingProvider.DEFAULT_TIMEOUT_MS) {
23
30
  this.baseUrl = baseUrl;
24
31
  this.apiKey = apiKey;
32
+ this.timeoutMs = timeoutMs;
25
33
  }
26
34
  /**
27
35
  * Batch-embedding az OpenAI-kompatibilis `/embeddings` végpontra. A `texts` sorrendje == a
28
- * visszaadott `number[][]` sorrendje. HTTP-/parse-hiba deskriptív üzenettel dobódik (a
36
+ * visszaadott `number[][]` sorrendje. HTTP-/parse-/timeout-hiba deskriptív üzenettel dobódik (a
29
37
  * `FAM_Embedding_ControlService` `FAM-EMB-PROVIDER-002`-vé alakítja — dsgn-008).
30
38
  */
31
39
  async embedTexts(set) {
@@ -34,11 +42,11 @@ class FAM_LMStudio_EmbeddingProvider {
34
42
  if (this.apiKey && this.apiKey.trim().length) {
35
43
  headers.Authorization = `Bearer ${this.apiKey.trim()}`;
36
44
  }
37
- const response = await fetch(url, {
45
+ const response = await this.fetchWithTimeout(url, {
38
46
  method: 'POST',
39
47
  headers: headers,
40
48
  body: JSON.stringify({ model: set.model, input: set.texts }),
41
- });
49
+ }, set.model);
42
50
  const rawText = await response.text();
43
51
  if (!response.ok) {
44
52
  throw new Error(`LM Studio (OpenAI-compatible) embeddings HTTP ${response.status} | url=${url} | `
@@ -68,6 +76,35 @@ class FAM_LMStudio_EmbeddingProvider {
68
76
  return false;
69
77
  }
70
78
  }
79
+ /**
80
+ * `fetch` request-timeout-tal (AbortController) — a KULCS-robusztosság (user-FR 2026-06-24): ha az LM Studio
81
+ * elfogadja a kérést, de SOHA nem válaszol (a modell crash-elt menet közben), a natív `fetch` a végtelenségig
82
+ * függne → a scan-job örökre beragad. A timeout abortálja a kérést → deskriptív hiba, amit a hívó retry-zhat +
83
+ * `FAM-EMB-PROVIDER-002`-ként tárolhat. `timeoutMs<=0` → nincs timeout (régi viselkedés). A timer minden ágon
84
+ * törlődik (a `finally`-ben), hogy ne tartsa életben az event-loop-ot.
85
+ */
86
+ async fetchWithTimeout(url, init, model) {
87
+ if (this.timeoutMs <= 0) {
88
+ return fetch(url, init);
89
+ }
90
+ const controller = new AbortController();
91
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
92
+ try {
93
+ return await fetch(url, { ...init, signal: controller.signal });
94
+ }
95
+ catch (error) {
96
+ // Az AbortController-abort → deskriptív timeout-hiba (a generikus `fetch failed`/`AbortError` helyett),
97
+ // hogy a tárolt rekord egyértelmű legyen: az LM Studio elfogadta, de nem válaszolt időben.
98
+ if (controller.signal.aborted) {
99
+ throw new Error(`LM Studio embeddings: NINCS válasz ${this.timeoutMs}ms alatt (timeout) — a modell valószínűleg `
100
+ + `crash-elt/lefagyott a kérés közben | url=${url} | model=${model}`);
101
+ }
102
+ throw error;
103
+ }
104
+ finally {
105
+ clearTimeout(timer);
106
+ }
107
+ }
71
108
  /** Trailing `/`-ek levágása (kettős slash elkerülése a `/embeddings` join-nál). */
72
109
  trimTrailingSlashes(value) {
73
110
  return value.replace(/\/+$/, '');
@@ -3,9 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_VectorSearch_ControlService = void 0;
4
4
  const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
5
  const local_vector_search_1 = require("@futdevpro/nts-dynamo/local-vector-search");
6
+ const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
6
7
  const error_codes_const_1 = require("../../../_collections/error-codes.const");
7
8
  const fam_error_factory_util_1 = require("../../../_collections/fam-error-factory.util");
8
9
  const fam_store_registry_const_1 = require("../_collections/fam-store-registry.const");
10
+ const fam_hydration_coordinator_control_service_1 = require("./fam-hydration-coordinator.control-service");
9
11
  const fam_embedding_control_service_1 = require("./fam-embedding.control-service");
10
12
  const fam_entry_data_service_1 = require("./fam-entry.data-service");
11
13
  /**
@@ -48,9 +50,16 @@ class FAM_VectorSearch_ControlService {
48
50
  * de a többi tár hidratálása FOLYTATÓDIK (egy tár bukása nem dönti le az egészet).
49
51
  */
50
52
  async hydrateAll() {
53
+ const startedAt = Date.now();
54
+ let totalVectors = 0;
55
+ // Readiness-koordinátor (user-FR 2026-06-22): MINDEN tárat ELŐRE `hydrating`-re jelölünk a soros boot-iteráció
56
+ // ELŐTT, így a korán beérkező read VÁR (a per-tár promise-on), nem kap `pool ÜRES`-t, akkor sem, ha az ő
57
+ // tárának `hydrateTable`-je a sorban még nem futott le.
58
+ fam_hydration_coordinator_control_service_1.FAM_HydrationCoordinator_ControlService.getInstance()
59
+ .beginBoot(fam_store_registry_const_1.FAM_STORE_REGISTRY.map((entry) => entry.table));
51
60
  for (const entry of fam_store_registry_const_1.FAM_STORE_REGISTRY) {
52
61
  try {
53
- await this.hydrateTable(entry.table);
62
+ totalVectors += await this.hydrateTable(entry.table);
54
63
  }
55
64
  catch (error) {
56
65
  // No-silent-failure: a hidratálás-hiba persistált (a global handler kapja), de a többi
@@ -58,6 +67,11 @@ class FAM_VectorSearch_ControlService {
58
67
  fsm_dynamo_1.DyFM_Log.warn(`[FAM hydrate] '${entry.table}' tár hidratálása sikertelen: ${error?.message}`);
59
68
  }
60
69
  }
70
+ // Összesítő (user-FR 2026-06-22): a boot-memória-lábnyom egy ránézésre — összes vektor + idő + heap/RSS.
71
+ const mem = process.memoryUsage();
72
+ fsm_dynamo_1.DyFM_Log.log(`[FAM hydrate] ÖSSZESEN ${totalVectors} vektor ${fam_store_registry_const_1.FAM_STORE_REGISTRY.length} táron, `
73
+ + `${Math.round((Date.now() - startedAt) / 1000)}s alatt — heap ${Math.round(mem.heapUsed / 1048576)} MB / `
74
+ + `RSS ${Math.round(mem.rss / 1048576)} MB.`);
61
75
  }
62
76
  /**
63
77
  * Egy tár pool-jának hidratálása (dsgn-006 §4.1). A `completed`-státuszú, nem-üres-vektorú entry-ket
@@ -67,13 +81,24 @@ class FAM_VectorSearch_ControlService {
67
81
  async hydrateTable(table) {
68
82
  const registryEntry = fam_store_registry_const_1.FAM_StoreRegistry_Util.getEntry(table);
69
83
  if (!registryEntry) {
70
- return;
84
+ return 0;
71
85
  }
86
+ // Readiness-koordinátor: a hidratálás KEZDETE (a pool-törlés ELŐTT) — boot ÉS re-hidratálás egyaránt. A
87
+ // `ready`→`hydrating` átmenet a re-hidratálás kulcsa: a `pool.clearPool()` alatt a beérkező read VÁR (friss
88
+ // deferred), nem lát fél-üres pool-t.
89
+ const coordinator = fam_hydration_coordinator_control_service_1.FAM_HydrationCoordinator_ControlService.getInstance();
90
+ coordinator.markHydrating(table);
72
91
  const pool = this.getOrCreatePool(table);
73
92
  pool.clearPool();
74
93
  this.poolDims.set(table, null);
75
94
  const dataService = new fam_entry_data_service_1.FAM_Entry_DataService({ dataParams: registryEntry.dataParams, issuer: this.issuer });
76
95
  const filter = { embeddingStatus: 'completed' };
96
+ // dsgn-013 §5.2 (MAM F3) — az ALVÓ (`dormant`) memóriák NEM hidratálódnak a hot LVS-poolba (RAM-/boot-spórolás);
97
+ // cold (Mongo) kereséssel elérhetők + megtaláláskor reaktiválódnak. A flag CSAK a memory táron létezik; ha a
98
+ // dormancy ki van kapcsolva, nincs `dormant:true` entry → a szűrő no-op (zéró hatás a többi tárra + a mai bootra).
99
+ if (table === fam_table_type_enum_1.FAM_Table.memory) {
100
+ filter.dormant = { $ne: true };
101
+ }
77
102
  let loaded = 0;
78
103
  try {
79
104
  // PERF: lean + {_id, contentVector}-projekció + STREAMING (cursor) — a hidratálás CSAK ezt a két
@@ -87,6 +112,8 @@ class FAM_VectorSearch_ControlService {
87
112
  });
88
113
  }
89
114
  catch (error) {
115
+ // Readiness-koordinátor: a hidratálás MEGBUKOTT → a várók tiszta hibát kapnak (nem végtelen blokk).
116
+ coordinator.markFailed(table);
90
117
  throw fam_error_factory_util_1.FAM_Error_Util.create({
91
118
  errorCode: error_codes_const_1.FAM_ERROR_CODES.vecHydrate,
92
119
  message: `A(z) '${table}' tár vektor-hidratálása sikertelen a Mongo-olvasásnál.`,
@@ -96,7 +123,16 @@ class FAM_VectorSearch_ControlService {
96
123
  });
97
124
  }
98
125
  this.hydrated.set(table, true);
99
- fsm_dynamo_1.DyFM_Log.log(`[FAM hydrate] '${table}' tár: ${loaded} vektor betöltve az in-memory pool-ba.`);
126
+ // Readiness-koordinátor: a tár KÉSZ a várakozó read-ek továbbengedése (mind egyszerre, a per-tár promise-on).
127
+ coordinator.markReady(table);
128
+ // Memória-info táranként (user-FR 2026-06-22): dim + becsült pool-méret (loaded × dim × 8 byte / number[])
129
+ // + a process heap/RSS — boot-kor azonnal látszik a per-tár memória-lábnyom.
130
+ const mem = process.memoryUsage();
131
+ const dim = this.poolDims.get(table) ?? 0;
132
+ const poolMb = Math.round((loaded * dim * 8) / 1048576);
133
+ fsm_dynamo_1.DyFM_Log.log(`[FAM hydrate] '${table}' tár: ${loaded} vektor (dim ${dim}, ~${poolMb} MB pool) — `
134
+ + `heap ${Math.round(mem.heapUsed / 1048576)} MB / RSS ${Math.round(mem.rss / 1048576)} MB.`);
135
+ return loaded;
100
136
  }
101
137
  // =========================================================================
102
138
  // PERSIST-ON-WRITE upsert (dsgn-006 §3) — a write/scan path hívja mentés után
@@ -130,6 +166,9 @@ class FAM_VectorSearch_ControlService {
130
166
  * koszinusz (BFR-AM-007). Üres pool / hiányzó embedding → `FAM-VEC-SEARCH-001` (bootstrap-skip jelzés).
131
167
  */
132
168
  async search(input) {
169
+ // Readiness-gate (user-FR 2026-06-22): ha a tár pool-ja épp (kezdő VAGY újra-) hidratál, MEGVÁRJUK a végét a
170
+ // queue-mag promise-án (NEM `pool ÜRES`-t dobunk). Time-out / hidratálás-bukás → tiszta `FAM-VEC-HYDRATE-002`.
171
+ await fam_hydration_coordinator_control_service_1.FAM_HydrationCoordinator_ControlService.getInstance().awaitReady(input.table);
133
172
  const queryVector = await this.resolveQueryVector(input);
134
173
  if (!queryVector.length) {
135
174
  throw fam_error_factory_util_1.FAM_Error_Util.create({
@@ -143,15 +182,16 @@ class FAM_VectorSearch_ControlService {
143
182
  const pool = this.getOrCreatePool(input.table);
144
183
  const poolSize = pool.size();
145
184
  if (!poolSize) {
146
- // Bootstrap-skip jelzés (dsgn-006 §4.1): üres pool (embedding nincs konfigurálva / nincs
147
- // completed entry). NEM néma a capability + UI piros banner ezt jelzi (dsgn-008/dsgn-011).
148
- throw fam_error_factory_util_1.FAM_Error_Util.create({
149
- errorCode: error_codes_const_1.FAM_ERROR_CODES.vecSearch,
150
- message: `A(z) '${input.table}' tár vektor-pool-ja ÜRES (bootstrap-skip): nincs hidratált, `
151
- + `completed-embeddingű vektor. Ellenőrizd az embedding-konfigurációt (test_embedding_provider).`,
152
- issuer: this.issuer,
153
- context: { operation: 'search', table: input.table },
154
- });
185
+ // Ide a readiness-gate UTÁN jutunk: a tár hidratálása BEFEJEZŐDÖTT, mégis üres a pool a tár GENUINELY
186
+ // üres (friss telepítés / még nincs embeddelt tartalom). Ez LEGITIM állapot, a keresés eredménye
187
+ // triviálisan 0 találat — dsgn-002/dsgn-003: „0-találat → üres hits, NEM hiba". (Korábban itt
188
+ // FAM-VEC-SEARCH-001 dobódott → a `read` HTTP 500-at adott MINDEN üres tárra — friss DB-ben a
189
+ // multi-query/edge read-ek és a UI-search borultak; 2026-07-24 e2e-gyökérok-fix.) A hidratálás-BUKÁS
190
+ // esete ettől FÜGGETLENÜL fedett: azt az awaitReady dobja (FAM-VEC-HYDRATE-002). NEM néma: warn-log +
191
+ // a doctor/`test_vector_search` capability továbbra is explicit `empty-pool` státuszt jelez.
192
+ fsm_dynamo_1.DyFM_Log.warn(`[FAM vectorSearch] '${input.table}' tár: üres pool a hidratálás után (0 completed vektor) `
193
+ + `→ 0 találat (üres tár = legitim állapot, nem hiba).`);
194
+ return [];
155
195
  }
156
196
  // Prefilter→vektor (dsgn-006 §4.3): ha van candidate-szűkítés, a koszinusz CSAK a szűkített
157
197
  // halmazon fut (a teljes pool helyett egy ideiglenes szűkített pool-on) — kisebb számítás.
@@ -178,6 +218,21 @@ class FAM_VectorSearch_ControlService {
178
218
  isHydrated(table) {
179
219
  return this.hydrated.get(table) ?? false;
180
220
  }
221
+ /**
222
+ * Hidratálás-összegző (user-FR 2026-06-22): a `/health`-en kiadva, hogy a hívó lássa, kész-e a boot-hidratálás
223
+ * (indításkor időbe telik mind a tárat betölteni). `complete` csak ha MINDEN regisztrált tár pool-ja hidratált.
224
+ */
225
+ hydrationSummary() {
226
+ const pending = fam_store_registry_const_1.FAM_STORE_REGISTRY
227
+ .filter((entry) => !this.isHydrated(entry.table))
228
+ .map((entry) => String(entry.table));
229
+ return {
230
+ complete: pending.length === 0,
231
+ hydratedTables: fam_store_registry_const_1.FAM_STORE_REGISTRY.length - pending.length,
232
+ totalTables: fam_store_registry_const_1.FAM_STORE_REGISTRY.length,
233
+ pendingTables: pending,
234
+ };
235
+ }
181
236
  // =========================================================================
182
237
  // helpers
183
238
  // =========================================================================
@@ -8,7 +8,7 @@
8
8
  * (BFR-AM-002/008); a bedrock-csere (MP-15) a `FAM_EmbeddingProvider` interfész mögött non-breaking.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.FAM_EmbeddingBootstrap_ControlService = exports.FAM_DuplicateScan_ControlService = exports.FAM_EmbeddingCost_ControlService = exports.FAM_EmbeddingPipeline_ControlService = exports.FAM_EmbeddingPreset_ControlService = exports.FAM_StoreRegistry_Util = exports.FAM_STORE_REGISTRY = exports.FAM_Entry_DataService = exports.FAM_VectorSearch_ControlService = exports.FAM_MOCK_EMBEDDING_DIMS = exports.FAM_Mock_EmbeddingProvider = exports.FAM_LMStudio_EmbeddingProvider = exports.FAM_OpenAI_EmbeddingProvider = exports.FAM_Embedding_ControlService = void 0;
11
+ exports.FAM_EmbeddingBootstrap_ControlService = exports.FAM_DuplicateScan_ControlService = exports.FAM_EmbeddingCost_ControlService = exports.FAM_EmbeddingPipeline_ControlService = exports.FAM_EmbeddingPreset_ControlService = exports.FAM_StoreRegistry_Util = exports.FAM_STORE_REGISTRY = exports.FAM_DedupWarn_ControlService = exports.FAM_Entry_DataService = exports.FAM_HydrationCoordinator_ControlService = exports.FAM_VectorSearch_ControlService = exports.FAM_MOCK_EMBEDDING_DIMS = exports.FAM_Mock_EmbeddingProvider = exports.FAM_LMStudio_EmbeddingProvider = exports.FAM_OpenAI_EmbeddingProvider = exports.FAM_Embedding_ControlService = void 0;
12
12
  // SP-2.1 — provider-dispatch kapu + providerek
13
13
  var fam_embedding_control_service_1 = require("./_services/fam-embedding.control-service");
14
14
  Object.defineProperty(exports, "FAM_Embedding_ControlService", { enumerable: true, get: function () { return fam_embedding_control_service_1.FAM_Embedding_ControlService; } });
@@ -22,8 +22,12 @@ Object.defineProperty(exports, "FAM_MOCK_EMBEDDING_DIMS", { enumerable: true, ge
22
22
  // SP-2.2 — Memory Vector Search (Mongo-persist + LVS hidratálás)
23
23
  var fam_vector_search_control_service_1 = require("./_services/fam-vector-search.control-service");
24
24
  Object.defineProperty(exports, "FAM_VectorSearch_ControlService", { enumerable: true, get: function () { return fam_vector_search_control_service_1.FAM_VectorSearch_ControlService; } });
25
+ var fam_hydration_coordinator_control_service_1 = require("./_services/fam-hydration-coordinator.control-service");
26
+ Object.defineProperty(exports, "FAM_HydrationCoordinator_ControlService", { enumerable: true, get: function () { return fam_hydration_coordinator_control_service_1.FAM_HydrationCoordinator_ControlService; } });
25
27
  var fam_entry_data_service_1 = require("./_services/fam-entry.data-service");
26
28
  Object.defineProperty(exports, "FAM_Entry_DataService", { enumerable: true, get: function () { return fam_entry_data_service_1.FAM_Entry_DataService; } });
29
+ var fam_dedup_warn_control_service_1 = require("./_services/fam-dedup-warn.control-service");
30
+ Object.defineProperty(exports, "FAM_DedupWarn_ControlService", { enumerable: true, get: function () { return fam_dedup_warn_control_service_1.FAM_DedupWarn_ControlService; } });
27
31
  var fam_store_registry_const_1 = require("./_collections/fam-store-registry.const");
28
32
  Object.defineProperty(exports, "FAM_STORE_REGISTRY", { enumerable: true, get: function () { return fam_store_registry_const_1.FAM_STORE_REGISTRY; } });
29
33
  Object.defineProperty(exports, "FAM_StoreRegistry_Util", { enumerable: true, get: function () { return fam_store_registry_const_1.FAM_StoreRegistry_Util; } });
@@ -58,6 +58,12 @@ class FAM_FileRouting_Util {
58
58
  if (FAM_FileRouting_Util.isGeneratedNoise(fileName)) {
59
59
  return 'skip';
60
60
  }
61
+ // R3 (FAM-REV 2026-06-22, eval-lelet): a `.cursor/rules/*` (és `.cursor/.../rules/*`) KONVENCIÓ-fájlok a
62
+ // `rules` tárba — ezek SZABÁLYOK (naming/import/struktúra/tilalmak), nem általános dokumentumok. Korábban
63
+ // `.mdc`→`documents`-be estek → a `rules`-only query az agent-feedback felé húzott (tool-comparison lelet).
64
+ if (FAM_FileRouting_Util.isCursorRule(relativePath)) {
65
+ return fam_table_type_enum_1.FAM_Table.rules;
66
+ }
61
67
  if (FAM_FileRouting_Util.DOCUMENT_EXTENSIONS.includes(extension)) {
62
68
  return fam_table_type_enum_1.FAM_Table.documents;
63
69
  }
@@ -68,6 +74,10 @@ class FAM_FileRouting_Util {
68
74
  // codebase. A bináris-tartalmú álcázott fájlt a scanner null-byte tartalom-ellenőrzése szűri.
69
75
  return fam_table_type_enum_1.FAM_Table.codebase;
70
76
  }
77
+ /** `.cursor/rules/` alatti konvenció-fájl-e (R3 — a `rules` tárba routing). Normalizált `/`, case-insensitive. */
78
+ static isCursorRule(relativePath) {
79
+ return relativePath.replace(/\\/g, '/').toLowerCase().includes('.cursor/rules/');
80
+ }
71
81
  /**
72
82
  * Generált / zaj fájl-e (FAM-REV-064): a lock-fájlok (pnpm-lock.yaml / package-lock.json / yarn.lock /
73
83
  * `*.lock`), a minifikált bundle-k (`.min.js`/`.min.css`) — ezek hatalmasak + generáltak, NEM info-
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_Frontmatter_Util = void 0;
4
+ /**
5
+ * `FAM_Frontmatter_Util` (MP-3 / F-A) — **pure** fájl-fej `---`-frontmatter parser. Ha a tartalom egy `---` sorral
6
+ * KEZDŐDIK és van záró `---` (a `MAX_HEADER_LINES`-on belül), a köztes `key: value` sorokat ismert irányítókká
7
+ * olvassa, a törzset a záró fence UTÁNról adja vissza. Egyébként NO-OP (`hasFrontmatter:false`, `body===content`) —
8
+ * így a kód-/dok-fájlok (amik nem `---`-rel kezdődnek) viselkedése BYTE-AZONOS marad (zero behavior change).
9
+ *
10
+ * Szándékosan MINIMÁL (nem teljes YAML): skalár `string`/`number`/`boolean` + lista (`[a, b]` flow VAGY vesszős).
11
+ * A `#` komment-sor + üres sor kihagyva. Ismeretlen kulcs → ignorált. Defenzív: záró fence nélkül → nincs frontmatter.
12
+ */
13
+ class FAM_Frontmatter_Util {
14
+ /** A záró fence-t eddig a sorig keressük (egy `---`-rel kezdődő, de nem-frontmatter fájl ne nyelje el a törzset). */
15
+ static MAX_HEADER_LINES = 200;
16
+ /** Az ISMERT irányító-kulcsok (a többi ignorált). */
17
+ static KNOWN_KEYS = new Set([
18
+ 'table', 'splitMode', 'importance', 'userApproved', 'ruleScope', 'ruleScopeTopic',
19
+ 'ruleId', 'derivedFrom', 'globalRule', 'category', 'tags',
20
+ ]);
21
+ /** A fájl-fej frontmatter parse-je (pure). */
22
+ static parse(content) {
23
+ const empty = { directives: {}, body: content, hasFrontmatter: false };
24
+ if (!content) {
25
+ return empty;
26
+ }
27
+ const lines = content.split(/\r?\n/);
28
+ // A nyitó fence: az ELSŐ sor pontosan `---` (BOM-toleráns).
29
+ const first = lines[0].replace(/^/, '').trim();
30
+ if (first !== '---') {
31
+ return empty;
32
+ }
33
+ const closeIdx = FAM_Frontmatter_Util.findCloseFence(lines);
34
+ if (closeIdx < 0) {
35
+ return empty; // nincs záró fence → nem frontmatter (defenzív; pl. `---` thematic-break a tetején)
36
+ }
37
+ const directives = {};
38
+ for (let i = 1; i < closeIdx; i++) {
39
+ FAM_Frontmatter_Util.applyLine(lines[i], directives);
40
+ }
41
+ // CSAK akkor DIREKTÍVA-blokk (+ vágjuk a törzset), ha ≥1 ISMERT ingest-irányítót tartalmaz. Egy TISZTA
42
+ // metaadat-blokk (pl. a `memory`-fájlok `name`/`description`/`metadata` feje) NEM FAM-direktíva → a content
43
+ // ÉRINTETLEN marad (különben a re-scan elvágná a summary-t az embeddelt tartalomból → rontaná a retrievalt).
44
+ if (!Object.keys(directives).length) {
45
+ return empty;
46
+ }
47
+ // A törzs a záró fence UTÁNi soroktól (a vezető üres sorok levágva).
48
+ const body = lines.slice(closeIdx + 1).join('\n').replace(/^\n+/, '');
49
+ return { directives: directives, body: body, hasFrontmatter: true };
50
+ }
51
+ /** A záró `---` fence sor-indexe (1..MAX_HEADER_LINES), vagy -1. */
52
+ static findCloseFence(lines) {
53
+ const limit = Math.min(lines.length, FAM_Frontmatter_Util.MAX_HEADER_LINES);
54
+ for (let i = 1; i < limit; i++) {
55
+ if (lines[i].trim() === '---') {
56
+ return i;
57
+ }
58
+ }
59
+ return -1;
60
+ }
61
+ /** Egy `key: value` sor feldolgozása (ismert kulcs → típusos érték a `directives`-be). */
62
+ static applyLine(line, directives) {
63
+ const trimmed = line.trim();
64
+ if (!trimmed || trimmed.startsWith('#')) {
65
+ return;
66
+ }
67
+ const sep = trimmed.indexOf(':');
68
+ if (sep < 0) {
69
+ return;
70
+ }
71
+ const key = trimmed.slice(0, sep).trim();
72
+ if (!FAM_Frontmatter_Util.KNOWN_KEYS.has(key)) {
73
+ return;
74
+ }
75
+ const rawValue = FAM_Frontmatter_Util.stripQuotes(trimmed.slice(sep + 1).trim());
76
+ switch (key) {
77
+ case 'table':
78
+ case 'ruleScope':
79
+ case 'ruleScopeTopic':
80
+ case 'ruleId':
81
+ case 'derivedFrom':
82
+ case 'category':
83
+ if (rawValue) {
84
+ directives[key] = rawValue;
85
+ }
86
+ return;
87
+ case 'importance': {
88
+ const num = Number(rawValue);
89
+ if (Number.isFinite(num)) {
90
+ directives.importance = num;
91
+ }
92
+ return;
93
+ }
94
+ case 'userApproved':
95
+ directives.userApproved = FAM_Frontmatter_Util.toBool(rawValue);
96
+ return;
97
+ case 'globalRule':
98
+ directives.globalRule = FAM_Frontmatter_Util.toBool(rawValue);
99
+ return;
100
+ case 'splitMode':
101
+ directives.splitMode = FAM_Frontmatter_Util.toSplitMode(rawValue);
102
+ return;
103
+ case 'tags':
104
+ directives.tags = FAM_Frontmatter_Util.toList(rawValue);
105
+ return;
106
+ }
107
+ }
108
+ /** Idéző-jelek levágása (`"x"` / `'x'` → `x`). */
109
+ static stripQuotes(value) {
110
+ if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\'')))) {
111
+ return value.slice(1, -1);
112
+ }
113
+ return value;
114
+ }
115
+ /** `'true'`/`'yes'`/`'1'` → `true`; minden más → `false`. */
116
+ static toBool(value) {
117
+ const lower = value.toLowerCase();
118
+ return lower === 'true' || lower === 'yes' || lower === '1';
119
+ }
120
+ /** `'heading'`/`'list-item'` → az adott mód; `true`/`'true'`/`'yes'` → `'heading'`; egyébként `false`. */
121
+ static toSplitMode(value) {
122
+ const lower = value.toLowerCase();
123
+ if (lower === 'heading' || lower === 'list-item') {
124
+ return lower;
125
+ }
126
+ return FAM_Frontmatter_Util.toBool(value) ? 'heading' : false;
127
+ }
128
+ /** `[a, b]` flow VAGY `a, b` vesszős → string-lista (üresek kiszűrve). */
129
+ static toList(value) {
130
+ const inner = value.startsWith('[') && value.endsWith(']') ? value.slice(1, -1) : value;
131
+ return inner.split(',').map((entry) => FAM_Frontmatter_Util.stripQuotes(entry.trim())).filter((entry) => entry.length > 0);
132
+ }
133
+ }
134
+ exports.FAM_Frontmatter_Util = FAM_Frontmatter_Util;
@@ -95,6 +95,36 @@ class FAM_ProjectIdentity_Util {
95
95
  static hasIdentity(identity) {
96
96
  return Boolean(identity.packageName || identity.isGit);
97
97
  }
98
+ /**
99
+ * `canonicalRepoKey(identity, projectDirName?)` (scope-prevenció, user-FR 2026-06-22; **path-aware fix
100
+ * 2026-06-24**) — a STABIL projekt-identitás-kulcs, amit a SCAN a project-scope-ra stamp-el
101
+ * (`FAM_Scope.canonicalRepoKey`), és amire a duplikátum-detekció épül.
102
+ *
103
+ * **KRITIKUS path-aware fix:** a régi forma CSAK a git `org/repo`-t használta → MONOREPO sub-projektek ÉS
104
+ * forkok (egy git-repóban élő, jogosan KÜLÖNBÖZŐ projektek) UGYANAZT a kulcsot kapták → a duplikátum-detekció
105
+ * tévesen jelezte őket egynek → egy téves `merge_scopes` ÖSSZEKEVERTE a tartalmukat (pl. `ccap-revisioned`
106
+ * 42k fájlja a `ccap-speech-recognition` scope-ba). A javítás: a kulcs `${repo}::${diszkriminátor}`, ahol a
107
+ * diszkriminátor a projekt-azonosító A REPÓN BELÜL (package-név bare alak VAGY a projekt-mappa neve). Így két
108
+ * külön projekt EGY repóban is külön kulcsot kap, miközben UGYANANNAK a projektnek az import-/scan-változata
109
+ * (azonos package-név) továbbra is egy kulcsra esik (a dedup eredeti célja megmarad).
110
+ *
111
+ * Prioritás: (1) git `org/repo` van → `${repo}::${pkgBare || dir}` (path-aware), különben csak `repo`;
112
+ * (2) nincs git → a package-név bare alakja VAGY a mappanév. Minden LOWERCASE + trim. Nincs semmi → `undefined`.
113
+ * PURE — IO nélkül tesztelhető.
114
+ */
115
+ static canonicalRepoKey(identity, projectDirName) {
116
+ const repo = identity.gitRepo?.trim().toLowerCase();
117
+ const pkgBare = identity.packageName
118
+ ? identity.packageName.replace(/^@[^/]+\//, '').trim().toLowerCase()
119
+ : undefined;
120
+ const dir = projectDirName?.trim().toLowerCase() || undefined;
121
+ const discriminator = pkgBare || dir;
122
+ if (repo) {
123
+ // path-aware: a repón belüli projekt-azonosító (ha van) → a monorepo/fork sub-projektek külön kulcsot kapnak.
124
+ return discriminator ? `${repo}::${discriminator}` : repo;
125
+ }
126
+ return discriminator || undefined;
127
+ }
98
128
  /** A kereshető `knowledge` „project-identity" entry szövege (PURE). */
99
129
  static buildContent(identity, scopeLabel, root) {
100
130
  const lines = [];
@@ -40,7 +40,7 @@ class FAM_ScanProgress_Util {
40
40
  /**
41
41
  * Egy futás-közbeni progress-sor: `processed/total` fájl + %, chunk-szám, verdikt-bontás, eltelt idő + ETA.
42
42
  * Az ETA a MÉRT sebességből (`processed / elapsed`) → a hátralévő fájlok ideje (csak ha van haladás + maradék).
43
- * `[famIngest]` prefix → az activity-monitor allowlistjén keresztül megjelenik a konzol-ablakban.
43
+ * `[famIngest]` prefix → a szerver látható konzol-ablakában (és a `FAM_LOG_FILE`-ban) megjelenik.
44
44
  */
45
45
  static progressLine(set) {
46
46
  const pct = set.total ? Math.round((set.processed / set.total) * 100) : 100;
@@ -49,7 +49,10 @@ class FAM_ScanProgress_Util {
49
49
  const eta = rate > 0 && remaining > 0
50
50
  ? `, ETA ~${FAM_ScanProgress_Util.formatDuration(remaining / rate)}`
51
51
  : '';
52
- return `[famIngest] feldolgozás ${set.processed}/${set.total} fájl (${pct}%), ${set.chunkCount} chunk, `
52
+ // A projekt-/scope-név a sor elejére (user-FR 2026-06-22) multi-projekt scan-nél a sor magában
53
+ // is megmondja, MELYIK projektnél tart (nem csak a külön project-identity/summary sor).
54
+ const scope = set.scopeLabel ? `[${set.scopeLabel}] ` : '';
55
+ return `[famIngest] ${scope}feldolgozás ${set.processed}/${set.total} fájl (${pct}%), ${set.chunkCount} chunk, `
53
56
  + `${set.verdicts.new} új / ${set.verdicts.modified} mód / ${set.verdicts.equal} vált., `
54
57
  + `eltelt ${FAM_ScanProgress_Util.formatDuration(set.elapsedSeconds)}${eta}`;
55
58
  }