@omnitron-dev/titan-lumen 0.0.0-stage → 0.4.6
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/LICENSE +21 -0
- package/README.md +250 -2
- package/dist/access/access-filter.service.d.ts +25 -0
- package/dist/access/access-filter.service.d.ts.map +1 -0
- package/dist/access/access-filter.service.js +61 -0
- package/dist/access/access-filter.service.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +3 -0
- package/dist/constants.js.map +1 -0
- package/dist/corpus-files.d.ts +12 -0
- package/dist/corpus-files.d.ts.map +1 -0
- package/dist/corpus-files.js +114 -0
- package/dist/corpus-files.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/live/live-edit.service.d.ts +47 -0
- package/dist/live/live-edit.service.d.ts.map +1 -0
- package/dist/live/live-edit.service.js +253 -0
- package/dist/live/live-edit.service.js.map +1 -0
- package/dist/live/migration.d.ts +4 -0
- package/dist/live/migration.d.ts.map +1 -0
- package/dist/live/migration.js +38 -0
- package/dist/live/migration.js.map +1 -0
- package/dist/live/schema.d.ts +19 -0
- package/dist/live/schema.d.ts.map +1 -0
- package/dist/live/schema.js +2 -0
- package/dist/live/schema.js.map +1 -0
- package/dist/lumen.module.d.ts +8 -0
- package/dist/lumen.module.d.ts.map +1 -0
- package/dist/lumen.module.js +58 -0
- package/dist/lumen.module.js.map +1 -0
- package/dist/lumen.service.d.ts +32 -0
- package/dist/lumen.service.d.ts.map +1 -0
- package/dist/lumen.service.js +710 -0
- package/dist/lumen.service.js.map +1 -0
- package/dist/mcp.d.ts +16 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +52 -0
- package/dist/mcp.js.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +54 -3
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { Worker } from 'node:worker_threads';
|
|
12
|
+
import { extname, resolve } from 'node:path';
|
|
13
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
14
|
+
import ts from 'typescript';
|
|
15
|
+
import { Injectable, Service } from '@omnitron-dev/titan';
|
|
16
|
+
import { Public, getCurrentInvocationContext } from '@omnitron-dev/titan/netron';
|
|
17
|
+
import { Errors } from '@omnitron-dev/titan/errors';
|
|
18
|
+
import { parseDocument } from '@omnitron-dev/lumen-ast';
|
|
19
|
+
import { PLAYGROUND_LIMITS } from '@omnitron-dev/lumen-ast/execution/limits';
|
|
20
|
+
import { DEFAULT_DOCS_DIR } from './constants.js';
|
|
21
|
+
import { isCanonicalDocumentPath, readCorpusFile, scanCorpusFiles } from './corpus-files.js';
|
|
22
|
+
import { AccessFilterService, extractDocumentAccess, extractDocumentMetadata, } from './access/access-filter.service.js';
|
|
23
|
+
import { LiveEditService } from './live/live-edit.service.js';
|
|
24
|
+
export const MAX_PLAYGROUND_CODE_BYTES = PLAYGROUND_LIMITS.codeBytes;
|
|
25
|
+
export const PLAYGROUND_EXECUTION_TIMEOUT_MS = PLAYGROUND_LIMITS.timeoutMs;
|
|
26
|
+
export const PLAYGROUND_WALL_TIMEOUT_MS = 5000;
|
|
27
|
+
const MAX_ACTIVE_PLAYGROUNDS = 4;
|
|
28
|
+
export const MAX_DOC_FILE_BYTES = 5 * 1024 * 1024;
|
|
29
|
+
const MAX_SOURCE_FILE_BYTES = 2 * 1024 * 1024;
|
|
30
|
+
const ALLOWED_DOC_EXTENSIONS = new Set(['.pdx', '.md', '.mdx']);
|
|
31
|
+
const ALLOWED_TS_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']);
|
|
32
|
+
const PLAYGROUND_WORKER_SCRIPT = `
|
|
33
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
34
|
+
import(workerData.engine).then(async ({ evaluatePlayground }) => {
|
|
35
|
+
try {
|
|
36
|
+
parentPort.postMessage({ ok: true, result: await evaluatePlayground(workerData.code) });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
parentPort.postMessage({ ok: false, error: String(error?.message ?? error).slice(0, 2048) });
|
|
39
|
+
}
|
|
40
|
+
}).catch(error => parentPort.postMessage({ ok: false, error: String(error?.message ?? error).slice(0, 2048) }));
|
|
41
|
+
`;
|
|
42
|
+
function cloneModuleOptions(opts) {
|
|
43
|
+
const cloned = { ...opts };
|
|
44
|
+
if (opts.accessFilter) {
|
|
45
|
+
cloned.accessFilter = {
|
|
46
|
+
...opts.accessFilter,
|
|
47
|
+
...(opts.accessFilter.grants ? { grants: { ...opts.accessFilter.grants } } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (opts.corpusLimits)
|
|
51
|
+
cloned.corpusLimits = { ...opts.corpusLimits };
|
|
52
|
+
if (opts.liveEdit) {
|
|
53
|
+
cloned.liveEdit = { ...opts.liveEdit };
|
|
54
|
+
}
|
|
55
|
+
return cloned;
|
|
56
|
+
}
|
|
57
|
+
function extractFirstHeading(content) {
|
|
58
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
59
|
+
return match?.[1]?.trim();
|
|
60
|
+
}
|
|
61
|
+
function stripFrontmatter(content) {
|
|
62
|
+
if (!content.startsWith('---')) {
|
|
63
|
+
return content;
|
|
64
|
+
}
|
|
65
|
+
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/);
|
|
66
|
+
return match ? content.slice(match[0].length) : content;
|
|
67
|
+
}
|
|
68
|
+
function inferKbEntryAccess(content, tags) {
|
|
69
|
+
const fmAccess = extractDocumentAccess(content);
|
|
70
|
+
if (fmAccess !== 'public') {
|
|
71
|
+
return fmAccess;
|
|
72
|
+
}
|
|
73
|
+
for (const tag of tags) {
|
|
74
|
+
const normalized = tag.toLowerCase().trim();
|
|
75
|
+
if (normalized === 'confidential' || normalized === 'access:confidential') {
|
|
76
|
+
return 'confidential';
|
|
77
|
+
}
|
|
78
|
+
if (normalized === 'internal' || normalized === 'access:internal') {
|
|
79
|
+
return 'internal';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return 'public';
|
|
83
|
+
}
|
|
84
|
+
function hasExportModifier(node) {
|
|
85
|
+
if (!ts.canHaveModifiers(node)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const modifiers = ts.getModifiers(node);
|
|
89
|
+
return Boolean(modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword));
|
|
90
|
+
}
|
|
91
|
+
function extractSymbolFromSourceFile(sourceFile, targetSymbol, displayPath) {
|
|
92
|
+
const sourceText = sourceFile.text;
|
|
93
|
+
for (const stmt of sourceFile.statements) {
|
|
94
|
+
if (ts.isClassDeclaration(stmt) && stmt.name?.text === targetSymbol && hasExportModifier(stmt)) {
|
|
95
|
+
const methods = {};
|
|
96
|
+
for (const member of stmt.members) {
|
|
97
|
+
const isPrivateOrProtected = ts.canHaveModifiers(member) &&
|
|
98
|
+
Boolean(ts
|
|
99
|
+
.getModifiers(member)
|
|
100
|
+
?.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword));
|
|
101
|
+
if (isPrivateOrProtected || (member.name && ts.isPrivateIdentifier(member.name))) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if ((ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member)) && member.name) {
|
|
105
|
+
const memberName = member.name.getText(sourceFile);
|
|
106
|
+
const memberType = member.type ? member.type.getText(sourceFile) : 'unknown';
|
|
107
|
+
methods[memberName] = memberType;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
name: targetSymbol,
|
|
112
|
+
kind: 'class',
|
|
113
|
+
filePath: displayPath,
|
|
114
|
+
signatures: [`class ${targetSymbol}`],
|
|
115
|
+
...(Object.keys(methods).length > 0 ? { methods } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (ts.isInterfaceDeclaration(stmt) && stmt.name.text === targetSymbol && hasExportModifier(stmt)) {
|
|
119
|
+
const methods = {};
|
|
120
|
+
for (const member of stmt.members) {
|
|
121
|
+
if ((ts.isMethodSignature(member) || ts.isPropertySignature(member)) && member.name) {
|
|
122
|
+
const memberName = member.name.getText(sourceFile);
|
|
123
|
+
const memberType = member.type ? member.type.getText(sourceFile) : 'unknown';
|
|
124
|
+
methods[memberName] = memberType;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
name: targetSymbol,
|
|
129
|
+
kind: 'interface',
|
|
130
|
+
filePath: displayPath,
|
|
131
|
+
signatures: [`interface ${targetSymbol}`],
|
|
132
|
+
...(Object.keys(methods).length > 0 ? { methods } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (ts.isFunctionDeclaration(stmt) && stmt.name?.text === targetSymbol && hasExportModifier(stmt)) {
|
|
136
|
+
const start = stmt.getStart(sourceFile);
|
|
137
|
+
const bodyStart = stmt.body ? stmt.body.getStart(sourceFile) : stmt.getEnd();
|
|
138
|
+
const sig = sourceText
|
|
139
|
+
.slice(start, bodyStart)
|
|
140
|
+
.trim()
|
|
141
|
+
.replace(/^export\s+/, '');
|
|
142
|
+
return {
|
|
143
|
+
name: targetSymbol,
|
|
144
|
+
kind: 'function',
|
|
145
|
+
filePath: displayPath,
|
|
146
|
+
signatures: [sig],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (ts.isTypeAliasDeclaration(stmt) && stmt.name.text === targetSymbol && hasExportModifier(stmt)) {
|
|
150
|
+
return {
|
|
151
|
+
name: targetSymbol,
|
|
152
|
+
kind: 'type',
|
|
153
|
+
filePath: displayPath,
|
|
154
|
+
signatures: [
|
|
155
|
+
stmt
|
|
156
|
+
.getText(sourceFile)
|
|
157
|
+
.replace(/^export\s+/, '')
|
|
158
|
+
.split('\n')[0] ?? `type ${targetSymbol}`,
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (ts.isEnumDeclaration(stmt) && stmt.name.text === targetSymbol && hasExportModifier(stmt)) {
|
|
163
|
+
return {
|
|
164
|
+
name: targetSymbol,
|
|
165
|
+
kind: 'enum',
|
|
166
|
+
filePath: displayPath,
|
|
167
|
+
signatures: [`enum ${targetSymbol}`],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (ts.isVariableStatement(stmt) && hasExportModifier(stmt)) {
|
|
171
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
172
|
+
if (ts.isIdentifier(decl.name) && decl.name.text === targetSymbol) {
|
|
173
|
+
return {
|
|
174
|
+
name: targetSymbol,
|
|
175
|
+
kind: 'const',
|
|
176
|
+
filePath: displayPath,
|
|
177
|
+
signatures: [`const ${targetSymbol}`],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
let LumenService = class LumenService {
|
|
186
|
+
constructor(options = {}, accessFilter, liveEdit) {
|
|
187
|
+
this.activeCorpusReads = 0;
|
|
188
|
+
this.activePlaygrounds = 0;
|
|
189
|
+
this.options = cloneModuleOptions(options);
|
|
190
|
+
this.accessFilter = accessFilter ?? new AccessFilterService(this.options.accessFilter);
|
|
191
|
+
this.liveEdit = liveEdit ?? new LiveEditService(this.options.liveEdit);
|
|
192
|
+
}
|
|
193
|
+
getOptions() {
|
|
194
|
+
return cloneModuleOptions(this.options);
|
|
195
|
+
}
|
|
196
|
+
getAccessFilterService() {
|
|
197
|
+
return this.accessFilter;
|
|
198
|
+
}
|
|
199
|
+
getLiveEditService() {
|
|
200
|
+
return this.liveEdit;
|
|
201
|
+
}
|
|
202
|
+
callerRoles() {
|
|
203
|
+
return [...(getCurrentInvocationContext()?.auth?.roles ?? ['anonymous'])];
|
|
204
|
+
}
|
|
205
|
+
requireMutation(method) {
|
|
206
|
+
const invocation = getCurrentInvocationContext();
|
|
207
|
+
if (!invocation?.auth)
|
|
208
|
+
throw Errors.unauthorized('An authenticated Netron invocation is required');
|
|
209
|
+
if (invocation.serviceName !== 'Lumen' || invocation.methodName !== method) {
|
|
210
|
+
throw Errors.forbidden('The operation must enter through its published Netron method');
|
|
211
|
+
}
|
|
212
|
+
return invocation.auth;
|
|
213
|
+
}
|
|
214
|
+
async withCorpusRead(read) {
|
|
215
|
+
if (this.activeCorpusReads >= 4)
|
|
216
|
+
throw Errors.tooManyRequests(1);
|
|
217
|
+
this.activeCorpusReads++;
|
|
218
|
+
try {
|
|
219
|
+
return await read();
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
this.activeCorpusReads--;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async corpusRoot(directory) {
|
|
226
|
+
const root = await realpath(resolve(directory));
|
|
227
|
+
if (!(await stat(root)).isDirectory())
|
|
228
|
+
throw new Error('Configured corpus root is not a directory');
|
|
229
|
+
return root;
|
|
230
|
+
}
|
|
231
|
+
async readDocuments() {
|
|
232
|
+
const root = await this.corpusRoot(this.options.docsDir ?? DEFAULT_DOCS_DIR);
|
|
233
|
+
const stored = this.liveEdit.isEnabled() ? await this.liveEdit.readCorpus(this.options.corpusLimits) : [];
|
|
234
|
+
const remainingBytes = (this.options.corpusLimits?.maxBytes ?? 64 * 1024 * 1024) -
|
|
235
|
+
stored.reduce((bytes, document) => bytes + Buffer.byteLength(document.content), 0);
|
|
236
|
+
const files = await scanCorpusFiles(root, ALLOWED_DOC_EXTENSIONS, MAX_DOC_FILE_BYTES, { ...this.options.corpusLimits, maxBytes: remainingBytes }, new Set(stored.map(document => document.docId)));
|
|
237
|
+
files.push(...stored.map(document => ({ path: document.docId, content: document.content })));
|
|
238
|
+
if (files.length > (this.options.corpusLimits?.maxEntries ?? 10_000))
|
|
239
|
+
throw new RangeError('Combined corpus entry budget exceeded');
|
|
240
|
+
files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
241
|
+
const ids = new Set();
|
|
242
|
+
return files.map(({ path, content }) => {
|
|
243
|
+
const meta = extractDocumentMetadata(content);
|
|
244
|
+
const id = meta.id ?? path;
|
|
245
|
+
if (ids.has(id))
|
|
246
|
+
throw new Error('Duplicate document identity in the configured corpus');
|
|
247
|
+
ids.add(id);
|
|
248
|
+
return {
|
|
249
|
+
id,
|
|
250
|
+
title: meta.title ?? extractFirstHeading(stripFrontmatter(content)) ?? path.replace(/\.(pdx|mdx?)$/i, ''),
|
|
251
|
+
path, content, access: meta.access,
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async search(query, searchOptions) {
|
|
256
|
+
return this.withCorpusRead(async () => {
|
|
257
|
+
if (!query || typeof query !== 'string' || query.trim().length === 0) {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
if (Buffer.byteLength(query) > 4096)
|
|
261
|
+
throw Errors.badRequest('Search query exceeds 4096 bytes');
|
|
262
|
+
const limit = searchOptions?.limit ?? 10;
|
|
263
|
+
if (!Number.isSafeInteger(limit) || limit < 0 || limit > 100) {
|
|
264
|
+
throw Errors.badRequest('Search limit must be an integer from 0 to 100');
|
|
265
|
+
}
|
|
266
|
+
if (limit === 0)
|
|
267
|
+
return [];
|
|
268
|
+
const effectiveRoles = this.callerRoles();
|
|
269
|
+
const trimmedQuery = query.trim();
|
|
270
|
+
const lowerQuery = trimmedQuery.toLowerCase();
|
|
271
|
+
const results = [];
|
|
272
|
+
const seenIds = new Set();
|
|
273
|
+
if (this.options.knowledgeBase && this.accessFilter.isAllowed(effectiveRoles, this.options.knowledgeBaseAccess ?? 'internal')) {
|
|
274
|
+
try {
|
|
275
|
+
const kbResult = await this.options.knowledgeBase.query(trimmedQuery, { maxResults: limit });
|
|
276
|
+
for (const entry of kbResult.entries ?? []) {
|
|
277
|
+
const entryAccess = inferKbEntryAccess(entry.content ?? '', entry.tags ?? []);
|
|
278
|
+
const { content: projectedContent, strippedAny } = this.accessFilter.projectContent(entry.content ?? '', effectiveRoles, entryAccess);
|
|
279
|
+
if (projectedContent === null) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const projectedBody = stripFrontmatter(projectedContent).trim();
|
|
283
|
+
if (strippedAny &&
|
|
284
|
+
!entry.title.toLowerCase().includes(lowerQuery) &&
|
|
285
|
+
!projectedBody.toLowerCase().includes(lowerQuery)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (seenIds.has(entry.id)) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
seenIds.add(entry.id);
|
|
292
|
+
results.push({
|
|
293
|
+
documentId: entry.id,
|
|
294
|
+
title: entry.title,
|
|
295
|
+
path: `${entry.module}/${entry.id}`,
|
|
296
|
+
snippet: projectedBody.slice(0, 160),
|
|
297
|
+
score: 0.95,
|
|
298
|
+
});
|
|
299
|
+
if (results.length >= limit) {
|
|
300
|
+
return results;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
throw Errors.internal('Unable to search the configured knowledge base', error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const diskDocs = await this.readDocuments();
|
|
310
|
+
for (const doc of diskDocs) {
|
|
311
|
+
const projectedContent = this.accessFilter.filterRawMarkdownContent(doc.content, effectiveRoles, doc.access);
|
|
312
|
+
if (projectedContent === null) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const bodyText = stripFrontmatter(projectedContent);
|
|
316
|
+
const matchTitle = doc.title.toLowerCase().includes(lowerQuery);
|
|
317
|
+
const matchContent = bodyText.toLowerCase().includes(lowerQuery);
|
|
318
|
+
const matchPath = doc.path.toLowerCase().includes(lowerQuery);
|
|
319
|
+
if (matchTitle || matchContent || matchPath) {
|
|
320
|
+
if (!seenIds.has(doc.id)) {
|
|
321
|
+
seenIds.add(doc.id);
|
|
322
|
+
results.push({
|
|
323
|
+
documentId: doc.id,
|
|
324
|
+
title: doc.title,
|
|
325
|
+
path: doc.path,
|
|
326
|
+
snippet: bodyText.trim().slice(0, 160),
|
|
327
|
+
score: matchTitle ? 1.0 : 0.85,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (results.length >= limit) {
|
|
332
|
+
return results;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
throw Errors.internal('Unable to read the configured documentation corpus', error);
|
|
338
|
+
}
|
|
339
|
+
return results;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async executePlayground(code) {
|
|
343
|
+
this.requireMutation('executePlayground');
|
|
344
|
+
if (typeof code !== 'string') {
|
|
345
|
+
return { code: String(code), result: null, error: 'Code must be a string' };
|
|
346
|
+
}
|
|
347
|
+
if (this.options.enablePlayground === false) {
|
|
348
|
+
return {
|
|
349
|
+
code,
|
|
350
|
+
result: null,
|
|
351
|
+
error: 'Playground execution is disabled',
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (Buffer.byteLength(code, 'utf8') > MAX_PLAYGROUND_CODE_BYTES) {
|
|
355
|
+
return {
|
|
356
|
+
code,
|
|
357
|
+
result: null,
|
|
358
|
+
error: `Playground code exceeds maximum size limit of ${MAX_PLAYGROUND_CODE_BYTES} bytes`,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (this.activePlaygrounds >= MAX_ACTIVE_PLAYGROUNDS)
|
|
362
|
+
throw Errors.tooManyRequests(Math.ceil(PLAYGROUND_WALL_TIMEOUT_MS / 1000));
|
|
363
|
+
this.activePlaygrounds++;
|
|
364
|
+
return new Promise((resolvePromise) => {
|
|
365
|
+
let settled = false;
|
|
366
|
+
let worker;
|
|
367
|
+
const finish = (outcome) => {
|
|
368
|
+
if (settled) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
settled = true;
|
|
372
|
+
clearTimeout(wallClockTimer);
|
|
373
|
+
const release = () => { this.activePlaygrounds--; resolvePromise(outcome); };
|
|
374
|
+
if (worker)
|
|
375
|
+
void worker.terminate().then(release, release);
|
|
376
|
+
else
|
|
377
|
+
release();
|
|
378
|
+
};
|
|
379
|
+
const wallClockTimer = setTimeout(() => {
|
|
380
|
+
finish({
|
|
381
|
+
code,
|
|
382
|
+
result: null,
|
|
383
|
+
error: `Script execution timed out after ${PLAYGROUND_WALL_TIMEOUT_MS}ms`,
|
|
384
|
+
});
|
|
385
|
+
}, PLAYGROUND_WALL_TIMEOUT_MS);
|
|
386
|
+
wallClockTimer.unref?.();
|
|
387
|
+
try {
|
|
388
|
+
worker = new Worker(PLAYGROUND_WORKER_SCRIPT, {
|
|
389
|
+
eval: true,
|
|
390
|
+
execArgv: [],
|
|
391
|
+
workerData: {
|
|
392
|
+
code,
|
|
393
|
+
engine: import.meta.resolve('@omnitron-dev/lumen-ast/execution'),
|
|
394
|
+
},
|
|
395
|
+
env: {},
|
|
396
|
+
resourceLimits: {
|
|
397
|
+
maxOldGenerationSizeMb: 32,
|
|
398
|
+
maxYoungGenerationSizeMb: 8,
|
|
399
|
+
codeRangeSizeMb: 16,
|
|
400
|
+
stackSizeMb: 2,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
catch (spawnErr) {
|
|
405
|
+
finish({
|
|
406
|
+
code,
|
|
407
|
+
result: null,
|
|
408
|
+
error: spawnErr?.message ?? 'Failed to initialize sandbox worker',
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
worker.on('message', (msg) => {
|
|
413
|
+
if (msg && msg.ok) {
|
|
414
|
+
finish({
|
|
415
|
+
code,
|
|
416
|
+
result: msg.result !== undefined ? msg.result : null,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
finish({
|
|
421
|
+
code,
|
|
422
|
+
result: null,
|
|
423
|
+
error: msg?.error ?? 'Execution error',
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
worker.on('error', (err) => {
|
|
428
|
+
finish({
|
|
429
|
+
code,
|
|
430
|
+
result: null,
|
|
431
|
+
error: err?.message ?? 'Worker execution error',
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
worker.on('exit', (exitCode) => {
|
|
435
|
+
if (!settled) {
|
|
436
|
+
finish({
|
|
437
|
+
code,
|
|
438
|
+
result: null,
|
|
439
|
+
error: exitCode === 0
|
|
440
|
+
? 'Sandbox worker exited without returning a result'
|
|
441
|
+
: `Sandbox worker terminated (exit code ${exitCode})`,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
async getDocument(docPath) {
|
|
448
|
+
return this.withCorpusRead(async () => {
|
|
449
|
+
if (!isCanonicalDocumentPath(docPath, ALLOWED_DOC_EXTENSIONS)) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
const effectiveRoles = this.callerRoles();
|
|
453
|
+
const candidates = extname(docPath) ? [docPath] : [`${docPath}.pdx`, `${docPath}.md`, `${docPath}.mdx`];
|
|
454
|
+
for (const candidate of candidates) {
|
|
455
|
+
const document = await this.readDocumentState(candidate);
|
|
456
|
+
if (document)
|
|
457
|
+
return this.accessFilter.filterRawMarkdownContent(document.content, effectiveRoles, document.access);
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
async readDocumentState(path) {
|
|
463
|
+
if (this.liveEdit.isEnabled()) {
|
|
464
|
+
const stored = await this.liveEdit.getDocument(path);
|
|
465
|
+
if (stored)
|
|
466
|
+
return { docId: stored.docId, content: stored.content, revision: stored.revision, hash: stored.hash, access: stored.access };
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
const root = await this.corpusRoot(this.options.docsDir ?? DEFAULT_DOCS_DIR);
|
|
470
|
+
const content = await readCorpusFile(root, path, MAX_DOC_FILE_BYTES);
|
|
471
|
+
return content === null ? null : {
|
|
472
|
+
docId: path, content, revision: 0, hash: createHash('sha256').update(content).digest('hex'),
|
|
473
|
+
access: extractDocumentAccess(content),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
throw Errors.internal('Unable to read the configured documentation corpus', error);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async getEditableDocument(path) {
|
|
481
|
+
return this.withCorpusRead(async () => {
|
|
482
|
+
const actor = this.requireMutation('getEditableDocument');
|
|
483
|
+
if (!isCanonicalDocumentPath(path, ALLOWED_DOC_EXTENSIONS) || !extname(path)) {
|
|
484
|
+
throw Errors.badRequest('Editing requires a canonical document path with its extension');
|
|
485
|
+
}
|
|
486
|
+
if (!this.liveEdit.isEnabled())
|
|
487
|
+
throw Errors.badRequest('Durable authoring is not configured');
|
|
488
|
+
const document = await this.readDocumentState(path);
|
|
489
|
+
if (!document)
|
|
490
|
+
return null;
|
|
491
|
+
const projected = this.accessFilter.projectContent(document.content, [...(actor.roles ?? [])], document.access);
|
|
492
|
+
if (projected.content === null || projected.strippedAny)
|
|
493
|
+
throw Errors.forbidden('Editing requires access to the entire document');
|
|
494
|
+
return document;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
async getSymbolContract(symbol) {
|
|
498
|
+
return this.withCorpusRead(async () => {
|
|
499
|
+
if (!symbol || typeof symbol !== 'string' || symbol.trim().length === 0) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
if (symbol.length > 512)
|
|
503
|
+
throw Errors.badRequest('Symbol name exceeds 512 characters');
|
|
504
|
+
const trimmed = symbol.trim();
|
|
505
|
+
const effectiveRoles = this.callerRoles();
|
|
506
|
+
if (this.options.knowledgeBase && this.accessFilter.isAllowed(effectiveRoles, this.options.knowledgeBaseAccess ?? 'internal')) {
|
|
507
|
+
try {
|
|
508
|
+
const kbSymbol = await this.options.knowledgeBase.getApi(trimmed);
|
|
509
|
+
if (kbSymbol) {
|
|
510
|
+
const methods = {};
|
|
511
|
+
for (const member of kbSymbol.members ?? []) {
|
|
512
|
+
if (member.visibility !== 'private' && member.visibility !== 'protected') {
|
|
513
|
+
methods[member.name] = member.signature;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
name: kbSymbol.name,
|
|
518
|
+
kind: kbSymbol.kind,
|
|
519
|
+
filePath: kbSymbol.filePath,
|
|
520
|
+
signatures: kbSymbol.signature ? [kbSymbol.signature] : [],
|
|
521
|
+
...(Object.keys(methods).length > 0 ? { methods } : {}),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
throw Errors.internal('Unable to inspect the configured knowledge base', error);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (this.options.sourceDir && this.accessFilter.isAllowed(effectiveRoles, this.options.sourceAccess ?? 'internal')) {
|
|
530
|
+
try {
|
|
531
|
+
const root = await this.corpusRoot(this.options.sourceDir);
|
|
532
|
+
const files = await scanCorpusFiles(root, ALLOWED_TS_EXTENSIONS, MAX_SOURCE_FILE_BYTES, this.options.corpusLimits);
|
|
533
|
+
for (const { path, content } of files) {
|
|
534
|
+
if (!content.includes(trimmed))
|
|
535
|
+
continue;
|
|
536
|
+
const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
|
|
537
|
+
const found = extractSymbolFromSourceFile(sourceFile, trimmed, path);
|
|
538
|
+
if (found)
|
|
539
|
+
return found;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
throw Errors.internal('Unable to inspect the configured source corpus', error);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
async getTheoremGraph() {
|
|
550
|
+
return this.withCorpusRead(async () => {
|
|
551
|
+
const effectiveRoles = this.callerRoles();
|
|
552
|
+
try {
|
|
553
|
+
const diskDocs = await this.readDocuments();
|
|
554
|
+
const nodes = [];
|
|
555
|
+
const edges = [];
|
|
556
|
+
const nodeIds = new Set();
|
|
557
|
+
const collectSmartRefs = (subNodes, out) => {
|
|
558
|
+
for (const child of subNodes) {
|
|
559
|
+
if (!child || typeof child !== 'object')
|
|
560
|
+
continue;
|
|
561
|
+
if (child.type === 'smartRef') {
|
|
562
|
+
const target = String(child['target'] ?? '').trim();
|
|
563
|
+
if (target.length > 0) {
|
|
564
|
+
out.add(target);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (Array.isArray(child.children)) {
|
|
568
|
+
collectSmartRefs(child.children, out);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
const VALID_THEOREM_TYPES = new Set([
|
|
573
|
+
'axiom',
|
|
574
|
+
'theorem',
|
|
575
|
+
'definition',
|
|
576
|
+
'lemma',
|
|
577
|
+
'proposition',
|
|
578
|
+
'corollary',
|
|
579
|
+
'hypothesis',
|
|
580
|
+
'remark',
|
|
581
|
+
'example',
|
|
582
|
+
]);
|
|
583
|
+
const normalizeTheoremType = (raw) => {
|
|
584
|
+
const lower = raw.toLowerCase();
|
|
585
|
+
return VALID_THEOREM_TYPES.has(lower) ? lower : 'theorem';
|
|
586
|
+
};
|
|
587
|
+
const walkMathEnvs = (astNodes, defaultStatus, docPath) => {
|
|
588
|
+
for (const node of astNodes) {
|
|
589
|
+
if (!node || typeof node !== 'object')
|
|
590
|
+
continue;
|
|
591
|
+
if (node.type === 'mathEnvironment') {
|
|
592
|
+
const rec = node;
|
|
593
|
+
const envKind = String(rec['envKind'] ?? 'theorem');
|
|
594
|
+
const id = typeof rec['id'] === 'string' && rec['id'].trim().length > 0 ? rec['id'].trim() : undefined;
|
|
595
|
+
const title = typeof rec['title'] === 'string' && rec['title'].trim().length > 0
|
|
596
|
+
? rec['title'].trim()
|
|
597
|
+
: (id ?? envKind);
|
|
598
|
+
const attrs = rec['attributes'] ?? {};
|
|
599
|
+
const status = typeof attrs['status'] === 'string' ? attrs['status'] : defaultStatus;
|
|
600
|
+
const ofTarget = (typeof rec['of'] === 'string' ? rec['of'] : undefined) ??
|
|
601
|
+
(typeof attrs['of'] === 'string' ? attrs['of'] : undefined);
|
|
602
|
+
if (envKind !== 'proof' && id) {
|
|
603
|
+
if (nodeIds.has(id))
|
|
604
|
+
throw new Error('Duplicate mathematical identity in the configured corpus');
|
|
605
|
+
nodeIds.add(id);
|
|
606
|
+
nodes.push({ id, type: normalizeTheoremType(envKind), title, status, path: docPath });
|
|
607
|
+
}
|
|
608
|
+
const dependencyOwner = envKind === 'proof' ? ofTarget : id;
|
|
609
|
+
if (dependencyOwner && Array.isArray(node.children)) {
|
|
610
|
+
const refs = new Set();
|
|
611
|
+
collectSmartRefs(node.children, refs);
|
|
612
|
+
for (const refTarget of refs) {
|
|
613
|
+
if (refTarget !== dependencyOwner) {
|
|
614
|
+
edges.push({ source: dependencyOwner, target: refTarget, relation: 'dependsOn' });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (Array.isArray(node.children)) {
|
|
620
|
+
walkMathEnvs(node.children, defaultStatus, docPath);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
for (const doc of diskDocs) {
|
|
625
|
+
if (!this.accessFilter.isAllowed(effectiveRoles, doc.access)) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const ast = await parseDocument(doc.content, doc.path);
|
|
629
|
+
const filtered = this.accessFilter.filterAst(ast, effectiveRoles);
|
|
630
|
+
const docStatus = typeof filtered.frontmatter.status === 'string' ? filtered.frontmatter.status : 'canonical';
|
|
631
|
+
walkMathEnvs(filtered.children, docStatus, doc.path);
|
|
632
|
+
}
|
|
633
|
+
if (nodes.length > 0) {
|
|
634
|
+
const seenEdges = new Set();
|
|
635
|
+
const validEdges = edges.filter((edge) => {
|
|
636
|
+
const key = JSON.stringify([edge.source, edge.target, edge.relation]);
|
|
637
|
+
if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target) || seenEdges.has(key))
|
|
638
|
+
return false;
|
|
639
|
+
seenEdges.add(key);
|
|
640
|
+
return true;
|
|
641
|
+
});
|
|
642
|
+
return { nodes, edges: validEdges };
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
throw Errors.internal('Unable to read the configured documentation corpus', error);
|
|
647
|
+
}
|
|
648
|
+
return { nodes: [], edges: [] };
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
async saveLiveEdit(payload) {
|
|
652
|
+
const auth = this.requireMutation('saveLiveEdit');
|
|
653
|
+
const actor = { userId: auth.userId, roles: auth.roles ?? [] };
|
|
654
|
+
if (!this.liveEdit.isEnabled())
|
|
655
|
+
throw Errors.badRequest('Durable authoring is not configured');
|
|
656
|
+
if (!this.liveEdit.validatePayload(payload).valid)
|
|
657
|
+
return this.liveEdit.applyEdit(payload, actor);
|
|
658
|
+
const existing = await this.readDocumentState(payload.docId);
|
|
659
|
+
return this.liveEdit.applyEdit(payload, actor, existing?.revision === 0 ? existing.content : undefined);
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
__decorate([
|
|
663
|
+
Public({ auth: { allowAnonymous: true } }),
|
|
664
|
+
__metadata("design:type", Function),
|
|
665
|
+
__metadata("design:paramtypes", [String, Object]),
|
|
666
|
+
__metadata("design:returntype", Promise)
|
|
667
|
+
], LumenService.prototype, "search", null);
|
|
668
|
+
__decorate([
|
|
669
|
+
Public({ auth: { permissions: ['lumen.playground.execute'] } }),
|
|
670
|
+
__metadata("design:type", Function),
|
|
671
|
+
__metadata("design:paramtypes", [String]),
|
|
672
|
+
__metadata("design:returntype", Promise)
|
|
673
|
+
], LumenService.prototype, "executePlayground", null);
|
|
674
|
+
__decorate([
|
|
675
|
+
Public({ auth: { allowAnonymous: true } }),
|
|
676
|
+
__metadata("design:type", Function),
|
|
677
|
+
__metadata("design:paramtypes", [String]),
|
|
678
|
+
__metadata("design:returntype", Promise)
|
|
679
|
+
], LumenService.prototype, "getDocument", null);
|
|
680
|
+
__decorate([
|
|
681
|
+
Public({ auth: { permissions: ['lumen.documents.write'] } }),
|
|
682
|
+
__metadata("design:type", Function),
|
|
683
|
+
__metadata("design:paramtypes", [String]),
|
|
684
|
+
__metadata("design:returntype", Promise)
|
|
685
|
+
], LumenService.prototype, "getEditableDocument", null);
|
|
686
|
+
__decorate([
|
|
687
|
+
Public({ auth: { allowAnonymous: true } }),
|
|
688
|
+
__metadata("design:type", Function),
|
|
689
|
+
__metadata("design:paramtypes", [String]),
|
|
690
|
+
__metadata("design:returntype", Promise)
|
|
691
|
+
], LumenService.prototype, "getSymbolContract", null);
|
|
692
|
+
__decorate([
|
|
693
|
+
Public({ auth: { allowAnonymous: true } }),
|
|
694
|
+
__metadata("design:type", Function),
|
|
695
|
+
__metadata("design:paramtypes", []),
|
|
696
|
+
__metadata("design:returntype", Promise)
|
|
697
|
+
], LumenService.prototype, "getTheoremGraph", null);
|
|
698
|
+
__decorate([
|
|
699
|
+
Public({ auth: { permissions: ['lumen.documents.write'] } }),
|
|
700
|
+
__metadata("design:type", Function),
|
|
701
|
+
__metadata("design:paramtypes", [Object]),
|
|
702
|
+
__metadata("design:returntype", Promise)
|
|
703
|
+
], LumenService.prototype, "saveLiveEdit", null);
|
|
704
|
+
LumenService = __decorate([
|
|
705
|
+
Service('Lumen'),
|
|
706
|
+
Injectable(),
|
|
707
|
+
__metadata("design:paramtypes", [Object, AccessFilterService, LiveEditService])
|
|
708
|
+
], LumenService);
|
|
709
|
+
export { LumenService };
|
|
710
|
+
//# sourceMappingURL=lumen.service.js.map
|