@futdevpro/fdp-agent-memory 1.1.12 → 1.1.21

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@futdevpro/fdp-agent-memory",
3
- "version": "1.1.12",
3
+ "version": "1.1.21",
4
4
  "description": "Local-first, vector-backed multi-table agent memory exposed as an MCP server (read/write/capabilities). Public, FDP-Templates-free, no auth.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_Config_Util = void 0;
4
4
  const commander_1 = require("commander");
5
+ const fam_config_value_coerce_util_1 = require("../../_collections/fam-config-value-coerce.util");
5
6
  const fam_output_util_1 = require("../_collections/fam-output.util");
6
7
  const fam_client_service_1 = require("../_services/fam-client.service");
7
8
  const fam_cli_const_1 = require("../_collections/fam-cli.const");
@@ -67,9 +68,11 @@ class FAM_Config_Util {
67
68
  static async runSet(command) {
68
69
  const globals = command.optsWithGlobals();
69
70
  const local = command.opts();
71
+ // A CLI `--value` mindig string; a szerver típus-szigorúan validál → a katalógus-típusra konvertáljuk
72
+ // (boolean/number/string[] kulcsok így CLI-ből is állíthatók — FAM_ConfigValueCoerce_Util).
70
73
  const body = {
71
74
  key: local.key,
72
- value: local.value,
75
+ value: fam_config_value_coerce_util_1.FAM_ConfigValueCoerce_Util.coerce(local.key, local.value),
73
76
  table: local.table,
74
77
  scopeId: local.scope,
75
78
  };
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_ScanProjects_Util = void 0;
4
4
  const fs_1 = require("fs");
5
5
  const commander_1 = require("commander");
6
+ const fam_cooldown_util_1 = require("../../_collections/fam-cooldown.util");
6
7
  const fam_output_util_1 = require("../_collections/fam-output.util");
7
8
  const fam_client_service_1 = require("../_services/fam-client.service");
8
9
  const fam_cli_const_1 = require("../_collections/fam-cli.const");
@@ -26,6 +27,8 @@ class FAM_ScanProjects_Util {
26
27
  .option('--org <name>', 'default organization az auto-felderítéshez', 'FutDevPro')
27
28
  .option('--table <documents|codebase>', 'default cél-tár', 'codebase')
28
29
  .option('--dry-run', 'csak becslés MINDEN projektre, NEM ír', false)
30
+ .option('--cooldown <ms>', 'cooldown a projektek KÖZÉ (ms) — gép-tehermentesítő szünet nagy multi-scanhez; '
31
+ + 'felülírja a `scan.projectCooldownMs` configot (default: a config, ami 0 = ki)')
29
32
  .option('--stop-on-error', 'az első hibánál leáll (default: folytatja a többivel)', false)
30
33
  .addHelpText('before', '\n⚠️ TISZTA MULTI-SCAN — ELŐBB minden projektbe IGNORE-FÁJL:\n'
31
34
  + ' Mivel ez TÖBB projektet szkennel egyszerre, a junk gyorsan elszaporodik. MINDEN érintett\n'
@@ -77,7 +80,11 @@ class FAM_ScanProjects_Util {
77
80
  const client = new fam_client_service_1.FAM_CliClient_Service(globals.serverUrl);
78
81
  const projects = [];
79
82
  let okCount = 0;
80
- FAM_ScanProjects_Util.progress(globals, `=== ${specs.length} projekt SOROS scan-je${local.dryRun ? ' (dryRun)' : ''} ===`);
83
+ // Projekt-cooldown (user-FR): a `--cooldown <ms>` felülírja a szerver `scan.projectCooldownMs` configját
84
+ // (default 0 = ki). dryRun-on nincs cooldown (nincs valódi embed-munka). A projektek KÖZÉ vár, nem az utolsó után.
85
+ const projectCooldownMs = local.dryRun ? 0 : await FAM_ScanProjects_Util.resolveCooldownMs(client, local.cooldown);
86
+ FAM_ScanProjects_Util.progress(globals, `=== ${specs.length} projekt SOROS scan-je${local.dryRun ? ' (dryRun)' : ''}`
87
+ + `${projectCooldownMs > 0 ? `, cooldown ${projectCooldownMs}ms/projekt` : ''} ===`);
81
88
  for (let i = 0; i < specs.length; i++) {
82
89
  const spec = specs[i];
83
90
  FAM_ScanProjects_Util.progress(globals, `[${i + 1}/${specs.length}] ${spec.project} (${spec.path})`);
@@ -109,6 +116,11 @@ class FAM_ScanProjects_Util {
109
116
  break;
110
117
  }
111
118
  }
119
+ // Projekt-cooldown: a projektek KÖZÉ (NEM az utolsó után) — gép-tehermentesítő szünet nagy multi-scanhez.
120
+ if (projectCooldownMs > 0 && i < specs.length - 1) {
121
+ FAM_ScanProjects_Util.progress(globals, ` cooldown ${projectCooldownMs}ms…`);
122
+ await fam_cooldown_util_1.FAM_Cooldown_Util.sleep(projectCooldownMs);
123
+ }
112
124
  }
113
125
  FAM_ScanProjects_Util.progress(globals, `=== KÉSZ: ${okCount}/${specs.length} ok ===`);
114
126
  return fam_output_util_1.FAM_Output_Util.success({
@@ -116,6 +128,19 @@ class FAM_ScanProjects_Util {
116
128
  data: { total: specs.length, ok: okCount, projects: projects },
117
129
  });
118
130
  }
131
+ /**
132
+ * A projekt-cooldown ms feloldása: a CLI `--cooldown` felülírja a szerver `scan.projectCooldownMs` configját.
133
+ * Nincs CLI-érték → a szerver effektív configját kérdezi (`GET /api/config`); a config-lekérés hibája → 0 (a
134
+ * cooldown sosem buktatja a scant). Minden út a `FAM_Cooldown_Util.resolveMs`-en megy át (clamp + biztonság).
135
+ */
136
+ static async resolveCooldownMs(client, cliValue) {
137
+ if (cliValue !== undefined) {
138
+ return fam_cooldown_util_1.FAM_Cooldown_Util.resolveMs(cliValue);
139
+ }
140
+ const result = await client.get('/api/config');
141
+ const value = result.ok ? result.data?.effective?.['scan.projectCooldownMs'] : 0;
142
+ return fam_cooldown_util_1.FAM_Cooldown_Util.resolveMs(value);
143
+ }
119
144
  /** A scan-request body egy projekt-spec-hez (dryRun vagy valós). */
120
145
  static body(spec, dryRun, defaults) {
121
146
  return {
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FAM_Serve_Util = void 0;
4
+ const tslib_1 = require("tslib");
4
5
  const child_process_1 = require("child_process");
6
+ const net = tslib_1.__importStar(require("net"));
5
7
  const fs_1 = require("fs");
6
8
  const os_1 = require("os");
7
9
  const path_1 = require("path");
@@ -147,7 +149,13 @@ class FAM_Serve_Util {
147
149
  if (action === 'restart') {
148
150
  fam_output_util_1.FAM_Output_Util.logInfo(globals, `FAM szerver RESTART: a(z) ${port} porton futó szerver leállítása…`);
149
151
  await FAM_Serve_Util.stopServerOnPort(port);
150
- await FAM_Serve_Util.waitForPortDown(client, 8000);
152
+ // A health-down NEM elég: az új szerver bind-preflight-je a TÉNYLEGESEN SZABAD (bindolható) portot
153
+ // igényli (a kill utáni process-halál + TIME_WAIT lassú lehet) → a port bindolhatóságát várjuk.
154
+ const freed = await FAM_Serve_Util.waitForPortFree(port, 12000);
155
+ if (!freed) {
156
+ fam_output_util_1.FAM_Output_Util.logInfo(globals, `⚠ a(z) ${port} port 12s alatt nem szabadult fel — az indítás megpróbálom, `
157
+ + 'de ha bind-hiba van, futtasd újra a `fam serve --restart`-ot.');
158
+ }
151
159
  }
152
160
  return FAM_Serve_Util.spawnServer({ globals: globals, local: local, port: port, healthUrl: healthUrl, client: client });
153
161
  }
@@ -265,14 +273,24 @@ class FAM_Serve_Util {
265
273
  return false;
266
274
  }
267
275
  /** A port LE-állásának bevárása (a `--restart` kill után, mielőtt újraindítunk), felső korláttal. */
268
- static async waitForPortDown(client, timeoutMs) {
276
+ static async waitForPortFree(port, timeoutMs) {
269
277
  const deadline = Date.now() + timeoutMs;
270
278
  while (Date.now() < deadline) {
271
- if (!(await client.isReachable(1000))) {
272
- return;
279
+ if (await FAM_Serve_Util.isPortFree(port)) {
280
+ return true;
273
281
  }
274
282
  await FAM_Serve_Util.sleep(FAM_Serve_Util.HEALTH_POLL_MS);
275
283
  }
284
+ return false;
285
+ }
286
+ /** Bindolható-e (szabad-e) a port? Egy próba-`listen` a loopbackon, azonnali close — `true`, ha köthető. */
287
+ static isPortFree(port) {
288
+ return new Promise((resolveCheck) => {
289
+ const tester = net.createServer();
290
+ tester.once('error', () => resolveCheck(false));
291
+ tester.once('listening', () => tester.close(() => resolveCheck(true)));
292
+ tester.listen(port, '127.0.0.1');
293
+ });
276
294
  }
277
295
  /**
278
296
  * A megadott porton FIGYELŐ process leállítása (a `--restart` magja). Platform-natív, fail-safe: Windows →
@@ -284,7 +302,12 @@ class FAM_Serve_Util {
284
302
  const command = isWin ? (process.env.ComSpec ?? 'powershell.exe') : 'sh';
285
303
  const psScript = `$ErrorActionPreference='SilentlyContinue';`
286
304
  + `(Get-NetTCPConnection -LocalPort ${port} -State Listen).OwningProcess | Select-Object -Unique | `
287
- + `ForEach-Object { Stop-Process -Id $_ -Force }`;
305
+ + `ForEach-Object { Stop-Process -Id $_ -Force };`
306
+ // A serve-ablakot (a fam-server-<port>.ps1-t futtató powershell) is leállítjuk → nincs árva,
307
+ // „[FAM szerver kilépett]" zombi-ablak a restart után (különben halmozódnak).
308
+ + `Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" | `
309
+ + `Where-Object { $_.CommandLine -like '*fam-server-${port}*' } | `
310
+ + `ForEach-Object { Stop-Process -Id $_.ProcessId -Force }`;
288
311
  const args = isWin
289
312
  ? ['-NoProfile', '-Command', psScript]
290
313
  : ['-c', `lsof -ti tcp:${port} | xargs -r kill -9`];
@@ -205,12 +205,42 @@ exports.CONFIG_CATALOG = {
205
205
  type: 'boolean', default: false, levels: ALL_LEVELS,
206
206
  description: 'Symlink-követés (path-traversal védelem, dsgn-004).',
207
207
  },
208
+ 'scan.nestedFamignore': {
209
+ type: 'boolean', default: true, levels: ALL_LEVELS,
210
+ description: 'A NESTED (per-alkönyvtár) `.fdpfamignore`-ok figyelembevétele a bejárás közben. `true` (default) '
211
+ + '= ha egy al-projektben/al-mappában van saját `.fdpfamignore`, azt is betöltjük (a saját al-útjához '
212
+ + 'horgonyozva) és alkalmazzuk arra a subtree-re — így a teljes-workspace scan tiszteletben tartja az '
213
+ + 'al-projektek saját ignore-jait (eleve-tisztán-szkennelés). `false` = csak a scan-GYÖKÉR `.fdpfamignore`-ja '
214
+ + 'érvényes (a régi, v1-viselkedés — ha valaki kizárólag gyökér-szintű kontrollt akar).',
215
+ },
216
+ 'scan.reconcileIgnored': {
217
+ type: 'boolean', default: true, levels: ALL_LEVELS,
218
+ description: 'Re-scankor a MOST ignore-minta által kizárt (de korábban beolvasott) fájlok meglévő chunkjainak '
219
+ + 'kitakarítása a tárból. `true` (default — TISZTOGATÁS) = ha utólag hozzáadsz egy ignore-/famignore-mintát, '
220
+ + 'a re-scan az így kizárttá vált fájlok régi bejegyzéseit orphanként soft-delete-eli (nem ragad be, amit '
221
+ + 'ignore-olni akarsz). `false` = a régi viselkedés (az ignore-skippelt fájl „jelen-lévőként" számít, a régi '
222
+ + 'chunkjai BENT MARADNAK — ha valaki a korábbi beolvasást meg akarja őrizni). A lemezről ELTŰNT fájlokat '
223
+ + 'mindkét beállítás reconciliálja (ez attól független).',
224
+ },
208
225
  'scan.testFileWeight': {
209
226
  type: 'number', default: 0.5, min: 0.0, max: 10.0, levels: ALL_LEVELS,
210
227
  description: 'A teszt-/spec-fájlok (`*.spec.*` / `*.test.*`) DEFAULT retrieval-súlya (FAM-REV-058) — a próza-'
211
228
  + 'nehéz specek különben az IMPLEMENTÁCIÓ fölé rangsorolnak. <1 → a forrás alá; explicit scan-`weights` '
212
229
  + 'felülírja; 1 = nincs csökkentés.',
213
230
  },
231
+ 'scan.sectionCooldownMs': {
232
+ type: 'number', default: 0, min: 0, max: 60000, integer: true, levels: ALL_LEVELS,
233
+ description: 'Kikapcsolható cooldown (ms) a SZEKCIÓK (file-batch-ek) KÖZÉ egy scanen belül — a NAGY DB-k '
234
+ + 'beolvasásakor ad lélegzetvételi szünetet a gépnek (CPU/embed-provider/Mongo ne terhelődjön túl). '
235
+ + 'Default `0` = KIKAPCSOLVA (nincs lassítás); nagy scanre állítsd pl. 1000–3000-re. Csak a batch-ek '
236
+ + 'KÖZÖTT vár (az utolsó után nem), és a `dryRun` NEM vár (nincs valódi embed-munka).',
237
+ },
238
+ 'scan.projectCooldownMs': {
239
+ type: 'number', default: 0, min: 0, max: 60000, integer: true, levels: ALL_LEVELS,
240
+ description: 'Kikapcsolható cooldown (ms) a PROJEKTEK KÖZÉ a `scan-projects` soros multi-scan-jében — a sok-'
241
+ + 'projektes (L2/L6) beolvasás ne thrash-elje a gépet. Default `0` = KIKAPCSOLVA; nagy futásra pl. 2000–'
242
+ + '5000. A CLI `--cooldown <ms>` felülírja ezt a config-default-ot (a projektek KÖZÖTT vár, az utolsó után nem).',
243
+ },
214
244
  'scan.estimateMsPerChunk': {
215
245
  type: 'number', default: 250, min: 1, max: 10000, integer: true, levels: ALL_LEVELS,
216
246
  description: 'A `dryRun` pre-scan idő-becsléshez: becsült embed-idő chunkonként (ms). DURVA, gép-/modell-/'
@@ -239,6 +269,32 @@ exports.CONFIG_CATALOG = {
239
269
  type: 'number', default: 20, min: 1, max: 200, integer: true, levels: GLOBAL_TABLE,
240
270
  description: 'Párhuzamos ingest flow-limit (CCAP DEFAULT_INGEST_FLOW_LIMIT=20, cap 200).',
241
271
  },
272
+ // --- 4.8. Memória / heap-guard (OOM-hibakezelés, fogyasztó: FAM_HeapGuard_ControlService) ---
273
+ 'memory.guardEnabled': {
274
+ type: 'boolean', default: true, levels: GLOBAL_ONLY,
275
+ description: 'A heap-guard (OOM-hibakezelés) be/ki. `true` (default): watchdog (heap-arány + GC-hányad) + a '
276
+ + 'nehéz write-ek (scan/import/re-embed) load-shed-je critical nyomásnál + graceful-exit a near-limit '
277
+ + 'zónában (a kriptikus exit-134 helyett). `false`: csak a crash-handlerek (uncaught/unhandled) aktívak.',
278
+ },
279
+ 'memory.warnRatio': {
280
+ type: 'number', default: 0.70, min: 0.1, max: 1.0, levels: GLOBAL_ONLY,
281
+ description: 'Heap-arány (used/limit), ami fölött a guard `warn` szintet jelez (log; még nincs load-shed).',
282
+ },
283
+ 'memory.shedRatio': {
284
+ type: 'number', default: 0.82, min: 0.1, max: 1.0, levels: GLOBAL_ONLY,
285
+ description: 'Heap-arány, ami fölött `critical` → a nehéz write-ek (scan/import/re-embed) 503-mal elutasítva '
286
+ + '(a könnyű read/create/update fut tovább). A GC-hányad ≥60% is critical-t vált (mark-compact thrash-jel).',
287
+ },
288
+ 'memory.exitRatio': {
289
+ type: 'number', default: 0.92, min: 0.1, max: 1.0, levels: GLOBAL_ONLY,
290
+ description: 'Heap-arány, ami fölött tartósan (GC-vel sem csökken) a guard TISZTÁN kilép (exit 137) → a `fam '
291
+ + 'serve` ps1 prediktíven újraindít. A kriptikus „Ineffective mark-compacts" exit-134 elkerülése. shed < exit ajánlott.',
292
+ },
293
+ 'memory.pollMs': {
294
+ type: 'number', default: 3000, min: 500, max: 60000, integer: true, levels: GLOBAL_ONLY,
295
+ description: 'A heap-watchdog mintavételi időköze (ms) + a GC-hányad gördülő-ablaka. Kisebb = gyorsabb '
296
+ + 'reakció a hirtelen növekedésre, több CPU. 3000 = jó alapérték.',
297
+ },
242
298
  'patterns.defaultWeight': {
243
299
  type: 'number', default: 5, min: 0, levels: [fam_config_level_type_enum_1.FAM_ConfigLevel.table], tableOnly: fam_table_type_enum_1.FAM_Table.codingPatterns,
244
300
  description: 'A coding_patterns tár default retrieval-súlya (CCAP weight 5; csak coding_patterns).',
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_ConfigValueCoerce_Util = void 0;
4
+ const config_catalog_const_1 = require("./config-catalog.const");
5
+ /**
6
+ * `FAM_ConfigValueCoerce_Util` — a `config set` CLI nyers `--value` STRING-jét a katalógus-kulcs típusára konvertálja
7
+ * (a CLI argumentum mindig string, a szerver viszont típus-szigorúan validál — boolean/number kulcsot string-ként
8
+ * elutasít: `FAM-CONFIG-SET-002`). Ez teszi a config-kapcsolókat (pl. `scan.nestedFamignore` / `scan.reconcileIgnored`
9
+ * / `scan.followSymlinks`) ténylegesen CLI-ből állíthatóvá. A `CONFIG_CATALOG[key].type` alapján:
10
+ *
11
+ * - `boolean` → `'true'`/`'false'` → bool (más → változatlan string, hogy a szerver adjon egyértelmű hibát);
12
+ * - `number` → `Number(raw)` (NaN → változatlan string);
13
+ * - `number-or-null` → `'null'`/üres → `null`, különben `Number(raw)`;
14
+ * - `string[]` → JSON-tömb (`[...]`) VAGY vesszős lista → `string[]`;
15
+ * - `string` (+ enum) → változatlan.
16
+ *
17
+ * Ismeretlen kulcs → változatlan string (a szerver `FAM-CONFIG-SET-001`-gyel utasítja el). Tisztán pure.
18
+ */
19
+ class FAM_ConfigValueCoerce_Util {
20
+ /** A nyers CLI-string → a `key` katalógus-típusának megfelelő JS-érték (lásd az osztály-leírást). */
21
+ static coerce(key, raw) {
22
+ const entry = config_catalog_const_1.CONFIG_CATALOG[key];
23
+ if (!entry) {
24
+ return raw;
25
+ }
26
+ switch (entry.type) {
27
+ case 'boolean':
28
+ if (raw === 'true') {
29
+ return true;
30
+ }
31
+ if (raw === 'false') {
32
+ return false;
33
+ }
34
+ return raw;
35
+ case 'number': {
36
+ const value = Number(raw);
37
+ return Number.isNaN(value) ? raw : value;
38
+ }
39
+ case 'number-or-null': {
40
+ if (raw === 'null' || raw === '') {
41
+ return null;
42
+ }
43
+ const value = Number(raw);
44
+ return Number.isNaN(value) ? raw : value;
45
+ }
46
+ case 'string[]': {
47
+ const trimmed = raw.trim();
48
+ if (trimmed.startsWith('[')) {
49
+ try {
50
+ return JSON.parse(trimmed);
51
+ }
52
+ catch {
53
+ // nem valid JSON → essünk át a vesszős listára
54
+ }
55
+ }
56
+ return trimmed.length ? trimmed.split(',').map((item) => item.trim()).filter(Boolean) : [];
57
+ }
58
+ default:
59
+ return raw;
60
+ }
61
+ }
62
+ }
63
+ exports.FAM_ConfigValueCoerce_Util = FAM_ConfigValueCoerce_Util;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_Cooldown_Util = void 0;
4
+ /**
5
+ * `FAM_Cooldown_Util` (user-FR 2026-06-21) — opcionális, **kikapcsolható** cooldown (lélegzetvételi szünet) a
6
+ * scan-feldolgozás közé, hogy a NAGY adatbázisok beolvasása NE terhelje túl a gépet (CPU/embed-provider/Mongo).
7
+ * Két ponton fogyasztva: (a) a SZEKCIÓK (file-batch-ek) között egy scanen belül (`scan.sectionCooldownMs`, szerver-
8
+ * oldali ingest-loop); (b) a PROJEKTEK között a `scan-projects` soros orchestrátorban (`scan.projectCooldownMs` /
9
+ * CLI `--cooldown`). Default **0 = kikapcsolva** (nincs lassítás) → opt-in: a user nagy scanre bekapcsolja.
10
+ *
11
+ * Tisztán: a `resolveMs` (parse + clamp) pure + külön tesztelhető; a `sleep` az egyetlen side-effect.
12
+ */
13
+ class FAM_Cooldown_Util {
14
+ /** Biztonsági felső korlát egy cooldown-ra (ms) — egy elgépelt óriás-érték ne fagyassza be a scant. */
15
+ static MAX_MS = 60000;
16
+ /**
17
+ * Nyers config/CLI érték → biztonságos cooldown ms. Nem-szám / NaN / ≤0 → `0` (kikapcsolva); egyébként egész
18
+ * ms-re vágva, `[0, MAX_MS]`-re clamp-elve. Így a hibás/extrém input sosem buktatja vagy fagyasztja a scant.
19
+ */
20
+ static resolveMs(raw) {
21
+ const value = typeof raw === 'number' ? raw : Number(raw);
22
+ if (!Number.isFinite(value) || value <= 0) {
23
+ return 0;
24
+ }
25
+ return Math.min(Math.floor(value), FAM_Cooldown_Util.MAX_MS);
26
+ }
27
+ /** Vár `ms` ideig (≤0 → azonnal visszatér, nincs timer). A cooldown egyetlen tényleges késleltetése. */
28
+ static async sleep(ms) {
29
+ if (ms <= 0) {
30
+ return;
31
+ }
32
+ await new Promise((resolve) => setTimeout(resolve, ms));
33
+ }
34
+ }
35
+ exports.FAM_Cooldown_Util = FAM_Cooldown_Util;
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_HeapGuard_ControlService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const v8 = tslib_1.__importStar(require("v8"));
6
+ const perf_hooks_1 = require("perf_hooks");
7
+ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
8
+ /**
9
+ * `FAM_HeapGuard_ControlService` (user-FR 2026-06-21) — **OOM-hibakezelés**. A FAM-szerver V8-heapje a nagy korpuszon
10
+ * (126k vektor) + nehéz műveletekkor (scan/import/re-embed) a plafonhoz érhet → „Ineffective mark-compacts near heap
11
+ * limit" → FATAL exit 134 (nem catch-elhető). Ez a guard:
12
+ *
13
+ * 1. **Watchdog**: periodikusan méri a heap-arányt (`v8.getHeapStatistics`) ÉS a GC-hányadot (`perf_hooks` 'gc'
14
+ * observer — a thrash valódi jele: a trace-ben `mu = 0.306`, azaz a futás 70%-a GC). A szint-váltásokat logolja
15
+ * (a `/api/logs`-on az agent IS látja jönni a nyomást).
16
+ * 2. **Load-shed**: `shouldShedLoad()` → a NEHÉZ write-ek (scan/import/re-embed) `critical` nyomásnál 503-mal
17
+ * elutasítva, MIELŐTT egy újabb nagy allokáció átlöki a plafont (a könnyű read/create/update fut tovább).
18
+ * 3. **Graceful exit**: ha a nyomás a `exitRatio` fölött tartós (GC-vel sem csökken), strukturált diagnosztika +
19
+ * tiszta `process.exit` → a `fam serve` ps1-wrapper PREDIKTÍVAN újraindít (a kriptikus exit-134 helyett).
20
+ * 4. **Crash-handler**: `uncaughtException` / `unhandledRejection` → strukturált log + tiszta exit (eddig néma volt).
21
+ *
22
+ * Lokális, single-user (ADR-004) → a load-shed a saját gépet védi, nem multi-tenant fairness. A küszöbök
23
+ * config-/env-kapcsolósak; a tiszta szint-számítás (`computeLevel`) pure + külön tesztelt.
24
+ */
25
+ class FAM_HeapGuard_ControlService {
26
+ static _instance;
27
+ /** GC-hányad-küszöb a `warn`/`critical` szinthez (a thrash-jelhez; a ratio MELLETT). */
28
+ static GC_WARN_FRACTION = 0.40;
29
+ static GC_CRITICAL_FRACTION = 0.60;
30
+ thresholds = {
31
+ warnRatio: 0.70, shedRatio: 0.82, exitRatio: 0.92, pollMs: 3000, enabled: true,
32
+ };
33
+ installed = false;
34
+ timer;
35
+ gcObserver;
36
+ lastLevel = 'ok';
37
+ /** A GC-ben töltött ms gördülő ablaka (timestamp + duration párok az utolsó ~pollMs-re). */
38
+ gcEvents = [];
39
+ static getInstance() {
40
+ if (!FAM_HeapGuard_ControlService._instance) {
41
+ FAM_HeapGuard_ControlService._instance = new FAM_HeapGuard_ControlService();
42
+ }
43
+ return FAM_HeapGuard_ControlService._instance;
44
+ }
45
+ /** A küszöbök beállítása (install ELŐTT) — clamp `[0..1]` (a pollMs `[500..60000]`). Hiányzó mező → marad a default. */
46
+ configure(partial) {
47
+ const clampRatio = (value, fallback) => (typeof value === 'number' && value > 0 && value <= 1) ? value : fallback;
48
+ this.thresholds = {
49
+ warnRatio: clampRatio(partial.warnRatio, this.thresholds.warnRatio),
50
+ shedRatio: clampRatio(partial.shedRatio, this.thresholds.shedRatio),
51
+ exitRatio: clampRatio(partial.exitRatio, this.thresholds.exitRatio),
52
+ pollMs: typeof partial.pollMs === 'number' && partial.pollMs >= 500
53
+ ? Math.min(partial.pollMs, 60000) : this.thresholds.pollMs,
54
+ enabled: typeof partial.enabled === 'boolean' ? partial.enabled : this.thresholds.enabled,
55
+ };
56
+ }
57
+ /** A crash-handlerek + (ha enabled) a watchdog + GC-observer telepítése. Idempotens (egyszer fut). Best-effort. */
58
+ install() {
59
+ if (this.installed) {
60
+ return;
61
+ }
62
+ this.installed = true;
63
+ process.on('uncaughtException', (error) => this.onFatal('uncaughtException', error));
64
+ process.on('unhandledRejection', (reason) => this.onFatal('unhandledRejection', reason));
65
+ if (!this.thresholds.enabled) {
66
+ fsm_dynamo_1.DyFM_Log.info('[heap-guard] disabled (memory.guardEnabled=false) — csak a crash-handlerek aktívak.');
67
+ return;
68
+ }
69
+ this.installGcObserver();
70
+ this.timer = setInterval(() => this.tick(), this.thresholds.pollMs);
71
+ // A timer NE tartsa életben a process-t (a graceful shutdownt ne blokkolja).
72
+ this.timer.unref?.();
73
+ fsm_dynamo_1.DyFM_Log.info(`[heap-guard] aktív (poll ${this.thresholds.pollMs}ms; shed@${Math.round(this.thresholds.shedRatio * 100)}%, `
74
+ + `exit@${Math.round(this.thresholds.exitRatio * 100)}%).`);
75
+ }
76
+ /** Pure szint-számítás (külön tesztelhető): a ratio ÉS a GC-hányad közül a ROSSZABB dönt. */
77
+ static computeLevel(ratio, gcFraction, thresholds) {
78
+ if (ratio >= thresholds.shedRatio || gcFraction >= FAM_HeapGuard_ControlService.GC_CRITICAL_FRACTION) {
79
+ return 'critical';
80
+ }
81
+ if (ratio >= thresholds.warnRatio || gcFraction >= FAM_HeapGuard_ControlService.GC_WARN_FRACTION) {
82
+ return 'warn';
83
+ }
84
+ return 'ok';
85
+ }
86
+ /** A pillanatnyi heap-nyomás (ratio + GC-hányad → szint). Observability + load-shed forrása. */
87
+ pressure() {
88
+ const stats = v8.getHeapStatistics();
89
+ const usedMb = Math.round(stats.used_heap_size / 1048576);
90
+ const limitMb = Math.round(stats.heap_size_limit / 1048576);
91
+ const ratio = stats.heap_size_limit ? stats.used_heap_size / stats.heap_size_limit : 0;
92
+ const gcFraction = this.recentGcFraction();
93
+ const level = FAM_HeapGuard_ControlService.computeLevel(ratio, gcFraction, this.thresholds);
94
+ return { level: level, usedMb: usedMb, limitMb: limitMb, ratio: ratio, gcFraction: gcFraction };
95
+ }
96
+ /** `true` → a NEHÉZ write-eket (scan/import/re-embed) le kell shed-elni (critical nyomás). Disabled → soha. */
97
+ shouldShedLoad() {
98
+ return this.thresholds.enabled && this.pressure().level === 'critical';
99
+ }
100
+ /** A GC-hányad az utolsó `pollMs` ablakban (GC-ms / ablak-ms; clamp [0..1]). */
101
+ recentGcFraction() {
102
+ const windowMs = this.thresholds.pollMs;
103
+ const cutoff = Date.now() - windowMs;
104
+ this.gcEvents = this.gcEvents.filter((event) => event.at >= cutoff);
105
+ const gcMs = this.gcEvents.reduce((sum, event) => sum + event.ms, 0);
106
+ return Math.min(gcMs / windowMs, 1);
107
+ }
108
+ /** A 'gc' perf-entry-k figyelése — a GC-ben töltött időt gyűjti a gördülő ablakhoz. */
109
+ installGcObserver() {
110
+ try {
111
+ this.gcObserver = new perf_hooks_1.PerformanceObserver((list) => {
112
+ const now = Date.now();
113
+ for (const entry of list.getEntries()) {
114
+ this.gcEvents.push({ at: now, ms: entry.duration });
115
+ }
116
+ });
117
+ this.gcObserver.observe({ entryTypes: ['gc'] });
118
+ }
119
+ catch {
120
+ // A GC-observer nem kritikus (a ratio-jel marad) — best-effort.
121
+ }
122
+ }
123
+ /** A watchdog-tick: szint-váltás logolása + a tartós veszély-zóna graceful-exit-je. */
124
+ tick() {
125
+ const pressure = this.pressure();
126
+ if (pressure.level !== this.lastLevel) {
127
+ const line = `[heap-guard] ${this.lastLevel}→${pressure.level} `
128
+ + `(${pressure.usedMb}/${pressure.limitMb} MB ${Math.round(pressure.ratio * 100)}%, GC ${Math.round(pressure.gcFraction * 100)}%)`;
129
+ if (pressure.level === 'critical') {
130
+ fsm_dynamo_1.DyFM_Log.warn(`${line} — a nehéz write-ek (scan/import/re-embed) load-shed (503).`);
131
+ }
132
+ else if (pressure.level === 'warn') {
133
+ fsm_dynamo_1.DyFM_Log.warn(line);
134
+ }
135
+ else {
136
+ fsm_dynamo_1.DyFM_Log.info(`${line} — nyomás enyhült.`);
137
+ }
138
+ this.lastLevel = pressure.level;
139
+ }
140
+ // Tartós veszély-zóna: GC-kísérlet, majd ha nem csökken → tiszta, logolt exit (a ps1 újraindít).
141
+ if (pressure.ratio >= this.thresholds.exitRatio) {
142
+ this.attemptGcReclaim();
143
+ const after = this.pressure();
144
+ if (after.ratio >= this.thresholds.exitRatio) {
145
+ fsm_dynamo_1.DyFM_Log.error(`[heap-guard] HEAP-VÉSZ ${after.usedMb}/${after.limitMb} MB (${Math.round(after.ratio * 100)}%) — `
146
+ + 'graceful exit, hogy a `fam serve` TISZTÁN újraindítson (a kriptikus OOM exit-134 elkerülése).');
147
+ // Egy tick haladékot adunk a log flush-nek, majd restartolható kóddal kilépünk.
148
+ setTimeout(() => process.exit(137), 250).unref?.();
149
+ }
150
+ }
151
+ }
152
+ /** Best-effort GC-kényszerítés (csak ha `--expose-gc`). A near-limit utolsó esély a tiszta-exit ELŐTT. */
153
+ attemptGcReclaim() {
154
+ try {
155
+ const maybeGc = global.gc;
156
+ if (maybeGc) {
157
+ maybeGc();
158
+ }
159
+ }
160
+ catch {
161
+ // nincs --expose-gc → nincs mit tenni; a tiszta-exit a fallback.
162
+ }
163
+ }
164
+ /** Strukturált crash-log + tiszta exit (a `fam serve` ps1 újraindít). Az OOM-fatal NEM ide jön (nem catch-elhető). */
165
+ onFatal(kind, error) {
166
+ try {
167
+ const message = error instanceof Error ? `${error.message}\n${error.stack ?? ''}` : String(error);
168
+ fsm_dynamo_1.DyFM_Log.error(`[heap-guard] FATAL ${kind}: ${message}`);
169
+ }
170
+ catch {
171
+ // a log se buktasson — best-effort.
172
+ }
173
+ setTimeout(() => process.exit(1), 250).unref?.();
174
+ }
175
+ }
176
+ exports.FAM_HeapGuard_ControlService = FAM_HeapGuard_ControlService;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_title = void 0;
4
+ exports.FAM_title = `
5
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠤⠤⠤⠤⣄⠀⠀⠀⠀⠀⠀⢀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
6
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣶⡊⠉⠉⣉⣱⡷⠶⢢⣠⢴⣶⡝⠒⠉⢉⣭⡽⠟⢉⣀⡀⠹⢭⠒⢤⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
7
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠔⢚⣩⡽⠿⠊⢉⣉⡂⣀⣩⠭⢴⠟⠋⠉⠉⠉⠛⠳⢦⣬⣤⡴⠞⠛⠁⠛⠳⣾⣧⠀⠟⠀⠉⠲⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
8
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⢚⠁⠀⠰⠋⢡⠄⠀⠞⣫⢟⡥⠒⠉⠹⣿⡀⠀⠀⢦⡀⠀⠀⠀⠈⠻⡧⡀⠀⠀⠀⠀⠈⠻⣗⡶⠶⠶⢤⡀⠱⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀
9
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢮⣤⡾⠀⠀⣠⡴⠋⠀⡠⣚⠥⠒⢛⡲⠄⠀⠈⢻⡆⠀⠀⠻⣦⣀⠀⠀⠀⣿⠻⣦⣀⣴⠶⠂⠀⠘⣷⡄⠀⠀⢀⣴⡿⠈⠢⡀⠀⠀⠀⠀⠀⠀
10
+ ⠀⠀⠀⠀⠀⠀⠀⡠⠖⣉⣁⡀⠀⢀⣾⠋⠴⢿⣽⠋⠀⠞⢉⣉⣽⣳⣄⣀⠀⠋⠀⠀⠀⠈⠙⣷⡄⠀⠁⠀⠙⢤⣯⡀⠀⠀⠀⣼⡇⠀⡾⠋⠁⠀⠳⣄⠘⢆⠀⠀⠀⠀⠀
11
+ ⠀⠀⠀⠀⠀⡰⠋⠰⠛⢻⡞⢉⣠⣼⡇⢀⣴⠟⠛⠒⣴⠟⠋⠉⠀⠀⢀⣀⣀⡀⠀⠀⠀⠀⠀⣸⡇⠀⠀⠳⣄⠀⠉⢿⣄⠀⢰⣿⣧⡀⠀⣴⠶⠶⣦⡼⢧⠈⢣⠀⠀⠀⠀
12
+ ⠀⠀⠀⢀⡞⣡⣶⡄⠀⡟⣳⠿⠋⠙⡍⡽⠁⣀⣤⣤⣿⡄⠀⠀⠀⠀⡿⡉⣀⣀⣤⣤⣤⣴⠾⠥⠽⣦⣄⠀⠉⠻⢶⡼⢻⠀⠈⠇⠘⡷⡄⠘⠂⠀⢀⡍⠻⣷⣄⡇⠀⠀⠀
13
+ ⠀⠀⠀⠘⣺⠏⢸⢃⣼⠟⢁⡤⠀⣠⢟⡷⠟⢋⣉⣤⡿⠇⠀⠀⠀⢰⣣⠞⠋⠉⠉⠁⡀⠀⠀⠀⠀⠀⠙⢷⣄⠀⠀⢹⣾⠀⠀⠀⠀⢸⡇⠀⣀⡀⣾⠀⠀⠈⢻⡁⢦⠀⠀
14
+ ⠀⠀⣠⢚⣵⣄⠈⣼⡇⠀⢸⠧⢞⡵⠋⠠⠚⠉⠉⠀⠀⢀⡇⠀⣰⣟⣁⣀⠀⠀⠀⠀⠉⠒⠶⣤⣤⣀⠀⠀⠙⠀⠀⢸⡇⢰⣟⠛⢶⡋⣇⠀⠉⠻⡟⡄⠀⢀⢀⣿⠀⢧⠀
15
+ ⠀⣰⠃⢸⠁⣿⠀⠸⣧⠀⢸⢣⠋⠀⣠⣤⠶⢶⢒⣤⣔⣻⠣⢼⠟⠁⠀⠙⢷⡄⠀⠀⢀⠀⠀⠀⠈⠓⢟⢦⠀⠀⠀⢸⡇⠈⠻⣦⡀⠈⠻⣷⣄⠀⠘⣿⠀⠸⣿⣇⣀⢸⠀
16
+ ⠀⡇⠀⠀⣼⠇⠀⢀⣿⠀⣇⣇⣴⠟⠋⢠⣾⠟⠉⠀⠀⠈⠳⣼⠀⠀⠀⠀⠀⠳⠀⠀⠈⢳⣄⠀⠀⠀⢸⣼⠀⠀⠀⠈⡟⢆⠀⠈⢻⡀⠀⠈⢻⣆⠀⣻⠃⠀⠀⢹⡟⠻⡀
17
+ ⠀⢧⡆⣼⠏⠀⣾⠟⠁⢰⠃⡵⠃⢀⣴⡿⠁⠀⡀⠀⠀⠀⠀⠹⣧⡀⠰⣦⡀⠀⠀⠀⠀⠀⣻⢦⣀⣠⡾⣇⠀⠀⢀⣰⠟⠙⢷⣄⠀⠀⠀⠀⠀⣿⠀⠉⢠⠄⠀⣼⡇⠀⢧
18
+ ⢀⠞⢡⡟⠀⠀⣿⠀⢀⡏⡼⠁⣴⠟⠁⠀⠀⠀⣿⠀⣀⣀⢀⣴⠘⣷⡀⠈⢻⣦⣀⠀⢀⣾⠟⠉⠀⠀⠉⠻⣷⣄⠀⠀⠀⠀⠀⠙⢷⡄⠀⠀⠀⠉⠀⣠⡟⠀⣼⣟⠀⠀⢸
19
+ ⢸⠀⠘⣧⠀⡴⠛⠳⢸⢰⠁⢰⠏⠀⠀⠀⢀⣼⡯⠟⠋⠙⠻⣷⡀⠘⠀⠀⠀⠈⠉⠻⣿⠁⠀⠀⢰⡟⠉⠀⠈⢻⣦⠀⠀⠀⣄⠀⠀⡗⠀⢸⡇⣠⣾⠟⢀⣾⠋⠹⣷⢀⡇
20
+ ⠈⢆⠀⠹⢷⣤⣀⣠⠎⡇⠀⠸⠀⠀⢀⣴⠟⠉⠀⠀⠀⢄⠀⠹⣧⡀⠀⠀⣀⡀⠀⠀⣿⠀⠀⠀⠘⣿⡄⠀⠀⠀⢹⣦⡀⠀⢿⣄⠀⢀⣠⡿⠽⣯⡁⠀⠸⠃⠀⠀⡏⠉⠀
21
+ ⠀⢠⢷⣄⠀⠈⣉⣉⢢⢳⡀⠀⠀⠀⣾⡏⠀⠀⠠⣀⡤⢿⠀⠀⠙⠷⠶⠛⠉⠈⠀⣰⠟⠀⠀⠀⠀⠘⣷⡀⠀⠠⠛⠉⠉⠀⢈⣯⠗⠛⠁⠀⠀⠈⠃⠀⢀⣴⠇⢠⠇⠀⠀
22
+ ⠀⢸⡀⠻⣧⠈⠉⠹⣏⢀⣑⠤⣀⣀⠼⠳⣄⠀⠀⠀⠙⠺⠖⣦⣤⠤⣀⡀⠀⠀⠘⠁⠀⠀⠀⠀⠀⠀⢸⢧⡀⠀⠀⢀⣀⢴⣿⣅⡀⠠⠶⢿⢦⣀⣠⣴⠟⠃⡠⠋⠀⠀⠀
23
+ ⠀⠀⠳⡀⠘⠃⠀⡤⠸⣼⠀⠉⠛⠋⠉⠉⠙⠻⣦⣄⠀⠀⠀⠀⠈⠉⠙⠻⣦⠀⠀⠀⠀⡀⠀⠀⠀⢀⣾⠖⠚⠛⠛⠛⠋⠁⠀⠙⣷⠀⠀⣸⡴⠛⠉⢁⡤⠊⠁⠀⠀⠀⠀
24
+ ⠀⠀⠀⠘⢦⡀⠸⣧⠀⢻⢇⠀⠳⡤⣤⠆⠀⠀⠈⢻⡇⠀⠀⠀⢰⡄⠀⠀⣿⡇⢀⡾⠛⠛⠻⡝⣲⠟⠋⠀⢀⡄⠀⠀⠀⣀⡄⠀⠋⢀⡴⣻⡄⣤⡶⡍⠀⠀⠀⠀⠀⠀⠀
25
+ ⠀⠀⠀⠀⠀⠈⠙⠁⠉⠉⠈⠣⡀⠹⣇⠀⠀⠀⠀⠘⠀⠀⠀⢀⡾⢳⡶⠾⠋⠀⠈⠃⠀⠀⣠⠟⢄⣀⣠⡴⠋⠀⠀⠀⣼⢻⣤⣴⠶⠟⠋⣡⡷⣏⢿⡧⠀⠀⠀⠀⠀⠀⠀
26
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠓⢫⣲⣤⣀⣀⣀⣀⣤⣶⣻⠋⠛⠷⣦⣤⣤⣄⡤⢤⣺⠕⠋⠉⠉⠁⠀⠀⣀⣤⣾⠏⢩⠀⠀⢀⣤⣾⠛⣧⢻⣼⠀⠀⠀⠀⠀⠀⠀⠀
27
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠓⠮⢭⣉⣉⡩⠥⠚⠈⢇⠀⢠⡄⠀⠉⠉⠙⣿⠀⢠⠶⠖⢫⣩⠟⠛⠛⠉⠀⣠⣿⣦⠶⠿⣭⣸⣇⡿⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀
28
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⣌⡿⣄⠀⠒⠚⠋⠀⠀⠀⣠⡾⠃⠀⢀⣀⠴⠚⠉⠣⢍⣛⣶⡶⠝⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
29
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠒⠂⠀⠒⠒⠉⠀⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
30
+
31
+ FDP Agent Memory - By Future Development Program Kft.`;
@@ -4,6 +4,7 @@ exports.FAM_FamIgnore_Util = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const fs = tslib_1.__importStar(require("fs"));
6
6
  const path = tslib_1.__importStar(require("path"));
7
+ const fam_glob_match_util_1 = require("./fam-glob-match.util");
7
8
  /**
8
9
  * `FAM_FamIgnore_Util` (descriptive user feedback / nagy-scan ergonómia) — a **`.fdpfamignore`** in-repo,
9
10
  * gitignore-stílusú ignore-fájl támogatása. A projekt (vagy a workspace-gyökér) MAGÁBAN hordozza, mit hagyjon
@@ -13,7 +14,10 @@ const path = tslib_1.__importStar(require("path"));
13
14
  * **Szintaxis (gitignore-szerű, a `FAM_GlobMatch_Util`-ra fordítva):** soronként egy minta; `#` komment + üres
14
15
  * sor kihagyva. `node_modules` (csupasz név) → bárhol illeszkedik; `/build` (vezető `/`) → a gyökérhez horgonyzott;
15
16
  * `dist/` (záró `/`) → könyvtár; `src/gen` (beágyazott `/`) → gyökér-relatív; `*.tgz` → bárhol. **A `!` negáció
16
- * v1-ben NEM támogatott** (a flat „matchesAny skip" modellben — BACKLOG), az ilyen sorok kihagyva.
17
+ * (re-include) TÁMOGATOTT** (user-FR 2026-06-21): `parse` az ignore-sorokat, `parseNegations` a `!`-sorokat adja;
18
+ * a fogyasztó (scan) a fájlt akkor SKIP-eli, ha ignore-match ÉS NEM re-include-match — a könyvtár-prune pedig
19
+ * `dirOnReincludeLineage`-tudatos (egy ignore-olt mappát NEM vág le, ha re-included elem van rajta/alatta). Így
20
+ * a „ignore-mindent-kivéve-X" (pl. `ccap/` + `!ccap/client/`) helyesen működik a rekurzív bejárásban is.
17
21
  *
18
22
  * **v1 hatókör:** a scan-GYÖKÉR `.fdpfamignore`-ja (a teljes scanre). A NESTED (per-alkönyvtár) `.fdpfamignore`
19
23
  * a két-fázisú felderítés (walk → classify) miatt külön kör — BACKLOG (a `baseDir` paraméter már készen áll rá).
@@ -62,9 +66,61 @@ class FAM_FamIgnore_Util {
62
66
  }
63
67
  return patterns;
64
68
  }
69
+ /**
70
+ * A `!`-negáció (**re-include**) sorai — a vezető `!` levágva. Tisztán pure (a fordítás külön). Az `ignore`
71
+ * (`parse`) + a re-include (`parseNegations`) együtt adja a gitignore-szerű „ignore-mindent-kivéve" szemantikát.
72
+ */
73
+ static parseNegations(content) {
74
+ const result = [];
75
+ for (const rawLine of content.split(/\r?\n/)) {
76
+ const line = rawLine.trim();
77
+ if (line.startsWith('!') && line.length > 1) {
78
+ result.push(line.slice(1).trim());
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ /**
84
+ * A re-include (`!`) szabályok egy könyvtárból: **`globs`** (a fájl-szintű re-include-matchhez, `<base>` +
85
+ * `<base>/**`) + **`bases`** (a `/**` nélküli alap-utak — a KÖNYVTÁR-prune lineage-ellenőrzéséhez: egy ignore-olt
86
+ * könyvtárat NEM vágunk le, ha egy re-include alatta/fölötte/rajta van). Hiányzó fájl → üres. Fail-safe.
87
+ */
88
+ static loadReincludeRules(dirAbsolutePath, baseDir = '') {
89
+ try {
90
+ const filePath = path.join(dirAbsolutePath, FAM_FamIgnore_Util.FILENAME);
91
+ if (!fs.existsSync(filePath)) {
92
+ return { globs: [], bases: [] };
93
+ }
94
+ const negations = FAM_FamIgnore_Util.parseNegations(fs.readFileSync(filePath, 'utf-8'));
95
+ const globs = FAM_FamIgnore_Util.toGlobPatterns(negations, baseDir);
96
+ const bases = [...new Set(globs.map((glob) => glob.replace(/\/\*\*$/, '')))];
97
+ return { globs: globs, bases: bases };
98
+ }
99
+ catch {
100
+ return { globs: [], bases: [] };
101
+ }
102
+ }
103
+ /**
104
+ * `true`, ha a `dir` (root-relatív út) NEM prune-olható egy re-include miatt: (1) maga a dir re-included (vagy
105
+ * egy re-included alfa alatt van) — ezt **glob-match**-csel ellenőrizzük a re-include-`globs` ellen (így a
106
+ * `(glob)/client` minta a `client` dir-re, a `(glob)/client/(glob)` a `client/src` dir-re is illeszkedik); VAGY
107
+ * (2) a dir egy **anchored** (glob-mentes) re-include base ŐSE (van alatta re-included elem) — ezt string-prefixszel.
108
+ * (A bare-bázisok ős-esetét az (1) fedi.) Ez teszi a negációt traversal-helyessé (a szülőt nem vágjuk le idő előtt).
109
+ */
110
+ static dirOnReincludeLineage(dir, reincludeGlobs, reincludeBases) {
111
+ const d = dir.replace(/\\/g, '/').replace(/\/+$/, '');
112
+ if (fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(d, reincludeGlobs)) {
113
+ return true;
114
+ }
115
+ return reincludeBases.some((rawBase) => {
116
+ const base = rawBase.replace(/\/+$/, '');
117
+ return !base.includes('*') && base.startsWith(`${d}/`);
118
+ });
119
+ }
65
120
  /**
66
121
  * A `.fdpfamignore` beolvasása + fordítása egy könyvtárból (a `dirAbsolutePath`-ban). Hiányzó / olvashatatlan
67
122
  * fájl → `[]` (opt-in: csak ha létezik). A `baseDir` a root-relatív horgony (a gyökér-szintű hívásnál `''`).
123
+ * (Csak az IGNORE minták — a re-include-ot a `loadReincludeRules` adja.)
68
124
  */
69
125
  static loadFromDir(dirAbsolutePath, baseDir = '') {
70
126
  try {