@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.
- package/build/package.json +1 -1
- package/build/src/_cli/_commands/config.command.js +4 -1
- package/build/src/_cli/_commands/scan-projects.command.js +26 -1
- package/build/src/_cli/_commands/serve.command.js +28 -5
- package/build/src/_collections/config-catalog.const.js +56 -0
- package/build/src/_collections/fam-config-value-coerce.util.js +63 -0
- package/build/src/_collections/fam-cooldown.util.js +35 -0
- package/build/src/_collections/fam-heap-guard.control-service.js +176 -0
- package/build/src/_collections/title.const.js +31 -0
- package/build/src/_modules/ingest/_collections/fam-famignore.util.js +57 -1
- package/build/src/_modules/ingest/_collections/fam-git-repo.util.js +193 -0
- package/build/src/_modules/ingest/_collections/fam-scan-reconcile.util.js +40 -0
- package/build/src/_modules/ingest/_collections/fam-secret-content.util.js +49 -0
- package/build/src/_modules/ingest/_collections/fam-secret-exclude.util.js +12 -0
- package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +38 -8
- package/build/src/_modules/ingest/_services/fam-scan.control-service.js +77 -11
- package/build/src/_routes/server/api/api.controller.js +38 -2
- package/build/src/app.server.js +31 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
// (
|
|
107
|
-
|
|
108
|
-
|
|
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,
|
|
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
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
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,
|
|
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
|
-
|
|
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);
|