@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.
@@ -0,0 +1,134 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ import type { FileSystemNode, FileSystemProvider } from 'langium';
4
+ import { URI } from 'langium';
5
+ import { NodeFileSystemProvider } from 'langium/node';
6
+ import type { Connection } from 'vscode-languageserver';
7
+ import { FsRequests, type FsDirectoryEntry, type FsStatResult } from '../protocol/browser-fs-protocol.js';
8
+
9
+ const textDecoder = new TextDecoder('utf-8');
10
+
11
+ /**
12
+ * Hybrid file system provider that handles both local files and Live Share (vsls://) URIs.
13
+ * For local files, uses Node's fs module directly.
14
+ * For vsls:// URIs, proxies requests through the LSP connection to VS Code.
15
+ */
16
+ class HybridFileSystemProvider implements FileSystemProvider {
17
+ private readonly nodeProvider: NodeFileSystemProvider;
18
+
19
+ constructor(private readonly connection: Connection) {
20
+ this.nodeProvider = new NodeFileSystemProvider();
21
+ }
22
+
23
+ private isRemoteScheme(uri: URI): boolean {
24
+ return uri.scheme === 'vsls'
25
+ || uri.scheme === 'vscode-remote'
26
+ || uri.scheme === 'git';
27
+ }
28
+
29
+ async stat(uri: URI): Promise<FileSystemNode> {
30
+ if (this.isRemoteScheme(uri)) {
31
+ const result = await this.connection.sendRequest<FsStatResult | null>(FsRequests.stat, { uri: uri.toString() });
32
+ if (!result) {
33
+ throw new Error(`File not found: ${uri.toString()}`);
34
+ }
35
+ return {
36
+ uri,
37
+ isFile: result.type === 'file',
38
+ isDirectory: result.type === 'directory'
39
+ };
40
+ }
41
+ return this.nodeProvider.stat(uri);
42
+ }
43
+
44
+ statSync(uri: URI): FileSystemNode {
45
+ if (this.isRemoteScheme(uri)) {
46
+ throw new Error(`Synchronous file system access unavailable for remote URIs: ${uri.toString()}`);
47
+ }
48
+ return this.nodeProvider.statSync(uri);
49
+ }
50
+
51
+ async exists(uri: URI): Promise<boolean> {
52
+ try {
53
+ await this.stat(uri);
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ existsSync(uri: URI): boolean {
61
+ if (this.isRemoteScheme(uri)) {
62
+ return false;
63
+ }
64
+ return this.nodeProvider.existsSync(uri);
65
+ }
66
+
67
+ async readBinary(uri: URI): Promise<Uint8Array> {
68
+ if (this.isRemoteScheme(uri)) {
69
+ const content = await this.readFile(uri);
70
+ return new TextEncoder().encode(content);
71
+ }
72
+ return this.nodeProvider.readBinary(uri);
73
+ }
74
+
75
+ readBinarySync(uri: URI): Uint8Array {
76
+ if (this.isRemoteScheme(uri)) {
77
+ throw new Error(`Synchronous file system access unavailable for remote URIs: ${uri.toString()}`);
78
+ }
79
+ return this.nodeProvider.readBinarySync(uri);
80
+ }
81
+
82
+ async readFile(uri: URI): Promise<string> {
83
+ if (this.isRemoteScheme(uri)) {
84
+ const content = await this.connection.sendRequest<string | Uint8Array | ArrayBuffer | null>(
85
+ FsRequests.readFile,
86
+ { uri: uri.toString() }
87
+ );
88
+ if (typeof content === 'string') {
89
+ return content;
90
+ }
91
+ if (content instanceof Uint8Array) {
92
+ return textDecoder.decode(content);
93
+ }
94
+ if (content instanceof ArrayBuffer) {
95
+ return textDecoder.decode(new Uint8Array(content));
96
+ }
97
+ throw new Error(`Failed to read file: ${uri.toString()}`);
98
+ }
99
+ return this.nodeProvider.readFile(uri);
100
+ }
101
+
102
+ readFileSync(uri: URI): string {
103
+ if (this.isRemoteScheme(uri)) {
104
+ throw new Error(`Synchronous file system access unavailable for remote URIs: ${uri.toString()}`);
105
+ }
106
+ return this.nodeProvider.readFileSync(uri);
107
+ }
108
+
109
+ async readDirectory(uri: URI): Promise<FileSystemNode[]> {
110
+ if (this.isRemoteScheme(uri)) {
111
+ const entries = await this.connection.sendRequest<FsDirectoryEntry[]>(
112
+ FsRequests.readDirectory,
113
+ { uri: uri.toString() }
114
+ ) ?? [];
115
+ return entries.map((entry) => ({
116
+ uri: URI.parse(entry.uri),
117
+ isFile: entry.type === 'file',
118
+ isDirectory: entry.type === 'directory'
119
+ }));
120
+ }
121
+ return this.nodeProvider.readDirectory(uri);
122
+ }
123
+
124
+ readDirectorySync(uri: URI): FileSystemNode[] {
125
+ if (this.isRemoteScheme(uri)) {
126
+ return [];
127
+ }
128
+ return this.nodeProvider.readDirectorySync(uri);
129
+ }
130
+ }
131
+
132
+ export const HybridFileSystem = (connection: Connection) => ({
133
+ fileSystemProvider: () => new HybridFileSystemProvider(connection)
134
+ });
@@ -0,0 +1,118 @@
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 { checkConsistency, type ConsistencyResult } from '@oml/reasoner';
6
+
7
+ type RdfFormat = 'ttl' | 'trig' | 'nt' | 'nq' | 'n3';
8
+
9
+ type WorkspaceOwlEntry = { modelUri: string; ontologyIri: string; owlPath: string };
10
+
11
+ export type RestExportContext = {
12
+ workspaceRoot: string;
13
+ ensureInitialized: () => Promise<void>;
14
+ ensureWorkspaceCurrent: () => Promise<void>;
15
+ writeWorkspaceAssertedOwl: (
16
+ outputDir: string,
17
+ format: RdfFormat,
18
+ pretty: boolean,
19
+ ) => Promise<WorkspaceOwlEntry[]>;
20
+ exportAssertedWorkspace: (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
21
+ };
22
+
23
+ function normalizeRdfFormat(value: string | undefined): RdfFormat {
24
+ if (!value) {
25
+ return 'ttl';
26
+ }
27
+ const normalized = value.trim().toLowerCase();
28
+ if (normalized === 'trig' || normalized === 'nt' || normalized === 'nq' || normalized === 'n3') {
29
+ return normalized;
30
+ }
31
+ return 'ttl';
32
+ }
33
+
34
+ export async function exportAssertedWorkspace(client: RestExportContext, params: Record<string, unknown>): Promise<Record<string, unknown>> {
35
+ await client.ensureInitialized();
36
+ await client.ensureWorkspaceCurrent();
37
+ const workspaceRoot = path.resolve(client.workspaceRoot);
38
+ const outputFromOptions = typeof params.outputDir === 'string' && params.outputDir.trim().length > 0
39
+ ? params.outputDir.trim()
40
+ : (typeof params.owl === 'string' && params.owl.trim().length > 0 ? params.owl.trim() : 'build/owl');
41
+ const outputDir = path.resolve(
42
+ workspaceRoot,
43
+ outputFromOptions,
44
+ );
45
+ const format = normalizeRdfFormat(typeof params.format === 'string' ? params.format : undefined);
46
+ const pretty = params.pretty === true;
47
+ if (params.clean === true) {
48
+ await fs.rm(outputDir, { recursive: true, force: true });
49
+ }
50
+ await fs.mkdir(outputDir, { recursive: true });
51
+ const filesWritten = (await client.writeWorkspaceAssertedOwl(outputDir, format, pretty)).length;
52
+ return { success: true, filesWritten, outputDir, format, pretty };
53
+ }
54
+
55
+ export async function exportWorkspace(client: RestExportContext, params: Record<string, unknown>): Promise<Record<string, unknown>> {
56
+ await client.ensureInitialized();
57
+ await client.ensureWorkspaceCurrent();
58
+ const assertedExportResult = await client.exportAssertedWorkspace({
59
+ outputDir: params.outputDir ?? params.owl,
60
+ format: params.format,
61
+ pretty: params.pretty,
62
+ clean: params.clean,
63
+ });
64
+ const format = normalizeRdfFormat(typeof params.format === 'string' ? params.format : undefined);
65
+ const outputDir = String(assertedExportResult.outputDir ?? '');
66
+ const explanation = params.explanation !== false;
67
+ const uniqueNamesAssumption = params.uniqueNamesAssumption === true;
68
+ const profile = params.profile === true;
69
+ const timeout = typeof params.timeout === 'number' && Number.isFinite(params.timeout) && params.timeout > 0
70
+ ? Math.floor(params.timeout)
71
+ : 120_000;
72
+ const entries = await client.writeWorkspaceAssertedOwl(outputDir, format, params.pretty === true);
73
+ const results: Array<{ modelUri: string; ontologyIri: string; result?: ConsistencyResult; error?: string }> = [];
74
+ const failed: Array<{ modelUri: string; error: string }> = [];
75
+ for (const entry of entries) {
76
+ try {
77
+ const result = await checkConsistency({
78
+ input: entry.owlPath,
79
+ inputRoot: outputDir,
80
+ explanations: explanation,
81
+ uniqueNamesAssumption,
82
+ checkOnly: false,
83
+ profile,
84
+ timeout,
85
+ });
86
+ results.push({
87
+ modelUri: entry.modelUri,
88
+ ontologyIri: entry.ontologyIri,
89
+ result,
90
+ });
91
+ } catch (error) {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ failed.push({ modelUri: entry.modelUri, error: message || 'reason failed' });
94
+ results.push({
95
+ modelUri: entry.modelUri,
96
+ ontologyIri: entry.ontologyIri,
97
+ error: message || 'reason failed',
98
+ });
99
+ }
100
+ }
101
+ const reasonResult = {
102
+ success: failed.length === 0,
103
+ ontologiesReasoned: entries.length,
104
+ inconsistent: results
105
+ .filter((entry) => entry.result && entry.result.consistent === false)
106
+ .map((entry) => ({
107
+ modelUri: entry.modelUri,
108
+ validationWarnings: entry.result?.reports ?? [],
109
+ })),
110
+ failed,
111
+ results,
112
+ };
113
+ return {
114
+ success: assertedExportResult.success === true && reasonResult.success === true,
115
+ assertedExport: assertedExportResult,
116
+ reason: reasonResult,
117
+ };
118
+ }
@@ -0,0 +1,117 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ export const REST_ROUTES = [
4
+ { method: 'POST', path: '/v0/query', operationId: 'query' },
5
+ { method: 'POST', path: '/v0/update', operationId: 'update' },
6
+ { method: 'POST', path: '/v0/fuzzysearch', operationId: 'fuzzysearch' },
7
+ { method: 'POST', path: '/v0/lint', operationId: 'lint' },
8
+ { method: 'POST', path: '/v0/reason', operationId: 'reason' },
9
+ { method: 'POST', path: '/v0/validate', operationId: 'validate' },
10
+ { method: 'POST', path: '/v0/render', operationId: 'render' },
11
+ { method: 'POST', path: '/v0/export', operationId: 'export' },
12
+ ] as const;
13
+
14
+ export type RestRoute = (typeof REST_ROUTES)[number];
15
+
16
+ export type RestRouteClient = {
17
+ queryWorkspace: (body: Record<string, unknown>) => Promise<Record<string, unknown>>;
18
+ updateWorkspace: (body: Record<string, unknown>) => Promise<unknown>;
19
+ fuzzySearchWorkspace: (body: Record<string, unknown>) => Promise<Record<string, unknown>>;
20
+ lintWorkspace: (body: Record<string, unknown>) => Promise<unknown>;
21
+ reasonWorkspace: (body: Record<string, unknown>) => Promise<Record<string, unknown>>;
22
+ validateWorkspace: (body: Record<string, unknown>) => Promise<Record<string, unknown>>;
23
+ exportWorkspace: (body: Record<string, unknown>) => Promise<Record<string, unknown>>;
24
+ renderWorkspace: (body: Record<string, unknown>) => Promise<Record<string, unknown>>;
25
+ };
26
+
27
+ export async function dispatchRestRoute(
28
+ method: string,
29
+ pathname: string,
30
+ body: Record<string, unknown>,
31
+ client: RestRouteClient,
32
+ ): Promise<{ operationId: string; result: unknown } | undefined> {
33
+ if (method !== 'POST') {
34
+ return undefined;
35
+ }
36
+ if (pathname === '/v0/query') {
37
+ return { operationId: 'query', result: await client.queryWorkspace(body) };
38
+ }
39
+ if (pathname === '/v0/update') {
40
+ return { operationId: 'update', result: await client.updateWorkspace(body) };
41
+ }
42
+ if (pathname === '/v0/fuzzysearch') {
43
+ return { operationId: 'fuzzysearch', result: await client.fuzzySearchWorkspace(body) };
44
+ }
45
+ if (pathname === '/v0/lint') {
46
+ return { operationId: 'lint', result: await client.lintWorkspace(body) };
47
+ }
48
+ if (pathname === '/v0/reason') {
49
+ return { operationId: 'reason', result: await client.reasonWorkspace(body) };
50
+ }
51
+ if (pathname === '/v0/validate') {
52
+ return { operationId: 'validate', result: await client.validateWorkspace(body) };
53
+ }
54
+ if (pathname === '/v0/export') {
55
+ return { operationId: 'export', result: await client.exportWorkspace(body) };
56
+ }
57
+ if (pathname === '/v0/render') {
58
+ return { operationId: 'render', result: await client.renderWorkspace(body) };
59
+ }
60
+ return undefined;
61
+ }
62
+
63
+ export function createOpenApiSpec(httpHost: string, httpPort: number): Record<string, unknown> {
64
+ const paths: Record<string, unknown> = {
65
+ '/health': {
66
+ get: {
67
+ summary: 'Health check',
68
+ responses: {
69
+ '200': {
70
+ description: 'Service health',
71
+ },
72
+ },
73
+ },
74
+ },
75
+ '/v0/models': {
76
+ get: {
77
+ summary: 'List OML model files under the configured workspace root',
78
+ responses: {
79
+ '200': { description: 'Workspace model file list' },
80
+ },
81
+ },
82
+ },
83
+ };
84
+
85
+ for (const route of REST_ROUTES) {
86
+ paths[route.path] = {
87
+ post: {
88
+ summary: `Invoke ${route.operationId}`,
89
+ operationId: route.operationId,
90
+ requestBody: {
91
+ required: false,
92
+ content: {
93
+ 'application/json': {
94
+ schema: { type: 'object', additionalProperties: true },
95
+ },
96
+ },
97
+ },
98
+ responses: {
99
+ '200': { description: 'Successful OML response' },
100
+ '400': { description: 'Invalid request' },
101
+ '500': { description: 'Server error' },
102
+ },
103
+ },
104
+ };
105
+ }
106
+
107
+ return {
108
+ openapi: '3.0.3',
109
+ info: {
110
+ title: 'OML REST Server',
111
+ version: '1.0.0',
112
+ description: 'HTTP/JSON interface for OML server operations.',
113
+ },
114
+ servers: [{ url: `http://${httpHost}:${httpPort}` }],
115
+ paths,
116
+ };
117
+ }