@futdevpro/fdp-agent-memory 1.1.130 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@futdevpro/fdp-agent-memory",
3
- "version": "1.1.130",
3
+ "version": "1.1.133",
4
4
  "description": "Local-first, vector-backed multi-table agent memory exposed as an MCP server (read/write/capabilities). Public, FDP-Templates-free, no auth.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -473,6 +473,18 @@ exports.CONFIG_CATALOG = {
473
473
  + 'elleni védelem). `0` = még sosem futott. A scheduler írja; kézzel ne állítsd (a `fam config get`-tel '
474
474
  + 'olvasható, mikor futott utoljára).',
475
475
  },
476
+ 'scan.dupAuditEnabled': {
477
+ type: 'boolean', default: true, levels: GLOBAL_ONLY,
478
+ description: 'Heti duplikáció-őrjárat (dup-v2, task #20) be/ki: a HETI re-scan UTÁN egy exact-dup mérés + '
479
+ + 'coverage-preflight + baseline-delta + új-széles-csoport riasztás fut, a pillanatkép a `fam_dup_audits`-ba '
480
+ + 'kerül. `false` → nincs automata őrjárat (a `detect_exact_duplication` capability kézzel továbbra is hívható).',
481
+ },
482
+ 'scan.dupAuditLastRunAt': {
483
+ type: 'number', default: 0, min: 0, max: 9999999999999, integer: true, levels: GLOBAL_ONLY,
484
+ description: 'ÁLLAPOT/observability (NEM felhasználói hangoló): a legutóbbi dup-őrjárat TRIGGER epoch-ms-e. A '
485
+ + 'dup-audit scheduler ezzel dönti el, hogy a legutóbbi heti re-scan után futott-e már audit (markRan-before-run '
486
+ + '— crash-tűrő idempotencia). `0` = még sosem futott. A scheduler írja; kézzel ne állítsd.',
487
+ },
476
488
  // --- 4.6. Reference / scope (fogyasztó: dsgn-002) ---
477
489
  'reference.cacheTtlMs': {
478
490
  type: 'number', default: 60000, min: 0, max: 3600000, integer: true, levels: GLOBAL_ONLY,
@@ -126,6 +126,8 @@ exports.FAM_ERROR_CODES = {
126
126
  scanDelta: 'FAM-SCAN-DELTA-001',
127
127
  /** 0 fájl matchelt a konfigurált mintára (level=info, NEM error, dsgn-008 §5). */
128
128
  scanEmpty: 'FAM-SCAN-EMPTY-001',
129
+ /** dup-őrjárat (dup-v2, task #20): ÚJ ≥4-projektes exact-dup csoport jelent meg az előző audit óta (level=warning). */
130
+ scanDupAuditNewGroup: 'FAM-SCAN-DUPAUDIT-001',
129
131
  // --- DB (FAM-DB-*) ---
130
132
  /** MongoDB connect sikertelen (critical). */
131
133
  dbConnect: 'FAM-DB-CONNECT-001',
@@ -5,6 +5,7 @@ const fam_codebase_data_model_1 = require("../_models/data-models/fam-codebase.d
5
5
  const fam_coding_patterns_data_model_1 = require("../_models/data-models/fam-coding-patterns.data-model");
6
6
  const fam_config_data_model_1 = require("../_models/data-models/fam-config.data-model");
7
7
  const fam_documents_data_model_1 = require("../_models/data-models/fam-documents.data-model");
8
+ const fam_dup_audit_data_model_1 = require("../_models/data-models/fam-dup-audit.data-model");
8
9
  const fam_error_data_model_1 = require("../_models/data-models/fam-error.data-model");
9
10
  const fam_ingest_run_data_model_1 = require("../_models/data-models/fam-ingest-run.data-model");
10
11
  const fam_knowledge_data_model_1 = require("../_models/data-models/fam-knowledge.data-model");
@@ -34,6 +35,7 @@ exports.FAM_DB_MODELS = [
34
35
  fam_scope_data_model_1.famScope_dataParams, // fam_scopes
35
36
  fam_ingest_run_data_model_1.famIngestRun_dataParams, // fam_ingest_runs
36
37
  fam_scan_job_data_model_1.famScanJob_dataParams, // fam_scan_jobs (FEAT-008 Phase 4 — durable scan-job + auto-resume)
38
+ fam_dup_audit_data_model_1.famDupAudit_dataParams, // fam_dup_audits (dup-v2, task #20 — heti dup-őrjárat baseline/trend)
37
39
  // Config-réteg (MP-7, dsgn-007): DB-backed scoped config (global<table<scope).
38
40
  fam_config_data_model_1.famConfig_dataParams, // fam_config
39
41
  ];
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.famDupAudit_dataParams = exports.FAM_DupAudit_DataModel = void 0;
4
+ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
+ /**
6
+ * `FAM_DupAudit_DataModel` (dup-v2, task #20) — egy heti/manuális duplikáció-őrjárat PERZISZTENS pillanatképe
7
+ * a `fam_dup_audits` collection-ben. Cél a **baseline/trend**: az egymást követő futások összevethetők (delta-%),
8
+ * és az ÚJONNAN megjelenő széles (≥4 projektes) csoportok riaszthatók — a duplikáció-terjedés nem néma.
9
+ * `extends DyFM_Metadata`. A rekord bounded (top-50 csoport + hash-lista), NEM a teljes mérés-dump.
10
+ */
11
+ class FAM_DupAudit_DataModel extends fsm_dynamo_1.DyFM_Metadata {
12
+ /** A futás epoch-ms-e. */
13
+ runAt;
14
+ /** A vizsgált tár (`FAM_Table` érték stringként). */
15
+ table;
16
+ /** A trigger (`weekly` | `manual`). */
17
+ trigger;
18
+ /** Az alkalmazott szűrők visszhangja (a delta-összevethetőség kulcsa: csak AZONOS filter-echo hasonlítható). */
19
+ filters;
20
+ /** A szűrt halmaz ≥2 példányos csoportjainak száma. */
21
+ totalGroups;
22
+ /** A redundáns példányok száma. */
23
+ redundantCopies;
24
+ /** A pazarolt karakterek. */
25
+ wastedChars;
26
+ /** A top-50 csoport (wasted DESC) — a trend-vizualizáció/riport magja. */
27
+ topGroups = [];
28
+ /** MIND a ≥4 projektet átfedő csoport hash-e — az új-széles-csoport diff bemenete. */
29
+ multiProjectGroupHashes = [];
30
+ /** A 0-entry-s (a mérésből kimaradó) projektek a futáskor (coverage-preflight). */
31
+ zeroEntryProjects = [];
32
+ /** Az elavult-korpuszú projektek száma a futáskor. */
33
+ staleProjectCount;
34
+ /** A csoport-szám %-os változása az előző ÖSSZEVETHETŐ futáshoz képest (nincs előzmény → undefined). */
35
+ deltaGroupsPct;
36
+ /** A pazarlás %-os változása az előző ÖSSZEVETHETŐ futáshoz képest (nincs előzmény → undefined). */
37
+ deltaWastedPct;
38
+ /** True, ha volt előző futás, de MÁS filter-echo-val (a delta ilyenkor nem számolható — nincs hamis trend). */
39
+ previousIncomparable;
40
+ constructor(set) {
41
+ super(set);
42
+ if (set) {
43
+ fsm_dynamo_1.DyFM_Object.cleanAssign(this, set);
44
+ }
45
+ }
46
+ }
47
+ exports.FAM_DupAudit_DataModel = FAM_DupAudit_DataModel;
48
+ /**
49
+ * MongoDB data-params a `fam_dup_audits` collection-höz (dup-v2). `addArchive:false` — az audit-pillanatkép
50
+ * nem user-tartalom, a retention (utolsó N futás) fizikai törléssel bounded (lásd `FAM_DupAudit_DataService`).
51
+ * A `runAt`/`table`/`trigger` indexelt (a latest-lekérdezés + a trend-szűrés gyors útja).
52
+ */
53
+ exports.famDupAudit_dataParams = new fsm_dynamo_1.DyFM_DataModel_Params({
54
+ dataName: 'fam_dup_audits',
55
+ addArchive: false,
56
+ properties: {
57
+ runAt: { type: 'number', index: true },
58
+ table: { type: 'string', index: true },
59
+ trigger: { type: 'string', index: true },
60
+ filters: { type: 'object' },
61
+ totalGroups: { type: 'number' },
62
+ redundantCopies: { type: 'number' },
63
+ wastedChars: { type: 'number' },
64
+ topGroups: { type: 'array', default: [] },
65
+ multiProjectGroupHashes: { type: 'string[]', default: [] },
66
+ zeroEntryProjects: { type: 'string[]', default: [] },
67
+ staleProjectCount: { type: 'number' },
68
+ deltaGroupsPct: { type: 'number' },
69
+ deltaWastedPct: { type: 'number' },
70
+ previousIncomparable: { type: 'boolean' },
71
+ },
72
+ });
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_DupFileRollup_Util = void 0;
4
+ /**
5
+ * `FAM_DupFileRollup_Util` (dup-v2, task #20) — **PURE** fájl-szintű rollup a near-dup clusterekből. A per-chunk
6
+ * cluster-lista nehezen akcionálható (ugyanaz a fájl 10 clusterben is felbukkanhat); a rollup fájlonként összegzi,
7
+ * HÁNY chunk duplikált és MELY clusterekben — így a konszolidáció-döntés fájl-szinten hozható („ez a fájl 14
8
+ * chunkkal érintett 5 projektben → bedrock-jelölt"). Side-effect-mentes → unit-tesztelhető.
9
+ */
10
+ class FAM_DupFileRollup_Util {
11
+ /** A visszaadott fájl-sorok cap-je (a legtöbb chunkkal érintett fájlok elöl). */
12
+ static MAX_FILES = 200;
13
+ /**
14
+ * A rollup felépítése a (MÁR cap-elt, visszaadandó) clusterekből. A `clusterRefs` a bemeneti tömb indexei —
15
+ * ezért a hívó a VÉGSŐ (cap-elt) cluster-listát adja át, hogy a referenciák a riporttal konzisztensek legyenek.
16
+ * `sourceFilePath` nélküli tag kimarad (nem fájl-eredetű chunk). Tiszta.
17
+ */
18
+ static build(clusters) {
19
+ const byFile = new Map();
20
+ clusters.forEach((cluster, clusterIndex) => {
21
+ for (const member of cluster.members) {
22
+ if (!member.sourceFilePath) {
23
+ continue;
24
+ }
25
+ const entry = byFile.get(member.sourceFilePath) ?? {
26
+ sourceFilePath: member.sourceFilePath, chunkCount: 0, projects: [], clusterRefs: [],
27
+ };
28
+ entry.chunkCount++;
29
+ // A cluster projekt-lefedettsége adja a fájl-sor projektjeit (distinct — lentebb rendezzük).
30
+ for (const project of cluster.projects) {
31
+ if (!entry.projects.includes(project)) {
32
+ entry.projects.push(project);
33
+ }
34
+ }
35
+ if (!entry.clusterRefs.includes(clusterIndex)) {
36
+ entry.clusterRefs.push(clusterIndex);
37
+ }
38
+ byFile.set(member.sourceFilePath, entry);
39
+ }
40
+ });
41
+ return [...byFile.values()]
42
+ .map((entry) => ({ ...entry, projects: [...entry.projects].sort() }))
43
+ .sort((left, right) => right.chunkCount - left.chunkCount
44
+ || left.sourceFilePath.localeCompare(right.sourceFilePath))
45
+ .slice(0, FAM_DupFileRollup_Util.MAX_FILES);
46
+ }
47
+ }
48
+ exports.FAM_DupFileRollup_Util = FAM_DupFileRollup_Util;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_DupForkPair_Util = void 0;
4
+ /**
5
+ * `FAM_DupForkPair_Util` (dup-v2, task #20) — **PURE** fork-pár statisztika + szuppresszió. A fleet-mérések
6
+ * visszatérő zaja a FORK-párok (pl. `ccap` + `ccap-revisioned`): két projekt-scope, ami valójában ugyanaz a
7
+ * kódbázis — a köztük lévő „duplikáció" nem konszolidáció-jelölt, hanem fork-lenyomat. Ez a util a dup-csoport
8
+ * pár-gyakoriságából + pazarlás-részarányából + a repo-evidenciából ismeri fel a gyanús párokat, és (opt-in)
9
+ * szuppresszálja a CSAK e párokra eső csoportokat. Side-effect-mentes → unit-tesztelhető DB nélkül.
10
+ */
11
+ class FAM_DupForkPair_Util {
12
+ /** A riportban visszaadott párok cap-je (a legnagyobb wasted elöl). */
13
+ static MAX_PAIRS = 50;
14
+ /** A fork-gyanú pazarlás-részarány küszöbe (a teljes pazarlás ≥20%-a egyetlen páron → gyanús). */
15
+ static SUSPECT_WASTED_SHARE = 0.2;
16
+ /** A kombinatorikus robbanás elleni per-sor projekt-cap: e fölött a sor pár-bontását kihagyjuk (sok-projektes
17
+ * csoport nem fork-jel — az széles cross-project duplikáció, a pár-statisztikát csak zajosítaná). */
18
+ static MAX_PROJECTS_PER_ROW = 8;
19
+ /**
20
+ * A pár-riport felépítése a forrás-sorokból. Minden (≤ `MAX_PROJECTS_PER_ROW` projektes) sor összes
21
+ * rendezetlen projekt-párjára akkumulálja a `groupCount`/`wasted`-et; a `sameRepoEvidence` CSAK a pontosan
22
+ * 2-projektes, egy-repós sorokból jön (az a valódi fork-lenyomat). Tiszta.
23
+ */
24
+ static buildReport(rows, totalWasted) {
25
+ const byKey = new Map();
26
+ for (const row of rows) {
27
+ const projects = [...new Set(row.projects)].sort();
28
+ if (projects.length < 2 || projects.length > FAM_DupForkPair_Util.MAX_PROJECTS_PER_ROW) {
29
+ continue;
30
+ }
31
+ // A pontosan-2-projektes, egy-repós csoport a fork-evidencia (mindkét „projekt" ugyanabból a repóból).
32
+ const rowSameRepo = projects.length === 2 && new Set(row.repoNames).size === 1 && row.repoNames.length > 0;
33
+ for (let i = 0; i < projects.length; i++) {
34
+ for (let j = i + 1; j < projects.length; j++) {
35
+ const key = `${projects[i]}|${projects[j]}`;
36
+ const pair = byKey.get(key) ?? {
37
+ projects: [projects[i], projects[j]], groupCount: 0, wasted: 0,
38
+ wastedShare: 0, sameRepoEvidence: false, suspectedFork: false,
39
+ };
40
+ pair.groupCount++;
41
+ pair.wasted += row.wasted;
42
+ pair.sameRepoEvidence = pair.sameRepoEvidence || rowSameRepo;
43
+ byKey.set(key, pair);
44
+ }
45
+ }
46
+ }
47
+ // A részarány + a gyanú-flag számítása (a teljes pazarlás 0-ja ellen védve), majd rendezés + cap.
48
+ const pairs = [...byKey.values()]
49
+ .map((pair) => {
50
+ const wastedShare = totalWasted > 0 ? pair.wasted / totalWasted : 0;
51
+ return {
52
+ ...pair,
53
+ wastedShare: wastedShare,
54
+ suspectedFork: wastedShare >= FAM_DupForkPair_Util.SUSPECT_WASTED_SHARE || pair.sameRepoEvidence,
55
+ };
56
+ })
57
+ .sort((left, right) => right.wasted - left.wasted)
58
+ .slice(0, FAM_DupForkPair_Util.MAX_PAIRS);
59
+ return { pairs: pairs, suspectedForkCount: pairs.filter((pair) => pair.suspectedFork).length };
60
+ }
61
+ /**
62
+ * Szuppresszálandó-e egy csoport a gyanús párok mellett (opt-in `suppressForkPairs`). A szabály:
63
+ * (a) a csoport PONTOSAN 2 projektet fed és a pár fork-gyanús, VAGY
64
+ * (b) a csoport projektjei egy gyanús pár UNIÓJÁNAK részhalmaza ÉS a csoport egy-repós (distinct repoName = 1).
65
+ * Tiszta.
66
+ */
67
+ static shouldSuppress(group, suspectedPairs) {
68
+ const projects = [...new Set(group.projects)].sort();
69
+ const singleRepo = new Set(group.repoNames).size === 1 && group.repoNames.length > 0;
70
+ for (const pair of suspectedPairs) {
71
+ if (!pair.suspectedFork) {
72
+ continue;
73
+ }
74
+ const union = new Set(pair.projects);
75
+ const isSubset = projects.every((project) => union.has(project));
76
+ if (projects.length === 2 && isSubset) {
77
+ return true;
78
+ }
79
+ if (isSubset && singleRepo) {
80
+ return true;
81
+ }
82
+ }
83
+ return false;
84
+ }
85
+ }
86
+ exports.FAM_DupForkPair_Util = FAM_DupForkPair_Util;
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_DupAuditScheduler_ControlService = void 0;
4
+ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
+ const fam_config_level_type_enum_1 = require("../../../_enums/fam-config-level.type-enum");
6
+ const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
7
+ const fam_scan_job_control_service_1 = require("../../ingest/_services/fam-scan-job.control-service");
8
+ const fam_dup_audit_control_service_1 = require("./fam-dup-audit.control-service");
9
+ /**
10
+ * `FAM_DupAuditScheduler_ControlService` (dup-v2, task #20) — a **heti duplikáció-őrjárat** in-process ütemezője
11
+ * (a `FAM_ScanScheduler_ControlService` minta tükre). NEM saját heti slotot számol: az őrjárat a HETI RE-SCAN
12
+ * UTÁN esedékes (friss korpuszon mérni) — a kapu: `scan.weeklyRescanLastRunAt > scan.dupAuditLastRunAt` ÉS a
13
+ * re-scan már NEM fut (`hasActiveJob` single-flight). Így az audit hetente pontosan egyszer, a friss scan után fut.
14
+ *
15
+ * **Telepítés (KRITIKUS):** a `boot()` a scheduler-t a lassú boot-karbantartás (hidratálás/resume) ELŐTT telepíti
16
+ * — a timer BIZTOSAN felkerül az event-loopra (boot-timer TILOS `await` mögé). **markRan-before-run:** a
17
+ * `lastRunAt` a futás ELŐTT íródik → egy crash mid-audit sem indít restart-stormot (a következő heti slot pótol,
18
+ * a kiesés warn-logolt). Az audit-hiba best-effort (try/catch) — a tick és a szerver nem bukik el.
19
+ */
20
+ class FAM_DupAuditScheduler_ControlService {
21
+ static _instance;
22
+ timer;
23
+ ticking = false;
24
+ running = false;
25
+ /** A due-ellenőrzés fix pollozási időköze (ms) — 10 perc (a scan-scheduler mintája). */
26
+ static CHECK_INTERVAL_MS = 600000;
27
+ static getInstance() {
28
+ if (!FAM_DupAuditScheduler_ControlService._instance) {
29
+ FAM_DupAuditScheduler_ControlService._instance = new FAM_DupAuditScheduler_ControlService();
30
+ }
31
+ return FAM_DupAuditScheduler_ControlService._instance;
32
+ }
33
+ /** Teszt-reset (a singleton + a timer ürítése). */
34
+ static resetForTest() {
35
+ FAM_DupAuditScheduler_ControlService._instance?.uninstall();
36
+ FAM_DupAuditScheduler_ControlService._instance = undefined;
37
+ }
38
+ /**
39
+ * Periodikus tick telepítése + azonnali első tick (boot-catch-up). Idempotens (kétszeri install → egy timer).
40
+ * A timer `unref`-elt (nem tartja életben a processt önmagában).
41
+ */
42
+ install() {
43
+ if (this.timer) {
44
+ return;
45
+ }
46
+ this.timer = setInterval(() => { void this.tick(Date.now()); }, FAM_DupAuditScheduler_ControlService.CHECK_INTERVAL_MS);
47
+ if (typeof this.timer.unref === 'function') {
48
+ this.timer.unref();
49
+ }
50
+ fsm_dynamo_1.DyFM_Log.log(`[FAM dupAuditScheduler] heti dup-őrjárat figyelő telepítve (tick ${FAM_DupAuditScheduler_ControlService.CHECK_INTERVAL_MS / 60000} percenként).`);
51
+ void this.tick(Date.now());
52
+ }
53
+ /** A timer leállítása (teszt / graceful). */
54
+ uninstall() {
55
+ if (this.timer) {
56
+ clearInterval(this.timer);
57
+ this.timer = undefined;
58
+ }
59
+ }
60
+ /**
61
+ * Egy ellenőrzési ciklus: DUE-e az őrjárat (a PURE `computeDue` kapuival)? Ha igen → ELŐBB `markRan`
62
+ * (crash-tűrő idempotencia), MAJD a futás. Best-effort (nem dob). Re-entráns-védett (`ticking`).
63
+ */
64
+ async tick(nowMs) {
65
+ if (this.ticking) {
66
+ return;
67
+ }
68
+ this.ticking = true;
69
+ try {
70
+ const config = config_control_service_1.FAM_Config_ControlService.getInstance();
71
+ const enabled = (await config.resolve('scan.dupAuditEnabled')).value;
72
+ const weeklyLastRunAt = await this.resolveNum(config, 'scan.weeklyRescanLastRunAt');
73
+ const dupAuditLastRunAt = await this.resolveNum(config, 'scan.dupAuditLastRunAt');
74
+ const hasActiveJob = fam_scan_job_control_service_1.FAM_ScanJob_ControlService.getInstance().hasActiveJob();
75
+ const due = FAM_DupAuditScheduler_ControlService.computeDue({
76
+ enabled: enabled,
77
+ weeklyRescanLastRunAt: weeklyLastRunAt,
78
+ dupAuditLastRunAt: dupAuditLastRunAt,
79
+ hasActiveJob: hasActiveJob,
80
+ running: this.running,
81
+ });
82
+ if (!due) {
83
+ return;
84
+ }
85
+ // markRan-before-run: crash mid-audit → NEM lesz restart-storm; a következő heti re-scan után pótol
86
+ // (a kiesett futás a logból látszik — no-silent-failure).
87
+ fsm_dynamo_1.DyFM_Log.log('[FAM dupAuditScheduler] heti dup-őrjárat ESEDÉKES (a heti re-scan lefutott) — indítás…');
88
+ await this.markRan(config, nowMs);
89
+ this.running = true;
90
+ try {
91
+ await fam_dup_audit_control_service_1.FAM_DupAudit_ControlService.getInstance().runAudit('weekly');
92
+ }
93
+ catch (error) {
94
+ // Az audit-hiba NEM dönti a ticket/szervert; a kiesett futást a következő heti slot pótolja.
95
+ fsm_dynamo_1.DyFM_Log.warn(`[FAM dupAuditScheduler] a dup-őrjárat futása sikertelen (best-effort): ${error?.message}`);
96
+ }
97
+ finally {
98
+ this.running = false;
99
+ }
100
+ }
101
+ catch (error) {
102
+ fsm_dynamo_1.DyFM_Log.warn(`[FAM dupAuditScheduler] dup-őrjárat tick sikertelen (best-effort): ${error?.message}`);
103
+ }
104
+ finally {
105
+ this.ticking = false;
106
+ }
107
+ }
108
+ /**
109
+ * **PURE** due-döntés (unit-tesztelhető DB nélkül). Kapuk:
110
+ * (1) `scan.dupAuditEnabled !== false` (default engedélyezett),
111
+ * (2) volt már heti re-scan (`weeklyRescanLastRunAt > 0` — friss korpusz nélkül nincs mit mérni),
112
+ * (3) a legutóbbi re-scan ÚJABB, mint a legutóbbi audit (`weekly > dupAudit` — heti max 1 futás),
113
+ * (4) a re-scan már NEM fut (`!hasActiveJob` — single-flight, ne mérjünk fél-kész korpuszt),
114
+ * (5) nem fut már audit (`!running` — re-entráns védelem).
115
+ */
116
+ static computeDue(set) {
117
+ return set.enabled !== false
118
+ && set.weeklyRescanLastRunAt > 0
119
+ && set.weeklyRescanLastRunAt > set.dupAuditLastRunAt
120
+ && !set.hasActiveJob
121
+ && !set.running;
122
+ }
123
+ /** A `lastRunAt` állapot perzisztálása a config-store-ba (global szint). Best-effort. */
124
+ async markRan(config, nowMs) {
125
+ try {
126
+ await config.set('scan.dupAuditLastRunAt', nowMs, {
127
+ level: fam_config_level_type_enum_1.FAM_ConfigLevel.global, setBy: 'system', note: 'weekly dup-audit trigger',
128
+ });
129
+ }
130
+ catch (error) {
131
+ fsm_dynamo_1.DyFM_Log.warn(`[FAM dupAuditScheduler] lastRunAt perzisztálás sikertelen: ${error?.message}`);
132
+ }
133
+ }
134
+ /** Egy szám-config feloldása (nem-szám → 0). */
135
+ async resolveNum(config, key) {
136
+ const value = (await config.resolve(key)).value;
137
+ return typeof value === 'number' ? value : 0;
138
+ }
139
+ }
140
+ exports.FAM_DupAuditScheduler_ControlService = FAM_DupAuditScheduler_ControlService;
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_DupAudit_ControlService = void 0;
4
+ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
5
+ const error_codes_const_1 = require("../../../_collections/error-codes.const");
6
+ const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
7
+ const fam_dup_audit_data_model_1 = require("../../../_models/data-models/fam-dup-audit.data-model");
8
+ const errors_control_service_1 = require("../../../_routes/server/errors/errors.control-service");
9
+ const fam_dup_audit_data_service_1 = require("./fam-dup-audit.data-service");
10
+ const fam_exact_duplication_control_service_1 = require("./fam-exact-duplication.control-service");
11
+ /**
12
+ * `FAM_DupAudit_ControlService` (dup-v2, task #20) — a **duplikáció-őrjárat** motorja: egy exact dup-mérés a
13
+ * kanonikus default szűrőkkel + coverage-preflight + baseline-összevetés (delta-%) + az ÚJONNAN megjelenő
14
+ * széles (≥4 projektes) csoportok riasztása (`DyFM_Log.warn` + persistált warning-hiba). A pillanatkép a
15
+ * `fam_dup_audits`-ba kerül (bounded retention) — a trend dokumentált, nem csak memória.
16
+ */
17
+ class FAM_DupAudit_ControlService {
18
+ static _instance;
19
+ issuer = 'FAM_DupAudit_ControlService';
20
+ /** A perzisztált top-csoportok száma (bounded rekord — a trendhez a top-50 bőven elég). */
21
+ static TOP_GROUPS = 50;
22
+ /** A kanonikus default szűrők (a heti őrjárat MINDIG ezzel fut → a futások összevethetők). */
23
+ static DEFAULT_FILTERS = {
24
+ table: fam_table_type_enum_1.FAM_Table.codebase,
25
+ minProjects: 1,
26
+ minContentLength: 200,
27
+ excludeRootRegex: 'STALE-projects',
28
+ };
29
+ static getInstance() {
30
+ if (!FAM_DupAudit_ControlService._instance) {
31
+ FAM_DupAudit_ControlService._instance = new FAM_DupAudit_ControlService();
32
+ }
33
+ return FAM_DupAudit_ControlService._instance;
34
+ }
35
+ /**
36
+ * Egy teljes őrjárat-futás: exact-mérés (default szűrők + coverage) → delta az előző ÖSSZEVETHETŐ futáshoz →
37
+ * új-széles-csoport riasztás (első futásnál NINCS warn — nincs baseline) → persist + retention.
38
+ * A hívó (scheduler) felelőssége a try/catch (az audit-hiba nem döntheti a tick-et).
39
+ */
40
+ async runAudit(trigger) {
41
+ const runAt = Date.now();
42
+ const filters = {
43
+ ...FAM_DupAudit_ControlService.DEFAULT_FILTERS,
44
+ maxGroups: FAM_DupAudit_ControlService.TOP_GROUPS,
45
+ includeCoverage: true,
46
+ };
47
+ const result = await fam_exact_duplication_control_service_1.FAM_ExactDuplication_ControlService.getInstance().run(filters);
48
+ // Baseline: a legutóbbi audit UGYANERRE a tárra (lazy DS — eager-resolve elkerülés).
49
+ const dataService = new fam_dup_audit_data_service_1.FAM_DupAudit_DataService({ issuer: this.issuer });
50
+ const previous = await dataService.findLatest(String(result.table));
51
+ // Delta CSAK azonos filter-echo-hoz (különben previousIncomparable — nincs hamis trend).
52
+ const delta = FAM_DupAudit_ControlService.computeDelta({
53
+ currentFilters: result.filters,
54
+ currentTotalGroups: result.totals.totalGroups,
55
+ currentWastedChars: result.totals.wastedChars,
56
+ previous: previous,
57
+ });
58
+ // ÚJ ≥4-projektes csoportok az előző futáshoz képest (a duplikáció-TERJEDÉS jele). Első futásnál nincs warn.
59
+ const newWideHashes = FAM_DupAudit_ControlService.diffNewMultiProjectHashes(result.multiProjectGroupHashes, previous?.multiProjectGroupHashes);
60
+ if (previous && newWideHashes.length) {
61
+ const message = `[FAM dup-audit] ${newWideHashes.length} ÚJ ≥4-projektes exact-dup csoport jelent meg `
62
+ + `az előző őrjárat óta a '${result.table}' táron (terjedő duplikáció — bedrock-konszolidáció jelölt). `
63
+ + `Hash-ek (első 10): ${newWideHashes.slice(0, 10).join(', ')}`;
64
+ fsm_dynamo_1.DyFM_Log.warn(message);
65
+ await errors_control_service_1.FAM_Error_ControlService.getInstance().emit({
66
+ errorCode: error_codes_const_1.FAM_ERROR_CODES.scanDupAuditNewGroup,
67
+ message: message,
68
+ level: fsm_dynamo_1.DyFM_ErrorLevel.warning,
69
+ context: { operation: 'dup-audit:new-wide-groups' },
70
+ });
71
+ }
72
+ // Persist + retention (bounded history).
73
+ const audit = new fam_dup_audit_data_model_1.FAM_DupAudit_DataModel({
74
+ runAt: runAt,
75
+ table: String(result.table),
76
+ trigger: trigger,
77
+ filters: result.filters,
78
+ totalGroups: result.totals.totalGroups,
79
+ redundantCopies: result.totals.redundantCopies,
80
+ wastedChars: result.totals.wastedChars,
81
+ topGroups: result.groups.slice(0, FAM_DupAudit_ControlService.TOP_GROUPS),
82
+ multiProjectGroupHashes: result.multiProjectGroupHashes,
83
+ zeroEntryProjects: result.coverage?.zeroEntryProjects ?? [],
84
+ staleProjectCount: result.coverage?.staleProjects.length ?? 0,
85
+ deltaGroupsPct: delta.deltaGroupsPct,
86
+ deltaWastedPct: delta.deltaWastedPct,
87
+ previousIncomparable: delta.previousIncomparable,
88
+ });
89
+ const saved = await dataService.persistAudit(audit);
90
+ const purged = await dataService.applyRetention(String(result.table));
91
+ fsm_dynamo_1.DyFM_Log.log(`[FAM dup-audit] őrjárat KÉSZ (${trigger}): ${result.totals.totalGroups} csoport, `
92
+ + `${result.totals.wastedChars} wasted char, ${newWideHashes.length} új széles csoport`
93
+ + `${delta.deltaGroupsPct !== undefined ? `, Δcsoport ${delta.deltaGroupsPct.toFixed(1)}%` : ' (nincs baseline)'}`
94
+ + `${purged ? `, ${purged} régi audit-rekord törölve (retention)` : ''}.`);
95
+ return saved;
96
+ }
97
+ /**
98
+ * **PURE** delta-számítás: az előző futáshoz képesti %-os változás — de CSAK azonos filter-echo esetén
99
+ * (a szűrő-váltás utáni összevetés hamis trendet adna → `previousIncomparable:true` + nincs delta).
100
+ * Nincs előzmény → nincs delta és nem incomparable (első futás — baseline-építés).
101
+ */
102
+ static computeDelta(set) {
103
+ if (!set.previous) {
104
+ return { previousIncomparable: false };
105
+ }
106
+ const sameFilters = JSON.stringify(FAM_DupAudit_ControlService.normalizeFilters(set.currentFilters))
107
+ === JSON.stringify(FAM_DupAudit_ControlService.normalizeFilters(set.previous.filters ?? {}));
108
+ if (!sameFilters) {
109
+ return { previousIncomparable: true };
110
+ }
111
+ return {
112
+ deltaGroupsPct: FAM_DupAudit_ControlService.pctChange(set.previous.totalGroups ?? 0, set.currentTotalGroups),
113
+ deltaWastedPct: FAM_DupAudit_ControlService.pctChange(set.previous.wastedChars ?? 0, set.currentWastedChars),
114
+ previousIncomparable: false,
115
+ };
116
+ }
117
+ /** **PURE** diff: a jelen ≥4-projektes hash-ei MÍNUSZ az előzők (az ÚJONNAN megjelenő széles csoportok). */
118
+ static diffNewMultiProjectHashes(current, previous) {
119
+ const previousSet = new Set(previous ?? []);
120
+ return current.filter((hash) => !previousSet.has(hash));
121
+ }
122
+ /** %-os változás (előző 0 → 0, ha a jelen is 0; egyébként 100 — a nulláról-indulás teljes növekedés). */
123
+ static pctChange(previous, current) {
124
+ if (previous === 0) {
125
+ return current === 0 ? 0 : 100;
126
+ }
127
+ return ((current - previous) / previous) * 100;
128
+ }
129
+ /** A filter-echo determinista (kulcs-rendezett, undefined-mentes) normalizálása az összevetéshez. */
130
+ static normalizeFilters(filters) {
131
+ const raw = filters;
132
+ const normalized = {};
133
+ for (const key of Object.keys(raw).sort()) {
134
+ if (raw[key] !== undefined && raw[key] !== null) {
135
+ normalized[key] = raw[key];
136
+ }
137
+ }
138
+ return normalized;
139
+ }
140
+ }
141
+ exports.FAM_DupAudit_ControlService = FAM_DupAudit_ControlService;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_DupAudit_DataService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const mongoose_1 = tslib_1.__importDefault(require("mongoose"));
6
+ const nts_dynamo_1 = require("@futdevpro/nts-dynamo");
7
+ const fam_dup_audit_data_model_1 = require("../../../_models/data-models/fam-dup-audit.data-model");
8
+ /**
9
+ * `FAM_DupAudit_DataService` (dup-v2, task #20) — a `fam_dup_audits` collection CRUD-ja. A control-service-ek
10
+ * NEM tartanak élő példányt mezőként (memory: dynts_dataservice_eager_resolve — a base-ctor eager DB-resolve-ot
11
+ * hív) — minden művelet előtt lazy `new`.
12
+ */
13
+ class FAM_DupAudit_DataService extends nts_dynamo_1.DyNTS_DataService {
14
+ /** A megtartott audit-futások száma tárankint (bounded history — a trend ennyiből bőven látszik). */
15
+ static RETENTION_COUNT = 20;
16
+ constructor(set) {
17
+ super(new fam_dup_audit_data_model_1.FAM_DupAudit_DataModel(), fam_dup_audit_data_model_1.famDupAudit_dataParams, set.issuer);
18
+ }
19
+ /** Egy audit-pillanatkép perzisztálása. */
20
+ async persistAudit(audit) {
21
+ return this.saveData(audit);
22
+ }
23
+ /** A legutóbbi audit egy tárra (`runAt` DESC; nincs → undefined). A baseline/delta-összevetés bemenete. */
24
+ async findLatest(table) {
25
+ const collection = this.rawCollection();
26
+ if (!collection) {
27
+ return undefined;
28
+ }
29
+ const docs = await collection
30
+ .find({ table: table, _deleted: null })
31
+ .sort({ runAt: -1 })
32
+ .limit(1)
33
+ .toArray();
34
+ return docs[0];
35
+ }
36
+ /**
37
+ * Retention: tárankint csak az utolsó `RETENTION_COUNT` futás marad — a régebbiek RAW `deleteMany`-vel
38
+ * FIZIKAILAG törlődnek. **Indoklás (miért raw + hard delete):** az audit-rekord rendszer-generált,
39
+ * determinisztikusan ÚJRA-előállítható pillanatkép (nem user-tartalom, nem pótolhatatlan) — az
40
+ * `addArchive:false` + bounded fizikai törlés a helyes költség-profil; a soft-delete/archív itt csak
41
+ * korlátlanul növekvő junk-ot halmozna. Visszaadja a törölt rekordok számát.
42
+ */
43
+ async applyRetention(table) {
44
+ const collection = this.rawCollection();
45
+ if (!collection) {
46
+ return 0;
47
+ }
48
+ // A megtartandó legfrissebb N runAt-ja — az N+1-ediktől lefelé minden törölhető.
49
+ const keep = await collection
50
+ .find({ table: table }, { projection: { runAt: 1 } })
51
+ .sort({ runAt: -1 })
52
+ .limit(FAM_DupAudit_DataService.RETENTION_COUNT)
53
+ .toArray();
54
+ if (keep.length < FAM_DupAudit_DataService.RETENTION_COUNT) {
55
+ return 0;
56
+ }
57
+ const cutoff = keep[keep.length - 1]?.runAt ?? 0;
58
+ const result = await collection.deleteMany({ table: table, runAt: { $lt: cutoff } });
59
+ return result.deletedCount ?? 0;
60
+ }
61
+ /** A regisztrált mongoose-collection (a ctor DS-példányosítása regisztrálja a modellt). */
62
+ rawCollection() {
63
+ return mongoose_1.default.models[fam_dup_audit_data_model_1.famDupAudit_dataParams.dataName]?.collection;
64
+ }
65
+ }
66
+ exports.FAM_DupAudit_DataService = FAM_DupAudit_DataService;