@futdevpro/fdp-agent-memory 1.1.122 → 1.1.133
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/read.command.js +26 -3
- package/build/src/_collections/config-catalog.const.js +12 -0
- package/build/src/_collections/error-codes.const.js +2 -0
- package/build/src/_collections/fam-db-models.const.js +2 -0
- package/build/src/_models/data-models/fam-dup-audit.data-model.js +72 -0
- package/build/src/_modules/embedding/_collections/fam-dup-file-rollup.util.js +48 -0
- package/build/src/_modules/embedding/_collections/fam-dup-fork-pair.util.js +86 -0
- package/build/src/_modules/embedding/_services/fam-dup-audit-scheduler.control-service.js +140 -0
- package/build/src/_modules/embedding/_services/fam-dup-audit.control-service.js +141 -0
- package/build/src/_modules/embedding/_services/fam-dup-audit.data-service.js +66 -0
- package/build/src/_modules/embedding/_services/fam-dup-coverage.control-service.js +123 -0
- package/build/src/_modules/embedding/_services/fam-duplicate-scan.control-service.js +63 -10
- package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +9 -0
- package/build/src/_modules/embedding/_services/fam-exact-duplication.control-service.js +309 -0
- package/build/src/_modules/embedding/index.js +16 -1
- package/build/src/_modules/mcp/_collections/fam-read-response-shape.util.js +167 -0
- package/build/src/_modules/mcp/_services/fam-capability-registry.service.js +67 -0
- package/build/src/_modules/mcp/_services/fam-mcp-adapter.service.js +8 -3
- package/build/src/_modules/mcp/_services/fam-read-tool.service.js +55 -9
- package/build/src/_modules/mcp/_services/fam-write-tool.service.js +35 -0
- package/build/src/_modules/retrieval/_collections/fam-rule-propagation.util.js +1 -1
- package/client-dist/chunk-UKUX2JAV.js +1 -0
- package/client-dist/index.html +3 -3
- package/client-dist/{main-YK4YIVAQ.js → main-5TKTMY4X.js} +1 -1
- package/package.json +1 -1
- package/client-dist/chunk-NMUCMQY6.js +0 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FAM_ReadResponseShape_Util = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `FAM_ReadResponseShape_Util` — a `read` VÁLASZ-FORMÁZÁSA az agent-fogyasztóknak (user-FR 2026-07-29, agent-panaszok:
|
|
6
|
+
* „a FAM túl nagy csomag válaszokat ad" / „formázatlan, tördeletlen egy soros válaszokat kapnak" / „az eredményekből
|
|
7
|
+
* kiolvasható fájl elérési útvonalak nem állnak össze").
|
|
8
|
+
*
|
|
9
|
+
* **Alapelv: NULLA információ-veszteség.** Nem dobunk el tudást — csak (a) a DUPLIKÁCIÓT szüntetjük meg (ugyanaz az
|
|
10
|
+
* útvonal 3 mezőben, ugyanaz az abszolút út a `source`-ban is), (b) az agent számára HASZNÁLHATATLAN belső kulcsokat
|
|
11
|
+
* hagyjuk el (`contentHash` dedup-kulcs, opak `scopeId`-k), (c) a 17-jegyű float-okat értelmes pontosságra kerekítjük.
|
|
12
|
+
* Minden megmaradó tény továbbra is elérhető, csak EGY, egyértelmű helyen.
|
|
13
|
+
*
|
|
14
|
+
* A transzformáció a read-tool KIMENETÉN fut, így az MCP és a REST `/api/read` is ezt kapja; a belső retrieval-DTO
|
|
15
|
+
* (és a kliens/CLI fogyasztói) érintetlenek.
|
|
16
|
+
*/
|
|
17
|
+
class FAM_ReadResponseShape_Util {
|
|
18
|
+
/** A score-ok megjelenítési pontossága (0.5530494673617745 → 0.5530) — a rangsorhoz bőven elég. */
|
|
19
|
+
static SCORE_DECIMALS = 4;
|
|
20
|
+
/**
|
|
21
|
+
* Egy hit KOMPAKT, agent-barát alakja. **Útvonal-SSOT (a #3 panasz gyökere):** eddig HÁROM különböző útvonal-mező
|
|
22
|
+
* volt HÁROM különböző bázissal (`sourceFilePath` a scan-root-hoz relatív → az agent nem tudta feloldani;
|
|
23
|
+
* `absolutePath`; `source.repoRelativePath`), ezért „nem álltak össze". Mostantól:
|
|
24
|
+
* - **`path`** — a MEGNYITHATÓ útvonal: abszolút, VAGY a `basePath`-hoz relatív, ha a hívó adott ilyet. EZ a SSOT.
|
|
25
|
+
* - **`repoPath`** — repo-relatív út + repo-név (git-hivatkozáshoz), CSAK ha ismert.
|
|
26
|
+
* A régi mezők (`sourceFilePath`/`absolutePath`) MEGMARADNAK a visszafelé-kompatibilitásért, de a `path` az elsődleges.
|
|
27
|
+
*/
|
|
28
|
+
static compactHit(hit) {
|
|
29
|
+
const source = hit.source;
|
|
30
|
+
const round = (value) => typeof value === 'number' ? Number(value.toFixed(FAM_ReadResponseShape_Util.SCORE_DECIMALS)) : undefined;
|
|
31
|
+
const compact = {
|
|
32
|
+
id: hit.id,
|
|
33
|
+
table: hit.table,
|
|
34
|
+
// ÚTVONAL-SSOT: a `sourceFilePath` már a hívó `basePath`-jához igazított (rebaseToBasePath), különben az
|
|
35
|
+
// abszolút út — így MINDIG feloldható. A régi mezőket lentebb megtartjuk (kompatibilitás).
|
|
36
|
+
path: hit.sourceFilePath ?? hit.absolutePath ?? null,
|
|
37
|
+
score: round(hit.score),
|
|
38
|
+
finalScore: round(hit.finalScore),
|
|
39
|
+
};
|
|
40
|
+
if (hit.lexicalScore !== undefined) {
|
|
41
|
+
compact.lexicalScore = round(hit.lexicalScore);
|
|
42
|
+
}
|
|
43
|
+
if (hit.weight !== undefined && hit.weight !== 1) {
|
|
44
|
+
compact.weight = hit.weight;
|
|
45
|
+
}
|
|
46
|
+
// Lokáció a fájlon BELÜL (melyik szekció / hányadik sor) — a „hol nézzem meg" kérdésre.
|
|
47
|
+
if (hit.headingPath?.length) {
|
|
48
|
+
compact.heading = hit.headingPath.join(' › ');
|
|
49
|
+
}
|
|
50
|
+
const position = hit.position;
|
|
51
|
+
if (position?.lineStart) {
|
|
52
|
+
compact.lines = position.lineEnd && position.lineEnd !== position.lineStart
|
|
53
|
+
? `${position.lineStart}-${position.lineEnd}`
|
|
54
|
+
: String(position.lineStart);
|
|
55
|
+
}
|
|
56
|
+
if (hit.chunkType) {
|
|
57
|
+
compact.chunkType = hit.chunkType;
|
|
58
|
+
}
|
|
59
|
+
if (hit.kind) {
|
|
60
|
+
compact.kind = hit.kind;
|
|
61
|
+
}
|
|
62
|
+
if (hit.tags?.length) {
|
|
63
|
+
compact.tags = hit.tags;
|
|
64
|
+
}
|
|
65
|
+
// Scope: az opak `scopeId`-k helyett ember/agent-olvasható `layer=név` lánc (a szűkítéshez ezt kell megadni).
|
|
66
|
+
const scopePath = (hit.scopePath ?? []);
|
|
67
|
+
if (scopePath.length) {
|
|
68
|
+
compact.scope = scopePath.map((ref) => `${ref.layer}=${ref.canonicalName}`).join('/');
|
|
69
|
+
}
|
|
70
|
+
// Repo-hivatkozás (git-link) — csak ha van; a `source` többi mezője (root/path/absolutePath) DUPLIKÁCIÓ volt.
|
|
71
|
+
if (source?.repoName && source?.repoRelativePath) {
|
|
72
|
+
compact.repoPath = `${source.repoName}:${source.repoRelativePath}`;
|
|
73
|
+
}
|
|
74
|
+
if (source?.type) {
|
|
75
|
+
compact.sourceType = source.type;
|
|
76
|
+
}
|
|
77
|
+
// Rules-specifikus kurátor-mezők (ha vannak) — ezek döntik el, MENNYIRE kötelező a szabály.
|
|
78
|
+
for (const key of ['ruleId', 'ruleScope', 'importance', 'userApproved', 'category']) {
|
|
79
|
+
const value = hit[key];
|
|
80
|
+
if (value !== undefined && value !== null) {
|
|
81
|
+
compact[key] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (hit.lastModifiedMs) {
|
|
85
|
+
compact.lastModified = new Date(hit.lastModifiedMs).toISOString().slice(0, 10);
|
|
86
|
+
}
|
|
87
|
+
// A TARTALOM a végére — így a metaadat-fej egyben olvasható a hosszú szöveg előtt.
|
|
88
|
+
if (hit.content !== undefined) {
|
|
89
|
+
compact.content = hit.content;
|
|
90
|
+
}
|
|
91
|
+
// Visszafelé-kompatibilitás: a `sourceFilePath` MEGMARAD (a kliens `ma-search` erre a mezőnévre épül) — értéke
|
|
92
|
+
// AZONOS a `path`-szal, tehát alias, nem harmadik igazság. Az `absolutePath` viszont CSAK akkor kerül bele, ha
|
|
93
|
+
// ténylegesen ELTÉR (azaz `basePath`-rebase történt, és így külön információt hordoz) — különben szó szerinti
|
|
94
|
+
// duplikáció lenne, ami épp a panaszolt „nagy csomag" + „az útvonalak nem állnak össze" problémát okozta.
|
|
95
|
+
compact.sourceFilePath = hit.sourceFilePath ?? null;
|
|
96
|
+
if (hit.absolutePath && hit.absolutePath !== compact.path) {
|
|
97
|
+
compact.absolutePath = hit.absolutePath;
|
|
98
|
+
}
|
|
99
|
+
return compact;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* OPERATÍV VISSZAJELZÉS (user-FR 2026-07-29 — általános MCP-követelmény): „a tool-ok adjanak visszajelzést a
|
|
103
|
+
* működésről: összesen N elem között kerestünk, M tűnik relevánsnak, K megjelenítve + mit tegyél, hogy mindet
|
|
104
|
+
* lásd / fájlba írd / célzottabban keress". PURE — a számokból épít egy mondatot + konkrét, MÁSOLHATÓ tippeket.
|
|
105
|
+
*/
|
|
106
|
+
static buildSummary(set) {
|
|
107
|
+
const scope = set.tables.length ? set.tables.join('+') : 'minden tár';
|
|
108
|
+
const searched = set.searchedCount > 0
|
|
109
|
+
? `${FAM_ReadResponseShape_Util.humanCount(set.searchedCount)} elem között kerestünk`
|
|
110
|
+
: 'a keresés lefutott';
|
|
111
|
+
const relevant = `${set.totalRelevant} tűnik magabiztosan relevánsnak`;
|
|
112
|
+
const shown = set.truncated
|
|
113
|
+
? `${set.shown} megjelenítve (topK=${set.topK} vágta)`
|
|
114
|
+
: `mind a ${set.shown} megjelenítve`;
|
|
115
|
+
return `[${scope}] ${searched} · ${relevant} · ${shown}.`;
|
|
116
|
+
}
|
|
117
|
+
/** A summary melletti, KONKRÉT következő lépések (csak a releváns tippek — nincs zaj). PURE. */
|
|
118
|
+
static buildHints(set) {
|
|
119
|
+
const hints = [];
|
|
120
|
+
if (set.truncated) {
|
|
121
|
+
const all = Math.max(set.totalRelevant, set.shown);
|
|
122
|
+
hints.push(`MIND: futtasd újra \`topK: ${all}\`-val (vagy \`depth: "deep"\`-pel a mélyebb feltárásért).`);
|
|
123
|
+
hints.push('FÁJLBA: a CLI-vel `fam read --query "…" --json > talalatok.json` (nagy találat-halmaznál ez '
|
|
124
|
+
+ 'a javasolt út — a chat-választ nem terheli).');
|
|
125
|
+
}
|
|
126
|
+
if (set.totalRelevant > set.shown * 3 && !set.hasScopeFilter) {
|
|
127
|
+
hints.push('CÉLZOTTABBAN: szűkíts `scope`-pal (pl. `{"layer":"project","name":"<projekt>"}`), `tags`-szel '
|
|
128
|
+
+ 'vagy `kind`-dal — a sok találat általában túl tág kérdést jelez.');
|
|
129
|
+
}
|
|
130
|
+
if (!set.totalRelevant) {
|
|
131
|
+
hints.push('NINCS magabiztos találat: próbálj más megfogalmazást/kulcsszavakat, vagy emeld a `topK`-t; '
|
|
132
|
+
+ 'a `minScore: 0` a leggyengébb tippeket is visszaadja.');
|
|
133
|
+
}
|
|
134
|
+
return hints;
|
|
135
|
+
}
|
|
136
|
+
/** Rövid, ember-olvasható darabszám (12 400 → „12.4k"). PURE. */
|
|
137
|
+
static humanCount(count) {
|
|
138
|
+
if (count < 1000) {
|
|
139
|
+
return String(count);
|
|
140
|
+
}
|
|
141
|
+
if (count < 1000000) {
|
|
142
|
+
const thousands = count / 1000;
|
|
143
|
+
return `${thousands >= 100 ? Math.round(thousands) : Number(thousands.toFixed(1))}k`;
|
|
144
|
+
}
|
|
145
|
+
return `${Number((count / 1000000).toFixed(1))}M`;
|
|
146
|
+
}
|
|
147
|
+
/** Egy teljes result-blokk formázása: kompakt hit-ek + summary + hints (a result többi mezője érintetlen). */
|
|
148
|
+
static shapeResult(set) {
|
|
149
|
+
const hits = set.result.hits ?? [];
|
|
150
|
+
const shaped = { ...set.result };
|
|
151
|
+
shaped.summary = FAM_ReadResponseShape_Util.buildSummary({
|
|
152
|
+
tables: set.tables, searchedCount: set.searchedCount, totalRelevant: set.result.totalRelevant ?? 0,
|
|
153
|
+
shown: hits.length, truncated: Boolean(set.result.truncated), topK: set.topK,
|
|
154
|
+
});
|
|
155
|
+
const hints = FAM_ReadResponseShape_Util.buildHints({
|
|
156
|
+
totalRelevant: set.result.totalRelevant ?? 0, shown: hits.length,
|
|
157
|
+
truncated: Boolean(set.result.truncated), hasScopeFilter: Boolean(set.hasScopeFilter),
|
|
158
|
+
});
|
|
159
|
+
if (hints.length) {
|
|
160
|
+
shaped.hints = hints;
|
|
161
|
+
}
|
|
162
|
+
shaped.searchedCount = set.searchedCount;
|
|
163
|
+
shaped.hits = hits.map((hit) => FAM_ReadResponseShape_Util.compactHit(hit));
|
|
164
|
+
return shaped;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
exports.FAM_ReadResponseShape_Util = FAM_ReadResponseShape_Util;
|
|
@@ -337,6 +337,9 @@ class FAM_CapabilityRegistry_Service {
|
|
|
337
337
|
projects: { type: 'array', items: { type: 'string' }, description: 'Csak ezek a projektek (scopePath project-layer canonicalName).' },
|
|
338
338
|
excludeProjects: { type: 'array', items: { type: 'string' }, description: 'Kizárt projektek (fork-zaj — pl. ccap — elnyomása).' },
|
|
339
339
|
minProjects: { type: 'number', description: 'Csak a legalább ennyi KÜLÖNBÖZŐ projektet átfedő clusterek (default 2).' },
|
|
340
|
+
minContentLength: { type: 'number', description: 'Minimális content-hossz (karakter) — a rövid boilerplate-zaj kizárása a prefilterben (dup-v2).' },
|
|
341
|
+
suppressForkPairs: { type: 'boolean', description: 'A fork-gyanús projekt-párokra eső clusterek elnyomása a kimenetben (default false; dup-v2).' },
|
|
342
|
+
includeCoverage: { type: 'boolean', description: 'A lefedettség-preflight beágyazása az eredménybe (default true; dup-v2).' },
|
|
340
343
|
}),
|
|
341
344
|
handler: async (args) => {
|
|
342
345
|
const input = this.asObject(args);
|
|
@@ -352,9 +355,73 @@ class FAM_CapabilityRegistry_Service {
|
|
|
352
355
|
projects: this.asStringArray(input.projects),
|
|
353
356
|
excludeProjects: this.asStringArray(input.excludeProjects),
|
|
354
357
|
minProjects: this.asNumber(input.minProjects) ?? 2,
|
|
358
|
+
minContentLength: this.asNumber(input.minContentLength),
|
|
359
|
+
suppressForkPairs: input.suppressForkPairs === true,
|
|
360
|
+
includeCoverage: input.includeCoverage !== false,
|
|
355
361
|
}));
|
|
356
362
|
},
|
|
357
363
|
});
|
|
364
|
+
// detect_exact_duplication (dup-v2, task #20): EXACT (contentHash-azonos) duplikáció-mérés egyetlen
|
|
365
|
+
// Mongo-aggregációval — a TELJES szűrt halmazon fut (nincs maxEntries-cap, nincs vektor-számítás), így a
|
|
366
|
+
// flotta-szintű „mennyi az exact duplikáció?" kérdés kanonikus mérője. A near-dup scan (fent) a drift-elt
|
|
367
|
+
// másolatokra való; ez a bitre-azonos sokszorozódásra.
|
|
368
|
+
this.register({
|
|
369
|
+
name: 'detect_exact_duplication', category: category,
|
|
370
|
+
description: 'EXACT (contentHash-azonos) duplikáció-mérés (read-only; dup-v2 task #20): egyetlen Mongo-'
|
|
371
|
+
+ 'aggregáció a TELJES szűrt halmazon (a 300k-s codebase táron is cap nélkül fut). Kimenet: totals '
|
|
372
|
+
+ '(totalGroups/redundantCopies/wastedChars) + top-N csoport (wasted DESC, projekt-/repo-lefedettséggel '
|
|
373
|
+
+ '+ fájl-listával) + fork-pár statisztika (`forkPairs`) + a ≥4-projektes csoport-hash-ek + '
|
|
374
|
+
+ 'lefedettség-preflight (`coverage`, default be). A `suppressForkPairs` a fork-gyanús párokra eső '
|
|
375
|
+
+ 'csoportokat elnyomja a listából (a totals a mért valóság marad). A `minContentLength` a rövid '
|
|
376
|
+
+ 'boilerplate-zajt szűri (ajánlott: 200).',
|
|
377
|
+
inputSchema: this.objectSchema({
|
|
378
|
+
table: this.tableSchema(false),
|
|
379
|
+
pathRegex: { type: 'string', description: 'A sourceFilePath include-regexe (case-insensitive).' },
|
|
380
|
+
excludePathRegex: { type: 'string', description: 'A sourceFilePath exclude-regexe.' },
|
|
381
|
+
excludeRootRegex: { type: 'string', description: 'A source.root exclude-regexe (pl. "STALE-projects").' },
|
|
382
|
+
projects: { type: 'array', items: { type: 'string' }, description: 'Csak ezek a projektek (scopePath project-layer canonicalName).' },
|
|
383
|
+
excludeProjects: { type: 'array', items: { type: 'string' }, description: 'Kizárt projektek.' },
|
|
384
|
+
minProjects: { type: 'number', description: 'Csak a legalább ennyi KÜLÖNBÖZŐ projektet átfedő csoportok (default 1).' },
|
|
385
|
+
minContentLength: { type: 'number', description: 'Minimális content-hossz karakterben (default 0; ajánlott 200 a boilerplate-zaj ellen).' },
|
|
386
|
+
maxGroups: { type: 'number', description: 'A visszaadott top-csoportok száma (default 100, hard cap 500; a totals a teljes halmazon számolt).' },
|
|
387
|
+
maxFilesPerGroup: { type: 'number', description: 'Per-csoport fájl-lista cap (default 20; a filesTruncated jelzi a csonkolást).' },
|
|
388
|
+
suppressForkPairs: { type: 'boolean', description: 'A fork-gyanús projekt-párokra eső csoportok elnyomása a listából (default false).' },
|
|
389
|
+
includeCoverage: { type: 'boolean', description: 'A lefedettség-preflight beágyazása (default true).' },
|
|
390
|
+
}),
|
|
391
|
+
handler: async (args) => {
|
|
392
|
+
const input = this.asObject(args);
|
|
393
|
+
return this.ok(await embedding_1.FAM_ExactDuplication_ControlService.getInstance().run({
|
|
394
|
+
table: this.optionalTable(input.table),
|
|
395
|
+
pathRegex: this.asString(input.pathRegex),
|
|
396
|
+
excludePathRegex: this.asString(input.excludePathRegex),
|
|
397
|
+
excludeRootRegex: this.asString(input.excludeRootRegex),
|
|
398
|
+
projects: this.asStringArray(input.projects),
|
|
399
|
+
excludeProjects: this.asStringArray(input.excludeProjects),
|
|
400
|
+
minProjects: this.asNumber(input.minProjects),
|
|
401
|
+
minContentLength: this.asNumber(input.minContentLength),
|
|
402
|
+
maxGroups: this.asNumber(input.maxGroups),
|
|
403
|
+
maxFilesPerGroup: this.asNumber(input.maxFilesPerGroup),
|
|
404
|
+
suppressForkPairs: input.suppressForkPairs === true,
|
|
405
|
+
includeCoverage: input.includeCoverage !== false,
|
|
406
|
+
}));
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
// duplication_coverage_report (dup-v2, task #20): a dup-mérés lefedettség-preflight-je ÖNÁLLÓ eszközként —
|
|
410
|
+
// mely projektek hiányoznak (0 entry) vagy elavultak a korpuszból (a „nincs duplikáció X-szel" hamis-negatív
|
|
411
|
+
// gyanújának ellenőrzése a mérés ELŐTT/UTÁN).
|
|
412
|
+
this.register({
|
|
413
|
+
name: 'duplication_coverage_report', category: category,
|
|
414
|
+
description: 'A dup-mérés LEFEDETTSÉG-preflight riportja (read-only; dup-v2): a scope-fa project-layer '
|
|
415
|
+
+ 'listája × a tár per-projekt entry-száma + a legfrissebb entry kora. Kimenet: zeroEntryProjects '
|
|
416
|
+
+ '(a mérésből NÉMÁN kimaradók), staleProjects (> 8 nap), per-projekt sorok (entryCount DESC) + '
|
|
417
|
+
+ 'warnings. A dup-eredmény flotta-szintű értelmezésének előfeltétele.',
|
|
418
|
+
inputSchema: this.objectSchema({ table: this.tableSchema(false) }),
|
|
419
|
+
handler: async (args) => {
|
|
420
|
+
const input = this.asObject(args);
|
|
421
|
+
return this.ok(await embedding_1.FAM_DupCoverage_ControlService.getInstance()
|
|
422
|
+
.report(this.optionalTable(input.table) ?? fam_table_type_enum_1.FAM_Table.codebase));
|
|
423
|
+
},
|
|
424
|
+
});
|
|
358
425
|
}
|
|
359
426
|
/** maintenance → MP-2 (re-embed) + MP-3 (rebuild reference index). */
|
|
360
427
|
registerMaintenance() {
|
|
@@ -110,13 +110,18 @@ class FAM_Mcp_Adapter_Service {
|
|
|
110
110
|
}
|
|
111
111
|
return 'Ismeretlen hiba a tool-végrehajtás során.';
|
|
112
112
|
}
|
|
113
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* JSON-stringify (a transport text-content payload-ja) — **TÖRDELVE** (user-FR 2026-07-29, agent-panasz:
|
|
115
|
+
* „formázatlan, tördeletlen egy soros válaszokat kapnak"). A 2-szóközös indentálás soronként EGY mezőt ad, így a
|
|
116
|
+
* válasz olvasható/diffelhető; a token-költség elhanyagolható a beágyazott `content`-ek mellett, cserébe az agent
|
|
117
|
+
* (és a `truncate`-elő host) sokkal jobban tudja kezelni.
|
|
118
|
+
*/
|
|
114
119
|
stringify(data) {
|
|
115
120
|
try {
|
|
116
|
-
return JSON.stringify(data);
|
|
121
|
+
return JSON.stringify(data, null, 2);
|
|
117
122
|
}
|
|
118
123
|
catch {
|
|
119
|
-
return JSON.stringify({ ok: false, error: { message: 'A válasz nem szerializálható JSON-ra.' } });
|
|
124
|
+
return JSON.stringify({ ok: false, error: { message: 'A válasz nem szerializálható JSON-ra.' } }, null, 2);
|
|
120
125
|
}
|
|
121
126
|
}
|
|
122
127
|
}
|
|
@@ -9,6 +9,8 @@ const error_codes_const_1 = require("../../../_collections/error-codes.const");
|
|
|
9
9
|
const fam_error_factory_util_1 = require("../../../_collections/fam-error-factory.util");
|
|
10
10
|
const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
|
|
11
11
|
const retrieval_1 = require("../../retrieval");
|
|
12
|
+
const embedding_1 = require("../../embedding");
|
|
13
|
+
const fam_read_response_shape_util_1 = require("../_collections/fam-read-response-shape.util");
|
|
12
14
|
/**
|
|
13
15
|
* `FAM_ReadTool_Service` (SP-6.2, dsgn-003 §2) — a `read` core-tool **transport-agnosztikus**
|
|
14
16
|
* handler-je. Singleton. EGY belépési pont (`handle`), amit MIND az MCP-tool (stdio), MIND a REST
|
|
@@ -47,7 +49,33 @@ class FAM_ReadTool_Service {
|
|
|
47
49
|
// levágódik + a túlcsorduló hit-ek elhagyva, `truncated:true`-val. A FAM mindig DIREKT adja vissza a találatokat.
|
|
48
50
|
FAM_ReadTool_Service.applyTokenBudget(response, await FAM_ReadTool_Service.resolveMaxTokens(input));
|
|
49
51
|
this.logReadActivity(input, response);
|
|
50
|
-
|
|
52
|
+
// AGENT-BARÁT VÁLASZ-FORMA (user-FR 2026-07-29): kompakt hit-ek (duplikált útvonal-mezők + opak belső kulcsok
|
|
53
|
+
// nélkül, kerekített score-okkal) + OPERATÍV VISSZAJELZÉS (mennyi közt kerestünk / mennyi releváns / mennyi
|
|
54
|
+
// látszik + konkrét következő lépések). Információ NEM vész el — csak a duplikáció és a zaj.
|
|
55
|
+
return FAM_ReadTool_Service.shapeResponse(response, input);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A válasz agent-barát formázása (user-FR 2026-07-29). Per-query blokkonként hozzáadja a `summary`/`hints`/
|
|
59
|
+
* `searchedCount` mezőket és kompaktálja a hit-eket. A `searchedCount` a ténylegesen átkutatott vektor-pool
|
|
60
|
+
* mérete (a query tárainak összege) — ez adja a „N elem között kerestünk" visszajelzést.
|
|
61
|
+
*/
|
|
62
|
+
static shapeResponse(response, input) {
|
|
63
|
+
const vectorSearch = embedding_1.FAM_VectorSearch_ControlService.getInstance();
|
|
64
|
+
const shaped = (response.results ?? []).map((result, index) => {
|
|
65
|
+
const query = input.queries?.[index];
|
|
66
|
+
const tables = (query?.tables ?? []);
|
|
67
|
+
const pool = tables.length ? tables : embedding_1.FAM_STORE_REGISTRY.map((entry) => entry.table);
|
|
68
|
+
const searchedCount = pool.reduce((sum, table) => sum + vectorSearch.getPoolSize(table), 0);
|
|
69
|
+
return fam_read_response_shape_util_1.FAM_ReadResponseShape_Util.shapeResult({
|
|
70
|
+
result: result,
|
|
71
|
+
tables: tables.map((table) => String(table)),
|
|
72
|
+
searchedCount: searchedCount,
|
|
73
|
+
topK: query?.topK ?? result.hits?.length ?? 0,
|
|
74
|
+
// Ha a hívó MÁR szűkített (scope/tag/kind), a „szűkíts jobban" tipp fölösleges zaj lenne.
|
|
75
|
+
hasScopeFilter: Boolean(query?.scopeFilter?.length || query?.tagFilter?.length || query?.kindFilter),
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
return { ...response, results: shaped };
|
|
51
79
|
}
|
|
52
80
|
/**
|
|
53
81
|
* A `read.maxTokens` config feloldása (default/fallback 4000, min 256) — a QUERY-K TÁRAIRA feloldva (user-FR
|
|
@@ -89,7 +117,20 @@ class FAM_ReadTool_Service {
|
|
|
89
117
|
*/
|
|
90
118
|
static applyTokenBudget(response, maxTokens) {
|
|
91
119
|
const charBudget = Math.max(256, maxTokens) * 4;
|
|
92
|
-
|
|
120
|
+
// A per-hit metaadat TÉNYLEGES char-költsége (2026-07-29 lelet): eddig egy fix `220`-as becslés volt, miközben a
|
|
121
|
+
// valódi metaadat ~790-1400 char — vagyis a budget 3-6×-osan ALULBECSÜLT, és a válasz rendszeresen túllépte a
|
|
122
|
+
// `read.maxTokens`-ben ígért méretet (ez volt a „túl nagy csomag" panasz egyik oka). Most a KIMENŐ (kompaktált)
|
|
123
|
+
// alakot mérjük, mert az agent azt kapja meg — így a budget végre igazat mond.
|
|
124
|
+
const metaCost = (hit) => {
|
|
125
|
+
try {
|
|
126
|
+
const shaped = fam_read_response_shape_util_1.FAM_ReadResponseShape_Util.compactHit(hit);
|
|
127
|
+
delete shaped.content;
|
|
128
|
+
return JSON.stringify(shaped).length;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return 220;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
93
134
|
const results = response.results ?? [];
|
|
94
135
|
if (!results.length) {
|
|
95
136
|
return;
|
|
@@ -104,11 +145,12 @@ class FAM_ReadTool_Service {
|
|
|
104
145
|
if (!hit) {
|
|
105
146
|
continue;
|
|
106
147
|
}
|
|
107
|
-
|
|
148
|
+
const hitMeta = metaCost(hit);
|
|
149
|
+
if (used + hitMeta >= charBudget) {
|
|
108
150
|
budgetExhausted = true;
|
|
109
151
|
break;
|
|
110
152
|
}
|
|
111
|
-
used +=
|
|
153
|
+
used += hitMeta;
|
|
112
154
|
if (typeof hit.content === 'string' && hit.content.length > 0) {
|
|
113
155
|
const remaining = charBudget - used;
|
|
114
156
|
if (hit.content.length > remaining) {
|
|
@@ -137,14 +179,18 @@ class FAM_ReadTool_Service {
|
|
|
137
179
|
* hit (nem-scan, pl. agent-write) érintetlen. basePath nélkül no-op (a `sourceFilePath` a scan-gyökérhez relatív marad).
|
|
138
180
|
*/
|
|
139
181
|
rebaseToBasePath(response, basePath) {
|
|
140
|
-
if (!basePath) {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
182
|
for (const block of response.results ?? []) {
|
|
144
183
|
for (const hit of block.hits ?? []) {
|
|
145
|
-
if (hit.absolutePath) {
|
|
146
|
-
|
|
184
|
+
if (!hit.absolutePath) {
|
|
185
|
+
continue;
|
|
147
186
|
}
|
|
187
|
+
// ÚTVONAL-SSOT (user-FR 2026-07-29 — „az eredményekből kiolvasható fájl elérési útvonalak nem állnak
|
|
188
|
+
// össze"): a `sourceFilePath` eddig a SCAN-ROOT-hoz volt relatív (pl. `global/core-x.md`), amit a hívó
|
|
189
|
+
// NEM tudott feloldani (a scan-root nem szerepelt a válaszban). Mostantól: `basePath` esetén ahhoz
|
|
190
|
+
// relatív, KÜLÖNBEN az ABSZOLÚT út — vagyis a mező MINDIG megnyitható útvonalat ad.
|
|
191
|
+
hit.sourceFilePath = basePath
|
|
192
|
+
? path.relative(basePath, hit.absolutePath).replace(/\\/g, '/')
|
|
193
|
+
: hit.absolutePath;
|
|
148
194
|
}
|
|
149
195
|
}
|
|
150
196
|
}
|
|
@@ -46,8 +46,43 @@ class FAM_WriteTool_Service {
|
|
|
46
46
|
this.validate(input);
|
|
47
47
|
const output = await this.dispatch(input);
|
|
48
48
|
this.logWriteActivity(input, output);
|
|
49
|
+
// OPERATÍV VISSZAJELZÉS (user-FR 2026-07-29): egy mondat arról, MI TÖRTÉNT — az agentnek ne a nyers ID-tömbökből
|
|
50
|
+
// kelljen visszafejtenie az eredményt (ugyanaz az elv, mint a `read` summary-jénél).
|
|
51
|
+
output.summary = FAM_WriteTool_Service.buildSummary(output);
|
|
49
52
|
return output;
|
|
50
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* A write-eredmény egy mondatban (PURE, tesztelhető). Csak a TÉNYLEGESEN történt dolgokat sorolja (üres részeket
|
|
56
|
+
* nem említ), scan-nél a fájl/chunk-számot + a delta-verdikteket, és a figyelmeztetések számát.
|
|
57
|
+
*/
|
|
58
|
+
static buildSummary(output) {
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (output.created.length) {
|
|
61
|
+
parts.push(`${output.created.length} létrehozva`);
|
|
62
|
+
}
|
|
63
|
+
if (output.updated.length) {
|
|
64
|
+
parts.push(`${output.updated.length} frissítve`);
|
|
65
|
+
}
|
|
66
|
+
if (output.deleted.length) {
|
|
67
|
+
parts.push(`${output.deleted.length} törölve`);
|
|
68
|
+
}
|
|
69
|
+
if (output.scan) {
|
|
70
|
+
const verdicts = output.scan.verdicts;
|
|
71
|
+
parts.push(`${output.scan.filesProcessed} fájl → ${output.scan.chunks} chunk `
|
|
72
|
+
+ `(${verdicts.new} új / ${verdicts.modified} módosult / ${verdicts.equal} változatlan`
|
|
73
|
+
+ `${verdicts.deleted ? ` / ${verdicts.deleted} törölt` : ''})`);
|
|
74
|
+
}
|
|
75
|
+
if (output.reEmbed) {
|
|
76
|
+
parts.push(`re-embed: ${output.reEmbed.reembedded}/${output.reEmbed.total} `
|
|
77
|
+
+ `(${output.reEmbed.skipped} kihagyva, ${output.reEmbed.failed} hibás)`);
|
|
78
|
+
}
|
|
79
|
+
if (!parts.length) {
|
|
80
|
+
parts.push('nem változott semmi');
|
|
81
|
+
}
|
|
82
|
+
const status = output.ok ? 'OK' : 'HIBA';
|
|
83
|
+
const warnings = output.warnings.length ? ` · ${output.warnings.length} figyelmeztetés` : '';
|
|
84
|
+
return `[${output.operation} → ${output.table}] ${status}: ${parts.join(' · ')}${warnings}`;
|
|
85
|
+
}
|
|
51
86
|
/** Az operation-router (a `handle` validáció + activity-log közé ékelve, dsgn-003 §3). */
|
|
52
87
|
async dispatch(input) {
|
|
53
88
|
switch (input.operation) {
|
|
@@ -47,7 +47,7 @@ class FAM_RulePropagation_Util {
|
|
|
47
47
|
const lines = [];
|
|
48
48
|
lines.push(opts?.heading ?? '## FDP Agent Hard-Rules (synced from FAM)');
|
|
49
49
|
lines.push('');
|
|
50
|
-
lines.push('> Generated from the canonical rule set. Edit the rules in `documentations/rules/`, not here.');
|
|
50
|
+
lines.push('> Generated from the canonical rule set. Edit the rules in `fdp-documentations/rules/`, not here.');
|
|
51
51
|
for (const group of FAM_RulePropagation_Util.SCOPE_ORDER) {
|
|
52
52
|
const inScope = deduped
|
|
53
53
|
.filter((rule) => rule.ruleScope === group.scope)
|