@oml/server 0.14.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 +94 -0
- package/package.json +34 -0
- package/src/cli.ts +189 -0
- package/src/index.ts +9 -0
- package/src/lsp/diagram-server.ts +48 -0
- package/src/lsp/language-server.ts +423 -0
- package/src/lsp/protocol/browser-fs-protocol.ts +21 -0
- package/src/lsp/protocol/reasoner-protocol.ts +86 -0
- package/src/lsp/providers/browser-fs-provider.ts +85 -0
- package/src/lsp/providers/hybrid-fs-provider.ts +134 -0
- package/src/rest/export.ts +118 -0
- package/src/rest/routes.ts +117 -0
- package/src/rest/server.ts +2517 -0
- package/src/rest/template.ts +276 -0
- package/src/rest/validation.ts +555 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { checkConsistency, type ConsistencyResult } from '@oml/reasoner';
|
|
8
|
+
import { extractLeadingFrontMatter, MarkdownHandlerRegistry, MarkdownPreviewRuntime } from '@oml/markdown';
|
|
9
|
+
import { createDefaultShaclService } from '@oml/owl';
|
|
10
|
+
import { getOntologyModelIndex } from '@oml/language';
|
|
11
|
+
import {
|
|
12
|
+
buildTemplateCatalog,
|
|
13
|
+
expandTemplateComposeBlocks,
|
|
14
|
+
findFilesByExtension,
|
|
15
|
+
frontMatterString,
|
|
16
|
+
isTemplateMarkdownFile,
|
|
17
|
+
normalizeContextOntologyIri,
|
|
18
|
+
} from './template.js';
|
|
19
|
+
|
|
20
|
+
export interface RestLintProblem {
|
|
21
|
+
uri: string;
|
|
22
|
+
line: number;
|
|
23
|
+
column: number;
|
|
24
|
+
kind: 'error' | 'warning' | 'information' | 'hint' | 'unknown';
|
|
25
|
+
severity: number;
|
|
26
|
+
message: string;
|
|
27
|
+
source?: string;
|
|
28
|
+
code?: string | number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RestLintResult {
|
|
32
|
+
success: boolean;
|
|
33
|
+
filesChecked: number;
|
|
34
|
+
errors: number;
|
|
35
|
+
warnings: number;
|
|
36
|
+
elapsedMs: number;
|
|
37
|
+
totalProblems: number;
|
|
38
|
+
returnedProblems: number;
|
|
39
|
+
truncated: boolean;
|
|
40
|
+
limit?: number;
|
|
41
|
+
problems: RestLintProblem[];
|
|
42
|
+
problemsByModelUri: Array<{
|
|
43
|
+
modelUri: string;
|
|
44
|
+
errors: number;
|
|
45
|
+
warnings: number;
|
|
46
|
+
problems: RestLintProblem[];
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type RdfFormat = 'ttl' | 'trig' | 'nt' | 'nq' | 'n3';
|
|
51
|
+
|
|
52
|
+
type WorkspaceOwlEntry = { modelUri: string; ontologyIri: string; owlPath: string };
|
|
53
|
+
|
|
54
|
+
type ValidateBlockCachedResult = {
|
|
55
|
+
errors: number;
|
|
56
|
+
warnings: number;
|
|
57
|
+
detail: Record<string, unknown>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type ValidateBlockCacheEntry = {
|
|
61
|
+
fingerprint: string;
|
|
62
|
+
result: ValidateBlockCachedResult;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type ReasonModelCachedResult = {
|
|
66
|
+
consistent: boolean;
|
|
67
|
+
ontologyIri: string;
|
|
68
|
+
result?: ConsistencyResult;
|
|
69
|
+
error?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type ReasonModelCacheEntry = {
|
|
73
|
+
fingerprint: string;
|
|
74
|
+
result: ReasonModelCachedResult;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type ValidationRuntimeCache = {
|
|
78
|
+
validateBlockCache: Map<string, ValidateBlockCacheEntry>;
|
|
79
|
+
reasonModelCache: Map<string, ReasonModelCacheEntry>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const validationRuntimeCaches = new WeakMap<object, ValidationRuntimeCache>();
|
|
83
|
+
|
|
84
|
+
export type RestValidationContext = {
|
|
85
|
+
workspaceRoot: string;
|
|
86
|
+
runtime: { shared: any; Oml: any };
|
|
87
|
+
ensureInitialized: () => Promise<void>;
|
|
88
|
+
ensureWorkspaceCurrent: () => Promise<void>;
|
|
89
|
+
getWorkspaceOmlDocuments: () => any[];
|
|
90
|
+
writeWorkspaceAssertedOwl: (
|
|
91
|
+
outputDir: string,
|
|
92
|
+
format: RdfFormat,
|
|
93
|
+
pretty: boolean,
|
|
94
|
+
) => Promise<WorkspaceOwlEntry[]>;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function normalizeRdfFormat(value: string | undefined): RdfFormat {
|
|
98
|
+
if (!value) {
|
|
99
|
+
return 'ttl';
|
|
100
|
+
}
|
|
101
|
+
const normalized = value.trim().toLowerCase();
|
|
102
|
+
if (normalized === 'trig' || normalized === 'nt' || normalized === 'nq' || normalized === 'n3') {
|
|
103
|
+
return normalized;
|
|
104
|
+
}
|
|
105
|
+
return 'ttl';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function diagnosticKindFromSeverity(severity: unknown): RestLintProblem['kind'] {
|
|
109
|
+
if (severity === 1) return 'error';
|
|
110
|
+
if (severity === 2) return 'warning';
|
|
111
|
+
if (severity === 3) return 'information';
|
|
112
|
+
if (severity === 4) return 'hint';
|
|
113
|
+
return 'unknown';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hashText(value: string): string {
|
|
117
|
+
return createHash('sha256').update(value, 'utf-8').digest('hex');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getValidationRuntimeCache(client: RestValidationContext): ValidationRuntimeCache {
|
|
121
|
+
const key = client.runtime.shared as object;
|
|
122
|
+
const existing = validationRuntimeCaches.get(key);
|
|
123
|
+
if (existing) {
|
|
124
|
+
return existing;
|
|
125
|
+
}
|
|
126
|
+
const created: ValidationRuntimeCache = {
|
|
127
|
+
validateBlockCache: new Map(),
|
|
128
|
+
reasonModelCache: new Map(),
|
|
129
|
+
};
|
|
130
|
+
validationRuntimeCaches.set(key, created);
|
|
131
|
+
return created;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getModelContentFingerprints(client: RestValidationContext): Map<string, string> {
|
|
135
|
+
const fingerprints = new Map<string, string>();
|
|
136
|
+
const docs = client.getWorkspaceOmlDocuments();
|
|
137
|
+
for (const doc of docs) {
|
|
138
|
+
const modelUri = String(doc?.uri ?? '').trim();
|
|
139
|
+
if (!modelUri) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const text = typeof doc?.textDocument?.getText === 'function'
|
|
143
|
+
? String(doc.textDocument.getText())
|
|
144
|
+
: '';
|
|
145
|
+
fingerprints.set(modelUri, hashText(text));
|
|
146
|
+
}
|
|
147
|
+
return fingerprints;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function lintWorkspace(client: RestValidationContext, params: Record<string, unknown> = {}): Promise<RestLintResult> {
|
|
151
|
+
const startedAt = Date.now();
|
|
152
|
+
await client.ensureInitialized();
|
|
153
|
+
await client.ensureWorkspaceCurrent();
|
|
154
|
+
const rawLimit = params.limit;
|
|
155
|
+
const limit = typeof rawLimit === 'number' && Number.isFinite(rawLimit) && rawLimit > 0
|
|
156
|
+
? Math.floor(rawLimit)
|
|
157
|
+
: undefined;
|
|
158
|
+
const docs = client.getWorkspaceOmlDocuments();
|
|
159
|
+
const allProblems: RestLintProblem[] = [];
|
|
160
|
+
let errors = 0;
|
|
161
|
+
let warnings = 0;
|
|
162
|
+
for (const doc of docs) {
|
|
163
|
+
const modelUri = String(doc?.uri ?? '').trim();
|
|
164
|
+
const diagnostics = Array.isArray(doc?.diagnostics) ? doc.diagnostics : [];
|
|
165
|
+
for (const diagnostic of diagnostics) {
|
|
166
|
+
const severity = typeof diagnostic?.severity === 'number' ? diagnostic.severity : 0;
|
|
167
|
+
const kind = diagnosticKindFromSeverity(severity);
|
|
168
|
+
if (kind === 'error') {
|
|
169
|
+
errors += 1;
|
|
170
|
+
} else if (kind === 'warning') {
|
|
171
|
+
warnings += 1;
|
|
172
|
+
}
|
|
173
|
+
allProblems.push({
|
|
174
|
+
uri: modelUri,
|
|
175
|
+
line: Number(diagnostic?.range?.start?.line ?? 0) + 1,
|
|
176
|
+
column: Number(diagnostic?.range?.start?.character ?? 0) + 1,
|
|
177
|
+
kind,
|
|
178
|
+
severity,
|
|
179
|
+
message: String(diagnostic?.message ?? ''),
|
|
180
|
+
source: typeof diagnostic?.source === 'string' ? diagnostic.source : undefined,
|
|
181
|
+
code: typeof diagnostic?.code === 'string' || typeof diagnostic?.code === 'number'
|
|
182
|
+
? diagnostic.code
|
|
183
|
+
: undefined,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
allProblems.sort((left, right) =>
|
|
188
|
+
left.uri.localeCompare(right.uri)
|
|
189
|
+
|| left.message.localeCompare(right.message)
|
|
190
|
+
|| left.line - right.line
|
|
191
|
+
|| left.column - right.column
|
|
192
|
+
);
|
|
193
|
+
const limitedProblems = limit === undefined ? allProblems : allProblems.slice(0, limit);
|
|
194
|
+
const grouped = new Map<string, RestLintProblem[]>();
|
|
195
|
+
for (const problem of limitedProblems) {
|
|
196
|
+
const existing = grouped.get(problem.uri) ?? [];
|
|
197
|
+
existing.push(problem);
|
|
198
|
+
grouped.set(problem.uri, existing);
|
|
199
|
+
}
|
|
200
|
+
const problemsByModelUri = Array.from(grouped.entries())
|
|
201
|
+
.map(([modelUri, problems]) => ({
|
|
202
|
+
modelUri,
|
|
203
|
+
errors: problems.filter((problem) => problem.kind === 'error').length,
|
|
204
|
+
warnings: problems.filter((problem) => problem.kind === 'warning').length,
|
|
205
|
+
problems: problems.sort((left, right) =>
|
|
206
|
+
left.message.localeCompare(right.message) || left.line - right.line || left.column - right.column
|
|
207
|
+
),
|
|
208
|
+
}))
|
|
209
|
+
.sort((left, right) => left.modelUri.localeCompare(right.modelUri));
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
success: errors === 0 && warnings === 0,
|
|
213
|
+
filesChecked: docs.length,
|
|
214
|
+
errors,
|
|
215
|
+
warnings,
|
|
216
|
+
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
217
|
+
totalProblems: allProblems.length,
|
|
218
|
+
returnedProblems: limitedProblems.length,
|
|
219
|
+
truncated: limitedProblems.length < allProblems.length,
|
|
220
|
+
limit,
|
|
221
|
+
problems: limitedProblems,
|
|
222
|
+
problemsByModelUri,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function reasonWorkspace(client: RestValidationContext, params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
227
|
+
const startedAt = Date.now();
|
|
228
|
+
await client.ensureInitialized();
|
|
229
|
+
await client.ensureWorkspaceCurrent();
|
|
230
|
+
let validateResult: Record<string, unknown> | undefined;
|
|
231
|
+
if (params.only !== true) {
|
|
232
|
+
validateResult = await validateWorkspace(client, {
|
|
233
|
+
only: false,
|
|
234
|
+
lintLimit: params.lintLimit,
|
|
235
|
+
});
|
|
236
|
+
if (validateResult.success !== true) {
|
|
237
|
+
return {
|
|
238
|
+
success: false,
|
|
239
|
+
ontologiesReasoned: 0,
|
|
240
|
+
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
241
|
+
inconsistent: [],
|
|
242
|
+
failed: [{
|
|
243
|
+
modelUri: '',
|
|
244
|
+
error: String(validateResult.error ?? 'validate failed'),
|
|
245
|
+
}],
|
|
246
|
+
validate: validateResult,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const format = normalizeRdfFormat(typeof params.format === 'string' ? params.format : undefined);
|
|
251
|
+
const tempRoot = await fs.mkdtemp(path.join(client.workspaceRoot, '.oml-reason-'));
|
|
252
|
+
const entries = await client.writeWorkspaceAssertedOwl(tempRoot, format, false);
|
|
253
|
+
if (entries.length === 0) {
|
|
254
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
255
|
+
return { success: true, ontologiesReasoned: 0, inconsistent: [], failed: [] };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const explanation = params.explanation !== false;
|
|
259
|
+
const uniqueNamesAssumption = params.uniqueNamesAssumption === true;
|
|
260
|
+
const profile = params.profile === true;
|
|
261
|
+
const timeout = typeof params.timeout === 'number' && Number.isFinite(params.timeout) && params.timeout > 0
|
|
262
|
+
? Math.floor(params.timeout)
|
|
263
|
+
: 120_000;
|
|
264
|
+
const inconsistent: Array<{ modelUri: string; validationWarnings: string[] }> = [];
|
|
265
|
+
const failed: Array<{ modelUri: string; error: string }> = [];
|
|
266
|
+
const results: Array<{
|
|
267
|
+
modelUri: string;
|
|
268
|
+
ontologyIri: string;
|
|
269
|
+
consistent: boolean;
|
|
270
|
+
result?: ConsistencyResult;
|
|
271
|
+
error?: string;
|
|
272
|
+
}> = [];
|
|
273
|
+
const runtimeCache = getValidationRuntimeCache(client);
|
|
274
|
+
const modelFingerprints = getModelContentFingerprints(client);
|
|
275
|
+
const seenReasonKeys = new Set<string>();
|
|
276
|
+
|
|
277
|
+
const reasonStartedAt = Date.now();
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
const modelFingerprint = modelFingerprints.get(entry.modelUri) ?? '';
|
|
280
|
+
const reasonFingerprint = hashText([
|
|
281
|
+
modelFingerprint,
|
|
282
|
+
String(explanation),
|
|
283
|
+
String(uniqueNamesAssumption),
|
|
284
|
+
String(profile),
|
|
285
|
+
String(timeout),
|
|
286
|
+
].join('\n'));
|
|
287
|
+
seenReasonKeys.add(entry.modelUri);
|
|
288
|
+
const cached = runtimeCache.reasonModelCache.get(entry.modelUri);
|
|
289
|
+
if (cached?.fingerprint === reasonFingerprint) {
|
|
290
|
+
const cachedResult = cached.result;
|
|
291
|
+
const warnings = Array.isArray(cachedResult.result?.reports) ? cachedResult.result.reports : [];
|
|
292
|
+
if (!cachedResult.consistent) {
|
|
293
|
+
inconsistent.push({
|
|
294
|
+
modelUri: entry.modelUri,
|
|
295
|
+
validationWarnings: warnings,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (cachedResult.error) {
|
|
299
|
+
failed.push({
|
|
300
|
+
modelUri: entry.modelUri,
|
|
301
|
+
error: cachedResult.error,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
results.push({
|
|
305
|
+
modelUri: entry.modelUri,
|
|
306
|
+
ontologyIri: entry.ontologyIri,
|
|
307
|
+
consistent: cachedResult.consistent,
|
|
308
|
+
result: cachedResult.result,
|
|
309
|
+
error: cachedResult.error,
|
|
310
|
+
});
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const result = await checkConsistency({
|
|
315
|
+
input: entry.owlPath,
|
|
316
|
+
inputRoot: tempRoot,
|
|
317
|
+
explanations: explanation,
|
|
318
|
+
uniqueNamesAssumption,
|
|
319
|
+
checkOnly: true,
|
|
320
|
+
profile,
|
|
321
|
+
timeout,
|
|
322
|
+
});
|
|
323
|
+
const warnings = Array.isArray(result.reports) ? result.reports : [];
|
|
324
|
+
const modelResult: ReasonModelCachedResult = {
|
|
325
|
+
consistent: result.consistent,
|
|
326
|
+
ontologyIri: entry.ontologyIri,
|
|
327
|
+
result,
|
|
328
|
+
};
|
|
329
|
+
runtimeCache.reasonModelCache.set(entry.modelUri, {
|
|
330
|
+
fingerprint: reasonFingerprint,
|
|
331
|
+
result: modelResult,
|
|
332
|
+
});
|
|
333
|
+
if (!result.consistent) {
|
|
334
|
+
inconsistent.push({
|
|
335
|
+
modelUri: entry.modelUri,
|
|
336
|
+
validationWarnings: warnings,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
results.push({
|
|
340
|
+
modelUri: entry.modelUri,
|
|
341
|
+
ontologyIri: entry.ontologyIri,
|
|
342
|
+
consistent: result.consistent,
|
|
343
|
+
result,
|
|
344
|
+
});
|
|
345
|
+
} catch (error) {
|
|
346
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
347
|
+
const modelError = message || 'reason failed';
|
|
348
|
+
runtimeCache.reasonModelCache.set(entry.modelUri, {
|
|
349
|
+
fingerprint: reasonFingerprint,
|
|
350
|
+
result: {
|
|
351
|
+
consistent: false,
|
|
352
|
+
ontologyIri: entry.ontologyIri,
|
|
353
|
+
error: modelError,
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
failed.push({
|
|
357
|
+
modelUri: entry.modelUri,
|
|
358
|
+
error: modelError,
|
|
359
|
+
});
|
|
360
|
+
results.push({
|
|
361
|
+
modelUri: entry.modelUri,
|
|
362
|
+
ontologyIri: entry.ontologyIri,
|
|
363
|
+
consistent: false,
|
|
364
|
+
error: modelError,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const key of runtimeCache.reasonModelCache.keys()) {
|
|
369
|
+
if (!seenReasonKeys.has(key)) {
|
|
370
|
+
runtimeCache.reasonModelCache.delete(key);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
success: failed.length === 0 && inconsistent.length === 0,
|
|
377
|
+
ontologiesReasoned: entries.length,
|
|
378
|
+
elapsedMs: Math.max(0, Date.now() - reasonStartedAt),
|
|
379
|
+
totalElapsedMs: Math.max(0, Date.now() - startedAt),
|
|
380
|
+
inconsistent,
|
|
381
|
+
failed,
|
|
382
|
+
results,
|
|
383
|
+
validate: validateResult,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export async function validateWorkspace(client: RestValidationContext, params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
388
|
+
const startedAt = Date.now();
|
|
389
|
+
await client.ensureInitialized();
|
|
390
|
+
await client.ensureWorkspaceCurrent();
|
|
391
|
+
let lint: RestLintResult | undefined;
|
|
392
|
+
if (params.only !== true) {
|
|
393
|
+
lint = await lintWorkspace(client, { limit: params.lintLimit });
|
|
394
|
+
if (lint.errors > 0 || lint.warnings > 0) {
|
|
395
|
+
return {
|
|
396
|
+
success: false,
|
|
397
|
+
filesChecked: 0,
|
|
398
|
+
errors: 0,
|
|
399
|
+
warnings: 0,
|
|
400
|
+
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
401
|
+
error: `lint failed with ${lint.errors} error(s) and ${lint.warnings} warning(s)`,
|
|
402
|
+
lint,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const workspaceRoot = path.resolve(client.workspaceRoot);
|
|
407
|
+
const markdownFiles = await findFilesByExtension(workspaceRoot, '.md');
|
|
408
|
+
const templateCatalog = await buildTemplateCatalog(workspaceRoot);
|
|
409
|
+
const runtime = new MarkdownPreviewRuntime(new MarkdownHandlerRegistry());
|
|
410
|
+
const ontologyIndex = getOntologyModelIndex(client.runtime.shared as any);
|
|
411
|
+
const reasoningService = client.runtime.Oml.reasoning.ReasoningService as any;
|
|
412
|
+
const shaclService = createDefaultShaclService(
|
|
413
|
+
reasoningService.getSparqlService(),
|
|
414
|
+
(uri) => reasoningService.ensureQueryContext(uri),
|
|
415
|
+
(uri) => reasoningService.getContextIri(uri),
|
|
416
|
+
);
|
|
417
|
+
const runtimeCache = getValidationRuntimeCache(client);
|
|
418
|
+
const modelFingerprints = getModelContentFingerprints(client);
|
|
419
|
+
const seenValidateKeys = new Set<string>();
|
|
420
|
+
let filesChecked = 0;
|
|
421
|
+
let errors = 0;
|
|
422
|
+
let warnings = 0;
|
|
423
|
+
const details: Array<Record<string, unknown>> = [];
|
|
424
|
+
|
|
425
|
+
for (const markdownPath of markdownFiles) {
|
|
426
|
+
const markdown = await fs.readFile(markdownPath, 'utf-8');
|
|
427
|
+
if (isTemplateMarkdownFile(markdown)) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const frontMatter = extractLeadingFrontMatter(markdown);
|
|
431
|
+
const contextOntologyIri = normalizeContextOntologyIri(
|
|
432
|
+
frontMatter?.contextOntologyIri
|
|
433
|
+
?? frontMatterString(frontMatter?.data, 'ontologyIri')
|
|
434
|
+
?? frontMatterString(frontMatter?.data, 'ontology')
|
|
435
|
+
);
|
|
436
|
+
const contextMemberIri = frontMatterString(frontMatter?.data, 'memberIri')
|
|
437
|
+
?? frontMatterString(frontMatter?.data, 'contextMemberIri');
|
|
438
|
+
const markdownUri = pathToFileURL(markdownPath).toString();
|
|
439
|
+
const modelUri = contextOntologyIri
|
|
440
|
+
? ontologyIndex.resolveModelUri(contextOntologyIri, markdownUri)
|
|
441
|
+
: undefined;
|
|
442
|
+
const expanded = await expandTemplateComposeBlocks(markdown, templateCatalog, {
|
|
443
|
+
workspaceRoot,
|
|
444
|
+
sourceMarkdownPath: markdownPath,
|
|
445
|
+
contextOntologyIri,
|
|
446
|
+
contextModelUri: modelUri,
|
|
447
|
+
contextMemberIri,
|
|
448
|
+
});
|
|
449
|
+
const prepared = runtime.prepare(expanded);
|
|
450
|
+
const executableBlocks = prepared.codeBlocks
|
|
451
|
+
.filter((block) => block.language === 'table-editor')
|
|
452
|
+
.map((block) => ({ id: block.id, source: block.content }));
|
|
453
|
+
if (executableBlocks.length === 0) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
for (const block of executableBlocks) {
|
|
457
|
+
filesChecked += 1;
|
|
458
|
+
const shacl = block.source.trim();
|
|
459
|
+
if (!contextOntologyIri) {
|
|
460
|
+
errors += 1;
|
|
461
|
+
details.push({
|
|
462
|
+
markdownUri,
|
|
463
|
+
blockId: block.id,
|
|
464
|
+
status: 'error',
|
|
465
|
+
message: 'Missing ontologyIri in markdown frontmatter for table-editor block.',
|
|
466
|
+
});
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (!modelUri) {
|
|
470
|
+
errors += 1;
|
|
471
|
+
details.push({
|
|
472
|
+
markdownUri,
|
|
473
|
+
blockId: block.id,
|
|
474
|
+
status: 'error',
|
|
475
|
+
message: `Unable to resolve modelUri for ontologyIri '${contextOntologyIri}'.`,
|
|
476
|
+
});
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (!shacl) {
|
|
480
|
+
errors += 1;
|
|
481
|
+
const detail = {
|
|
482
|
+
markdownUri,
|
|
483
|
+
blockId: block.id,
|
|
484
|
+
modelUri,
|
|
485
|
+
status: 'error',
|
|
486
|
+
message: 'Empty SHACL shape.',
|
|
487
|
+
};
|
|
488
|
+
details.push(detail);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const modelFingerprint = modelFingerprints.get(modelUri) ?? '';
|
|
492
|
+
const cacheKey = `${markdownUri}::${block.id}`;
|
|
493
|
+
const validateFingerprint = hashText([modelUri, modelFingerprint, shacl].join('\n'));
|
|
494
|
+
seenValidateKeys.add(cacheKey);
|
|
495
|
+
const cached = runtimeCache.validateBlockCache.get(cacheKey);
|
|
496
|
+
if (cached?.fingerprint === validateFingerprint) {
|
|
497
|
+
errors += cached.result.errors;
|
|
498
|
+
warnings += cached.result.warnings;
|
|
499
|
+
details.push(cached.result.detail);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const result = await shaclService.validateShacl(modelUri, shacl);
|
|
503
|
+
let blockErrors = 0;
|
|
504
|
+
let blockWarnings = 0;
|
|
505
|
+
if (result.error) {
|
|
506
|
+
blockErrors += 1;
|
|
507
|
+
} else if (!result.conforms) {
|
|
508
|
+
for (const issue of result.issues) {
|
|
509
|
+
if (String(issue.severity ?? '').toLowerCase().includes('warning')) {
|
|
510
|
+
blockWarnings += 1;
|
|
511
|
+
} else {
|
|
512
|
+
blockErrors += 1;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (result.issues.length === 0) {
|
|
516
|
+
blockErrors += 1;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
errors += blockErrors;
|
|
520
|
+
warnings += blockWarnings;
|
|
521
|
+
const detail = {
|
|
522
|
+
markdownUri,
|
|
523
|
+
blockId: block.id,
|
|
524
|
+
modelUri,
|
|
525
|
+
status: result.error ? 'error' : (result.conforms ? 'ok' : 'failed'),
|
|
526
|
+
conforms: result.conforms,
|
|
527
|
+
error: result.error,
|
|
528
|
+
issues: result.issues,
|
|
529
|
+
};
|
|
530
|
+
details.push(detail);
|
|
531
|
+
runtimeCache.validateBlockCache.set(cacheKey, {
|
|
532
|
+
fingerprint: validateFingerprint,
|
|
533
|
+
result: {
|
|
534
|
+
errors: blockErrors,
|
|
535
|
+
warnings: blockWarnings,
|
|
536
|
+
detail,
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
for (const key of runtimeCache.validateBlockCache.keys()) {
|
|
542
|
+
if (!seenValidateKeys.has(key)) {
|
|
543
|
+
runtimeCache.validateBlockCache.delete(key);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
success: errors === 0 && warnings === 0,
|
|
548
|
+
filesChecked,
|
|
549
|
+
errors,
|
|
550
|
+
warnings,
|
|
551
|
+
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
552
|
+
details,
|
|
553
|
+
lint,
|
|
554
|
+
};
|
|
555
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "out",
|
|
6
|
+
"declarationDir": "out"
|
|
7
|
+
},
|
|
8
|
+
"references": [
|
|
9
|
+
{
|
|
10
|
+
"path": "../owl/tsconfig.json"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"path": "../markdown/tsconfig.json"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"path": "../language/tsconfig.src.json"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"include": [
|
|
20
|
+
"src/**/*.ts"
|
|
21
|
+
]
|
|
22
|
+
}
|