@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.
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_GitRepo_Util = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ /**
8
+ * `FAM_GitRepo_Util` (user-FR 2026-06-21) — egy scannelt fájlhoz felderíti a **git-repo provenance-t**:
9
+ * a repo-gyökeret (`.git` felfelé keresve), a **GitHub/remote URL-t** (`.git/config` `remote.origin.url`,
10
+ * normalizálva), a **branch-et** (`.git/HEAD`) és a **repo-gyökérhez relatív utat**. **Függőség-mentes**
11
+ * (NEM `git`-parancs — közvetlenül a `.git/` fájlokból olvas; gyors + offline). A scan **repoRoot-onként cache-eli**
12
+ * (egy mappa-scan ~1 repo → egyszer olvas), így a per-fájl költség elhanyagolható.
13
+ *
14
+ * Miért így (a user „relative path + GitHub repo" döntése): a repo-URL + a **REPO-relatív** út KANONIKUS,
15
+ * gép-független, kattintható citáció (`<repoUrl>/blob/<branch>/<repoRelativePath>`) — szemben a gép-specifikus
16
+ * abszolút úttal vagy a törékeny „projektnévből kiszámolt" relatív úttal.
17
+ */
18
+ class FAM_GitRepo_Util {
19
+ /** Meddig megyünk felfelé `.git`-et keresve (biztonsági korlát a végtelen ciklus ellen). */
20
+ static MAX_WALK_UP = 40;
21
+ /** Belső repoRoot→info cache (a scan-során a `.git` nem változik; egy repo → egyszer olvas). */
22
+ static _cache = new Map();
23
+ /**
24
+ * Egy scannelt fájl **scan-source git-mezői** (a `FAM_Source`-ba spread-elve): `repoUrl` / `repoName` /
25
+ * `repoRelativePath` (a repo-gyökérhez) / `repoBranch`. Cache-elt (belső statikus Map) + fail-safe (üres
26
+ * objektum, ha a fájl NINCS git-repóban / hiba) — a scan SOHA nem bukik el a git-felderítésen.
27
+ */
28
+ static sourceFieldsFor(absFilePath) {
29
+ try {
30
+ const info = FAM_GitRepo_Util.resolve(absFilePath, FAM_GitRepo_Util._cache);
31
+ if (!info) {
32
+ return {};
33
+ }
34
+ return {
35
+ repoUrl: info.repoUrl,
36
+ repoName: info.repoName,
37
+ repoRelativePath: FAM_GitRepo_Util.relativePath(absFilePath, info.repoRoot),
38
+ repoBranch: info.repoBranch,
39
+ };
40
+ }
41
+ catch {
42
+ return {};
43
+ }
44
+ }
45
+ /**
46
+ * A teljes repo-info felderítése egy fájl abszolút útjából, **cache-elve a repoRoot-on** (a hívó adja a
47
+ * Map-et; egy scan végig egyet használ). `null`, ha a fájl NINCS git-repóban. A relatív utat a hívó a
48
+ * `relativePath`-szal számolja (a repoRoot ismeretében).
49
+ */
50
+ static resolve(absFilePath, cache) {
51
+ const repoRoot = FAM_GitRepo_Util.findRepoRoot(absFilePath);
52
+ if (!repoRoot) {
53
+ return null;
54
+ }
55
+ const cached = cache.get(repoRoot);
56
+ if (cached !== undefined) {
57
+ return cached;
58
+ }
59
+ const remote = FAM_GitRepo_Util.readRemoteUrl(repoRoot);
60
+ const info = {
61
+ repoRoot: repoRoot,
62
+ repoUrl: remote?.url,
63
+ repoName: remote?.name,
64
+ repoBranch: FAM_GitRepo_Util.readBranch(repoRoot),
65
+ };
66
+ cache.set(repoRoot, info);
67
+ return info;
68
+ }
69
+ /** A repo-gyökér keresése felfelé (`.git` mappa VAGY fájl — a submodule `.git` fájl). `null`, ha nincs. */
70
+ static findRepoRoot(absStartPath) {
71
+ let dir = absStartPath;
72
+ try {
73
+ // Ha a start egy fájl, a könyvtárából indulunk; ha mappa, magából.
74
+ if (fs.existsSync(absStartPath) && fs.statSync(absStartPath).isFile()) {
75
+ dir = path.dirname(absStartPath);
76
+ }
77
+ }
78
+ catch {
79
+ dir = path.dirname(absStartPath);
80
+ }
81
+ for (let i = 0; i < FAM_GitRepo_Util.MAX_WALK_UP; i++) {
82
+ if (fs.existsSync(path.join(dir, '.git'))) {
83
+ return dir;
84
+ }
85
+ const parent = path.dirname(dir);
86
+ if (parent === dir) {
87
+ return null; // elértük a filesystem-gyökeret
88
+ }
89
+ dir = parent;
90
+ }
91
+ return null;
92
+ }
93
+ /**
94
+ * A `remote.origin.url` kiolvasása a `.git/config`-ból + normalizálva (`https://github.com/owner/repo`).
95
+ * `undefined`, ha nincs origin / nem olvasható. (A submodule `.git` FÁJL `gitdir:`-re mutat — ezt is követjük.)
96
+ */
97
+ static readRemoteUrl(repoRoot) {
98
+ try {
99
+ const gitPath = path.join(repoRoot, '.git');
100
+ const configPath = fs.statSync(gitPath).isDirectory()
101
+ ? path.join(gitPath, 'config')
102
+ : path.join(FAM_GitRepo_Util.resolveGitdir(repoRoot, gitPath), 'config');
103
+ if (!fs.existsSync(configPath)) {
104
+ return undefined;
105
+ }
106
+ const raw = fs.readFileSync(configPath, 'utf-8');
107
+ const rawUrl = FAM_GitRepo_Util.extractOriginUrl(raw);
108
+ return rawUrl ? FAM_GitRepo_Util.normalizeRepoUrl(rawUrl) : undefined;
109
+ }
110
+ catch {
111
+ return undefined;
112
+ }
113
+ }
114
+ /** A `.git/HEAD`-ből az aktuális branch (`ref: refs/heads/<branch>`); detached SHA / hiba → `undefined`. */
115
+ static readBranch(repoRoot) {
116
+ try {
117
+ const gitPath = path.join(repoRoot, '.git');
118
+ const headPath = fs.statSync(gitPath).isDirectory()
119
+ ? path.join(gitPath, 'HEAD')
120
+ : path.join(FAM_GitRepo_Util.resolveGitdir(repoRoot, gitPath), 'HEAD');
121
+ const head = fs.readFileSync(headPath, 'utf-8').trim();
122
+ const match = head.match(/^ref:\s*refs\/heads\/(.+)$/);
123
+ return match ? match[1].trim() : undefined;
124
+ }
125
+ catch {
126
+ return undefined;
127
+ }
128
+ }
129
+ /** A fájl REPO-gyökérhez relatív útja (POSIX-szeparátorral, a kattintható citációhoz). */
130
+ static relativePath(absFilePath, repoRoot) {
131
+ return path.relative(repoRoot, absFilePath).replace(/\\/g, '/');
132
+ }
133
+ /**
134
+ * A nyers remote-URL normalizálása kattintható HTTPS-formára + `owner/repo` névre. Lefedi:
135
+ * `git@host:owner/repo.git`, `https://host/owner/repo.git`, `ssh://git@host/owner/repo.git`. A `.git`-suffix
136
+ * + a beágyazott credential levágva. Nem-github host esetén a host marad (gitlab/bitbucket is kattintható).
137
+ */
138
+ static normalizeRepoUrl(rawUrl) {
139
+ let url = rawUrl.trim();
140
+ // scp-szerű: git@host:owner/repo(.git) → https://host/owner/repo
141
+ const scp = url.match(/^[\w.-]+@([\w.-]+):(.+)$/);
142
+ if (scp) {
143
+ url = `https://${scp[1]}/${scp[2]}`;
144
+ }
145
+ url = url
146
+ .replace(/^ssh:\/\/(?:[\w.-]+@)?/, 'https://')
147
+ .replace(/^git:\/\//, 'https://')
148
+ .replace(/^https?:\/\/[^@/]+@/, 'https://') // beágyazott credential levágása
149
+ .replace(/\.git$/, '')
150
+ .replace(/\/+$/, '');
151
+ const nameMatch = url.match(/^https?:\/\/[\w.-]+\/(.+?\/[^/]+)$/);
152
+ return { url: url, name: nameMatch ? nameMatch[1] : undefined };
153
+ }
154
+ /** A `[remote "origin"]` szekció `url = …` sora a `.git/config`-ból (defenzív, szekció-tudatos). */
155
+ static extractOriginUrl(configText) {
156
+ let inOrigin = false;
157
+ for (const rawLine of configText.split(/\r?\n/)) {
158
+ const line = rawLine.trim();
159
+ const section = line.match(/^\[remote\s+"([^"]+)"\]$/);
160
+ if (section) {
161
+ inOrigin = section[1] === 'origin';
162
+ continue;
163
+ }
164
+ if (line.startsWith('[')) {
165
+ inOrigin = false;
166
+ continue;
167
+ }
168
+ if (inOrigin) {
169
+ const url = line.match(/^url\s*=\s*(.+)$/);
170
+ if (url) {
171
+ return url[1].trim();
172
+ }
173
+ }
174
+ }
175
+ return undefined;
176
+ }
177
+ /** A submodule `.git` FÁJL (`gitdir: <relpath>`) feloldása a tényleges git-könyvtárra. */
178
+ static resolveGitdir(repoRoot, gitFilePath) {
179
+ try {
180
+ const content = fs.readFileSync(gitFilePath, 'utf-8').trim();
181
+ const match = content.match(/^gitdir:\s*(.+)$/);
182
+ if (match) {
183
+ const target = match[1].trim();
184
+ return path.isAbsolute(target) ? target : path.resolve(repoRoot, target);
185
+ }
186
+ }
187
+ catch {
188
+ /* fall through */
189
+ }
190
+ return gitFilePath;
191
+ }
192
+ }
193
+ exports.FAM_GitRepo_Util = FAM_GitRepo_Util;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_ScanReconcile_Util = void 0;
4
+ /**
5
+ * `FAM_ScanReconcile_Util` (user-FR 2026-06-21) — a re-scan **whole-file orphan reconciliation** present-set
6
+ * számítása. A „present" halmaz az a fájl-útvonal-készlet, amit a friss scan a lemezen MEGTALÁLT (és ezért a
7
+ * táron meglévő, e halmazon KÍVÜLI `scan`-entry-k orphanok → soft-delete). A finomság a **most ignore-mintával
8
+ * kizárt** fájlok kezelése (`scan.reconcileIgnored`):
9
+ *
10
+ * - **`reconcileIgnored = true` (default — TISZTOGATÁS):** az ignore-minta által skippelt fájl NEM számít
11
+ * present-nek → a korábban beolvasott, de most ignore-olttá vált fájl régi chunkjai orphanként kitakaríthatók
12
+ * (ne ragadjon be, amit ignore-olni akarunk). A többi skip-ok (secret / méret / unrouted / include-filter) MARAD
13
+ * present (a fájl a lemezen van, csak most nem ingesteljük — NEM töröljük a régi tudást).
14
+ * - **`reconcileIgnored = false` (régi viselkedés):** MINDEN skippelt fájl present → a régi chunkok megmaradnak.
15
+ *
16
+ * A lemezről ELTŰNT fájlok mindkét módban orphanok (eleve nincsenek a routed/skipped halmazban) — ez a beállítástól
17
+ * független. Tisztán pure (Set-számítás), külön tesztelhető; a `reconcileDeletedFiles` ezt hívja.
18
+ */
19
+ class FAM_ScanReconcile_Util {
20
+ /**
21
+ * A friss scan present-path halmaza (abszolút utak). A `routedFiles` mind present; a `skippedFiles` present,
22
+ * KIVÉVE az ignore-minta-kizártakat (`excludedByIgnore`), ha `reconcileIgnored` — azok orphanná válnak, hogy a
23
+ * régi chunkjaik kitakaríthatók legyenek. Üres input → üres halmaz.
24
+ */
25
+ static computePresentPaths(routedFiles, skippedFiles, reconcileIgnored) {
26
+ const present = new Set();
27
+ for (const routed of routedFiles) {
28
+ present.add(routed.absolutePath);
29
+ }
30
+ for (const skipped of skippedFiles) {
31
+ // A most ignore-mintával kizárt fájl NEM present (ha tisztogatunk) → a régi bejegyzése orphan → törölhető.
32
+ if (reconcileIgnored && skipped.excludedByIgnore) {
33
+ continue;
34
+ }
35
+ present.add(skipped.absolutePath);
36
+ }
37
+ return present;
38
+ }
39
+ }
40
+ exports.FAM_ScanReconcile_Util = FAM_ScanReconcile_Util;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_SecretContent_Util = void 0;
4
+ /**
5
+ * `FAM_SecretContent_Util` (user-FR 2026-06-21) — **tartalmi** (fájlon-BELÜLi) secret-redakció. A `FAM_SecretExclude_Util`
6
+ * csak fájlNÉV-alapú (a `.env`/`*.key` típusú titok-FÁJLOKAT zárja ki); de egy `.ts`/`.py` FORRÁSBA hardcode-olt
7
+ * API-kulcs (pl. `apiKey: 'sk-…'`) eddig beolvasódott a vektor-tárba (élő lelet 2026-06-21: 1 OpenAI-kulcs 4 config-
8
+ * fájlban). Ez a util a fájl-tartalmat OLVASÁSKOR redaktálja (a chunk/contentHash/embedding/tárolt szöveg MIND a
9
+ * redaktált változatot kapja) → a titok soha nem kerül a DB-be, és egy korábban-szivárgott fájl RE-SCANKOR
10
+ * auto-gyógyul (a contentHash változik a redakciótól → `modified` → a tárolt content felülíródik).
11
+ *
12
+ * **Magas-konfidenciájú, jellegzetes alakú token-minták** (alacsony false-positive): OpenAI `sk-…`, AWS `AKIA…`,
13
+ * GitHub `ghp_/gho_…`, Google `AIza…`, Slack `xox.-…`, privát-kulcs PEM-blokk, JWT. NEM próbál generikus
14
+ * `password=…`-t fogni (az túl sok forrás-konstanst redaktálna) — a cél a VALÓDI kulcs-szivárgás, nem a kód-RAG rontása.
15
+ */
16
+ class FAM_SecretContent_Util {
17
+ /** A redaktált tokenek helyére kerülő jelölő (a kód-kontextus megmarad, csak a titok tűnik el). */
18
+ static PLACEHOLDER = '***REDACTED-SECRET***';
19
+ /** Magas-konfidenciájú secret-token minták (jellegzetes prefix + hossz → kevés false-positive). */
20
+ static PATTERNS = [
21
+ { name: 'openai', re: /sk-(?:proj-)?[A-Za-z0-9_-]{32,}/g },
22
+ { name: 'aws-access-key', re: /\bAKIA[0-9A-Z]{16}\b/g },
23
+ { name: 'github-token', re: /\bgh[posur]_[A-Za-z0-9]{36,}\b/g },
24
+ { name: 'google-api', re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
25
+ { name: 'slack-token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
26
+ { name: 'private-key-block', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g },
27
+ { name: 'jwt', re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}\b/g },
28
+ ];
29
+ /**
30
+ * A `text` magas-konfidenciájú secret-tokenjeit a `PLACEHOLDER`-re cseréli. Visszaadja a redaktált szöveget +
31
+ * a redakciók számát (a per-fájl audit-loghoz). Ha nincs találat → az eredeti szöveg + `count: 0` (no-op).
32
+ */
33
+ static redact(text) {
34
+ let count = 0;
35
+ let result = text;
36
+ for (const pattern of FAM_SecretContent_Util.PATTERNS) {
37
+ result = result.replace(pattern.re, () => {
38
+ count++;
39
+ return FAM_SecretContent_Util.PLACEHOLDER;
40
+ });
41
+ }
42
+ return { text: result, count: count };
43
+ }
44
+ /** `true`, ha a `text` legalább egy magas-konfidenciájú secret-tokent tartalmaz (a `redact`-re épül, állapotmentes). */
45
+ static hasSecret(text) {
46
+ return FAM_SecretContent_Util.redact(text).count > 0;
47
+ }
48
+ }
49
+ exports.FAM_SecretContent_Util = FAM_SecretContent_Util;
@@ -45,6 +45,14 @@ class FAM_SecretExclude_Util {
45
45
  /secret/i, // *secret* (csak nem-forrás kiterjesztésen)
46
46
  /credential/i, // *credential* (csak nem-forrás kiterjesztésen)
47
47
  ];
48
+ /**
49
+ * EXPLICIT key-store fájlnevek (user-FR 2026-06-21) — kiterjesztéstől függetlenül kizárnak, mert a fájl MAGA egy
50
+ * kulcs-tár (élő lelet: egy OpenAI-kulcs `gpt-keys.const.ts`-ben). TIGHT lista (api/gpt/secret/access + `key(s)`)
51
+ * → alacsony false-positive (egy general `environment.ts`-t NEM zár ki — abban a tartalmi redaktor fogja a kulcsot).
52
+ */
53
+ static SECRET_KEYSTORE_NAME_PATTERNS = [
54
+ /(^|[-_.])(api|gpt|secret|access|private|auth)[-_]?keys?([-_.]|$)/i, // gpt-keys.const.ts, api-keys.ts, apikeys.js
55
+ ];
48
56
  /** Ismert FORRÁS-/DOC-kiterjesztések — ezeken a SOFT névrész-minta NEM zár ki (legit kód/doc). */
49
57
  static SOURCE_DOC_EXTENSION = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|md|markdown)$/i;
50
58
  /** A secret-mappák (az útvonal BÁRMELY szegmense — pl. `secrets/`, `.ssh/`). */
@@ -67,6 +75,10 @@ class FAM_SecretExclude_Util {
67
75
  if (FAM_SecretExclude_Util.HARD_SECRET_PATTERNS.some((pattern) => pattern.test(fileName))) {
68
76
  return true;
69
77
  }
78
+ // (2b) EXPLICIT key-store fájlnév (api-/gpt-/secret-keys…) — kiterjesztéstől függetlenül (a fájl maga kulcs-tár).
79
+ if (FAM_SecretExclude_Util.SECRET_KEYSTORE_NAME_PATTERNS.some((pattern) => pattern.test(fileName))) {
80
+ return true;
81
+ }
70
82
  // (3) SOFT névrész-minta — CSAK ha NEM ismert forrás-/doc-kiterjesztés (a "secret"-nevű .ts legit kód).
71
83
  if (FAM_SecretExclude_Util.SOURCE_DOC_EXTENSION.test(fileName)) {
72
84
  return false;
@@ -14,7 +14,11 @@ const fam_entry_data_model_1 = require("../../../_models/data-models/fam-entry.d
14
14
  const fam_reference_code_util_1 = require("../../../_collections/fam-reference-code.util");
15
15
  const embedding_1 = require("../../embedding");
16
16
  const scope_reference_1 = require("../../scope-reference");
17
+ const fam_cooldown_util_1 = require("../../../_collections/fam-cooldown.util");
17
18
  const fam_content_hash_util_1 = require("../_collections/fam-content-hash.util");
19
+ const fam_git_repo_util_1 = require("../_collections/fam-git-repo.util");
20
+ const fam_scan_reconcile_util_1 = require("../_collections/fam-scan-reconcile.util");
21
+ const fam_secret_content_util_1 = require("../_collections/fam-secret-content.util");
18
22
  const fam_project_identity_util_1 = require("../_collections/fam-project-identity.util");
19
23
  const fam_scan_path_util_1 = require("../_collections/fam-scan-path.util");
20
24
  const fam_scan_progress_util_1 = require("../_collections/fam-scan-progress.util");
@@ -185,6 +189,11 @@ class FAM_Ingest_ControlService {
185
189
  // Futás-közbeni progress (descriptive user feedback): a NAGY (workspace-szintű) scan haladását
186
190
  // throttle-olt `[famIngest]` sorokkal jelezzük a konzolon (a `dryRun` NEM embeddel → ott nincs progress).
187
191
  const progressIntervalMs = await this.resolveScanNumber('scan.progressIntervalMs', canonicalScope, 2000);
192
+ // Kikapcsolható szekció-cooldown (user-FR): a NAGY scan ne terhelje túl a gépet — a batch-ek KÖZÉ
193
+ // opcionális szünet (default 0 = ki; dryRun-on sosem várunk, mert ott nincs valódi embed-munka).
194
+ const sectionCooldownMs = isDryRun
195
+ ? 0
196
+ : fam_cooldown_util_1.FAM_Cooldown_Util.resolveMs((await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.sectionCooldownMs', { scopePath: canonicalScope })).value);
188
197
  let lastProgressAt = startedAt;
189
198
  for (let i = 0; i < total; i += flowLimit) {
190
199
  const batch = discovery.routedFiles.slice(i, i + flowLimit);
@@ -211,13 +220,19 @@ class FAM_Ingest_ControlService {
211
220
  verdicts: verdicts, elapsedSeconds: (now - startedAt) / 1000,
212
221
  }));
213
222
  }
223
+ // Szekció-cooldown: a batch-ek KÖZÉ (NEM az utolsó után) — opcionális gép-tehermentesítő szünet.
224
+ if (sectionCooldownMs > 0 && processed < total) {
225
+ await fam_cooldown_util_1.FAM_Cooldown_Util.sleep(sectionCooldownMs);
226
+ }
214
227
  }
215
228
  // (4b) Törölt-fájl reconciliation (whole-file orphan) — CSAK folder/project scan (a `scan-file`
216
229
  // egyetlen fájlt céloz, nem reconciliál). A lemezről eltűnt fájlok chunkjai soft-delete-elődnek; a
217
230
  // számuk a `verdicts.deleted`-be (a per-fájl, fájlon-belüli `deleted`-ek mellé).
218
231
  if (request.operation !== 'scan-file') {
232
+ const reconcileIgnored = await this.resolveReconcileIgnored(canonicalScope);
219
233
  verdicts.deleted += await this.reconcileDeletedFiles({
220
234
  discovery: discovery, scopePath: canonicalScope, issuer: issuer, dryRun: request.dryRun ?? false,
235
+ reconcileIgnored: reconcileIgnored,
221
236
  });
222
237
  }
223
238
  // (4c) File-rendszer összefoglaló (knowledge) — generált „mi van a projektben" entry a mappastruktúrából
@@ -271,7 +286,15 @@ class FAM_Ingest_ControlService {
271
286
  async processFile(set) {
272
287
  const table = set.file.route;
273
288
  try {
274
- const content = fs.readFileSync(set.file.absolutePath, 'utf-8');
289
+ const rawContent = fs.readFileSync(set.file.absolutePath, 'utf-8');
290
+ // Tartalmi secret-redakció (user-FR): a forrásba hardcode-olt API-kulcs (sk-/AKIA/ghp_/PEM/JWT) a tárolt
291
+ // szöveg/embedding ELŐTT redaktálódik → a titok sose kerül a DB-be (a contentHash is a redaktáltból → egy
292
+ // korábban-szivárgott fájl RE-SCANKOR `modified` → auto-gyógyul). A redaktálatlan kód-kontextus megmarad.
293
+ const redaction = fam_secret_content_util_1.FAM_SecretContent_Util.redact(rawContent);
294
+ if (redaction.count > 0) {
295
+ fsm_dynamo_1.DyFM_Log.warn(`[famIngest] secret-redakció: ${redaction.count} token redaktálva (${set.file.relativePath})`);
296
+ }
297
+ const content = redaction.text;
275
298
  // FAM-REV-064 bináris-tartalom guard: a denylist-en átcsúszott, ismeretlen-kiterjesztésű bináris
276
299
  // (null-byte az első ~8KB-ban) NE kerüljön a generic chunkerbe (szemét-chunk ellen). Skip → 0 chunk.
277
300
  if (FAM_Ingest_ControlService.isLikelyBinary(content)) {
@@ -408,6 +431,8 @@ class FAM_Ingest_ControlService {
408
431
  headingPath: set.chunk.headingPath,
409
432
  source: {
410
433
  type: 'scan', path: set.file.relativePath, root: set.root, absolutePath: set.file.absolutePath,
434
+ // Git-repo provenance (user-FR): repoUrl + repoRelativePath + repoBranch → kattintható citáció.
435
+ ...fam_git_repo_util_1.FAM_GitRepo_Util.sourceFieldsFor(set.file.absolutePath),
411
436
  },
412
437
  addedBy: 'scan',
413
438
  ingestRunId: set.runId,
@@ -591,15 +616,15 @@ class FAM_Ingest_ControlService {
591
616
  * így egy al-mappa scan a tágabb gyökérről ingestelt, de a saját subtree-jébe eső fájlokat is helyesen
592
617
  * reconciliálja (és kívülre sosem nyúl). Az `absolutePath` nélküli (régi, migrálatlan) entry-ket kihagyja
593
618
  * (no-migration safe). `dryRun` → csak számol. Visszaadja a reconciliált (soft-deleted) chunkok számát.
619
+ *
620
+ * **`reconcileIgnored` (user-FR 2026-06-21, default true):** a most ignore-mintával (hard/config/`.fdpfamignore`)
621
+ * kizárttá vált, de KORÁBBAN beolvasott fájlok is orphanná válnak → a régi chunkjaik kitakarítódnak (ne ragadjon
622
+ * be, amit ignore-olni akarunk). A present-set számítása a `FAM_ScanReconcile_Util.computePresentPaths`-ban (pure).
594
623
  */
595
624
  async reconcileDeletedFiles(set) {
596
- const presentPaths = new Set();
597
- for (const routed of set.discovery.routedFiles) {
598
- presentPaths.add(routed.absolutePath);
599
- }
600
- for (const skipped of set.discovery.skippedFiles) {
601
- presentPaths.add(skipped.absolutePath);
602
- }
625
+ // A present-set (lemezen megtalált fájlok). `reconcileIgnored` → a MOST ignore-mintával kizárt fájl NEM present
626
+ // (a régi chunkja orphan → kitakarítható); a többi skip (secret/méret/unrouted) present marad (FAM_ScanReconcile_Util).
627
+ const presentPaths = fam_scan_reconcile_util_1.FAM_ScanReconcile_Util.computePresentPaths(set.discovery.routedFiles, set.discovery.skippedFiles, set.reconcileIgnored);
603
628
  const leafScopeId = set.scopePath.length ? set.scopePath[set.scopePath.length - 1].scopeId : undefined;
604
629
  let reconciled = 0;
605
630
  for (const table of set.discovery.tables) {
@@ -648,6 +673,11 @@ class FAM_Ingest_ControlService {
648
673
  const value = typeof resolved.value === 'number' ? resolved.value : 20;
649
674
  return Math.min(Math.max(value, 1), 200);
650
675
  }
676
+ /** Az ignore-add reconciliation (most-kizárt fájlok kitakarítása) a config-ból (`scan.reconcileIgnored`, default true). */
677
+ async resolveReconcileIgnored(scopePath) {
678
+ const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.reconcileIgnored', { scopePath: scopePath });
679
+ return typeof resolved.value === 'boolean' ? resolved.value : true;
680
+ }
651
681
  /** Egy scan-szám-config feloldása a scope-kontextussal + fallback-kel (a `scan.estimateMsPerChunk` / `scan.progressIntervalMs`-hez). */
652
682
  async resolveScanNumber(key, scopePath, fallback) {
653
683
  const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve(key, { scopePath: scopePath });
@@ -51,16 +51,35 @@ class FAM_Scan_ControlService {
51
51
  if (famIgnore.length) {
52
52
  fsm_dynamo_1.DyFM_Log.info(`[famIngest] .fdpfamignore betöltve (${resolvedRoot}): ${famIgnore.length / 2} minta-sor`);
53
53
  }
54
- const ignorePatterns = [...baseIgnore, ...famIgnore];
54
+ // KÉT külön ignore-réteg: a `hardIgnore` (config-default `node_modules`/`.git`/`build`/`dist`/… + config +
55
+ // input-exclude) — ezt a `!`-re-include SOHA nem éleszti fel (különben a `!client/` a `client/node_modules`-t
56
+ // is visszahozná → katasztrofális walk). A `softIgnore` (a .fdpfamignore `*`/saját mintái) — EZT írhatja felül
57
+ // a negáció. Így a „minden-kivéve-X" a hard-defaultokat tiszteletben tartja (eleve-tisztán-szkennelés).
58
+ const hardIgnore = baseIgnore;
59
+ const softIgnore = famIgnore;
60
+ // Re-include (`!`) szabályok a .fdpfamignore-ból (negáció — user-FR): a fájl-szintű re-include-match (`globs`)
61
+ // + a könyvtár-prune lineage-bázisok (`bases`). Üres a scan-file-nál / ha nincs negáció.
62
+ const reinclude = set.operation === 'scan-file'
63
+ ? { globs: [], bases: [] }
64
+ : fam_famignore_util_1.FAM_FamIgnore_Util.loadReincludeRules(resolvedRoot);
55
65
  const maxFileSizeBytes = await this.resolveMaxFileSize(set.scopePath);
56
66
  const followSymlinks = await this.resolveFollowSymlinks(set.scopePath);
57
67
  const testFileWeight = await this.resolveTestFileWeight(set.scopePath);
68
+ // NESTED `.fdpfamignore` (config-kapcsolós, default ON): a bejárás közben minden al-mappa saját famignore-ja
69
+ // is érvényesül (a saját al-útjához horgonyozva). A `collected` a walk-közben FELFEDEZETT nested-mintákat
70
+ // gyűjti, hogy a MÁSODIK (classify) kör is lássa őket (a minták al-út-horgonyzottak → globálisan egyértelműek).
71
+ const nestedEnabled = set.operation === 'scan-file' ? false : await this.resolveNestedFamignore(set.scopePath);
72
+ const collected = { soft: [], reGlobs: [], reBases: [] };
58
73
  const routedFiles = [];
59
74
  const skippedFiles = [];
60
75
  // A felderített abszolút fájlok (file = 1 db; folder/project = rekurzív bejárás).
61
76
  const absoluteFiles = set.operation === 'scan-file'
62
77
  ? [resolvedRoot]
63
- : this.walkDirectory(resolvedRoot, resolvedRoot, ignorePatterns, followSymlinks);
78
+ : this.walkDirectory(resolvedRoot, resolvedRoot, hardIgnore, softIgnore, reinclude, followSymlinks, { enabled: nestedEnabled, collected: collected });
79
+ // A classify-kör effektív soft-ignore + re-include halmaza: a gyökér-szintű + MINDEN walk-közben felfedezett
80
+ // nested minta (a nested fájl-szintű skip-ekhez — a dir-prune-t a walk már elvégezte). Üres collector → no-op.
81
+ const classifySoft = collected.soft.length ? [...softIgnore, ...collected.soft] : softIgnore;
82
+ const classifyReGlobs = collected.reGlobs.length ? [...reinclude.globs, ...collected.reGlobs] : reinclude.globs;
64
83
  for (const absolutePath of absoluteFiles) {
65
84
  const relativePath = set.operation === 'scan-file'
66
85
  ? path.basename(resolvedRoot)
@@ -68,7 +87,9 @@ class FAM_Scan_ControlService {
68
87
  const routed = this.classifyFile({
69
88
  absolutePath: absolutePath,
70
89
  relativePath: relativePath,
71
- ignorePatterns: ignorePatterns,
90
+ hardIgnorePatterns: hardIgnore,
91
+ softIgnorePatterns: classifySoft,
92
+ reincludePatterns: classifyReGlobs,
72
93
  includePatterns: set.include,
73
94
  maxFileSizeBytes: maxFileSizeBytes,
74
95
  tableOverride: set.tableOverride,
@@ -103,9 +124,16 @@ class FAM_Scan_ControlService {
103
124
  if (fam_secret_exclude_util_1.FAM_SecretExclude_Util.isSecret(set.relativePath)) {
104
125
  return { ...base, route: 'skip', skipReason: 'secret-exclude (filename/path-based; content-PII scan = BACKLOG)' };
105
126
  }
106
- // (2) Ignore-patterns (dsgn-004 §6.2) — node_modules/.git/build/dist/lock/... + a config + input-exclude.
107
- if (fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.ignorePatterns)) {
108
- return { ...base, route: 'skip', skipReason: 'ignore-pattern' };
127
+ // (2a) HARD ignore (dsgn-004 §6.2) — node_modules/.git/build/dist/lock/… + a config + input-exclude.
128
+ // Ezt a `!`-re-include SOHA nem éleszti fel (a `!client/` ne hozza vissza a `client/node_modules`-t).
129
+ if (fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.hardIgnorePatterns)) {
130
+ return { ...base, route: 'skip', skipReason: 'ignore-pattern', excludedByIgnore: true };
131
+ }
132
+ // (2b) SOFT ignore (.fdpfamignore saját mintái) — EZT a `!`-RE-INCLUDE felülírja: soft-ignore-match SKIP,
133
+ // KIVÉVE ha re-include-match (negáció). Így a „minden-kivéve-X" csak a famignore-szintet vonja vissza.
134
+ if (set.softIgnorePatterns && fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.softIgnorePatterns)
135
+ && !(set.reincludePatterns && fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.reincludePatterns))) {
136
+ return { ...base, route: 'skip', skipReason: 'ignore-pattern (famignore)', excludedByIgnore: true };
109
137
  }
110
138
  // (3) Include-szűrés (ha megadva: csak az illeszkedő fájl megy tovább, dsgn-003 §3.1).
111
139
  if (set.includePatterns && set.includePatterns.length && !fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(set.relativePath, set.includePatterns)) {
@@ -133,8 +161,13 @@ class FAM_Scan_ControlService {
133
161
  * KÖNYVTÁRAKAT bejárás KÖZBEN kihagyja (ne lépjen `node_modules`-ba — teljesítmény + biztonság).
134
162
  * A symlinkeket alapból NEM követi (`followSymlinks` config); a path-traversal guardot minden
135
163
  * fájlra futtatja (a feloldott valódi útvonal a gyökér alatt kell maradjon).
164
+ *
165
+ * **NESTED `.fdpfamignore` (`nested.enabled`):** minden NEM-gyökér mappánál betölti a mappa saját famignore-ját
166
+ * (a mappa al-útjához horgonyozva), MERGE-eli az örökölt soft-ignore/re-include-ba (a leszármazottak öröklik), és
167
+ * a `nested.collected`-be is gyűjti (a discover második, classify-köre fájl-szinten is alkalmazza). A gyökér
168
+ * famignore-ját a hívó (`discover`) tölti — itt csak az al-mappákét.
136
169
  */
137
- walkDirectory(currentDir, resolvedRoot, ignorePatterns, followSymlinks) {
170
+ walkDirectory(currentDir, resolvedRoot, hardIgnore, softIgnore, reinclude, followSymlinks, nested) {
138
171
  const result = [];
139
172
  let entries;
140
173
  try {
@@ -144,6 +177,24 @@ class FAM_Scan_ControlService {
144
177
  // Olvashatatlan könyvtár → kihagyjuk (a per-fájl olvasás-hiba a chunk-fázisban kezelt).
145
178
  return result;
146
179
  }
180
+ // NESTED famignore: a NEM-gyökér mappa saját `.fdpfamignore`-ja az al-útjához horgonyozva → MERGE az örököltbe
181
+ // (a leszármazottak ezt öröklik) + gyűjtés a classify-körhöz. A gyökér famignore-ját már a `discover` betöltötte.
182
+ let effSoftIgnore = softIgnore;
183
+ let effReinclude = reinclude;
184
+ if (nested.enabled && currentDir !== resolvedRoot) {
185
+ const baseDir = fam_scan_path_util_1.FAM_ScanPath_Util.toRelative(resolvedRoot, currentDir);
186
+ const nestedIgnore = fam_famignore_util_1.FAM_FamIgnore_Util.loadFromDir(currentDir, baseDir);
187
+ const nestedReinc = fam_famignore_util_1.FAM_FamIgnore_Util.loadReincludeRules(currentDir, baseDir);
188
+ if (nestedIgnore.length) {
189
+ effSoftIgnore = [...softIgnore, ...nestedIgnore];
190
+ nested.collected.soft.push(...nestedIgnore);
191
+ }
192
+ if (nestedReinc.globs.length) {
193
+ effReinclude = { globs: [...reinclude.globs, ...nestedReinc.globs], bases: [...reinclude.bases, ...nestedReinc.bases] };
194
+ nested.collected.reGlobs.push(...nestedReinc.globs);
195
+ nested.collected.reBases.push(...nestedReinc.bases);
196
+ }
197
+ }
147
198
  for (const entry of entries) {
148
199
  const entryPath = path.join(currentDir, entry.name);
149
200
  const relativePath = fam_scan_path_util_1.FAM_ScanPath_Util.toRelative(resolvedRoot, entryPath);
@@ -152,16 +203,26 @@ class FAM_Scan_ControlService {
152
203
  continue;
153
204
  }
154
205
  if (entry.isDirectory()) {
155
- // Az ignore-patterns a könyvtárat bejárás közben kihagyja (ne lépjen be).
156
- if (fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath, ignorePatterns)
157
- || fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath + '/', ignorePatterns)) {
206
+ // HARD ignore (node_modules/.git/build/…): a könyvtárat bejárás közben FELTÉTEL NÉLKÜL kihagyja
207
+ // a `!`-re-include NEM élesztheti fel (a `!client/` ne lépjen `client/node_modules`-ba). Ez tartja a
208
+ // walkot eleve-tisztán (a katasztrofális node_modules-bejárás kizárva még re-included dir alatt is).
209
+ if (fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath, hardIgnore)
210
+ || fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath + '/', hardIgnore)) {
211
+ continue;
212
+ }
213
+ // SOFT ignore (.fdpfamignore saját mintái): a mappát kihagyja — DE re-include-tudatosan: egy soft-ignore-olt
214
+ // mappát NEM vágunk le, ha re-included elem van rajta/alatta/fölötte (különben a `*` + `!client/` esetén a
215
+ // `client` szülőjét/magát levágnánk → a re-included client soha nem érhető el).
216
+ const softIgnored = fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath, effSoftIgnore)
217
+ || fam_glob_match_util_1.FAM_GlobMatch_Util.matchesAny(relativePath + '/', effSoftIgnore);
218
+ if (softIgnored && !fam_famignore_util_1.FAM_FamIgnore_Util.dirOnReincludeLineage(relativePath, effReinclude.globs, effReinclude.bases)) {
158
219
  continue;
159
220
  }
160
221
  // Path-traversal guard (a feloldott valódi útvonal a gyökér alatt kell maradjon).
161
222
  if (!this.isInsideRootSafe(resolvedRoot, entryPath)) {
162
223
  continue;
163
224
  }
164
- result.push(...this.walkDirectory(entryPath, resolvedRoot, ignorePatterns, followSymlinks));
225
+ result.push(...this.walkDirectory(entryPath, resolvedRoot, hardIgnore, effSoftIgnore, effReinclude, followSymlinks, nested));
165
226
  continue;
166
227
  }
167
228
  if (entry.isFile()) {
@@ -225,6 +286,11 @@ class FAM_Scan_ControlService {
225
286
  const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.followSymlinks', { scopePath: scopePath });
226
287
  return typeof resolved.value === 'boolean' ? resolved.value : false;
227
288
  }
289
+ /** A nested (per-alkönyvtár) `.fdpfamignore` figyelembevétele a config-ból (`scan.nestedFamignore`, default true). */
290
+ async resolveNestedFamignore(scopePath) {
291
+ const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.nestedFamignore', { scopePath: scopePath });
292
+ return typeof resolved.value === 'boolean' ? resolved.value : true;
293
+ }
228
294
  /** A teszt-/spec-fájlok default súlya a config-ból (`scan.testFileWeight`, default 0.5 — FAM-REV-058). */
229
295
  async resolveTestFileWeight(scopePath) {
230
296
  const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.testFileWeight', { scopePath: scopePath });
@@ -4,6 +4,7 @@ exports.Api_Controller = void 0;
4
4
  const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
5
  const nts_dynamo_1 = require("@futdevpro/nts-dynamo");
6
6
  const fam_version_const_1 = require("../../../_collections/fam-version.const");
7
+ const fam_heap_guard_control_service_1 = require("../../../_collections/fam-heap-guard.control-service");
7
8
  const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
8
9
  const fam_config_level_type_enum_1 = require("../../../_enums/fam-config-level.type-enum");
9
10
  const config_control_service_1 = require("../config/config.control-service");
@@ -104,6 +105,9 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
104
105
  async (req, res) => {
105
106
  await this.run(res, async () => {
106
107
  const input = (req.body ?? {});
108
+ // Heap-load-shed (OOM-hibakezelés): a NEHÉZ write-eket (scan/import/re-embed) critical
109
+ // memória-nyomásnál elutasítjuk (503), MIELŐTT egy nagy allokáció átlökné a plafont.
110
+ this.assertHeapCapacity(String(input.operation));
107
111
  return mcp_1.FAM_WriteTool_Service.getInstance().handle(input);
108
112
  });
109
113
  },
@@ -137,7 +141,13 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
137
141
  endpoint: '/health',
138
142
  tasks: [
139
143
  async (req, res) => {
140
- res.send({ ok: true, service: 'fdp-agent-memory', version: fam_version_const_1.FAM_VERSION, uptime: process.uptime() });
144
+ // A heap-nyomás IS kimegy a health-en (observability a CLI/UI/agent látja a memória-állapotot).
145
+ const memory = fam_heap_guard_control_service_1.FAM_HeapGuard_ControlService.getInstance().pressure();
146
+ res.send({
147
+ ok: true, service: 'fdp-agent-memory', version: fam_version_const_1.FAM_VERSION, uptime: process.uptime(),
148
+ memory: { level: memory.level, usedMb: memory.usedMb, limitMb: memory.limitMb,
149
+ ratio: Math.round(memory.ratio * 100) / 100, gcFraction: Math.round(memory.gcFraction * 100) / 100 },
150
+ });
141
151
  },
142
152
  ],
143
153
  });
@@ -406,11 +416,37 @@ class Api_Controller extends nts_dynamo_1.DyNTS_Controller {
406
416
  res.status(status).send({ ok: false, error: { errorCode: errorCode, message: message } });
407
417
  }
408
418
  }
409
- /** A FAM hibakód → HTTP-státusz (a user/validation-hibák 400, a többi 500; dsgn-008). */
419
+ /** A FAM hibakód → HTTP-státusz (a memória-shed 503, a user/validation-hibák 400, a többi 500; dsgn-008). */
410
420
  httpStatusForCode(errorCode) {
421
+ if (errorCode.startsWith('FAM-MEM-SHED-')) {
422
+ return 503;
423
+ }
411
424
  const userPrefixes = ['FAM-VAL-', 'FAM-REF-RESOLVE-', 'FAM-SCOPE-WRITE-', 'FAM-MCP-DISPATCH-'];
412
425
  return userPrefixes.some((prefix) => errorCode.startsWith(prefix)) ? 400 : 500;
413
426
  }
427
+ /** A NEHÉZ write-ek (scan/import/re-embed) listája — a load-shed CSAK ezeket utasítja el (a könnyűek futnak). */
428
+ static HEAVY_WRITE_OPERATIONS = ['scan-file', 'scan-folder', 'scan-project', 'import', 're-embed'];
429
+ /**
430
+ * Heap-load-shed (OOM-hibakezelés): ha a `operation` NEHÉZ ÉS a heap-guard critical nyomást jelez → 503 `DyFM_Error`
431
+ * (a könnyű create/update/delete fut tovább). Így egy újabb nagy scan/import nem löki OOM-ba a már-feszített heapet.
432
+ */
433
+ assertHeapCapacity(operation) {
434
+ if (!Api_Controller.HEAVY_WRITE_OPERATIONS.includes(operation)) {
435
+ return;
436
+ }
437
+ const guard = fam_heap_guard_control_service_1.FAM_HeapGuard_ControlService.getInstance();
438
+ if (!guard.shouldShedLoad()) {
439
+ return;
440
+ }
441
+ const pressure = guard.pressure();
442
+ throw new fsm_dynamo_1.DyFM_Error({
443
+ errorCode: 'FAM-MEM-SHED-001',
444
+ message: `Memória-nyomás (${pressure.usedMb}/${pressure.limitMb} MB, ${Math.round(pressure.ratio * 100)}%, `
445
+ + `GC ${Math.round(pressure.gcFraction * 100)}%) — a nehéz '${operation}' művelet ideiglenesen elutasítva `
446
+ + '(load-shed). Próbáld újra, amikor a nyomás enyhül (lásd /api/logs), vagy szakaszold a scant (cooldown).',
447
+ issuer: this.issuer,
448
+ });
449
+ }
414
450
  /** A `:table` path-param → `FAM_Table` (ismeretlen → hiba). */
415
451
  parseTable(raw) {
416
452
  const match = Object.values(fam_table_type_enum_1.FAM_Table).find((table) => table === raw);