@futdevpro/fdp-agent-memory 1.1.21 → 1.1.112

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.
Files changed (89) hide show
  1. package/build/package.json +2 -2
  2. package/build/src/_cli/_commands/find-duplicates.command.js +41 -16
  3. package/build/src/_cli/_commands/scan-projects.command.js +113 -113
  4. package/build/src/_cli/_commands/scan.command.js +2 -0
  5. package/build/src/_cli/_commands/serve.command.js +110 -10
  6. package/build/src/_cli/_services/fam-client.service.js +25 -1
  7. package/build/src/_cli/register-commands.js +28 -0
  8. package/build/src/_collections/config-catalog.const.js +152 -33
  9. package/build/src/_collections/error-codes.const.js +2 -0
  10. package/build/src/_collections/fam-console.util.js +38 -260
  11. package/build/src/_collections/fam-db-models.const.js +2 -0
  12. package/build/src/_collections/fam-entry-bootstrap.util.js +20 -6
  13. package/build/src/_collections/fam-error-context.util.js +1 -0
  14. package/build/src/_collections/fam-mcp-bridge.util.js +15 -1
  15. package/build/src/_collections/fam-operation-queue.service.js +85 -0
  16. package/build/src/_collections/fam-project-discovery.util.js +148 -0
  17. package/build/src/_collections/fam-request-origin.util.js +41 -0
  18. package/build/src/_collections/fam-retry.util.js +48 -0
  19. package/build/src/_collections/fam-scan-progress-sink.service.js +26 -0
  20. package/build/src/_collections/title.const.js +26 -26
  21. package/build/src/_enums/fam-rule-scope.type-enum.js +25 -0
  22. package/build/src/_models/data-models/fam-entry.data-model.js +29 -0
  23. package/build/src/_models/data-models/fam-memory.data-model.js +5 -0
  24. package/build/src/_models/data-models/fam-rules.data-model.js +24 -0
  25. package/build/src/_models/data-models/fam-scan-job.data-model.js +73 -0
  26. package/build/src/_models/data-models/fam-scope.data-model.js +9 -0
  27. package/build/src/_modules/capture/_services/fam-auto-capture.control-service.js +1 -1
  28. package/build/src/_modules/embedding/_services/fam-dedup-warn.control-service.js +40 -0
  29. package/build/src/_modules/embedding/_services/fam-duplicate-scan.control-service.js +74 -16
  30. package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +50 -0
  31. package/build/src/_modules/embedding/_services/fam-embedding-pipeline.control-service.js +32 -6
  32. package/build/src/_modules/embedding/_services/fam-embedding.control-service.js +142 -9
  33. package/build/src/_modules/embedding/_services/fam-entry.data-service.js +32 -1
  34. package/build/src/_modules/embedding/_services/fam-hydration-coordinator.control-service.js +159 -0
  35. package/build/src/_modules/embedding/_services/fam-lmstudio-embedding.provider.js +41 -4
  36. package/build/src/_modules/embedding/_services/fam-vector-search.control-service.js +67 -12
  37. package/build/src/_modules/embedding/index.js +5 -1
  38. package/build/src/_modules/ingest/_collections/fam-file-routing.util.js +10 -0
  39. package/build/src/_modules/ingest/_collections/fam-frontmatter.util.js +134 -0
  40. package/build/src/_modules/ingest/_collections/fam-project-identity.util.js +30 -0
  41. package/build/src/_modules/ingest/_collections/fam-scan-progress.util.js +5 -2
  42. package/build/src/_modules/ingest/_collections/fam-scan-summary.util.js +55 -29
  43. package/build/src/_modules/ingest/_collections/fam-split-chunker.util.js +139 -0
  44. package/build/src/_modules/ingest/_models/interfaces/fam-scan-job.interface.js +2 -0
  45. package/build/src/_modules/ingest/_services/fam-chunker.control-service.js +10 -0
  46. package/build/src/_modules/ingest/_services/fam-delta-compare.util.js +68 -1
  47. package/build/src/_modules/ingest/_services/fam-ingest-run.data-service.js +1 -1
  48. package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +177 -37
  49. package/build/src/_modules/ingest/_services/fam-scan-job.control-service.js +429 -0
  50. package/build/src/_modules/ingest/_services/fam-scan-job.data-service.js +34 -0
  51. package/build/src/_modules/ingest/_services/fam-scan.control-service.js +6 -4
  52. package/build/src/_modules/ingest/index.js +6 -1
  53. package/build/src/_modules/mcp/_collections/fam-core-tools.const.js +1 -1
  54. package/build/src/_modules/mcp/_services/fam-capability-registry.service.js +264 -41
  55. package/build/src/_modules/mcp/_services/fam-read-tool.service.js +71 -1
  56. package/build/src/_modules/mcp/_services/fam-write-tool.service.js +36 -0
  57. package/build/src/_modules/migration/_services/fam-claude-mem-import.control-service.js +1 -1
  58. package/build/src/_modules/retrieval/_collections/fam-lexical-match.util.js +69 -0
  59. package/build/src/_modules/retrieval/_collections/fam-memory-activation.util.js +66 -0
  60. package/build/src/_modules/retrieval/_collections/fam-rule-injection.util.js +125 -0
  61. package/build/src/_modules/retrieval/_collections/fam-rule-propagation.util.js +95 -0
  62. package/build/src/_modules/retrieval/_collections/fam-rule-scope-context.util.js +74 -0
  63. package/build/src/_modules/retrieval/_services/fam-global-rule.control-service.js +82 -0
  64. package/build/src/_modules/retrieval/_services/fam-memory-cold-search.control-service.js +121 -0
  65. package/build/src/_modules/retrieval/_services/fam-memory-dormancy.control-service.js +99 -0
  66. package/build/src/_modules/retrieval/_services/fam-memory-reactivation.control-service.js +68 -0
  67. package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +15 -2
  68. package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +197 -5
  69. package/build/src/_modules/retrieval/_services/fam-rule-injection.control-service.js +102 -0
  70. package/build/src/_modules/retrieval/index.js +12 -1
  71. package/build/src/_modules/scope-reference/_collections/fam-scope-maintenance.util.js +134 -0
  72. package/build/src/_modules/scope-reference/_services/fam-scope-maintenance.control-service.js +423 -0
  73. package/build/src/_modules/scope-reference/_services/fam-scope-resolver.control-service.js +2 -2
  74. package/build/src/_modules/scope-reference/_services/fam-scope.data-service.js +25 -0
  75. package/build/src/_modules/scope-reference/index.js +4 -1
  76. package/build/src/_routes/server/api/api.controller.js +324 -23
  77. package/build/src/_routes/server/api/fam-doctor.control-service.js +156 -0
  78. package/build/src/_routes/server/config/config.control-service.js +2 -2
  79. package/build/src/app.server.js +29 -27
  80. package/client-dist/{chunk-YXHWCJ5O.js → chunk-GF3FJP7E.js} +71 -71
  81. package/client-dist/chunk-NMUCMQY6.js +1 -0
  82. package/client-dist/favicon.ico +0 -0
  83. package/client-dist/index.html +3 -3
  84. package/client-dist/main-YK4YIVAQ.js +2 -0
  85. package/package.json +2 -2
  86. package/build/src/_cli/_collections/fam-project-discovery.util.js +0 -98
  87. package/build/src/_collections/fam-heap-guard.control-service.js +0 -176
  88. package/client-dist/chunk-I77GXVAQ.js +0 -1
  89. package/client-dist/main-PJPEDVJT.js +0 -2
@@ -0,0 +1,429 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_ScanJob_ControlService = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const fs_1 = require("fs");
6
+ const path_1 = require("path");
7
+ const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
8
+ const fam_table_type_enum_1 = require("../../../_enums/fam-table.type-enum");
9
+ const fam_cooldown_util_1 = require("../../../_collections/fam-cooldown.util");
10
+ const fam_operation_queue_service_1 = require("../../../_collections/fam-operation-queue.service");
11
+ const fam_scan_progress_sink_service_1 = require("../../../_collections/fam-scan-progress-sink.service");
12
+ const config_control_service_1 = require("../../../_routes/server/config/config.control-service");
13
+ const fam_write_tool_service_1 = require("../../mcp/_services/fam-write-tool.service");
14
+ const fam_mcp_interface_1 = require("../../mcp/_models/interfaces/fam-mcp.interface");
15
+ const fam_project_discovery_util_1 = require("../../../_collections/fam-project-discovery.util");
16
+ const fam_scan_job_data_service_1 = require("./fam-scan-job.data-service");
17
+ const fam_scan_job_data_model_1 = require("../../../_models/data-models/fam-scan-job.data-model");
18
+ /**
19
+ * `FAM_ScanJob_ControlService` (FEAT-008 Phase 2) — **szerver-oldali async multi-projekt scan-orchestráció**. A user-
20
+ * direktíva: a SZERVER vezényelje a teljes scant (felderítés + soros feldolgozás + cooldown + queue + progress), NE a
21
+ * CLI. A `start` AZONNAL visszaadja a `jobId`-t (nincs hosszú request → nincs Node-requestTimeout), a feldolgozás
22
+ * háttérben fut: projektenként a meglévő `FAM_WriteTool_Service.handle()` (scan-project) az `FAM_OperationQueue_Service`
23
+ * queue-n át (szerializáció), majd FELTÉTELES szerver-cooldown (≥`cooldownMinChunks` embed → `cooldownMs` várás). Az
24
+ * állapot in-memory (single-server, single-user) — a `getStatus` poll-ozható. Statikus singleton.
25
+ */
26
+ class FAM_ScanJob_ControlService {
27
+ static instance;
28
+ static getInstance() {
29
+ if (!FAM_ScanJob_ControlService.instance) {
30
+ FAM_ScanJob_ControlService.instance = new FAM_ScanJob_ControlService();
31
+ }
32
+ return FAM_ScanJob_ControlService.instance;
33
+ }
34
+ issuer = 'FAM_ScanJob_ControlService';
35
+ /** Stall-küszöb default (ms), ha a config nem oldódott fel (FEAT-008 Phase 5) — 2 perc. */
36
+ static DEFAULT_STALL_THRESHOLD_MS = 120000;
37
+ /** Az aktív + befejezett job-ok (in-memory; a server-restart eldobja — followup: Mongo-perzisztencia). */
38
+ jobs = new Map();
39
+ /** Per-job feloldott stall-küszöb (ms) — a `start`/`resume` resolve-olja, a `getStatus` poll sync-ben olvassa. */
40
+ stallThresholdMsByJob = new Map();
41
+ /** Teszt-override a per-projekt futtatóra (a default valós scan-t indít a queue-n át). */
42
+ scanRunnerOverride;
43
+ /** TESZT-only: a per-projekt scan-futtató cseréje (valós Mongo/embedding nélkül). */
44
+ setScanRunnerForTest(runner) {
45
+ this.scanRunnerOverride = runner;
46
+ }
47
+ /** TESZT-only: a singleton + állapot eldobása. */
48
+ static resetForTest() {
49
+ FAM_ScanJob_ControlService.instance = undefined;
50
+ }
51
+ /**
52
+ * Egy scan-job INDÍTÁSA. Feloldja a projekt-listát (explicit `projects` VAGY `root`-felderítés), létrehozza a
53
+ * job-ot, ELINDÍTJA a háttér-feldolgozást (fire-and-forget), és AZONNAL visszaadja a `jobId`-t + a `total`-t.
54
+ * Üres lista → `Error` (a hívó 400-at ad). A cooldown a configból oldódik fel (override-olható a bemenetből).
55
+ */
56
+ async start(input) {
57
+ const specs = FAM_ScanJob_ControlService.resolveSpecs(input);
58
+ if (specs.length === 0) {
59
+ throw new Error('Nincs feldolgozandó projekt (üres `projects`/`configPath` és a `root`-felderítés sem adott találatot).');
60
+ }
61
+ const cooldownMs = input.cooldownMs !== undefined
62
+ ? Math.max(0, Math.floor(input.cooldownMs))
63
+ : await FAM_ScanJob_ControlService.resolveConfigNumber('scan.projectCooldownMs', 10000);
64
+ const cooldownMinChunks = input.cooldownMinChunks !== undefined
65
+ ? Math.max(0, Math.floor(input.cooldownMinChunks))
66
+ : await FAM_ScanJob_ControlService.resolveConfigNumber('scan.projectCooldownMinChunks', 100);
67
+ const now = Date.now();
68
+ const job = {
69
+ jobId: (0, crypto_1.randomUUID)(),
70
+ status: 'pending',
71
+ total: specs.length,
72
+ currentIndex: 0,
73
+ okCount: 0,
74
+ cooldownMs: cooldownMs,
75
+ cooldownMinChunks: cooldownMinChunks,
76
+ startedAt: now,
77
+ lastProgressAt: now,
78
+ resumeCount: 0,
79
+ // KI indította (user-FR 2026-06-25): a beazonosíthatósághoz a job-on rögzítjük a hívó aláírását.
80
+ origin: input.origin,
81
+ projects: specs.map((spec) => ({
82
+ project: spec.project, path: spec.path, status: 'pending',
83
+ })),
84
+ cancelRequested: false,
85
+ };
86
+ this.jobs.set(job.jobId, job);
87
+ // Stall-küszöb feloldása (FEAT-008 Phase 5) — in-memory tárolva, hogy a sync `getStatus` poll olvashassa.
88
+ this.stallThresholdMsByJob.set(job.jobId, await FAM_ScanJob_ControlService.resolveConfigNumber('scan.stallThresholdMs', FAM_ScanJob_ControlService.DEFAULT_STALL_THRESHOLD_MS));
89
+ // DURABLE perzisztencia (FEAT-008 Phase 4): a job + a specs + a per-projekt állapot Mongo-ba → túléli a
90
+ // server-restart/crash/close-t, és a szerver AUTO-RESUME-olja a megszakítási ponttól. Best-effort (a perzisztencia
91
+ // hibája NE buktassa a scant — a futás in-memory akkor is megy).
92
+ await this.persistCreate(job, specs, input.sourceLocation);
93
+ // Fire-and-forget: a feldolgozás háttérben fut (a request NEM várja meg → nincs requestTimeout).
94
+ void this.process(job, specs).catch((error) => {
95
+ job.status = 'failed';
96
+ job.finishedAt = Date.now();
97
+ job.failureReason = `feldolgozás összeomlott: ${error?.message ?? 'ismeretlen'}`;
98
+ void this.persistUpdate(job);
99
+ fsm_dynamo_1.DyFM_Log.warn(`[scanJob ${job.jobId}] ${job.failureReason}`);
100
+ });
101
+ fsm_dynamo_1.DyFM_Log.info(`[scanJob ${job.jobId}] indítva: ${specs.length} projekt, cooldown ${cooldownMs}ms `
102
+ + `(ha ≥${cooldownMinChunks} embed chunk)`);
103
+ // Fast-path (user-FR 2026-06-22): a `start` legfeljebb `waitMs`-ig (default 15s) megvárja a befejezést — ha
104
+ // a job ennyin belül kész, a TELJES eredményt adjuk vissza AZONNAL (nincs külön poll-hívás); különben a futó
105
+ // job-ot (`jobId` + `running` + ETA) → a hívó poll-oz. `0` = ne várj.
106
+ const waitMs = input.waitMs !== undefined ? Math.max(0, Math.floor(input.waitMs)) : 15000;
107
+ await FAM_ScanJob_ControlService.awaitTerminalOrTimeout(job, waitMs);
108
+ return this.withEta(job);
109
+ }
110
+ /** A job befejezésének (vagy a `waitMs` lejártának) megvárása — a fast-path szinkron eredményéhez. */
111
+ static async awaitTerminalOrTimeout(job, waitMs) {
112
+ if (waitMs <= 0) {
113
+ return;
114
+ }
115
+ const deadline = Date.now() + waitMs;
116
+ while (Date.now() < deadline) {
117
+ if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
118
+ return;
119
+ }
120
+ await fam_cooldown_util_1.FAM_Cooldown_Util.sleep(100);
121
+ }
122
+ }
123
+ /** Egy job állapota (poll). `undefined`, ha ismeretlen (a hívó 404-et ad). Az ETA + stall frissítve. */
124
+ getStatus(jobId) {
125
+ const job = this.jobs.get(jobId);
126
+ return job ? this.withEta(job) : undefined;
127
+ }
128
+ /**
129
+ * AZ ÖSSZES (futó + befejezett) job KOMPAKT összefoglalója (FEAT-008 Phase 5) — legfrissebb ELŐL. Így egy elárvult/
130
+ * futó job a `jobId` ismerete NÉLKÜL is felfedezhető (a user kérdése: „fut egyáltalán valami?"). A stall-jelzés is benne.
131
+ */
132
+ listJobs() {
133
+ return Array.from(this.jobs.values())
134
+ .map((job) => this.withEta(job))
135
+ .sort((a, b) => b.startedAt - a.startedAt)
136
+ .map((job) => ({
137
+ jobId: job.jobId, status: job.status, total: job.total, okCount: job.okCount,
138
+ currentIndex: job.currentIndex, startedAt: job.startedAt, finishedAt: job.finishedAt,
139
+ etaSeconds: job.etaSeconds, stalled: job.stalled, stalledForSeconds: job.stalledForSeconds,
140
+ origin: job.origin,
141
+ }));
142
+ }
143
+ /** A job ETA + within-project sub-progress + STALL-jelzés frissítése (mutál + visszaad). Csak futó job-on értelmes. */
144
+ withEta(job) {
145
+ job.etaSeconds = FAM_ScanJob_ControlService.computeEtaSeconds(job);
146
+ const live = fam_scan_progress_sink_service_1.FAM_ScanProgress_Sink.get();
147
+ if (job.status === 'running') {
148
+ job.currentProgress = live
149
+ ? { scopeLabel: live.scopeLabel, processed: live.processed, total: live.total, pct: live.pct, chunkCount: live.chunkCount }
150
+ : undefined;
151
+ }
152
+ else {
153
+ job.currentProgress = undefined;
154
+ }
155
+ // STALL: a per-target `lastProgressAt` ÉS a within-target progress-sink `updatedAt` közül a frissebbhez mérjük a
156
+ // tétlenséget. A küszöb a job indításakor feloldott `scan.stallThresholdMs` (in-memory, sync-elérhető a poll-on).
157
+ const stall = FAM_ScanJob_ControlService.computeStall({
158
+ status: job.status,
159
+ lastProgressAt: job.lastProgressAt ?? job.startedAt,
160
+ sinkUpdatedAt: (job.status === 'running' && live) ? live.updatedAt : undefined,
161
+ nowMs: Date.now(),
162
+ thresholdMs: this.stallThresholdMsByJob.get(job.jobId) ?? FAM_ScanJob_ControlService.DEFAULT_STALL_THRESHOLD_MS,
163
+ });
164
+ job.stalled = stall.stalled;
165
+ job.stalledForSeconds = stall.stalledForSeconds;
166
+ return job;
167
+ }
168
+ /**
169
+ * A STALL determinisztikus számítása (pure, tesztelhető — nincs Date.now/config). CSAK `running` job-on: a `nowMs` és
170
+ * a LEGFRISSEBB haladás-jel (`max(lastProgressAt, sinkUpdatedAt)`) különbsége. `stalled=true`, ha ez ≥ `thresholdMs`.
171
+ * A `stalledForSeconds` mindig a tétlenség (futó job-on); terminál/`thresholdMs<=0` → nincs stall.
172
+ */
173
+ static computeStall(set) {
174
+ if (set.status !== 'running') {
175
+ return { stalled: false, stalledForSeconds: undefined };
176
+ }
177
+ const freshest = Math.max(set.lastProgressAt, set.sinkUpdatedAt ?? 0);
178
+ const idleMs = Math.max(0, set.nowMs - freshest);
179
+ const stalled = set.thresholdMs > 0 && idleMs >= set.thresholdMs;
180
+ return { stalled: stalled, stalledForSeconds: Math.round(idleMs / 1000) };
181
+ }
182
+ /**
183
+ * A becsült hátralévő idő (mp) — pure. CSAK futó job-on értelmes: a már (ok/error) kész projektek átlag-ideje ×
184
+ * a hátralévő projektek száma. `undefined`, ha terminál, vagy ha még semmi sem készült el (nincs mérés).
185
+ */
186
+ static computeEtaSeconds(job) {
187
+ if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
188
+ return undefined;
189
+ }
190
+ const done = job.projects.filter((project) => project.status === 'ok' || project.status === 'error');
191
+ if (done.length === 0) {
192
+ return undefined;
193
+ }
194
+ const elapsedSeconds = (Date.now() - job.startedAt) / 1000;
195
+ const remaining = job.total - done.length;
196
+ return Math.max(0, Math.round((elapsedSeconds / done.length) * remaining));
197
+ }
198
+ /** Megszakítás-kérés: a processor a következő projekt ELŐTT ellenőrzi. `true`, ha a job létezett + futott. */
199
+ cancel(jobId) {
200
+ const job = this.jobs.get(jobId);
201
+ if (!job || job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
202
+ return false;
203
+ }
204
+ job.cancelRequested = true;
205
+ return true;
206
+ }
207
+ /** A háttér-feldolgozás: soros projekt-loop a queue-n át + feltételes szerver-cooldown. */
208
+ async process(job, specs) {
209
+ job.status = 'running';
210
+ const runner = this.scanRunnerOverride ?? ((spec) => this.defaultRunProject(spec));
211
+ for (let index = 0; index < specs.length; index++) {
212
+ if (job.cancelRequested) {
213
+ job.status = 'cancelled';
214
+ job.finishedAt = Date.now();
215
+ job.failureReason = `felhasználói megszakítás a(z) ${index + 1}/${specs.length} projektnél`;
216
+ await this.persistUpdate(job);
217
+ fsm_dynamo_1.DyFM_Log.info(`[scanJob ${job.jobId}] ${job.failureReason}`);
218
+ return;
219
+ }
220
+ // RESUME (FEAT-008 Phase 4): a már `ok` projekteket ÁTUGORJUK (a megszakítási ponttól folytatunk —
221
+ // nem scan-eljük újra, amit egy korábbi futás befejezett). Az `error`/`pending`/`running` újrafut.
222
+ if (job.projects[index].status === 'ok') {
223
+ continue;
224
+ }
225
+ job.currentIndex = index;
226
+ job.projects[index].status = 'running';
227
+ job.lastProgressAt = Date.now(); // egy projekt feldolgozásának KEZDETE is haladás (a stall-óra resetelődik)
228
+ // Per-projekt PROGRESS a process-logba (user-FR 2026-06-25 — „hanyadik projektből hányadikon járunk"): a
229
+ // detached FAM-ablakot figyelve egyből látszik a pozíció (`[12/48] <projekt>`), nem kell a REST-et poll-ozni.
230
+ fsm_dynamo_1.DyFM_Log.info(`[scanJob ${job.jobId}] [${index + 1}/${specs.length}] ${specs[index].project} — feldolgozás…`);
231
+ const result = await runner(specs[index]);
232
+ job.projects[index] = result;
233
+ job.lastProgressAt = Date.now(); // per-target BEFEJEZÉS → friss haladás-jel a stall-detekciónak
234
+ if (result.status === 'ok') {
235
+ job.okCount++;
236
+ }
237
+ // Per-projekt CHECKPOINT (durable) — a job állapota minden projekt után Mongo-ba, hogy a resume innen folytasson.
238
+ await this.persistUpdate(job);
239
+ // Feltételes szerver-cooldown (FEAT-008): a projektek KÖZÉ (nem az utolsó után) CSAK ha az előző projekt
240
+ // ≥ cooldownMinChunks valódi embed-munkát végzett — `equal`-csak projekt után nincs gép-terhelés.
241
+ const embedded = result.embedded ?? 0;
242
+ if (FAM_ScanJob_ControlService.shouldCooldown({
243
+ embedded: embedded, minChunks: job.cooldownMinChunks, cooldownMs: job.cooldownMs,
244
+ isLast: index === specs.length - 1,
245
+ })) {
246
+ fsm_dynamo_1.DyFM_Log.info(`[scanJob ${job.jobId}] cooldown ${job.cooldownMs}ms (${embedded} chunk embed-elve) — `
247
+ + `${index + 1}/${specs.length} kész`);
248
+ await fam_cooldown_util_1.FAM_Cooldown_Util.sleep(job.cooldownMs);
249
+ }
250
+ }
251
+ job.currentIndex = specs.length;
252
+ job.status = 'completed';
253
+ job.finishedAt = Date.now();
254
+ await this.persistUpdate(job);
255
+ fsm_dynamo_1.DyFM_Log.info(`[scanJob ${job.jobId}] KÉSZ: ${job.okCount}/${specs.length} ok`);
256
+ }
257
+ /** A job durable rekordjának LÉTREHOZÁSA (best-effort — a perzisztencia hibája nem buktatja a scant). */
258
+ async persistCreate(job, specs, sourceLocation) {
259
+ try {
260
+ await new fam_scan_job_data_service_1.FAM_ScanJob_DataService({ issuer: this.issuer }).createJob(new fam_scan_job_data_model_1.FAM_ScanJob_DataModel({
261
+ jobId: job.jobId, status: job.status, total: job.total, currentIndex: job.currentIndex,
262
+ okCount: job.okCount, cooldownMs: job.cooldownMs, cooldownMinChunks: job.cooldownMinChunks,
263
+ startedAt: job.startedAt, projects: job.projects, specs: specs, sourceLocation: sourceLocation,
264
+ resumeCount: job.resumeCount ?? 0,
265
+ }));
266
+ }
267
+ catch (error) {
268
+ fsm_dynamo_1.DyFM_Log.warn(`[scanJob ${job.jobId}] durable-create sikertelen (a scan in-memory folytatódik): ${error?.message}`);
269
+ }
270
+ }
271
+ /** A job durable rekordjának FRISSÍTÉSE (atomikus $set, per-projekt checkpoint) — best-effort. */
272
+ async persistUpdate(job) {
273
+ try {
274
+ await new fam_scan_job_data_service_1.FAM_ScanJob_DataService({ issuer: this.issuer }).updateJobByJobId(job.jobId, {
275
+ status: job.status, currentIndex: job.currentIndex, okCount: job.okCount,
276
+ projects: job.projects, finishedAt: job.finishedAt,
277
+ resumeCount: job.resumeCount, failureReason: job.failureReason,
278
+ });
279
+ }
280
+ catch (error) {
281
+ fsm_dynamo_1.DyFM_Log.warn(`[scanJob ${job.jobId}] durable-update sikertelen: ${error?.message}`);
282
+ }
283
+ }
284
+ /**
285
+ * AUTO-RESUME (FEAT-008 Phase 4, user-FR 2026-06-22) — a startup-on (a bootstrap hidratálás után) hívva: a Mongo-ban
286
+ * `running`/`pending`-en ragadt (megszakadt) job-okat folytatja a megszakítási ponttól (a már `ok` projekteket a
287
+ * `process` átugorja). Best-effort. A visszaadott szám = hány job-ot indított újra.
288
+ */
289
+ async resumeInterrupted() {
290
+ let resumed = 0;
291
+ try {
292
+ const dataService = new fam_scan_job_data_service_1.FAM_ScanJob_DataService({ issuer: this.issuer });
293
+ const maxResumeCount = await FAM_ScanJob_ControlService.resolveConfigNumber('scan.maxResumeCount', 5);
294
+ const stallThresholdMs = await FAM_ScanJob_ControlService.resolveConfigNumber('scan.stallThresholdMs', FAM_ScanJob_ControlService.DEFAULT_STALL_THRESHOLD_MS);
295
+ const docs = await dataService.findResumable();
296
+ for (const doc of docs) {
297
+ if (!doc.jobId || !Array.isArray(doc.specs) || doc.specs.length === 0) {
298
+ continue;
299
+ }
300
+ // RESUME-CAP (FEAT-008 Phase 5): egy folyton-elakadó job ne induljon újra a végtelenségig minden restartkor.
301
+ // A cap elérésekor a durable rekordot `failed`-re zárjuk (failureReason) — így a `findResumable` többé nem hozza.
302
+ const priorResumes = doc.resumeCount ?? 0;
303
+ if (maxResumeCount <= 0 || priorResumes >= maxResumeCount) {
304
+ const reason = `resume-cap túllépve (${priorResumes}/${maxResumeCount}) — nem indítjuk újra automatikusan`;
305
+ await dataService.updateJobByJobId(doc.jobId, { status: 'failed', finishedAt: Date.now(), failureReason: reason });
306
+ fsm_dynamo_1.DyFM_Log.warn(`[scanJob ${doc.jobId}] ${reason}`);
307
+ continue;
308
+ }
309
+ const now = Date.now();
310
+ const job = {
311
+ jobId: doc.jobId, status: 'running', total: doc.total ?? doc.specs.length,
312
+ currentIndex: doc.currentIndex ?? 0, okCount: doc.okCount ?? 0,
313
+ cooldownMs: doc.cooldownMs ?? 0, cooldownMinChunks: doc.cooldownMinChunks ?? 100,
314
+ startedAt: doc.startedAt ?? now,
315
+ lastProgressAt: now, // a resume pillanata friss haladás-jel (a stall-óra innen indul)
316
+ resumeCount: priorResumes + 1,
317
+ cancelRequested: false,
318
+ projects: doc.projects ?? doc.specs.map((spec) => ({ project: spec.project, path: spec.path, status: 'pending' })),
319
+ };
320
+ this.jobs.set(job.jobId, job);
321
+ this.stallThresholdMsByJob.set(job.jobId, stallThresholdMs);
322
+ // A megnövelt resumeCount-ot AZONNAL persistáljuk (ha ez a resume is megszakad, a következő boot a magasabb
323
+ // számról indul → a cap konvergál; különben egy crash-loop sosem érné el a capet).
324
+ await dataService.updateJobByJobId(job.jobId, { resumeCount: job.resumeCount });
325
+ const doneCount = job.projects.filter((project) => project.status === 'ok').length;
326
+ fsm_dynamo_1.DyFM_Log.info(`[scanJob ${job.jobId}] RESUME (${job.resumeCount}/${maxResumeCount}): ${doneCount}/${job.total} kész — folytatás a megszakítási ponttól`);
327
+ void this.process(job, doc.specs).catch((error) => {
328
+ job.status = 'failed';
329
+ job.finishedAt = Date.now();
330
+ job.failureReason = `resume-feldolgozás összeomlott: ${error?.message ?? 'ismeretlen'}`;
331
+ void this.persistUpdate(job);
332
+ fsm_dynamo_1.DyFM_Log.warn(`[scanJob ${job.jobId}] ${job.failureReason}`);
333
+ });
334
+ resumed++;
335
+ }
336
+ }
337
+ catch (error) {
338
+ fsm_dynamo_1.DyFM_Log.warn(`[scanJob] resume-keresés sikertelen: ${error?.message}`);
339
+ }
340
+ return resumed;
341
+ }
342
+ /** A default per-projekt futtató: a meglévő `scan-project` write-handler az operation-queue-n át. */
343
+ async defaultRunProject(spec) {
344
+ const startedAt = Date.now();
345
+ // Per-target scope: az EXPLICIT `spec.scopePath` nyer (heterogén „scan-all"-hoz, pl. a memóriám root-szintű
346
+ // scope-ja); különben a default `[organization=org, project=project]`.
347
+ const scopePath = (spec.scopePath && spec.scopePath.length)
348
+ ? spec.scopePath
349
+ : [
350
+ { layer: 'organization', rawName: spec.org ?? 'FutDevPro' },
351
+ { layer: 'project', rawName: spec.project },
352
+ ];
353
+ const input = {
354
+ table: spec.table ?? fam_table_type_enum_1.FAM_Table.codebase,
355
+ operation: fam_mcp_interface_1.FAM_WriteOperation.scanProject,
356
+ scopePath: scopePath,
357
+ scan: {
358
+ path: spec.path,
359
+ preserveHistory: spec.preserveHistory,
360
+ tableOverride: spec.tableOverride ? spec.tableOverride : undefined,
361
+ include: spec.include,
362
+ exclude: spec.exclude,
363
+ },
364
+ };
365
+ try {
366
+ const output = await fam_operation_queue_service_1.FAM_OperationQueue_Service.getInstance()
367
+ .enqueue(() => fam_write_tool_service_1.FAM_WriteTool_Service.getInstance().handle(input));
368
+ const scan = output.scan;
369
+ const verdicts = scan?.verdicts;
370
+ return {
371
+ project: spec.project, path: spec.path, status: 'ok',
372
+ filesProcessed: scan?.filesProcessed, chunks: scan?.chunks, verdicts: verdicts,
373
+ embedded: verdicts ? verdicts.new + verdicts.modified : 0,
374
+ seconds: Math.round((Date.now() - startedAt) / 1000),
375
+ };
376
+ }
377
+ catch (error) {
378
+ return {
379
+ project: spec.project, path: spec.path, status: 'error',
380
+ seconds: Math.round((Date.now() - startedAt) / 1000),
381
+ error: { message: error?.message ?? 'ismeretlen scan-hiba' },
382
+ };
383
+ }
384
+ }
385
+ /**
386
+ * A feltételes cooldown DÖNTÉSE (pure, tesztelhető): a projektek KÖZÉ vár, ha cooldown be van kapcsolva
387
+ * (`cooldownMs>0`), NEM az utolsó projekt után (`!isLast`), ÉS az előző projekt ≥ `minChunks`-ot embeddelt.
388
+ */
389
+ static shouldCooldown(set) {
390
+ return set.cooldownMs > 0 && !set.isLast && set.embedded >= set.minChunks;
391
+ }
392
+ /**
393
+ * A projekt-lista feloldása: explicit `projects` > `configPath` (a szerver olvassa) > `root`-felderítés. A relatív
394
+ * ú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):
395
+ * a `configPath`/`root` ÉS a végső per-projekt `path` is.
396
+ */
397
+ static resolveSpecs(input) {
398
+ const base = (target) => (target && input.sourceLocation && !(0, path_1.isAbsolute)(target)) ? (0, path_1.resolve)(input.sourceLocation, target) : target;
399
+ let specs = [];
400
+ if (input.projects && input.projects.length > 0) {
401
+ specs = input.projects;
402
+ }
403
+ else if (input.configPath) {
404
+ specs = fam_project_discovery_util_1.FAM_ProjectDiscovery_Util.parseConfig((0, fs_1.readFileSync)(base(input.configPath) ?? input.configPath, 'utf-8'));
405
+ }
406
+ else if (input.root) {
407
+ specs = fam_project_discovery_util_1.FAM_ProjectDiscovery_Util.discover(base(input.root) ?? input.root, input.depth ?? 3, { org: input.org, table: input.table });
408
+ }
409
+ // „scan-all": az EXTRA célok (pl. a memóriám additív szkenje) a feloldott lista UTÁN (akkor is, ha a fő lista üres).
410
+ if (input.extraTargets && input.extraTargets.length) {
411
+ specs = [...specs, ...input.extraTargets];
412
+ }
413
+ // A per-projekt `path` relatív → a sourceLocation-höz feloldva (a discover már abszolútat ad; a config relatív lehet).
414
+ return specs.map((spec) => ({ ...spec, path: base(spec.path) ?? spec.path }));
415
+ }
416
+ /** Egy szám-config feloldása (a hiba/hiány a fallback-ra esik — a cooldown sosem buktatja a job-ot). */
417
+ static async resolveConfigNumber(key, fallback) {
418
+ try {
419
+ const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve(key, {});
420
+ const value = resolved.value;
421
+ const num = typeof value === 'number' ? value : Number(value);
422
+ return Number.isFinite(num) && num >= 0 ? Math.floor(num) : fallback;
423
+ }
424
+ catch {
425
+ return fallback;
426
+ }
427
+ }
428
+ }
429
+ exports.FAM_ScanJob_ControlService = FAM_ScanJob_ControlService;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAM_ScanJob_DataService = void 0;
4
+ const nts_dynamo_1 = require("@futdevpro/nts-dynamo");
5
+ const fam_scan_job_data_model_1 = require("../../../_models/data-models/fam-scan-job.data-model");
6
+ /**
7
+ * `FAM_ScanJob_DataService` (FEAT-008 Phase 4) — a `fam_scan_jobs` collection CRUD-ja a DURABLE scan-job-okhoz.
8
+ * A job a `start`-kor nyílik (`createJob`, friss `saveData`), és per-projekt FRISSÜL (`updateJobByJobId`, atomikus
9
+ * `$set` — a Mixed-szerű `projects[]`/`specs[]` ne dropoljon silent-en `findOne→mutate→save`-vel). A resume a
10
+ * `findResumable`-lel keresi a megszakadt (`running`/`pending`) job-okat. `extends DyNTS_DataService` (addArchive).
11
+ */
12
+ class FAM_ScanJob_DataService extends nts_dynamo_1.DyNTS_DataService {
13
+ constructor(set) {
14
+ super(set.data instanceof fam_scan_job_data_model_1.FAM_ScanJob_DataModel ? set.data : new fam_scan_job_data_model_1.FAM_ScanJob_DataModel(set.data), fam_scan_job_data_model_1.famScanJob_dataParams, set.issuer);
15
+ }
16
+ /** A move-to-archive soft-delete archív-service-e (a base `deleteData` ezt hívja; addArchive). */
17
+ getArchiveDataService() {
18
+ return new nts_dynamo_1.DyNTS_ArchiveDataService(new fam_scan_job_data_model_1.FAM_ScanJob_DataModel(), this.dataParams, this.issuer);
19
+ }
20
+ /** Egy ÚJ job durable rekordja (friss `saveData` — nincs Mixed-mutáció). */
21
+ async createJob(model) {
22
+ return this.saveData(model);
23
+ }
24
+ /** Egy job FRISSÍTÉSE a `jobId` alapján — atomikus `$set` (Mixed silent-drop ellen; per-projekt checkpoint). */
25
+ async updateJobByJobId(jobId, setBlock) {
26
+ const update = { $set: setBlock };
27
+ await this.updateData({ filterBy: { jobId: jobId }, update: update });
28
+ }
29
+ /** A megszakadt (resume-olandó) job-ok: `status ∈ {running, pending}`. A startup-resume ezeket folytatja. */
30
+ async findResumable() {
31
+ return this.findDataList({ status: { $in: ['running', 'pending'] } }, true);
32
+ }
33
+ }
34
+ exports.FAM_ScanJob_DataService = FAM_ScanJob_DataService;
@@ -266,15 +266,17 @@ class FAM_Scan_ControlService {
266
266
  return Array.from(tables);
267
267
  }
268
268
  /**
269
- * Az effektív ignore-patterns: a config `scan.ignorePatterns` (default `SCAN_IGNORE_PATTERNS_DEFAULT`)
270
- * + az input `exclude` HOZZÁADVA (dsgn-004 §6.2 a `write.scan.exclude` HOZZÁADÓDIK, NEM felülír).
269
+ * Az effektív ignore-patterns. **SAFETY-FLOOR (2026-06-22):** a `SCAN_IGNORE_PATTERNS_DEFAULT` MINDIG
270
+ * érvényesül a build/dist/.local-state/secrets-féle junk + személyes-adat SOSE eshet ki egy config-
271
+ * override-tól (ez volt a footgun: a régi config `scan.ignorePatterns` REPLACE-elte a defaultot). A config
272
+ * `scan.ignorePatterns` + az input `exclude` ezen FELÜL HOZZÁADÓDIK (dsgn-004 §6.2). Dedup-olt union.
271
273
  */
272
274
  async resolveIgnorePatterns(scopePath, exclude) {
273
275
  const resolved = await config_control_service_1.FAM_Config_ControlService.getInstance().resolve('scan.ignorePatterns', { scopePath: scopePath });
274
276
  const configPatterns = Array.isArray(resolved.value)
275
277
  ? resolved.value.filter((item) => typeof item === 'string')
276
- : config_catalog_const_1.SCAN_IGNORE_PATTERNS_DEFAULT;
277
- return [...configPatterns, ...(exclude ?? [])];
278
+ : [];
279
+ return [...new Set([...config_catalog_const_1.SCAN_IGNORE_PATTERNS_DEFAULT, ...configPatterns, ...(exclude ?? [])])];
278
280
  }
279
281
  /** A méret-plafon a config-ból (`scan.maxFileSizeBytes`, default 1 MB). */
280
282
  async resolveMaxFileSize(scopePath) {
@@ -12,7 +12,7 @@
12
12
  * (MVP1 csak fájlnév-/útvonal-alapú secret-exclude).
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.FAM_IngestRun_DataService = exports.FAM_ContentHash_Util = exports.FAM_DeltaCompare_Util = exports.FAM_SlidingChunker_Util = exports.FAM_MdChunker_Util = exports.FAM_TsChunker_Util = exports.FAM_Chunker_ControlService = exports.FAM_GlobMatch_Util = exports.FAM_SecretExclude_Util = exports.FAM_ScanPath_Util = exports.FAM_ScanProgress_Util = exports.FAM_FileRouting_Util = exports.FAM_Scan_ControlService = exports.FAM_Ingest_ControlService = void 0;
15
+ exports.FAM_IngestRun_DataService = exports.FAM_ContentHash_Util = exports.FAM_DeltaCompare_Util = exports.FAM_SplitChunker_Util = exports.FAM_Frontmatter_Util = exports.FAM_SlidingChunker_Util = exports.FAM_MdChunker_Util = exports.FAM_TsChunker_Util = exports.FAM_Chunker_ControlService = exports.FAM_GlobMatch_Util = exports.FAM_SecretExclude_Util = exports.FAM_ScanPath_Util = exports.FAM_ScanProgress_Util = exports.FAM_FileRouting_Util = exports.FAM_Scan_ControlService = exports.FAM_Ingest_ControlService = void 0;
16
16
  // SP-4.4 — ingest-orchestrátor (a `write(scan-*)` belépési pontja + group-by-run/delete-run)
17
17
  var fam_ingest_control_service_1 = require("./_services/fam-ingest.control-service");
18
18
  Object.defineProperty(exports, "FAM_Ingest_ControlService", { enumerable: true, get: function () { return fam_ingest_control_service_1.FAM_Ingest_ControlService; } });
@@ -38,6 +38,11 @@ var fam_md_chunker_util_1 = require("./_collections/fam-md-chunker.util");
38
38
  Object.defineProperty(exports, "FAM_MdChunker_Util", { enumerable: true, get: function () { return fam_md_chunker_util_1.FAM_MdChunker_Util; } });
39
39
  var fam_sliding_chunker_util_1 = require("./_collections/fam-sliding-chunker.util");
40
40
  Object.defineProperty(exports, "FAM_SlidingChunker_Util", { enumerable: true, get: function () { return fam_sliding_chunker_util_1.FAM_SlidingChunker_Util; } });
41
+ // Rules-Management MP-3 — fájl-fej frontmatter (F-A) + strukturális split-chunker (F-B)
42
+ var fam_frontmatter_util_1 = require("./_collections/fam-frontmatter.util");
43
+ Object.defineProperty(exports, "FAM_Frontmatter_Util", { enumerable: true, get: function () { return fam_frontmatter_util_1.FAM_Frontmatter_Util; } });
44
+ var fam_split_chunker_util_1 = require("./_collections/fam-split-chunker.util");
45
+ Object.defineProperty(exports, "FAM_SplitChunker_Util", { enumerable: true, get: function () { return fam_split_chunker_util_1.FAM_SplitChunker_Util; } });
41
46
  // SP-4.3 — idempotens delta-detection
42
47
  var fam_delta_compare_util_1 = require("./_services/fam-delta-compare.util");
43
48
  Object.defineProperty(exports, "FAM_DeltaCompare_Util", { enumerable: true, get: function () { return fam_delta_compare_util_1.FAM_DeltaCompare_Util; } });
@@ -25,7 +25,7 @@ const MAIN_TABLE_ENUM = Object.values(fam_table_type_enum_1.FAM_Table).filter((t
25
25
  /** A nyers scope-lánc array-schema (`{ layer, rawName }[]`). */
26
26
  const SCOPE_LAYER_ARRAY = {
27
27
  type: 'array',
28
- description: 'Nyers scope-lánc (root→leaf): [{ layer, rawName }] párok (pl. organization=FutDevPro, project=Adventor).',
28
+ description: 'Nyers scope-lánc (root→leaf): [{ layer: layer, rawName: rawName }] párok (pl. organization=FutDevPro, project=Adventor).',
29
29
  items: {
30
30
  type: 'object',
31
31
  properties: {