@futdevpro/fdp-agent-memory 0.1.0
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/README.md +345 -0
- package/build/package.json +96 -0
- package/build/src/_assets/mcp-client-config/README.md +29 -0
- package/build/src/_assets/mcp-client-config/claude_desktop_config.json +15 -0
- package/build/src/_assets/mcp-client-config/mcp.json +15 -0
- package/build/src/_collections/config-catalog.const.js +180 -0
- package/build/src/_collections/config-error-codes.const.js +30 -0
- package/build/src/_collections/config-presets.const.js +25 -0
- package/build/src/_collections/error-banners.const.js +100 -0
- package/build/src/_collections/error-codes.const.js +150 -0
- package/build/src/_collections/fam-db-models.const.js +37 -0
- package/build/src/_collections/fam-entry-bootstrap.util.js +80 -0
- package/build/src/_collections/fam-error-context.util.js +90 -0
- package/build/src/_collections/fam-error-factory.util.js +64 -0
- package/build/src/_enums/fam-config-level.type-enum.js +15 -0
- package/build/src/_enums/fam-table.type-enum.js +20 -0
- package/build/src/_integration-tests/_helpers/fam-integration-test-setup.util.js +105 -0
- package/build/src/_models/data-models/fam-codebase.data-model.js +51 -0
- package/build/src/_models/data-models/fam-coding-patterns.data-model.js +58 -0
- package/build/src/_models/data-models/fam-config.data-model.js +68 -0
- package/build/src/_models/data-models/fam-documents.data-model.js +53 -0
- package/build/src/_models/data-models/fam-entry-base-properties.const.js +43 -0
- package/build/src/_models/data-models/fam-entry.data-model.js +81 -0
- package/build/src/_models/data-models/fam-error.data-model.js +88 -0
- package/build/src/_models/data-models/fam-ingest-run.data-model.js +74 -0
- package/build/src/_models/data-models/fam-knowledge.data-model.js +48 -0
- package/build/src/_models/data-models/fam-memory.data-model.js +55 -0
- package/build/src/_models/data-models/fam-reference.data-model.js +67 -0
- package/build/src/_models/data-models/fam-rules.data-model.js +51 -0
- package/build/src/_models/data-models/fam-scope.data-model.js +52 -0
- package/build/src/_models/interfaces/fam-common.interface.js +23 -0
- package/build/src/_models/interfaces/fam-config.interface.js +2 -0
- package/build/src/_models/interfaces/fam-error.interface.js +2 -0
- package/build/src/_modules/embedding/_collections/fam-embedding-pricing.const.js +22 -0
- package/build/src/_modules/embedding/_collections/fam-store-registry.const.js +63 -0
- package/build/src/_modules/embedding/_models/interfaces/fam-embedding-cost.interface.js +10 -0
- package/build/src/_modules/embedding/_models/interfaces/fam-embedding-provider.interface.js +2 -0
- package/build/src/_modules/embedding/_models/interfaces/fam-resolved-provider.interface.js +2 -0
- package/build/src/_modules/embedding/_services/fam-embedding-bootstrap.control-service.js +52 -0
- package/build/src/_modules/embedding/_services/fam-embedding-cost.control-service.js +175 -0
- package/build/src/_modules/embedding/_services/fam-embedding-pipeline.control-service.js +202 -0
- package/build/src/_modules/embedding/_services/fam-embedding-preset.control-service.js +66 -0
- package/build/src/_modules/embedding/_services/fam-embedding.control-service.js +253 -0
- package/build/src/_modules/embedding/_services/fam-entry.data-service.js +64 -0
- package/build/src/_modules/embedding/_services/fam-lmstudio-embedding.provider.js +112 -0
- package/build/src/_modules/embedding/_services/fam-mock-embedding.provider.js +64 -0
- package/build/src/_modules/embedding/_services/fam-openai-embedding.provider.js +64 -0
- package/build/src/_modules/embedding/_services/fam-vector-search.control-service.js +244 -0
- package/build/src/_modules/embedding/index.js +40 -0
- package/build/src/_modules/ingest/_collections/fam-content-hash.util.js +35 -0
- package/build/src/_modules/ingest/_collections/fam-file-routing.util.js +95 -0
- package/build/src/_modules/ingest/_collections/fam-glob-match.util.js +84 -0
- package/build/src/_modules/ingest/_collections/fam-md-chunker.util.js +164 -0
- package/build/src/_modules/ingest/_collections/fam-scan-path.util.js +91 -0
- package/build/src/_modules/ingest/_collections/fam-secret-exclude.util.js +54 -0
- package/build/src/_modules/ingest/_collections/fam-sliding-chunker.util.js +76 -0
- package/build/src/_modules/ingest/_collections/fam-ts-chunker.util.js +316 -0
- package/build/src/_modules/ingest/_models/interfaces/fam-ingest.interface.js +2 -0
- package/build/src/_modules/ingest/_services/fam-chunker.control-service.js +114 -0
- package/build/src/_modules/ingest/_services/fam-delta-compare.util.js +74 -0
- package/build/src/_modules/ingest/_services/fam-ingest-run.data-service.js +85 -0
- package/build/src/_modules/ingest/_services/fam-ingest.control-service.js +384 -0
- package/build/src/_modules/ingest/_services/fam-scan.control-service.js +211 -0
- package/build/src/_modules/ingest/index.js +46 -0
- package/build/src/_modules/mcp/_collections/fam-core-tools.const.js +186 -0
- package/build/src/_modules/mcp/_models/interfaces/fam-mcp.interface.js +31 -0
- package/build/src/_modules/mcp/_services/fam-capabilities-tool.service.js +111 -0
- package/build/src/_modules/mcp/_services/fam-capability-registry.service.js +1180 -0
- package/build/src/_modules/mcp/_services/fam-mcp-adapter.service.js +123 -0
- package/build/src/_modules/mcp/_services/fam-mcp-server.service.js +69 -0
- package/build/src/_modules/mcp/_services/fam-read-tool.service.js +99 -0
- package/build/src/_modules/mcp/_services/fam-write-tool.service.js +460 -0
- package/build/src/_modules/mcp/index.js +35 -0
- package/build/src/_modules/migration/_collections/fam-claude-mem-normalize.util.js +166 -0
- package/build/src/_modules/migration/_collections/fam-import-content-hash.util.js +38 -0
- package/build/src/_modules/migration/_collections/fam-target-mapping.util.js +90 -0
- package/build/src/_modules/migration/_enums/fam-claude-mem-source.type-enum.js +20 -0
- package/build/src/_modules/migration/_models/interfaces/fam-claude-mem.interface.js +26 -0
- package/build/src/_modules/migration/_services/fam-claude-mem-export-reader.service.js +134 -0
- package/build/src/_modules/migration/_services/fam-claude-mem-import.control-service.js +533 -0
- package/build/src/_modules/migration/_services/fam-claude-mem-sqlite-reader.service.js +144 -0
- package/build/src/_modules/migration/_services/fam-claude-mem-worker-reader.service.js +115 -0
- package/build/src/_modules/migration/_services/fam-import-dedup.data-service.js +102 -0
- package/build/src/_modules/migration/index.js +38 -0
- package/build/src/_modules/retrieval/_models/interfaces/fam-retrieval.interface.js +2 -0
- package/build/src/_modules/retrieval/_services/fam-retrieval-candidate.data-service.js +67 -0
- package/build/src/_modules/retrieval/_services/fam-retrieval-suggestions.util.js +182 -0
- package/build/src/_modules/retrieval/_services/fam-retrieval.control-service.js +282 -0
- package/build/src/_modules/retrieval/index.js +22 -0
- package/build/src/_modules/scope-reference/_collections/fam-fuzzy-match.util.js +86 -0
- package/build/src/_modules/scope-reference/_collections/fam-scope-normalize.util.js +47 -0
- package/build/src/_modules/scope-reference/_models/interfaces/fam-reference-resolution.interface.js +2 -0
- package/build/src/_modules/scope-reference/_models/interfaces/fam-resolution-trace.interface.js +2 -0
- package/build/src/_modules/scope-reference/_services/fam-reference.data-service.js +179 -0
- package/build/src/_modules/scope-reference/_services/fam-scope-resolver.control-service.js +473 -0
- package/build/src/_modules/scope-reference/_services/fam-scope.data-service.js +215 -0
- package/build/src/_modules/scope-reference/index.js +26 -0
- package/build/src/_routes/server/api/api.controller.js +400 -0
- package/build/src/_routes/server/client-app/client-app.control-service.js +132 -0
- package/build/src/_routes/server/client-app/client-app.controller.js +35 -0
- package/build/src/_routes/server/config/config.control-service.js +476 -0
- package/build/src/_routes/server/config/config.data-service.js +49 -0
- package/build/src/_routes/server/errors/errors.control-service.js +123 -0
- package/build/src/_routes/server/errors/errors.controller.js +65 -0
- package/build/src/_routes/server/errors/errors.data-service.js +80 -0
- package/build/src/_routes/server/server-status/server-status.control-service.js +19 -0
- package/build/src/_routes/server/server-status/server-status.controller.js +39 -0
- package/build/src/app.server.js +122 -0
- package/build/src/environments/environment.js +20 -0
- package/build/src/index.js +18 -0
- package/client-dist/chunk-GHKRM4SM.js +1 -0
- package/client-dist/chunk-LMTL7GA3.js +575 -0
- package/client-dist/index.html +17 -0
- package/client-dist/main-2KWB3QYK.js +2 -0
- package/client-dist/polyfills-HGDOEU5L.js +2 -0
- package/client-dist/styles-3J7JD5YE.css +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClientApp_ControlService = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const express_1 = tslib_1.__importDefault(require("express"));
|
|
8
|
+
const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
|
|
9
|
+
const nts_dynamo_1 = require("@futdevpro/nts-dynamo");
|
|
10
|
+
/**
|
|
11
|
+
* A beépített Angular admin-UI (`client-dist`) kiszolgálása. A klienst a build-lépés (`build-ui-for-publish`,
|
|
12
|
+
* dsgn-012 §9) a `server/client-dist`-be másolja, és a publikált package `files`-ében ship-elődik — így
|
|
13
|
+
* EGY `fam start` (vagy a lokál Docker-image) a teljes felületet adja localhoston, külön kliens-process
|
|
14
|
+
* NÉLKÜL (dsgn-011 / dsgn-012; a `ccap-revisioned` `SC_ClientStatic` mintára).
|
|
15
|
+
*
|
|
16
|
+
* Mount: a routing-modul route `/app`, a DyNTS `baseUrl='/api'` elé fűzi → a UI a **`/api/app`** alatt él
|
|
17
|
+
* (a build base-href ezért `/api/app/`). A req.url-ből MINDKÉT lehetséges prefixet (`/api/app`, `/app`)
|
|
18
|
+
* levágjuk, mert a framework levághatja az `/api`-t.
|
|
19
|
+
*/
|
|
20
|
+
const CLIENT_DIST_CANDIDATES = [
|
|
21
|
+
// build: server/build/src/_routes/server/client-app/ → 5 szint fel a server/-ig
|
|
22
|
+
path.resolve(__dirname, '..', '..', '..', '..', '..', 'client-dist'),
|
|
23
|
+
// ts-node: server/src/_routes/server/client-app/ → 4 szint fel a server/-ig
|
|
24
|
+
path.resolve(__dirname, '..', '..', '..', '..', 'client-dist'),
|
|
25
|
+
// futtatási könyvtárból (pl. Docker WORKDIR /app)
|
|
26
|
+
path.resolve(process.cwd(), 'client-dist'),
|
|
27
|
+
];
|
|
28
|
+
const CLIENT_DIST_PATH = CLIENT_DIST_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ?? CLIENT_DIST_CANDIDATES[0];
|
|
29
|
+
/** A UI mount-prefixei (a static-fájl feloldáshoz a req.url-ből levágandók). */
|
|
30
|
+
const API_APP_MOUNT_PATH = '/api/app';
|
|
31
|
+
const APP_MOUNT_PATH = '/app';
|
|
32
|
+
/** Base href az index.html-be — az asset-ek így `/api/app/` alól töltődnek. */
|
|
33
|
+
const BASE_HREF_FOR_APP = '/api/app/';
|
|
34
|
+
/** Fallback HTML, ha nincs `client-dist` (pl. dev-ben még nem épült) — a gyökér ne adjon 404-et. */
|
|
35
|
+
const FALLBACK_HTML = `<!DOCTYPE html>
|
|
36
|
+
<html lang="en">
|
|
37
|
+
<head>
|
|
38
|
+
<meta charset="UTF-8">
|
|
39
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
40
|
+
<title>FDP Agent Memory — server running</title>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<h1>FDP Agent Memory server is running</h1>
|
|
44
|
+
<p>The admin UI is not built yet. Options:</p>
|
|
45
|
+
<ul>
|
|
46
|
+
<li>From the repo: run <code>pnpm run build-ui-for-publish</code> (server), then reload.</li>
|
|
47
|
+
<li>Dev: start the Angular dev server in <code>client/</code> (<code>pnpm start</code>) at <code>http://localhost:4205</code>.</li>
|
|
48
|
+
</ul>
|
|
49
|
+
</body>
|
|
50
|
+
</html>`;
|
|
51
|
+
/**
|
|
52
|
+
* `ClientApp_ControlService` — a beépített kliens-UI statikus kiszolgálásának logikája (a thin controller
|
|
53
|
+
* ide delegál). `init()` a mount-kor ellenőrzi a `client-dist` meglétét; `handleRequest` static-fájlt vagy
|
|
54
|
+
* SPA-fallback (base-href-injektált index.html) szolgál ki, hiányzó dist esetén fallback HTML-t.
|
|
55
|
+
*/
|
|
56
|
+
class ClientApp_ControlService extends nts_dynamo_1.DyNTS_SingletonService {
|
|
57
|
+
static getInstance() {
|
|
58
|
+
return ClientApp_ControlService.getSingletonInstance();
|
|
59
|
+
}
|
|
60
|
+
staticMiddleware = null;
|
|
61
|
+
indexPath = '';
|
|
62
|
+
isClientDistAvailable = false;
|
|
63
|
+
/** Mount-kori inicializálás (a controller `setupEndpoints`-ból): a `client-dist` meglét + static-middleware. */
|
|
64
|
+
init() {
|
|
65
|
+
this.isClientDistAvailable = fs.existsSync(CLIENT_DIST_PATH);
|
|
66
|
+
if (!this.isClientDistAvailable) {
|
|
67
|
+
fsm_dynamo_1.DyFM_Log.info('FDP Agent Memory UI: client-dist hiányzik — fallback HTML szolgál ki a gyökéren.');
|
|
68
|
+
return { isClientDistAvailable: false, clientDistPath: CLIENT_DIST_PATH };
|
|
69
|
+
}
|
|
70
|
+
// index:false — az index.html-t NE az express.static adja ki közvetlenül; a SPA-fallbacken megy át,
|
|
71
|
+
// ami a helyes base-href-et (`/api/app/`) injektálja, függetlenül a build base-href-jétől.
|
|
72
|
+
this.staticMiddleware = express_1.default.static(CLIENT_DIST_PATH, {
|
|
73
|
+
index: false,
|
|
74
|
+
setHeaders: (res, filePath) => {
|
|
75
|
+
if (filePath.endsWith('.map')) {
|
|
76
|
+
res.setHeader('Content-Type', 'application/json');
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
this.indexPath = path.resolve(CLIENT_DIST_PATH, 'index.html');
|
|
81
|
+
fsm_dynamo_1.DyFM_Log.info(`FDP Agent Memory UI kiszolgálása: ${CLIENT_DIST_PATH} (statikus + SPA-fallback, /api/app).`);
|
|
82
|
+
return { isClientDistAvailable: true, clientDistPath: CLIENT_DIST_PATH };
|
|
83
|
+
}
|
|
84
|
+
/** A `GET *` handler: dist-állapot szerint static+SPA-fallback, vagy fallback HTML. */
|
|
85
|
+
async handleRequest(req, res, issuer) {
|
|
86
|
+
if (this.isClientDistAvailable) {
|
|
87
|
+
await this.handleServeClient(req, res, issuer);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await this.handleFallback(req, res, issuer);
|
|
91
|
+
}
|
|
92
|
+
/** Fallback HTML (nincs client-dist). */
|
|
93
|
+
async handleFallback(_req, res, _issuer) {
|
|
94
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
95
|
+
res.status(200).send(FALLBACK_HTML);
|
|
96
|
+
}
|
|
97
|
+
/** Static-fájl vagy SPA-fallback (base-href-injektált index.html). */
|
|
98
|
+
async handleServeClient(req, res, _issuer) {
|
|
99
|
+
const staticMiddleware = this.staticMiddleware;
|
|
100
|
+
const indexPath = this.indexPath;
|
|
101
|
+
const serveIndex = () => {
|
|
102
|
+
if (fs.existsSync(indexPath)) {
|
|
103
|
+
const raw = fs.readFileSync(indexPath, 'utf-8');
|
|
104
|
+
const withBase = raw.replace(/<base\s+href="[^"]*"\s*\/?>/i, `<base href="${BASE_HREF_FOR_APP}">`);
|
|
105
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
106
|
+
res.status(200).send(withBase);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
res.status(404).send('Not found');
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
if (!staticMiddleware) {
|
|
113
|
+
serveIndex();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// A static-middleware a client-dist gyökeréhez viszonyít — a mount-prefixeket le kell vágni.
|
|
117
|
+
const originalUrl = req.url;
|
|
118
|
+
let relativeUrl = originalUrl.replace(this.prefixRegExp(API_APP_MOUNT_PATH), '/');
|
|
119
|
+
relativeUrl = relativeUrl.replace(this.prefixRegExp(APP_MOUNT_PATH), '/');
|
|
120
|
+
req.url = relativeUrl || '/';
|
|
121
|
+
staticMiddleware(req, res, () => {
|
|
122
|
+
req.url = originalUrl;
|
|
123
|
+
serveIndex();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/** Egy mount-prefix-levágó regexp (a regexp-meta karakterek escapelve). */
|
|
127
|
+
prefixRegExp(mountPath) {
|
|
128
|
+
const escaped = mountPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
129
|
+
return new RegExp(`^${escaped}/?`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
exports.ClientApp_ControlService = ClientApp_ControlService;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClientApp_Controller = void 0;
|
|
4
|
+
const fsm_dynamo_1 = require("@futdevpro/fsm-dynamo");
|
|
5
|
+
const nts_dynamo_1 = require("@futdevpro/nts-dynamo");
|
|
6
|
+
const client_app_control_service_1 = require("./client-app.control-service");
|
|
7
|
+
/**
|
|
8
|
+
* `ClientApp_Controller` — a beépített Angular admin-UI kiszolgálása (route `/app` → DyNTS `baseUrl='/api'`
|
|
9
|
+
* elé fűzve → **`/api/app`**). Egyetlen `GET *` catch-all: static-fájl, ha van; egyébként SPA-fallback
|
|
10
|
+
* (base-href-injektált index.html); ha nincs `client-dist`, fallback HTML (a gyökér ne 404-ezzen).
|
|
11
|
+
*
|
|
12
|
+
* Thin controller (csak delegálás a `ClientApp_ControlService`-re — nincs logika/validáció). Singleton.
|
|
13
|
+
*/
|
|
14
|
+
class ClientApp_Controller extends nts_dynamo_1.DyNTS_Controller {
|
|
15
|
+
static getInstance() {
|
|
16
|
+
return ClientApp_Controller.getSingletonInstance();
|
|
17
|
+
}
|
|
18
|
+
clientApp_CS = client_app_control_service_1.ClientApp_ControlService.getInstance();
|
|
19
|
+
setupEndpoints() {
|
|
20
|
+
this.clientApp_CS.init();
|
|
21
|
+
this.endpoints = [
|
|
22
|
+
new nts_dynamo_1.DyNTS_Endpoint_Params({
|
|
23
|
+
name: 'serveClientOrFallback',
|
|
24
|
+
type: fsm_dynamo_1.DyFM_HttpCallType.get,
|
|
25
|
+
endpoint: '*',
|
|
26
|
+
tasks: [
|
|
27
|
+
async (req, res, issuer) => {
|
|
28
|
+
await this.clientApp_CS.handleRequest(req, res, issuer);
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
}),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.ClientApp_Controller = ClientApp_Controller;
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FAM_Config_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_catalog_const_1 = require("../../../_collections/config-catalog.const");
|
|
7
|
+
const config_error_codes_const_1 = require("../../../_collections/config-error-codes.const");
|
|
8
|
+
const config_presets_const_1 = require("../../../_collections/config-presets.const");
|
|
9
|
+
const fam_config_data_model_1 = require("../../../_models/data-models/fam-config.data-model");
|
|
10
|
+
const config_data_service_1 = require("./config.data-service");
|
|
11
|
+
/**
|
|
12
|
+
* `FAM_Config_ControlService` — a DB-backed, scoped config feloldó-szolgáltatás (dsgn-007 §8).
|
|
13
|
+
*
|
|
14
|
+
* Singleton. Tartja a `CONFIG_CATALOG`-ot (§4), a preseteket (§5), a validációt (§7) és a cache-t (§3).
|
|
15
|
+
* `resolve(key, ctx)` a legspecifikusabb beállított értéket adja **scope > table > global > builtin**
|
|
16
|
+
* precedenciával; `resolveAll(ctx)` a teljes effektív config egy Mongo-körrel. **BFR-AM-004
|
|
17
|
+
* projekt-lokális workaround** — a `resolve`/`resolveAll`/`set` API-kontraktusa STABIL, hogy a
|
|
18
|
+
* jövőbeli `DyNTS_ScopedConfig_*` bedrock-csere non-breaking legyen.
|
|
19
|
+
*
|
|
20
|
+
* **FIGYELEM (memory: dynts_dataservice_eager_resolve):** NEM tartunk élő `FAM_Config_DataService`
|
|
21
|
+
* mezőt (eager DB-resolve a base-ctor-ban) — minden DB-művelet előtt lazy `new FAM_Config_DataService`.
|
|
22
|
+
*/
|
|
23
|
+
class FAM_Config_ControlService {
|
|
24
|
+
static _instance;
|
|
25
|
+
/** Default issuer a config-műveletekhez (a hibák `issuer`-éhez, dsgn-008). */
|
|
26
|
+
issuer = 'FAM_Config_ControlService';
|
|
27
|
+
/** Rövid-TTL memória-cache (kulcs: kontextus-szelet; `set`/`applyPreset` invalidálja, dsgn-007 §3). */
|
|
28
|
+
cache = new Map();
|
|
29
|
+
/** A cache TTL ms-ben (~60s, a `reference.cacheTtlMs` builtin-jével összhangban). */
|
|
30
|
+
cacheTtlMs = 60000;
|
|
31
|
+
static getInstance() {
|
|
32
|
+
if (!FAM_Config_ControlService._instance) {
|
|
33
|
+
FAM_Config_ControlService._instance = new FAM_Config_ControlService();
|
|
34
|
+
}
|
|
35
|
+
return FAM_Config_ControlService._instance;
|
|
36
|
+
}
|
|
37
|
+
// =========================================================================
|
|
38
|
+
// RESOLVE (dsgn-007 §3) — precedencia: scope (leaf→root) > table > global > builtin
|
|
39
|
+
// =========================================================================
|
|
40
|
+
/**
|
|
41
|
+
* Egy kulcs feloldása a legspecifikusabb beállított értékre (dsgn-007 §3). A precedencia:
|
|
42
|
+
* (1) SCOPE szint a `scopePath` LEVELÉBŐL a gyökér felé (az első/legmélyebb találat nyer),
|
|
43
|
+
* (2) TABLE szint, (3) GLOBAL szint, (4) `CONFIG_CATALOG[key].default` builtin (mindig van).
|
|
44
|
+
*
|
|
45
|
+
* A `scopePath` canonical formában érkezik (a read/write-path a reference-resolveren átfuttatta).
|
|
46
|
+
*/
|
|
47
|
+
async resolve(key, context) {
|
|
48
|
+
this.assertKnownKey(key);
|
|
49
|
+
const records = await this.loadEffectiveRecords(context);
|
|
50
|
+
return this.resolveFromRecords(key, records, context);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A teljes effektív config egy adott kontextusra (dsgn-007 §3 batch). EGY Mongo-kör (cache-elt),
|
|
54
|
+
* majd memóriában merge: minden katalógus-kulcsra a feloldott érték + forrás-szint. A hot-path
|
|
55
|
+
* (read/write) ezt használja, nem per-key kört.
|
|
56
|
+
*/
|
|
57
|
+
async resolveAll(context) {
|
|
58
|
+
const records = await this.loadEffectiveRecords(context);
|
|
59
|
+
const result = {};
|
|
60
|
+
for (const key of this.allKeys()) {
|
|
61
|
+
result[key] = this.resolveFromRecords(key, records, context);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A feloldott effektív érték + forrás-szint (dsgn-007 §7.2). A `capabilities` `get_config_status`
|
|
67
|
+
* / CLI `config get` / UI ezt használja: átlátható, HONNAN jön egy érték.
|
|
68
|
+
*/
|
|
69
|
+
async getConfigStatus(key, context) {
|
|
70
|
+
return this.resolve(key, context);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A `resolve` mag (memóriában, a betöltött rekord-halmazon). Külön metódus, hogy a `resolveAll`
|
|
74
|
+
* is ezt használja (egyetlen Mongo-kör után minden kulcsra).
|
|
75
|
+
*/
|
|
76
|
+
resolveFromRecords(key, records, context) {
|
|
77
|
+
const table = context?.table;
|
|
78
|
+
const scopePath = context?.scopePath ?? [];
|
|
79
|
+
// 1. SCOPE szint — a scopePath leveléből a gyökér felé (leaf→root); az első találat nyer.
|
|
80
|
+
if (table && scopePath.length) {
|
|
81
|
+
for (let i = scopePath.length - 1; i >= 0; i--) {
|
|
82
|
+
const scopeId = scopePath[i].scopeId;
|
|
83
|
+
const hit = records.find((rec) => rec.level === fam_config_level_type_enum_1.FAM_ConfigLevel.scope && rec.tableScope === table && rec.scopeId === scopeId && rec.key === key);
|
|
84
|
+
if (hit) {
|
|
85
|
+
return this.toResolved(hit, 'scope');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// 2. TABLE szint.
|
|
90
|
+
if (table) {
|
|
91
|
+
const hit = records.find((rec) => rec.level === fam_config_level_type_enum_1.FAM_ConfigLevel.table && rec.tableScope === table && rec.key === key);
|
|
92
|
+
if (hit) {
|
|
93
|
+
return this.toResolved(hit, 'table');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// 3. GLOBAL szint.
|
|
97
|
+
const globalHit = records.find((rec) => rec.level === fam_config_level_type_enum_1.FAM_ConfigLevel.global && rec.key === key);
|
|
98
|
+
if (globalHit) {
|
|
99
|
+
return this.toResolved(globalHit, 'global');
|
|
100
|
+
}
|
|
101
|
+
// 4. builtin default (mindig van — dsgn-007 §3 / §10.3).
|
|
102
|
+
return {
|
|
103
|
+
value: config_catalog_const_1.CONFIG_CATALOG[key].default,
|
|
104
|
+
resolvedFrom: 'builtin',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Egy DB-rekordot a feloldott-érték shape-re (a forrás-szinttel + audittal). */
|
|
108
|
+
toResolved(record, resolvedFrom) {
|
|
109
|
+
return {
|
|
110
|
+
value: record.value,
|
|
111
|
+
resolvedFrom: resolvedFrom,
|
|
112
|
+
scopeId: record.scopeId,
|
|
113
|
+
setBy: record.setBy,
|
|
114
|
+
setByDetail: record.setByDetail,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A kontextusra releváns aktív rekordok betöltése EGY Mongo-körrel (dsgn-007 §3), cache-elve.
|
|
119
|
+
* A filter a global + (ha van) table + a scopePath összes scopeId-jára szűkít — így a merge
|
|
120
|
+
* memóriában fut. `set`/`applyPreset` invalidál.
|
|
121
|
+
*/
|
|
122
|
+
async loadEffectiveRecords(context) {
|
|
123
|
+
const cacheKey = this.cacheKey(context);
|
|
124
|
+
const cached = this.cache.get(cacheKey);
|
|
125
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
126
|
+
return cached.records;
|
|
127
|
+
}
|
|
128
|
+
const orClauses = [{ level: fam_config_level_type_enum_1.FAM_ConfigLevel.global }];
|
|
129
|
+
if (context?.table) {
|
|
130
|
+
orClauses.push({ level: fam_config_level_type_enum_1.FAM_ConfigLevel.table, tableScope: context.table });
|
|
131
|
+
const scopeIds = (context.scopePath ?? []).map((ref) => ref.scopeId);
|
|
132
|
+
if (scopeIds.length) {
|
|
133
|
+
orClauses.push({ level: fam_config_level_type_enum_1.FAM_ConfigLevel.scope, tableScope: context.table, scopeId: { $in: scopeIds } });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const dataService = new config_data_service_1.FAM_Config_DataService({ issuer: this.issuer });
|
|
137
|
+
const records = await dataService.findActiveList({ $or: orClauses });
|
|
138
|
+
this.cache.set(cacheKey, { records: records, expiresAt: Date.now() + this.cacheTtlMs });
|
|
139
|
+
return records;
|
|
140
|
+
}
|
|
141
|
+
/** A cache-kulcs egy kontextus-szeletre (table + a scopePath scopeId-lánca). */
|
|
142
|
+
cacheKey(context) {
|
|
143
|
+
const table = context?.table ?? '-';
|
|
144
|
+
const scopeIds = (context?.scopePath ?? []).map((ref) => ref.scopeId).join('>') || '-';
|
|
145
|
+
return `${table}|${scopeIds}`;
|
|
146
|
+
}
|
|
147
|
+
/** A teljes cache invalidálása (`set`/`applyPreset` után — dsgn-007 §3). */
|
|
148
|
+
invalidateCache() {
|
|
149
|
+
this.cache.clear();
|
|
150
|
+
}
|
|
151
|
+
// =========================================================================
|
|
152
|
+
// SET (dsgn-007 §7) — validate → archive-old → write-new → invalidate cache
|
|
153
|
+
// =========================================================================
|
|
154
|
+
/**
|
|
155
|
+
* Egy config-érték beállítása egy adott szinten (dsgn-007 §7). A `set`:
|
|
156
|
+
* (1) validál a `CONFIG_CATALOG` ellen (típus / tartomány / szint / scope; `FAM-CONFIG-SET-00x`),
|
|
157
|
+
* (2) a ugyanazon `(level,tableScope,scopeId,key)` kulcson lévő AKTÍV régi rekordot soft-delete-eli
|
|
158
|
+
* (archív → history), (3) az új aktív rekordot `$set`-tel írja (Mixed atomikus), (4) invalidál.
|
|
159
|
+
*/
|
|
160
|
+
async set(key, value, options) {
|
|
161
|
+
await this.validate(key, value, options);
|
|
162
|
+
const filter = this.levelKeyFilter(key, options);
|
|
163
|
+
const dataService = new config_data_service_1.FAM_Config_DataService({ issuer: options.issuer ?? this.issuer });
|
|
164
|
+
// Régi aktív rekord (ha van) → soft-delete (archív; history-megőrzés, dsgn-007 §7.2).
|
|
165
|
+
const existing = await dataService.findActive(filter);
|
|
166
|
+
if (existing && existing._id) {
|
|
167
|
+
await dataService.deleteData(existing._id);
|
|
168
|
+
}
|
|
169
|
+
// Új aktív rekord — a `value` Mixed; a saveData $set-tel ír (Mongoose Mixed silent-drop ellen).
|
|
170
|
+
const record = new fam_config_data_model_1.FAM_Config_DataModel({
|
|
171
|
+
level: options.level,
|
|
172
|
+
tableScope: options.tableScope,
|
|
173
|
+
scopeId: options.scopeId,
|
|
174
|
+
key: key,
|
|
175
|
+
value: value,
|
|
176
|
+
setBy: options.setBy ?? 'system',
|
|
177
|
+
setByDetail: options.setByDetail,
|
|
178
|
+
note: options.note,
|
|
179
|
+
});
|
|
180
|
+
const writeService = new config_data_service_1.FAM_Config_DataService({ data: record, issuer: options.issuer ?? this.issuer });
|
|
181
|
+
const saved = await writeService.saveData(record);
|
|
182
|
+
this.invalidateCache();
|
|
183
|
+
return saved;
|
|
184
|
+
}
|
|
185
|
+
/** A `(level, tableScope, scopeId, key)` feloldási-kulcs filtere (a logikai unique-constraint). */
|
|
186
|
+
levelKeyFilter(key, options) {
|
|
187
|
+
const filter = { level: options.level, key: key };
|
|
188
|
+
if (options.level === fam_config_level_type_enum_1.FAM_ConfigLevel.table || options.level === fam_config_level_type_enum_1.FAM_ConfigLevel.scope) {
|
|
189
|
+
filter.tableScope = options.tableScope;
|
|
190
|
+
}
|
|
191
|
+
if (options.level === fam_config_level_type_enum_1.FAM_ConfigLevel.scope) {
|
|
192
|
+
filter.scopeId = options.scopeId;
|
|
193
|
+
}
|
|
194
|
+
return filter;
|
|
195
|
+
}
|
|
196
|
+
// =========================================================================
|
|
197
|
+
// VALIDATE (dsgn-007 §7.1) — FAM-CONFIG-SET-001..006
|
|
198
|
+
// =========================================================================
|
|
199
|
+
/**
|
|
200
|
+
* A `set`-kori validáció (dsgn-007 §7.1). Lehetetlen érték → deskriptív hiba (a "néma hiba tilos");
|
|
201
|
+
* a hibakódok a dsgn-007 §7 SSOT-ból (`FAM-CONFIG-SET-001..006`).
|
|
202
|
+
*
|
|
203
|
+
* A scope-létezés (`FAM-CONFIG-SET-006`) ellenőrzése MP-3 (`FAM_Scope`) függő: itt a `scopeId`
|
|
204
|
+
* jelenlétét kötelezővé tesszük scope-szinten; a tényleges scope-lét ellenőrzés MP-3 után
|
|
205
|
+
* aktiválható (a kontraktus stabil).
|
|
206
|
+
*/
|
|
207
|
+
async validate(key, value, options) {
|
|
208
|
+
const entry = this.assertKnownKey(key);
|
|
209
|
+
// Szint-értelmezés (FAM-CONFIG-SET-005).
|
|
210
|
+
this.validateLevel(key, entry, options);
|
|
211
|
+
// Típus (FAM-CONFIG-SET-002).
|
|
212
|
+
this.validateType(key, entry, value);
|
|
213
|
+
// Tartomány / enum (FAM-CONFIG-SET-003).
|
|
214
|
+
this.validateRange(key, entry, value);
|
|
215
|
+
// Cross-field invariáns (FAM-CONFIG-SET-004) a feloldott kontextusban.
|
|
216
|
+
await this.validateCrossField(key, value, options);
|
|
217
|
+
// Scope-azonosítás (FAM-CONFIG-SET-006) — scope-szinten a scopeId kötelező.
|
|
218
|
+
if (options.level === fam_config_level_type_enum_1.FAM_ConfigLevel.scope && !options.scopeId) {
|
|
219
|
+
throw this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.setScopeNotFound, `Config '${key}' scope-szintű beállításához kötelező a scopeId (létező aktív FAM_Scope).`, options.issuer);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Ismeretlen kulcs → `FAM-CONFIG-SET-001` (hasonló-kulcs javaslattal). */
|
|
223
|
+
assertKnownKey(key) {
|
|
224
|
+
const entry = config_catalog_const_1.CONFIG_CATALOG[key];
|
|
225
|
+
if (!entry) {
|
|
226
|
+
const suggestions = this.similarKeys(key);
|
|
227
|
+
const hint = suggestions.length ? ` Hasonló kulcsok: ${suggestions.join(', ')}.` : '';
|
|
228
|
+
throw this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.setUnknownKey, `Ismeretlen config-kulcs: '${key}'. A kulcs nincs a CONFIG_CATALOG-ban.${hint}`);
|
|
229
|
+
}
|
|
230
|
+
return entry;
|
|
231
|
+
}
|
|
232
|
+
/** Szint-sértés → `FAM-CONFIG-SET-005` (a kulcs nem értelmezett a kért level-en / tableOnly). */
|
|
233
|
+
validateLevel(key, entry, options) {
|
|
234
|
+
if (!entry.levels.includes(options.level)) {
|
|
235
|
+
throw this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.setLevelViolation, `Config '${key}' nem értelmezett a(z) '${options.level}' szinten. Megengedett: ${entry.levels.join(', ')}.`, options.issuer);
|
|
236
|
+
}
|
|
237
|
+
if ((options.level === fam_config_level_type_enum_1.FAM_ConfigLevel.table || options.level === fam_config_level_type_enum_1.FAM_ConfigLevel.scope) && !options.tableScope) {
|
|
238
|
+
throw this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.setLevelViolation, `Config '${key}' '${options.level}' szintű beállításához kötelező a tableScope.`, options.issuer);
|
|
239
|
+
}
|
|
240
|
+
if (entry.tableOnly && options.tableScope && options.tableScope !== entry.tableOnly) {
|
|
241
|
+
throw this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.setLevelViolation, `Config '${key}' CSAK a(z) '${entry.tableOnly}' táron állítható (kapott: '${options.tableScope}').`, options.issuer);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Típus-eltérés → `FAM-CONFIG-SET-002`. */
|
|
245
|
+
validateType(key, entry, value) {
|
|
246
|
+
const fail = (expected) => {
|
|
247
|
+
throw this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.setTypeMismatch, `Config '${key}' várt típusa '${expected}', kapott: '${this.typeOf(value)}'.`);
|
|
248
|
+
};
|
|
249
|
+
switch (entry.type) {
|
|
250
|
+
case 'number':
|
|
251
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
252
|
+
fail('number');
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
case 'number-or-null':
|
|
256
|
+
if (value !== null && (typeof value !== 'number' || Number.isNaN(value))) {
|
|
257
|
+
fail('number | null');
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
case 'string':
|
|
261
|
+
if (typeof value !== 'string') {
|
|
262
|
+
fail('string');
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
case 'boolean':
|
|
266
|
+
if (typeof value !== 'boolean') {
|
|
267
|
+
fail('boolean');
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
case 'string[]':
|
|
271
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
272
|
+
fail('string[]');
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
default:
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** Tartomány-/enum-sértés → `FAM-CONFIG-SET-003`. */
|
|
280
|
+
validateRange(key, entry, value) {
|
|
281
|
+
// number / number-or-null tartomány + integer.
|
|
282
|
+
if ((entry.type === 'number' || entry.type === 'number-or-null') && typeof value === 'number') {
|
|
283
|
+
if (entry.integer && !Number.isInteger(value)) {
|
|
284
|
+
throw this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.setRangeViolation, `Config '${key}' egész számot vár (kapott tört: ${value}).`);
|
|
285
|
+
}
|
|
286
|
+
if (entry.min !== undefined && value < entry.min) {
|
|
287
|
+
throw this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.setRangeViolation, `Config '${key}' minimum ${entry.min} (kapott: ${value}).`);
|
|
288
|
+
}
|
|
289
|
+
if (entry.max !== undefined && value > entry.max) {
|
|
290
|
+
throw this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.setRangeViolation, `Config '${key}' maximum ${entry.max} (kapott: ${value}).`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// string enum.
|
|
294
|
+
if (entry.type === 'string' && entry.enumValues && typeof value === 'string' && !entry.enumValues.includes(value)) {
|
|
295
|
+
throw this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.setRangeViolation, `Config '${key}' megengedett értékei: ${entry.enumValues.join(' | ')} (kapott: '${value}').`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Cross-field invariáns (dsgn-007 §7.1, `FAM-CONFIG-SET-004`) a feloldott kontextusban:
|
|
300
|
+
* `read.topK <= read.maxResults`, minden `chunk.*.overlap < chunk.*.maxSize`,
|
|
301
|
+
* `embedding.costWarnUsd <= embedding.costCapUsd` (ha mindkettő adott). A "másik" mező a hozzá
|
|
302
|
+
* tartozó kontextuson feloldva (a most beállítandó értéket az adott kulcsra behelyettesítve).
|
|
303
|
+
*/
|
|
304
|
+
async validateCrossField(key, value, options) {
|
|
305
|
+
const context = {
|
|
306
|
+
table: options.tableScope,
|
|
307
|
+
scopePath: options.scopeId ? [{ scopeId: options.scopeId, layer: '', canonicalName: '' }] : undefined,
|
|
308
|
+
};
|
|
309
|
+
const resolvedNumber = async (otherKey) => {
|
|
310
|
+
const resolved = await this.resolve(otherKey, context);
|
|
311
|
+
return typeof resolved.value === 'number' ? resolved.value : null;
|
|
312
|
+
};
|
|
313
|
+
// read.topK <= read.maxResults
|
|
314
|
+
if (key === 'read.topK' && typeof value === 'number') {
|
|
315
|
+
const maxResults = await resolvedNumber('read.maxResults');
|
|
316
|
+
if (maxResults !== null && value > maxResults) {
|
|
317
|
+
throw this.crossFieldError(`read.topK (${value}) nem haladhatja meg a read.maxResults-t (${maxResults}).`, options);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (key === 'read.maxResults' && typeof value === 'number') {
|
|
321
|
+
const topK = await resolvedNumber('read.topK');
|
|
322
|
+
if (topK !== null && topK > value) {
|
|
323
|
+
throw this.crossFieldError(`read.maxResults (${value}) nem lehet kisebb a read.topK-nál (${topK}).`, options);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// chunk.*.overlap < chunk.*.maxSize
|
|
327
|
+
const overlapMatch = /^chunk\.(ts|md|generic)\.overlap$/.exec(key);
|
|
328
|
+
if (overlapMatch && typeof value === 'number') {
|
|
329
|
+
const maxSizeKey = `chunk.${overlapMatch[1]}.maxSize`;
|
|
330
|
+
const maxSize = await resolvedNumber(maxSizeKey);
|
|
331
|
+
if (maxSize !== null && value >= maxSize) {
|
|
332
|
+
throw this.crossFieldError(`${key} (${value}) kisebb kell legyen, mint ${maxSizeKey} (${maxSize}).`, options);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const maxSizeMatch = /^chunk\.(ts|md|generic)\.maxSize$/.exec(key);
|
|
336
|
+
if (maxSizeMatch && typeof value === 'number') {
|
|
337
|
+
const overlapKey = `chunk.${maxSizeMatch[1]}.overlap`;
|
|
338
|
+
const overlap = await resolvedNumber(overlapKey);
|
|
339
|
+
if (overlap !== null && overlap >= value) {
|
|
340
|
+
throw this.crossFieldError(`${key} (${value}) nagyobb kell legyen, mint ${overlapKey} (${overlap}).`, options);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// embedding.costWarnUsd <= embedding.costCapUsd
|
|
344
|
+
if (key === 'embedding.costWarnUsd' && typeof value === 'number') {
|
|
345
|
+
const cap = await resolvedNumber('embedding.costCapUsd');
|
|
346
|
+
if (cap !== null && value > cap) {
|
|
347
|
+
throw this.crossFieldError(`embedding.costWarnUsd (${value}) nem haladhatja meg a costCapUsd-t (${cap}).`, options);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (key === 'embedding.costCapUsd' && typeof value === 'number') {
|
|
351
|
+
const warn = await resolvedNumber('embedding.costWarnUsd');
|
|
352
|
+
if (warn !== null && warn > value) {
|
|
353
|
+
throw this.crossFieldError(`embedding.costCapUsd (${value}) nem lehet kisebb a costWarnUsd-nál (${warn}).`, options);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
crossFieldError(message, options) {
|
|
358
|
+
return this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.setCrossFieldViolation, `Cross-field invariáns sérült: ${message}`, options.issuer);
|
|
359
|
+
}
|
|
360
|
+
// =========================================================================
|
|
361
|
+
// PRESETS (dsgn-007 §5) — atomikus, validált, auditált apply
|
|
362
|
+
// =========================================================================
|
|
363
|
+
/**
|
|
364
|
+
* Egy beépített preset alkalmazása egy szintre (dsgn-007 §5). A preset minden kulcsa ELŐBB átmegy
|
|
365
|
+
* a `set`-validáción (atomikus: egyetlen invalid kulcs → az egész apply elutasítva, egy kulcs sem
|
|
366
|
+
* íródik); majd minden kulcs `set`-en át íródik (`setBy='preset'`, `setByDetail=<presetNév>`).
|
|
367
|
+
* Idempotens: újra-apply felülír; a preseten kívüli kulcsokat nem törli.
|
|
368
|
+
*/
|
|
369
|
+
async applyPreset(name, target) {
|
|
370
|
+
const bundle = config_presets_const_1.CONFIG_PRESETS[name];
|
|
371
|
+
if (!bundle) {
|
|
372
|
+
throw this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.preset, `Ismeretlen embedding-preset: '${name}'. Elérhető: ${Object.keys(config_presets_const_1.CONFIG_PRESETS).join(', ')}.`, target.issuer);
|
|
373
|
+
}
|
|
374
|
+
const level = target.scopeId ? fam_config_level_type_enum_1.FAM_ConfigLevel.scope : fam_config_level_type_enum_1.FAM_ConfigLevel.table;
|
|
375
|
+
if (level === fam_config_level_type_enum_1.FAM_ConfigLevel.table && !target.table) {
|
|
376
|
+
throw this.error(config_error_codes_const_1.FAM_CONFIG_ERROR_CODES.preset, `A preset '${name}' alkalmazásához table vagy scopeId kötelező.`, target.issuer);
|
|
377
|
+
}
|
|
378
|
+
const baseOptions = {
|
|
379
|
+
level: level,
|
|
380
|
+
tableScope: target.table,
|
|
381
|
+
scopeId: target.scopeId,
|
|
382
|
+
setBy: 'preset',
|
|
383
|
+
setByDetail: name,
|
|
384
|
+
issuer: target.issuer ?? this.issuer,
|
|
385
|
+
};
|
|
386
|
+
const entries = Object.entries(bundle);
|
|
387
|
+
// Atomikus: ELŐBB minden kulcsot validálunk (egy bukás → semmit nem írunk).
|
|
388
|
+
for (const [key, value] of entries) {
|
|
389
|
+
await this.validate(key, value, baseOptions);
|
|
390
|
+
}
|
|
391
|
+
// Majd minden kulcsot kiírunk (a `set` újra-validál; a már validált halmaz konzisztens).
|
|
392
|
+
const written = [];
|
|
393
|
+
for (const [key, value] of entries) {
|
|
394
|
+
written.push(await this.set(key, value, baseOptions));
|
|
395
|
+
}
|
|
396
|
+
return written;
|
|
397
|
+
}
|
|
398
|
+
// =========================================================================
|
|
399
|
+
// HISTORY (dsgn-007 §7.2) — felülírás-history (soft-delete-elt régi rekordok)
|
|
400
|
+
// =========================================================================
|
|
401
|
+
/**
|
|
402
|
+
* Egy feloldási-kulcs felülírás-historyja (dsgn-007 §7.2): a soft-delete-elt (archív) régi rekordok
|
|
403
|
+
* az archív collection-ből (`DyNTS_ArchiveDataService`). Audit-trail / rollback-alap.
|
|
404
|
+
*/
|
|
405
|
+
async getHistory(key, options) {
|
|
406
|
+
const filter = this.levelKeyFilter(key, options);
|
|
407
|
+
const dataService = new config_data_service_1.FAM_Config_DataService({ issuer: options.issuer ?? this.issuer });
|
|
408
|
+
const archive_DS = dataService.getArchiveDataService();
|
|
409
|
+
return archive_DS.findDataList(filter, true);
|
|
410
|
+
}
|
|
411
|
+
// =========================================================================
|
|
412
|
+
// ENV SEED (dsgn-007 §6) — boot-hook; titok SOHA nem kerül DB-be
|
|
413
|
+
// =========================================================================
|
|
414
|
+
/**
|
|
415
|
+
* ENV → global default seed (dsgn-007 §6). Ha `fam_config` egy kulcsra üres, az
|
|
416
|
+
* `OPENAI_EMBEDDING_MODEL` / `EMBEDDING_PROVIDER` ENV-ből GLOBAL default-ot seedel
|
|
417
|
+
* (`setBy='env-seed'`). A titok (`OPENAI_API_KEY`, `LMSTUDIO_BASE_URL`) SOHA nem kerül DB-be;
|
|
418
|
+
* a többi default a `CONFIG_CATALOG` builtin-jéből jön (nincs szükség DB-rekordra). Egyszeri:
|
|
419
|
+
* csak hiányzó kulcsra ír, a runtime-szerkesztés felülírja és többé nem írja vissza.
|
|
420
|
+
*/
|
|
421
|
+
async seedDefaultsFromEnv() {
|
|
422
|
+
const provider = process.env.EMBEDDING_PROVIDER;
|
|
423
|
+
// Provider-aware model-env: lmstudio (DEFAULT) → LMSTUDIO_EMBEDDING_MODEL; openai → OPENAI_EMBEDDING_MODEL.
|
|
424
|
+
const model = provider === 'lmstudio'
|
|
425
|
+
? (process.env.LMSTUDIO_EMBEDDING_MODEL || process.env.EMBEDDING_MODEL)
|
|
426
|
+
: (process.env.OPENAI_EMBEDDING_MODEL || process.env.EMBEDDING_MODEL);
|
|
427
|
+
if (provider) {
|
|
428
|
+
await this.seedGlobalIfMissing('embedding.provider', provider);
|
|
429
|
+
}
|
|
430
|
+
if (model) {
|
|
431
|
+
await this.seedGlobalIfMissing('embedding.model', model);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** Egy global kulcsot seedel, ha még NINCS aktív rekord rá (env-seed; dsgn-007 §6). */
|
|
435
|
+
async seedGlobalIfMissing(key, value) {
|
|
436
|
+
const dataService = new config_data_service_1.FAM_Config_DataService({ issuer: this.issuer });
|
|
437
|
+
const existing = await dataService.findActive({ level: fam_config_level_type_enum_1.FAM_ConfigLevel.global, key: key });
|
|
438
|
+
if (existing && existing._id) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
await this.set(key, value, { level: fam_config_level_type_enum_1.FAM_ConfigLevel.global, setBy: 'env-seed', issuer: this.issuer });
|
|
442
|
+
}
|
|
443
|
+
// =========================================================================
|
|
444
|
+
// helpers
|
|
445
|
+
// =========================================================================
|
|
446
|
+
/** A teljes katalógus-kulcs lista (típusosan). */
|
|
447
|
+
allKeys() {
|
|
448
|
+
return Object.keys(config_catalog_const_1.CONFIG_CATALOG);
|
|
449
|
+
}
|
|
450
|
+
/** Egyszerű hasonló-kulcs javaslat (prefix-egyezés) az ismeretlen-kulcs hibához. */
|
|
451
|
+
similarKeys(key) {
|
|
452
|
+
const prefix = key.split('.')[0];
|
|
453
|
+
return this.allKeys().filter((known) => known.startsWith(prefix)).slice(0, 5);
|
|
454
|
+
}
|
|
455
|
+
/** Olvasható típus-jelölés a hiba-üzenethez. */
|
|
456
|
+
typeOf(value) {
|
|
457
|
+
if (value === null) {
|
|
458
|
+
return 'null';
|
|
459
|
+
}
|
|
460
|
+
if (Array.isArray(value)) {
|
|
461
|
+
return 'array';
|
|
462
|
+
}
|
|
463
|
+
return typeof value;
|
|
464
|
+
}
|
|
465
|
+
/** Strukturált config-hiba (a központi error-rétegnek; `level=error`, dsgn-008). */
|
|
466
|
+
error(errorCode, message, issuer) {
|
|
467
|
+
return new fsm_dynamo_1.DyFM_Error({
|
|
468
|
+
errorCode: errorCode,
|
|
469
|
+
message: message,
|
|
470
|
+
level: fsm_dynamo_1.DyFM_ErrorLevel.error,
|
|
471
|
+
issuer: issuer ?? this.issuer,
|
|
472
|
+
issuerService: this.issuer,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
exports.FAM_Config_ControlService = FAM_Config_ControlService;
|