@futdevpro/fdp-agent-memory 1.1.166 → 1.1.194
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 +8 -2
- package/build/src/_cli/_commands/prune-duplicates.command.js +93 -0
- package/build/src/_cli/_commands/retire-temporary.command.js +77 -0
- package/build/src/_cli/register-commands.js +4 -0
- package/build/src/_collections/config-catalog.const.js +75 -0
- package/build/src/_models/data-models/fam-entry.data-model.js +6 -0
- package/build/src/_models/data-models/fam-memory.data-model.js +3 -0
- package/build/src/_modules/embedding/_collections/fam-exact-duplicate-prune.util.js +85 -0
- package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +32 -0
- package/build/src/_modules/embedding/_services/fam-entry.data-service.js +21 -0
- package/build/src/_modules/embedding/_services/fam-exact-duplicate-prune.control-service.js +152 -0
- package/build/src/_modules/embedding/index.js +7 -1
- package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +21 -1
- package/build/src/_modules/ingest/_services/fam-scan-job.control-service.js +55 -1
- package/build/src/_modules/ingest/_services/fam-scan-scheduler.control-service.js +26 -0
- package/build/src/_modules/mcp/_collections/fam-core-tools.const.js +11 -0
- package/build/src/_modules/mcp/_services/fam-read-tool.service.js +2 -0
- package/build/src/_modules/mcp/_services/fam-write-tool.service.js +29 -7
- package/build/src/_modules/retrieval/_collections/fam-cross-table-note.util.js +136 -0
- package/build/src/_modules/retrieval/_collections/fam-lexical-match.util.js +75 -2
- package/build/src/_modules/retrieval/_collections/fam-literal-phrase.util.js +74 -0
- package/build/src/_modules/retrieval/_collections/fam-memory-retention.util.js +70 -0
- package/build/src/_modules/retrieval/_services/fam-memory-reactivation.control-service.js +8 -1
- package/build/src/_modules/retrieval/_services/fam-memory-retention.control-service.js +118 -0
- package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +33 -0
- package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +224 -5
- package/build/src/_modules/retrieval/index.js +6 -1
- package/build/src/_routes/server/api/api.controller.js +77 -15
- package/package.json +8 -2
- package/build/src/_integration-tests/_helpers/fam-integration-test-setup.util.js +0 -105
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* (BFR-AM-002/008); a bedrock-csere (MP-15) a `FAM_EmbeddingProvider` interfész mögött non-breaking.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.FAM_EmbeddingBootstrap_ControlService = exports.FAM_DupAuditScheduler_ControlService = exports.FAM_DupAudit_DataService = exports.FAM_DupAudit_ControlService = exports.FAM_DupFileRollup_Util = exports.FAM_DupForkPair_Util = exports.FAM_DupCoverage_ControlService = exports.FAM_ExactDuplication_ControlService = exports.FAM_DuplicateScan_ControlService = exports.FAM_EmbeddingCost_ControlService = exports.FAM_EmbeddingPipeline_ControlService = exports.FAM_EmbeddingPreset_ControlService = exports.FAM_StoreRegistry_Util = exports.FAM_STORE_REGISTRY = exports.FAM_DedupWarn_ControlService = exports.FAM_Entry_DataService = exports.FAM_HydrationCoordinator_ControlService = exports.FAM_VectorSearch_ControlService = exports.FAM_MOCK_EMBEDDING_DIMS = exports.FAM_Mock_EmbeddingProvider = exports.FAM_LMStudio_EmbeddingProvider = exports.FAM_OpenAI_EmbeddingProvider = exports.FAM_Embedding_ControlService = void 0;
|
|
11
|
+
exports.FAM_EmbeddingBootstrap_ControlService = exports.FAM_DupAuditScheduler_ControlService = exports.FAM_DupAudit_DataService = exports.FAM_DupAudit_ControlService = exports.FAM_DupFileRollup_Util = exports.FAM_DupForkPair_Util = exports.FAM_DupCoverage_ControlService = exports.FAM_ExactDuplication_ControlService = exports.FAM_DuplicateScan_ControlService = exports.FAM_ExactDuplicatePrune_Util = exports.FAM_ExactDuplicatePrune_ControlService = exports.FAM_EmbeddingCost_ControlService = exports.FAM_EmbeddingPipeline_ControlService = exports.FAM_EmbeddingPreset_ControlService = exports.FAM_StoreRegistry_Util = exports.FAM_STORE_REGISTRY = exports.FAM_DedupWarn_ControlService = exports.FAM_Entry_DataService = exports.FAM_HydrationCoordinator_ControlService = exports.FAM_VectorSearch_ControlService = exports.FAM_MOCK_EMBEDDING_DIMS = exports.FAM_Mock_EmbeddingProvider = exports.FAM_LMStudio_EmbeddingProvider = exports.FAM_OpenAI_EmbeddingProvider = exports.FAM_Embedding_ControlService = void 0;
|
|
12
12
|
// SP-2.1 — provider-dispatch kapu + providerek
|
|
13
13
|
var fam_embedding_control_service_1 = require("./_services/fam-embedding.control-service");
|
|
14
14
|
Object.defineProperty(exports, "FAM_Embedding_ControlService", { enumerable: true, get: function () { return fam_embedding_control_service_1.FAM_Embedding_ControlService; } });
|
|
@@ -39,6 +39,12 @@ var fam_embedding_pipeline_control_service_1 = require("./_services/fam-embeddin
|
|
|
39
39
|
Object.defineProperty(exports, "FAM_EmbeddingPipeline_ControlService", { enumerable: true, get: function () { return fam_embedding_pipeline_control_service_1.FAM_EmbeddingPipeline_ControlService; } });
|
|
40
40
|
var fam_embedding_cost_control_service_1 = require("./_services/fam-embedding-cost.control-service");
|
|
41
41
|
Object.defineProperty(exports, "FAM_EmbeddingCost_ControlService", { enumerable: true, get: function () { return fam_embedding_cost_control_service_1.FAM_EmbeddingCost_ControlService; } });
|
|
42
|
+
// BÁJTAZONOS ismétlés-NYESÉS (2026-09-01) — a szemantikus felderítővel ellentétben EZ töröl, de csak
|
|
43
|
+
// bizonyítottan azonos tartalmat, és `dryRun` az alapértelmezés.
|
|
44
|
+
var fam_exact_duplicate_prune_control_service_1 = require("./_services/fam-exact-duplicate-prune.control-service");
|
|
45
|
+
Object.defineProperty(exports, "FAM_ExactDuplicatePrune_ControlService", { enumerable: true, get: function () { return fam_exact_duplicate_prune_control_service_1.FAM_ExactDuplicatePrune_ControlService; } });
|
|
46
|
+
var fam_exact_duplicate_prune_util_1 = require("./_collections/fam-exact-duplicate-prune.util");
|
|
47
|
+
Object.defineProperty(exports, "FAM_ExactDuplicatePrune_Util", { enumerable: true, get: function () { return fam_exact_duplicate_prune_util_1.FAM_ExactDuplicatePrune_Util; } });
|
|
42
48
|
// near-duplikátum FELDERÍTŐ (read-only; user-direktíva 2026-06-21)
|
|
43
49
|
var fam_duplicate_scan_control_service_1 = require("./_services/fam-duplicate-scan.control-service");
|
|
44
50
|
Object.defineProperty(exports, "FAM_DuplicateScan_ControlService", { enumerable: true, get: function () { return fam_duplicate_scan_control_service_1.FAM_DuplicateScan_ControlService; } });
|
|
@@ -415,6 +415,10 @@ class FAM_Ingest_ControlService {
|
|
|
415
415
|
// A default display-relatív út a friss scan-gyökérhez igazodjon (FAM-REV-050) —
|
|
416
416
|
// egy korábban más root-reprezentációval (pl. scan-file basename) ingestelt chunk konzisztenssé.
|
|
417
417
|
await set.dataService.backfillSourceFilePath(item.existingId, set.file.relativePath);
|
|
418
|
+
// SCOPE-backfill (2026-09-01): a scope-vak fallback MÁS scope alatt álló chunkot is
|
|
419
|
+
// adoptál — enélkül az adoptált chunk a RÉGI projekt-név alatt ragadna, és a
|
|
420
|
+
// scope-szűrt keresés az ÚJ néven nem találná meg (egy hibát cserélnénk másikra).
|
|
421
|
+
await set.dataService.backfillScopePath(item.existingId, set.scopePath);
|
|
418
422
|
if (item.chunk) {
|
|
419
423
|
await set.dataService.backfillReferenceCodes(item.existingId, fam_reference_code_util_1.FAM_ReferenceCode_Util.extract(item.chunk.content));
|
|
420
424
|
// Pozíció-backfill (FAM-REV-061): a chunker pozíció-fix a régi equal-chunkokra is,
|
|
@@ -743,7 +747,23 @@ class FAM_Ingest_ControlService {
|
|
|
743
747
|
const leaf = scopePath[scopePath.length - 1];
|
|
744
748
|
filter['scopePath.scopeId'] = leaf.scopeId;
|
|
745
749
|
}
|
|
746
|
-
|
|
750
|
+
const scoped = await dataService.findHydratableList(filter);
|
|
751
|
+
if (scoped.length || !scopePath.length) {
|
|
752
|
+
return scoped;
|
|
753
|
+
}
|
|
754
|
+
// SCOPE-VAK FALLBACK (2026-09-01, mert gyoker-ok). A fajl IDENTITASA az abszolut utvonal — a scope csak
|
|
755
|
+
// besorolas. Ha ugyanaz a mappa MAS projekt-nev alatt kerul ujra-szkennelesre (atnevezes utan a heti
|
|
756
|
+
// re-scan cel-unioja MINDKET nevet megorzi, es ugyanazt a mappat KETSZER szkenneli), akkor a scope-ra
|
|
757
|
+
// szurt lekerdezes URESET ad -> minden chunk `new`-nak latszik -> TELJES DUPLIKALAS, minden heten.
|
|
758
|
+
//
|
|
759
|
+
// MERVE: 51 578 dupla-csoport, mind KULONBOZO scopeId alatt; a legfrissebb eset 2026-08-31 09:32, amikor
|
|
760
|
+
// a `fdp-e2e-full/package.json` egyetlen scan-futasban ketszer kerult be (`futdevpro-e2e` 08:39 es
|
|
761
|
+
// `fdp-e2e-full` 09:32). Osszesen ~59 000 folosleges chunk.
|
|
762
|
+
//
|
|
763
|
+
// Ezert: ha a scope-olt talalat URES, meg egyszer keresunk KIZAROLAG az utvonalra. Ami igy elokerul, az
|
|
764
|
+
// UGYANANNAK a fajlnak a korabbi feldolgozasa — a delta-compare igy `equal`/`modified`-nak latja
|
|
765
|
+
// (adoptalja), nem duplikal. Ha a scope tenyleg valtozott, a `backfill`/update agak vezetik at.
|
|
766
|
+
return dataService.findHydratableList({ 'source.absolutePath': absolutePath });
|
|
747
767
|
}
|
|
748
768
|
/**
|
|
749
769
|
* Törölt-fájl reconciliation (whole-file orphan, dsgn-004 §4.3 kiterjesztés). A scan-folder/scan-project
|
|
@@ -480,6 +480,59 @@ class FAM_ScanJob_ControlService {
|
|
|
480
480
|
* útvonalak a HÍVÓ `sourceLocation`-jéhez oldódnak fel (a szerver cwd-je eltér a CLI-étől — user-FR 2026-06-22):
|
|
481
481
|
* a `configPath`/`root` ÉS a végső per-projekt `path` is.
|
|
482
482
|
*/
|
|
483
|
+
/**
|
|
484
|
+
* UTVONAL-SZERINTI DEDUP a cel-listan (2026-09-01, mert gyoker-ok). Ha UGYANAZ a mappa TOBB projekt-nev alatt
|
|
485
|
+
* szerepel, a scan ugyanazt a fajlt tobbszor olvassa be — es mivel a delta-osszevetes scope-ra szurt, a ket
|
|
486
|
+
* menet VAK egymasra, igy TELJES DUPLIKALAS keletkezik.
|
|
487
|
+
*
|
|
488
|
+
* MERVE: az utolso heti scan (2026-08-31) a `fdp-e2e-full/package.json`-t KETSZER vitte be, ket nev alatt
|
|
489
|
+
* (`futdevpro-e2e` 08:39 es `fdp-e2e-full` 09:32). Flotta-szinten 51 578 dupla-csoport, MIND kulonbozo
|
|
490
|
+
* scopeId alatt, osszesen ~59 000 folosleges chunk. Az ok: a heti re-scan a KORABBAN szkennelt celok
|
|
491
|
+
* UNIOJAT hasznalja, igy egy projekt-atnevezes utan a REGI es az UJ nev is bent marad — orokre.
|
|
492
|
+
*
|
|
493
|
+
* Az ELSO elofordulas nyer (a hivo altal megadott sorrend/az ujabb felderites elol all). A kihagyast
|
|
494
|
+
* LOGOLJUK — nema szures nem lehet (`core-rule-integrity` szelleme: ami kimarad, az latszodjon).
|
|
495
|
+
*/
|
|
496
|
+
/** A projekt-név megegyezik-e a mappa nevével (a dedup döntőbírója — a lemez az igazság). */
|
|
497
|
+
static matchesFolderName(spec) {
|
|
498
|
+
const folder = String(spec.path ?? '')
|
|
499
|
+
.replace(/[\\/]+/g, '/')
|
|
500
|
+
.replace(/\/+$/, '')
|
|
501
|
+
.split('/')
|
|
502
|
+
.pop() ?? '';
|
|
503
|
+
return folder.length > 0 && folder.toLowerCase() === String(spec.project ?? '').toLowerCase();
|
|
504
|
+
}
|
|
505
|
+
static dedupeByPath(specs) {
|
|
506
|
+
const seen = new Map();
|
|
507
|
+
const dropped = [];
|
|
508
|
+
for (const spec of specs) {
|
|
509
|
+
// Az elvalasztot NORMALIZALJUK (a `\\` es a `/` ugyanaz a mappa Windowson), a zaro elvalasztot
|
|
510
|
+
// levagjuk, es kisbetusitunk — kulonben ugyanaz a cel ket irasmodban ketszer futna le.
|
|
511
|
+
const key = String(spec.path ?? '')
|
|
512
|
+
.replace(/[\\/]+/g, '/')
|
|
513
|
+
.replace(/\/+$/, '')
|
|
514
|
+
.toLowerCase();
|
|
515
|
+
const kept = seen.get(key);
|
|
516
|
+
if (kept) {
|
|
517
|
+
// DÖNTŐBÍRÓ: az a projekt-név nyer, ami a MAPPA NEVÉVEL egyezik. Átnevezés után a heti
|
|
518
|
+
// re-scan cél-uniója a RÉGI nevet is viszi, és az jellemzően előrébb áll — „első nyer"
|
|
519
|
+
// alapon a leváltott név maradna örökre. A mappa neve viszont a lemezen álló IGAZSÁG.
|
|
520
|
+
const winner = FAM_ScanJob_ControlService.matchesFolderName(spec)
|
|
521
|
+
&& !FAM_ScanJob_ControlService.matchesFolderName(kept) ? spec : kept;
|
|
522
|
+
const loser = winner === spec ? kept : spec;
|
|
523
|
+
dropped.push({ project: loser.project, keptAs: winner.project, path: spec.path });
|
|
524
|
+
seen.set(key, winner);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
seen.set(key, spec);
|
|
528
|
+
}
|
|
529
|
+
if (dropped.length) {
|
|
530
|
+
fsm_dynamo_1.DyFM_Log.warn(`[scanJob] UTVONAL-DEDUP: ${dropped.length} cel kihagyva, mert ugyanarra a mappara mutat, `
|
|
531
|
+
+ `mint egy masik projekt-nev (ez okozta a heti dupla-beolvasast): `
|
|
532
|
+
+ dropped.map((d) => `${d.project} -> ${d.keptAs}`).join(', '));
|
|
533
|
+
}
|
|
534
|
+
return [...seen.values()];
|
|
535
|
+
}
|
|
483
536
|
static resolveSpecs(input) {
|
|
484
537
|
const base = (target) => (target && input.sourceLocation && !(0, path_1.isAbsolute)(target)) ? (0, path_1.resolve)(input.sourceLocation, target) : target;
|
|
485
538
|
let specs = [];
|
|
@@ -497,7 +550,8 @@ class FAM_ScanJob_ControlService {
|
|
|
497
550
|
specs = [...specs, ...input.extraTargets];
|
|
498
551
|
}
|
|
499
552
|
// A per-projekt `path` relatív → a sourceLocation-höz feloldva (a discover már abszolútat ad; a config relatív lehet).
|
|
500
|
-
|
|
553
|
+
const resolved = specs.map((spec) => ({ ...spec, path: base(spec.path) ?? spec.path }));
|
|
554
|
+
return FAM_ScanJob_ControlService.dedupeByPath(resolved);
|
|
501
555
|
}
|
|
502
556
|
/** Egy szám-config feloldása (a hiba/hiány a fallback-ra esik — a cooldown sosem buktatja a job-ot). */
|
|
503
557
|
static async resolveConfigNumber(key, fallback) {
|
|
@@ -4,6 +4,7 @@ exports.FAM_ScanScheduler_ControlService = void 0;
|
|
|
4
4
|
const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
|
|
5
5
|
const fam_config_level_type_enum_1 = require("../../../_enums/fam-config-level.type-enum");
|
|
6
6
|
const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
|
|
7
|
+
const retrieval_1 = require("../../retrieval");
|
|
7
8
|
const fam_scan_job_control_service_1 = require("./fam-scan-job.control-service");
|
|
8
9
|
const fam_scan_job_data_service_1 = require("./fam-scan-job.data-service");
|
|
9
10
|
const embedding_1 = require("../../embedding");
|
|
@@ -120,6 +121,10 @@ class FAM_ScanScheduler_ControlService {
|
|
|
120
121
|
+ `(slot: ${FAM_ScanScheduler_ControlService.DAY_NAMES[day] ?? day} ${hour}:00). Indítás…`);
|
|
121
122
|
await this.markRan(config, nowMs);
|
|
122
123
|
await this.triggerRescan();
|
|
124
|
+
// RETENCIÓ (2026-09-02): a heti ütemben az alvó törmelék nyugdíjazása is fut — a lényege az
|
|
125
|
+
// automatizmus (különben a törmelék sosem vonul nyugdíjba). A scan-tól FÜGGETLEN munka, nem
|
|
126
|
+
// várja meg (a scan async, waitMs:0); best-effort: a hibája nem buktatja a ticket.
|
|
127
|
+
await this.runWeeklyRetention();
|
|
123
128
|
}
|
|
124
129
|
catch (error) {
|
|
125
130
|
fsm_dynamo_1.DyFM_Log.warn(`[FAM scanScheduler] heti re-scan tick sikertelen (best-effort): ${error?.message}`);
|
|
@@ -128,6 +133,27 @@ class FAM_ScanScheduler_ControlService {
|
|
|
128
133
|
this.ticking = false;
|
|
129
134
|
}
|
|
130
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* A heti retenció: az alvó törmelék (`user_prompt`/`temporary_context`, sosem felidézett, 30+ napos)
|
|
138
|
+
* nyugdíjazása. Kapu: `memory.retireTemporaryEnabled` (default true). A szabály + indoklás:
|
|
139
|
+
* `retrieval/_collections/fam-memory-retention.util.ts`. Best-effort — a hiba WARN, nem tick-bukás.
|
|
140
|
+
*/
|
|
141
|
+
async runWeeklyRetention() {
|
|
142
|
+
try {
|
|
143
|
+
const retention = retrieval_1.FAM_MemoryRetention_ControlService.getInstance();
|
|
144
|
+
if (!(await retention.isAutoEnabled())) {
|
|
145
|
+
fsm_dynamo_1.DyFM_Log.log('[FAM scanScheduler] heti retenció KIKAPCSOLVA (`memory.retireTemporaryEnabled=false`) — kihagyva.');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const report = await retention.retire({ dryRun: false });
|
|
149
|
+
fsm_dynamo_1.DyFM_Log.log(`[FAM scanScheduler] heti retenció kész: ${report.retired} nyugdíjazva `
|
|
150
|
+
+ `(${report.scanned} jelöltből; ${report.keptTooYoung} még fiatal, `
|
|
151
|
+
+ `${report.alreadySuperseded} már nyugdíjas).`);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
fsm_dynamo_1.DyFM_Log.warn(`[FAM scanScheduler] heti retenció sikertelen (best-effort): ${error?.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
131
157
|
/** A TELJES re-scan indítása: `scan.weeklyRescanRoot` (ha van) VAGY a legutóbbi durable scan-job cél-specjei. */
|
|
132
158
|
async triggerRescan() {
|
|
133
159
|
const origin = { agent: 'fam-scheduler', label: 'weekly-rescan' };
|
|
@@ -66,6 +66,17 @@ const READ_INPUT_SCHEMA = {
|
|
|
66
66
|
description: 'Feltárás-mélység: `deep` → mély feltárás (sokkal több chunk; a `read.deepTopK` '
|
|
67
67
|
+ 'az alap topK helyett). Default `normal`. Az explicit `topK` mindig felülír.',
|
|
68
68
|
},
|
|
69
|
+
skipReactivation: {
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
description: 'DIAGNOSZTIKA: a visszaadott memory-hitek NE kapjanak felidézés-megerősítést '
|
|
72
|
+
+ '(recallCount++). Mérő-/kalibrációs forgalomhoz — a mérés nem terhelheti a mért rendszert.',
|
|
73
|
+
},
|
|
74
|
+
includeCrossTableProbe: {
|
|
75
|
+
type: 'boolean',
|
|
76
|
+
description: 'DIAGNOSZTIKA (nem termék-funkció): a kereszt-tár szonda nyers döntési adata '
|
|
77
|
+
+ '(`crossTableProbe`) akkor is kerüljön a válaszba, ha a küszöb alatt maradt — a '
|
|
78
|
+
+ 'küszöb-kalibráció csak így mérhető a rendszer VALÓS viselkedésén. A `hits`-hez nem nyúl.',
|
|
79
|
+
},
|
|
69
80
|
},
|
|
70
81
|
required: ['tables', 'query'],
|
|
71
82
|
additionalProperties: false,
|
|
@@ -319,6 +319,8 @@ class FAM_ReadTool_Service {
|
|
|
319
319
|
kindFilter: query.kindFilter,
|
|
320
320
|
excludeIds: query.excludeIds,
|
|
321
321
|
depth: query.depth,
|
|
322
|
+
includeCrossTableProbe: query.includeCrossTableProbe,
|
|
323
|
+
skipReactivation: query.skipReactivation,
|
|
322
324
|
// Per-query tartalom-kapcsolo atvezetese (2026-08-14 lelet: eddig itt VESZETT EL).
|
|
323
325
|
includeContent: query.includeContent,
|
|
324
326
|
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FAM_WriteTool_Service = void 0;
|
|
4
4
|
const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
|
|
5
|
+
const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
|
|
5
6
|
const error_codes_const_1 = require("../../../_collections/error-codes.const");
|
|
6
7
|
const fam_error_factory_util_1 = require("../../../_collections/fam-error-factory.util");
|
|
7
8
|
const fam_entry_data_model_1 = require("../../../_models/data-models/fam-entry.data-model");
|
|
@@ -333,19 +334,40 @@ class FAM_WriteTool_Service {
|
|
|
333
334
|
// FAM-REV-051 bulk: `sourceFilePath`/`absolutePath` → egy fájl ÖSSZES chunkja; `id`/`query` → egyetlen.
|
|
334
335
|
const targets = await this.resolveDeleteTargets(input, dataService, 'write-delete');
|
|
335
336
|
const deletedIds = [];
|
|
337
|
+
// RETIRE-NOT-DELETE a `memory` táron (2026-09-02, owner-elv: „nem baj, ha fennmarad olyan memória,
|
|
338
|
+
// amit a Claude már elfelejtett — sőt!"). A memória-törlés NYUGDÍJAZÁS: `superseded + retiredAt`,
|
|
339
|
+
// a tartalom és a vektor marad (a penalty süllyeszti, a cold-search megtalálja). Más táron a
|
|
340
|
+
// viselkedés VÁLTOZATLAN — ott a fájl az igazság, a scan a törölt chunkot úgyis újra behozná.
|
|
341
|
+
// Valódi hard-delete-re a scope-maintenance reset az admin-út.
|
|
342
|
+
const retireInsteadOfDelete = input.table === fam_table_type_enum_1.FAM_Table.memory;
|
|
343
|
+
const now = Date.now();
|
|
336
344
|
for (const entry of targets) {
|
|
337
345
|
if (!entry._id) {
|
|
338
346
|
continue;
|
|
339
347
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
348
|
+
if (retireInsteadOfDelete) {
|
|
349
|
+
await dataService.updateData({
|
|
350
|
+
filterBy: { _id: entry._id },
|
|
351
|
+
update: { $set: { superseded: true, retiredAt: now } },
|
|
352
|
+
});
|
|
353
|
+
// A vektor a poolban MARAD — a superseded-szemantika része (a rangsor-büntetés süllyeszti).
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
// absolute=false → soft-delete (Dynamo-konvenció: `_deleted` marker, archív collection).
|
|
357
|
+
await dataService.deleteData(entry._id, false);
|
|
358
|
+
// Pool-coherence (FAM-REV-046): a törölt vektort a hívó-process in-memory LVS-pool-jából is kivesszük.
|
|
359
|
+
embedding_1.FAM_VectorSearch_ControlService.getInstance().removeVector(input.table, entry._id);
|
|
360
|
+
}
|
|
344
361
|
deletedIds.push(entry._id);
|
|
345
362
|
}
|
|
346
|
-
const warnings =
|
|
347
|
-
|
|
348
|
-
:
|
|
363
|
+
const warnings = [];
|
|
364
|
+
if (retireInsteadOfDelete && deletedIds.length) {
|
|
365
|
+
warnings.push(`A memory táron a törlés NYUGDÍJAZÁS: ${deletedIds.length} bejegyzés superseded+retiredAt `
|
|
366
|
+
+ 'jelölést kapott — hidegen kereshető marad, nem törlődött (owner-elv: a felejtett memória is érték).');
|
|
367
|
+
}
|
|
368
|
+
else if (deletedIds.length > 1) {
|
|
369
|
+
warnings.push(`Bulk file-delete: ${deletedIds.length} chunk soft-delete-elve (cél: ${input.target?.by}='${input.target?.value}').`);
|
|
370
|
+
}
|
|
349
371
|
return this.baseOutput({
|
|
350
372
|
operation: input.operation, table: input.table,
|
|
351
373
|
scopePath: (targets[0]?.scopePath ?? []),
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FAM_CrossTableNote_Util = void 0;
|
|
4
|
+
const fam_memory_retention_util_1 = require("./fam-memory-retention.util");
|
|
5
|
+
/**
|
|
6
|
+
* NEM-TUDÁS jelöltek: nyers beszélgetés-törmelék, amivel SOHA nem szabad „mellesleg"-et adni.
|
|
7
|
+
*
|
|
8
|
+
* **Mért indok (2026-09-01):** az aktív `memory` tár **20,2%-a (8 717 bejegyzés) `kind: user_prompt` /
|
|
9
|
+
* `tags: [temporary_context]`** — szó szerinti user-input, amit a session tárolt el kontextusnak. Ezek
|
|
10
|
+
* rövidek és tematikusak, ezért a nyers koszinuszuk MAGASRA fut, és a 44 esetes benchmarkban a 12
|
|
11
|
+
* legerősebb jelöltből **10 ilyen volt**. Egy nyers prompt-idézet nem válasz a „láttuk már ezt?"
|
|
12
|
+
* kérdésre — a javaslat tőle nem hasznos, hanem félrevezető.
|
|
13
|
+
*
|
|
14
|
+
* Ugyanezen az alapon esik ki a **`fs-summary` / `scan-summary`** is: az egy mappa-listázás gépi
|
|
15
|
+
* összefoglalója. A benchmarkban pont a döntési zónában ült (+0,022), és semmilyen kérdésre nem válasz.
|
|
16
|
+
* (A scan-summary zaja külön is mért, ismert probléma — `reference_fam_scan_summary_weight_cap`.)
|
|
17
|
+
*/
|
|
18
|
+
// A MEMORIA-tormelek definicioja a KOZOS utilbol jon (fam-memory-retention.util) — egy definicio,
|
|
19
|
+
// harom felhasznalas (retencio + reaktivacio-szuro + kereszt-tar szonda). Itt CSAK a knowledge-tar
|
|
20
|
+
// sajat zaja (fs-summary/scan-summary) egeszul ki.
|
|
21
|
+
const NON_KNOWLEDGE_KINDS = ['fs-summary'];
|
|
22
|
+
const NON_KNOWLEDGE_TAGS = ['scan-summary'];
|
|
23
|
+
class FAM_CrossTableNote_Util {
|
|
24
|
+
/**
|
|
25
|
+
* A döntés. `undefined` = **csend**: nincs mező a válaszban. Szándékosan NEM adunk „nem találtam
|
|
26
|
+
* erősebbet" üzenetet — az minden válaszra ráterhelne egy sort azért, hogy semmit ne közöljön.
|
|
27
|
+
*/
|
|
28
|
+
static decide(input) {
|
|
29
|
+
const usable = input.candidates.filter((candidate) => candidate.superseded !== true
|
|
30
|
+
&& !FAM_CrossTableNote_Util.isNonKnowledge(candidate)
|
|
31
|
+
&& Number.isFinite(candidate.score)
|
|
32
|
+
&& candidate.score >= input.minScore);
|
|
33
|
+
if (!usable.length || !(input.minDelta > 0)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
// LEGFELJEBB EGY tétel: a mérés szerint úgyis 0-1 van, és egy 3-elemű mellék-lista már hígítás
|
|
37
|
+
// — pontosan az a zaj, ami miatt nem a `hits`-be tesszük.
|
|
38
|
+
const best = [...usable].sort((a, b) => b.score - a.score)[0];
|
|
39
|
+
const delta = best.score - input.primaryTopScore;
|
|
40
|
+
if (delta < input.minDelta) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const heading = best.headingPath?.length ? best.headingPath.join(' › ') : undefined;
|
|
44
|
+
return {
|
|
45
|
+
table: best.table,
|
|
46
|
+
delta: Number(delta.toFixed(4)),
|
|
47
|
+
hit: {
|
|
48
|
+
id: best.id,
|
|
49
|
+
table: best.table,
|
|
50
|
+
score: best.score,
|
|
51
|
+
sourceFilePath: best.sourceFilePath,
|
|
52
|
+
heading: heading,
|
|
53
|
+
label: FAM_CrossTableNote_Util.label(best),
|
|
54
|
+
},
|
|
55
|
+
message: FAM_CrossTableNote_Util.message({
|
|
56
|
+
table: best.table,
|
|
57
|
+
candidateScore: best.score,
|
|
58
|
+
primaryTopScore: input.primaryTopScore,
|
|
59
|
+
location: FAM_CrossTableNote_Util.label(best),
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Beszélgetés-törmelék / gépi zaj-e a jelölt. A memória-törmelék részt a közös util dönti el. */
|
|
64
|
+
static isNonKnowledge(candidate) {
|
|
65
|
+
if (fam_memory_retention_util_1.FAM_MemoryRetention_Util.isDebris({ kind: candidate.kind, tags: candidate.tags })) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (candidate.kind && NON_KNOWLEDGE_KINDS.includes(candidate.kind)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return (candidate.tags ?? []).some((tag) => NON_KNOWLEDGE_TAGS.includes(tag));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A jelölt AZONOSÍTHATÓ megnevezése. A fájl-út a legjobb, de az MCP-n ÍRT memória-bejegyzésnek nincs
|
|
75
|
+
* (mérve: a 21 szonda-jelöltből 16-nak). Ott a nyers ObjectId semmit nem mond a hívónak — a `kind` +
|
|
76
|
+
* `tags` + `scope` viszont pontosan megmondja, MIRŐL szól a jegyzet, és hogy érdemes-e megnyitni.
|
|
77
|
+
*/
|
|
78
|
+
static label(candidate) {
|
|
79
|
+
if (candidate.sourceFilePath) {
|
|
80
|
+
return candidate.sourceFilePath;
|
|
81
|
+
}
|
|
82
|
+
if (candidate.headingPath?.length) {
|
|
83
|
+
return candidate.headingPath.join(' › ');
|
|
84
|
+
}
|
|
85
|
+
const parts = [];
|
|
86
|
+
if (candidate.kind) {
|
|
87
|
+
parts.push(candidate.kind);
|
|
88
|
+
}
|
|
89
|
+
if (candidate.tags?.length) {
|
|
90
|
+
parts.push(`[${candidate.tags.slice(0, 4).join(', ')}]`);
|
|
91
|
+
}
|
|
92
|
+
if (candidate.scope) {
|
|
93
|
+
parts.push(`(${candidate.scope})`);
|
|
94
|
+
}
|
|
95
|
+
// Az `id` MINDIG ott van a `hit.id`-ben — a szövegbe csak akkor kerül, ha tényleg nincs jobb.
|
|
96
|
+
return parts.length ? `${parts.join(' ')} — id: ${candidate.id}` : `id: ${candidate.id}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A szonda nyers döntési adata (diagnosztika). A `superseded` jelölt SZÁNDÉKOSAN benne marad a
|
|
100
|
+
* legjobb-választásban itt — a mérésnek látnia kell, ha a szonda amúgy egy leváltott generációt
|
|
101
|
+
* hozott volna; a `decide` ettől függetlenül kizárja.
|
|
102
|
+
*/
|
|
103
|
+
static describeProbe(primaryTopScore, candidates, fired) {
|
|
104
|
+
// A diagnosztika a KÜSZÖB-független képet adja, de a TARTALMI kizárásokat (leváltott generáció,
|
|
105
|
+
// beszélgetés-törmelék) ALKALMAZZA — különben a kalibráció olyan jelöltre számolna deltát, amit a
|
|
106
|
+
// rendszer sosem ajánlana, és megint nem azt mérnénk, ami ténylegesen történik.
|
|
107
|
+
const eligible = candidates.filter((candidate) => candidate.superseded !== true
|
|
108
|
+
&& !FAM_CrossTableNote_Util.isNonKnowledge(candidate)
|
|
109
|
+
&& Number.isFinite(candidate.score));
|
|
110
|
+
const best = [...eligible]
|
|
111
|
+
.sort((a, b) => b.score - a.score)[0];
|
|
112
|
+
return {
|
|
113
|
+
primaryTopScore: primaryTopScore,
|
|
114
|
+
candidateCount: candidates.length,
|
|
115
|
+
eligibleCount: eligible.length,
|
|
116
|
+
best: best ? {
|
|
117
|
+
table: best.table,
|
|
118
|
+
score: best.score,
|
|
119
|
+
delta: Number((best.score - primaryTopScore).toFixed(4)),
|
|
120
|
+
label: FAM_CrossTableNote_Util.label(best),
|
|
121
|
+
superseded: best.superseded,
|
|
122
|
+
} : undefined,
|
|
123
|
+
fired: fired,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Az ember-olvasható mondat. Megmondja, MI van ott és MIÉRT szólunk — a hívó dönt, mi lesz vele. */
|
|
127
|
+
static message(set) {
|
|
128
|
+
const comparison = set.primaryTopScore > 0
|
|
129
|
+
? `${set.candidateScore.toFixed(3)} vs ${set.primaryTopScore.toFixed(3)}`
|
|
130
|
+
: `${set.candidateScore.toFixed(3)}, miközben a kért tárban nem volt érdemi találat`;
|
|
131
|
+
return `Mellesleg: a(z) \`${set.table}\` tárban ÉRDEMBEN erősebb találat van erre (${comparison}) — `
|
|
132
|
+
+ `${set.location}. A fenti listát ez NEM módosította; ha a kérdés inkább „láttuk-e már ezt", `
|
|
133
|
+
+ 'érdemes lehet oda is belenézni.';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.FAM_CrossTableNote_Util = FAM_CrossTableNote_Util;
|
|
@@ -31,6 +31,25 @@ class FAM_LexicalMatch_Util {
|
|
|
31
31
|
];
|
|
32
32
|
/** A tő MINIMÁLIS hossza tövezés után (a túl-tövezés hamis egyezéseket hozna). */
|
|
33
33
|
static MIN_STEM_LEN = 4;
|
|
34
|
+
/**
|
|
35
|
+
* SZÓ SZERINTI IDÉZET-EGYEZÉS (2026-09-02, black-box futás lelete). A term-átfedés egy HALMAZ-arány:
|
|
36
|
+
* nem tudja megkülönböztetni a „ugyanazok a szavak szétszórva" esetet a „pontosan ez a mondat van benne"
|
|
37
|
+
* esettől — pedig az utóbbi nagyságrenddel erősebb relevancia-jel.
|
|
38
|
+
*
|
|
39
|
+
* **Mért eset:** a `402 Payment Required - You must sign up for private packages` hibaüzenet szó szerinti
|
|
40
|
+
* bemásolása (a legtermészetesebb fejlesztői reflex) a `fdp-never-public-packages` szabályt a **#7**
|
|
41
|
+
* helyre tette 60 találatból — holott a szabály-fájl **literálisan tartalmazza ezt a mondatot**, és övé
|
|
42
|
+
* volt a mezőny LEGMAGASABB term-átfedése (0,325). A nyers koszinusza viszont csak 0,480 volt (egy
|
|
43
|
+
* kontextus nélküli hibaüzenet gyenge szemantikus jel), és a ×1,195-ös boost nem hozta fel.
|
|
44
|
+
*
|
|
45
|
+
* A phrase-bónusz ezt a hibaosztályt fedi — bemásolt hibaüzenet, idézett mondat, pontos azonosító —, és
|
|
46
|
+
* a PARAFRAZEÁLT kérdéseket NEM érinti: ott nincs hosszú, szó szerinti egyezés, tehát nincs bónusz sem.
|
|
47
|
+
*/
|
|
48
|
+
static MIN_PHRASE_LEN = 3;
|
|
49
|
+
/** A phrase-bónusz maximuma (a teljes query-hossznyi szó szerinti egyezésre jár). */
|
|
50
|
+
static PHRASE_BONUS = 0.6;
|
|
51
|
+
/** A vizsgált query-tokenek plafonja — egy hosszú prompt ne csináljon O(n²) munkát hit-enként. */
|
|
52
|
+
static MAX_PHRASE_TOKENS = 24;
|
|
34
53
|
/** Tokenizálás: kisbetűsít, nem-alfanumerikuson tör, stopword + rövid token kihagyva, DEDUP (Set). */
|
|
35
54
|
static tokenize(text) {
|
|
36
55
|
const terms = new Set();
|
|
@@ -62,12 +81,53 @@ class FAM_LexicalMatch_Util {
|
|
|
62
81
|
}
|
|
63
82
|
return undefined;
|
|
64
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* NYERS, SORRENDHELYES token-sorozat (a phrase-egyezéshez). A `tokenize`-zal ellentétben **NEM** szűr
|
|
86
|
+
* stopwordöt és **NEM** dedupál: a szó szerinti idézetben a kötőszavak is a mondat részei — pont azok
|
|
87
|
+
* teszik idézetté. A rövid tokenek (`MIN_TOKEN_LEN` alatt) itt is kimaradnak, mert a normalizált
|
|
88
|
+
* szövegben sem szerepelnek.
|
|
89
|
+
*/
|
|
90
|
+
static tokenizeSequence(text) {
|
|
91
|
+
if (!text) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
return text.toLowerCase()
|
|
95
|
+
.split(/[^a-z0-9áéíóöőúüű]+/)
|
|
96
|
+
.filter((token) => token.length >= FAM_LexicalMatch_Util.MIN_TOKEN_LEN);
|
|
97
|
+
}
|
|
98
|
+
/** Az entry kereshető szövegének normalizált alakja (kisbetűs, egy-szóközös) — a phrase-kereséshez. */
|
|
99
|
+
static normalizeForPhrase(text) {
|
|
100
|
+
if (!text) {
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
return ` ${text.toLowerCase().split(/[^a-z0-9áéíóöőúüű]+/).filter(Boolean).join(' ')} `;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A leghosszabb SZÓ SZERINTI, összefüggő query-részlet hossza, ami az entry szövegében is megvan
|
|
107
|
+
* (0, ha nincs `MIN_PHRASE_LEN`-nél hosszabb egyezés). A leghosszabb ablaktól indul és az ELSŐ
|
|
108
|
+
* egyezésnél visszatér — így a tipikus „nincs idézet" eset is korlátos munkával zárul.
|
|
109
|
+
*/
|
|
110
|
+
static longestPhraseRun(queryTokens, normalizedEntryText) {
|
|
111
|
+
const tokens = queryTokens.slice(0, FAM_LexicalMatch_Util.MAX_PHRASE_TOKENS);
|
|
112
|
+
if (tokens.length < FAM_LexicalMatch_Util.MIN_PHRASE_LEN || !normalizedEntryText) {
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
for (let length = tokens.length; length >= FAM_LexicalMatch_Util.MIN_PHRASE_LEN; length--) {
|
|
116
|
+
for (let start = 0; start + length <= tokens.length; start++) {
|
|
117
|
+
const phrase = ` ${tokens.slice(start, start + length).join(' ')} `;
|
|
118
|
+
if (normalizedEntryText.includes(phrase)) {
|
|
119
|
+
return length;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
65
125
|
/**
|
|
66
126
|
* Lexikai átfedés-score (0..1): a `queryTerms` hányad része van jelen az entry kereshető szövegében.
|
|
67
127
|
* `content`-egyezés = base-átfedés; `headingPath`/`sourceFilePath`-egyezés = + `TITLE_PATH_BONUS` súly (1-re vágva).
|
|
68
128
|
* Üres query-term-halmaz → 0 (nincs jel → nincs boost). PURE.
|
|
69
129
|
*/
|
|
70
|
-
static score(queryTerms, entry) {
|
|
130
|
+
static score(queryTerms, entry, queryPhraseTokens) {
|
|
71
131
|
if (!queryTerms.size) {
|
|
72
132
|
return 0;
|
|
73
133
|
}
|
|
@@ -94,7 +154,20 @@ class FAM_LexicalMatch_Util {
|
|
|
94
154
|
const total = queryTerms.size;
|
|
95
155
|
const base = baseMatched / total;
|
|
96
156
|
const titleBonus = (titleMatched / total) * FAM_LexicalMatch_Util.TITLE_PATH_BONUS;
|
|
97
|
-
|
|
157
|
+
// SZÓ SZERINTI IDÉZET-BÓNUSZ: a query hány százaléka szerepel ÖSSZEFÜGGŐEN, szó szerint az entry
|
|
158
|
+
// szövegében. A halmaz-átfedés ezt nem látja (neki a szórt egyezés ugyanannyi) — pedig egy bemásolt
|
|
159
|
+
// hibaüzenet vagy idézett mondat majdnem biztos relevancia-jel. Az indoklás + a mért eset a
|
|
160
|
+
// `MIN_PHRASE_LEN` konstans doc-commentjében.
|
|
161
|
+
let phraseBonus = 0;
|
|
162
|
+
if (queryPhraseTokens && queryPhraseTokens.length) {
|
|
163
|
+
const searchable = FAM_LexicalMatch_Util.normalizeForPhrase(`${entry.content ?? ''} ${(entry.headingPath ?? []).join(' ')} ${entry.sourceFilePath ?? ''}`);
|
|
164
|
+
const run = FAM_LexicalMatch_Util.longestPhraseRun(queryPhraseTokens, searchable);
|
|
165
|
+
if (run > 0) {
|
|
166
|
+
const considered = Math.min(queryPhraseTokens.length, FAM_LexicalMatch_Util.MAX_PHRASE_TOKENS);
|
|
167
|
+
phraseBonus = FAM_LexicalMatch_Util.PHRASE_BONUS * (run / considered);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return Math.min(1, base + titleBonus + phraseBonus);
|
|
98
171
|
}
|
|
99
172
|
}
|
|
100
173
|
exports.FAM_LexicalMatch_Util = FAM_LexicalMatch_Util;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SZÓ SZERINTI IDÉZET-MENTÉS — a döntés, hogy egy query megéri-e a pontos szöveg-keresést (2026-09-02).
|
|
4
|
+
*
|
|
5
|
+
* **A mért hiba.** A black-box futás leletét külön ellenőrizve: a
|
|
6
|
+
* `402 Payment Required - You must sign up for private packages` hibaüzenet **szó szerinti bemásolása** —
|
|
7
|
+
* a legtermészetesebb fejlesztői reflex — a szabályt nem hozta be. Az első hipotézisem (rangsor-hiba) a
|
|
8
|
+
* mérésen **MEGBUKOTT**: a 60 visszaadott találatból **egy sem tartalmazta a „402"-t**, vagyis a mondatot
|
|
9
|
+
* hordozó chunk **be sem került a jelöltek közé**. Ez **recall-hiba**: egy kontextus nélküli hibaüzenet
|
|
10
|
+
* embeddingje távol esik a szabály-próza embeddingjétől, és amit a vektor-réteg nem hoz be, azt semmilyen
|
|
11
|
+
* rangsor-javítás nem érheti el.
|
|
12
|
+
*
|
|
13
|
+
* **A megoldás mintája MÁR LÉTEZIK:** a FEAT-003 kód-expanzió (`applyCodeExpansion`) pontosan ezt csinálja
|
|
14
|
+
* azonosító-kódokra — ha a query kódot tartalmaz, a citáló elemek determinisztikusan bekerülnek, a
|
|
15
|
+
* vektor-rangsortól függetlenül. Ez a util ugyanezt terjeszti ki a **szó szerinti idézetre**.
|
|
16
|
+
*
|
|
17
|
+
* **Miért kell szigorú kapu.** A mentés Mongo-oldali szöveg-keresés, ami a nagy tárakon collection-scan.
|
|
18
|
+
* Mérve (2026-09-02): `rules` 296 ms · `coding_patterns` 504 ms · `knowledge` 2,1 s · `memory` 4,9 s ·
|
|
19
|
+
* `documents` 13,1 s · `codebase` 45,2 s. Ezért a mentés (1) csak IDÉZET-SZERŰ query-re fut, és (2) csak
|
|
20
|
+
* a `read.literalRescueMaxDocs` alatti méretű tárakon.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.FAM_LiteralPhrase_Util = void 0;
|
|
24
|
+
/** A minimális token-szám: ennél rövidebb részlet nem idézet, hanem véletlen szó-egybeesés. */
|
|
25
|
+
const MIN_TOKENS = 5;
|
|
26
|
+
/** A maximális token-szám: a hosszú promptot levágjuk (a regex-költség és a találhatóság miatt is). */
|
|
27
|
+
const MAX_TOKENS = 16;
|
|
28
|
+
/**
|
|
29
|
+
* A „disztinktív" token minimális hossza. Egy csupa rövid, köznapi szóból álló mondat (`mi a teendo ha a
|
|
30
|
+
* build elszall`) nem idézet — az a normál szemantikus keresés dolga. Számjegyet tartalmazó token (hibakód,
|
|
31
|
+
* verzió, azonosító) MINDIG disztinktívnek számít.
|
|
32
|
+
*/
|
|
33
|
+
const DISTINCTIVE_LEN = 7;
|
|
34
|
+
class FAM_LiteralPhrase_Util {
|
|
35
|
+
/**
|
|
36
|
+
* IDÉZET-SZERŰ-e a query, és ha igen, mit keressünk. `undefined` = nem éri meg a szöveg-keresés.
|
|
37
|
+
*
|
|
38
|
+
* **Ismert korlát, nyíltan:** a keresett szöveg a query ELEJÉTŐL vett, `MAX_TOKENS`-ig tartó összefüggő
|
|
39
|
+
* részlet — vagyis a TISZTÁN bemásolt idézetre működik. Ha a felhasználó saját mondatba ágyazza az
|
|
40
|
+
* idézetet („npm publish failed with 402 … what should I do"), a szó szerinti egyezés elmarad, és a
|
|
41
|
+
* mentés nem talál. Az a hibaosztály viszont **mérve nem is szorul mentésre**: ott a normál vektor-út
|
|
42
|
+
* behozza a szabályt (#2 helyezés). A mentés a lyukat foltozza, nem a működő utat írja át.
|
|
43
|
+
*/
|
|
44
|
+
static extract(query) {
|
|
45
|
+
const tokens = String(query ?? '')
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.split(/[^a-z0-9áéíóöőúüű]+/)
|
|
48
|
+
.filter((token) => token.length >= 2);
|
|
49
|
+
if (tokens.length < MIN_TOKENS) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const window = tokens.slice(0, MAX_TOKENS);
|
|
53
|
+
if (!window.some((token) => FAM_LiteralPhrase_Util.isDistinctive(token))) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return { phrase: window.join(' '), tokenCount: window.length };
|
|
57
|
+
}
|
|
58
|
+
/** Disztinktív-e a token: számjegyet tartalmaz (hibakód/verzió/azonosító) VAGY elég hosszú. */
|
|
59
|
+
static isDistinctive(token) {
|
|
60
|
+
return /[0-9]/.test(token) || token.length >= DISTINCTIVE_LEN;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A szó szerinti kereséshez használt regex: a tokenek között TETSZŐLEGES nem-alfanumerikus elválasztó
|
|
64
|
+
* állhat, mert a korpuszban a mondat írásjelekkel, sortöréssel, backtickkel tagolt (`402 Payment
|
|
65
|
+
* Required — You must…`). A tokenek escape-elve — a query SOHA nem tud regexet injektálni.
|
|
66
|
+
*/
|
|
67
|
+
static toRegexSource(phrase) {
|
|
68
|
+
return phrase
|
|
69
|
+
.split(' ')
|
|
70
|
+
.map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
71
|
+
.join('[^a-z0-9áéíóöőúüű]+');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.FAM_LiteralPhrase_Util = FAM_LiteralPhrase_Util;
|