@oml/cli 0.19.3 → 0.20.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/out/cli.js +55 -16
- package/out/cli.js.map +1 -1
- package/out/commands/export.js +3 -3
- package/out/commands/export.js.map +1 -1
- package/out/commands/reason.d.ts +2 -3
- package/out/commands/reason.js +181 -166
- package/out/commands/reason.js.map +1 -1
- package/package.json +3 -3
- package/src/cli.ts +57 -16
- package/src/commands/export.ts +6 -8
- package/src/commands/reason.ts +201 -192
package/src/commands/export.ts
CHANGED
|
@@ -14,12 +14,10 @@ export type ExportOptions = {
|
|
|
14
14
|
authToken?: string;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
type
|
|
17
|
+
type OwlPayload = {
|
|
18
18
|
success: boolean;
|
|
19
|
-
filesWritten: number;
|
|
20
19
|
outputDir: string;
|
|
21
|
-
|
|
22
|
-
pretty: boolean;
|
|
20
|
+
files: Array<{ path: string; imports: string[] }>;
|
|
23
21
|
filesChecked?: number;
|
|
24
22
|
errors?: number;
|
|
25
23
|
warnings?: number;
|
|
@@ -38,10 +36,10 @@ function normalizeRdfFormat(value: string | undefined): 'ttl' | 'trig' | 'nt' |
|
|
|
38
36
|
export const exportAction = async (opts: ExportOptions): Promise<void> => {
|
|
39
37
|
const startedAt = Date.now();
|
|
40
38
|
const format = normalizeRdfFormat(opts.format);
|
|
41
|
-
const result = await restPost<
|
|
39
|
+
const result = await restPost<OwlPayload>(
|
|
42
40
|
'/v0/export',
|
|
43
41
|
{
|
|
44
|
-
owl: opts.owl,
|
|
42
|
+
owl: opts.owl ?? 'build/owl',
|
|
45
43
|
format,
|
|
46
44
|
pretty: opts.pretty === true,
|
|
47
45
|
} as Record<string, unknown>,
|
|
@@ -65,12 +63,12 @@ export const exportAction = async (opts: ExportOptions): Promise<void> => {
|
|
|
65
63
|
warnings: Number(result.warnings ?? 0),
|
|
66
64
|
problems: result.problems,
|
|
67
65
|
});
|
|
68
|
-
if (result.
|
|
66
|
+
if (result.files.length === 0) {
|
|
69
67
|
console.log(chalk.yellow('No .oml files found in server workspace.'));
|
|
70
68
|
return;
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
console.log(chalk.green(
|
|
74
|
-
`export: ${result.
|
|
72
|
+
`export: ${result.files.length} OML file(s) exported in ${path.relative(process.cwd(), result.outputDir) || result.outputDir} [${formatDuration(Date.now() - startedAt)}]`
|
|
75
73
|
));
|
|
76
74
|
};
|
package/src/commands/reason.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
2
|
|
|
3
|
-
import { checkConsistency } from '@oml/reasoner';
|
|
3
|
+
import { checkConsistency, checkConsistencyBatch } from '@oml/reasoner';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import * as fs from 'node:fs/promises';
|
|
6
6
|
import * as os from 'node:os';
|
|
@@ -8,57 +8,47 @@ import * as path from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { failCli } from '../cli-error.js';
|
|
10
10
|
import { formatDuration } from '../util.js';
|
|
11
|
-
import { printLintDiagnostics, type
|
|
11
|
+
import { printLintDiagnostics, type LintProblem } from './lint.js';
|
|
12
12
|
import { restPost } from './server/rest.js';
|
|
13
13
|
|
|
14
|
-
type RdfFormat = 'ttl' | 'trig' | 'nt' | 'nq' | 'n3';
|
|
15
|
-
|
|
16
14
|
export type ReasonOptions = {
|
|
17
15
|
owl?: string;
|
|
18
|
-
format?:
|
|
16
|
+
format?: string;
|
|
19
17
|
pretty?: boolean;
|
|
20
18
|
explanation?: boolean;
|
|
21
19
|
uniqueNamesAssumption?: boolean;
|
|
22
20
|
profile?: boolean;
|
|
21
|
+
hoist?: boolean;
|
|
23
22
|
timeout?: number;
|
|
24
23
|
authToken?: string;
|
|
25
24
|
};
|
|
26
25
|
|
|
27
|
-
type
|
|
26
|
+
type ExportFile = {
|
|
27
|
+
path: string;
|
|
28
|
+
imports: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type ExportPayload = {
|
|
28
32
|
success: boolean;
|
|
29
33
|
error?: string;
|
|
30
|
-
|
|
34
|
+
outputDir: string;
|
|
31
35
|
filesChecked?: number;
|
|
32
36
|
errors?: number;
|
|
33
37
|
warnings?: number;
|
|
34
38
|
problems?: LintProblem[];
|
|
35
|
-
files:
|
|
36
|
-
modelUri: string;
|
|
37
|
-
ontologyIri: string;
|
|
38
|
-
content: string;
|
|
39
|
-
}>;
|
|
39
|
+
files: ExportFile[];
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
function normalizeRdfFormat(value: string | undefined): RdfFormat {
|
|
45
|
-
const normalized = String(value ?? 'ttl').trim().toLowerCase();
|
|
46
|
-
if (normalized === 'trig' || normalized === 'nt' || normalized === 'nq' || normalized === 'n3') {
|
|
47
|
-
return normalized;
|
|
48
|
-
}
|
|
49
|
-
return 'ttl';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function prettyPrintRdf(serialized: string, format: RdfFormat): string {
|
|
42
|
+
/** Add a blank line between top-level statements for ttl/trig readability. */
|
|
43
|
+
function prettyPrintRdf(serialized: string, format: string): string {
|
|
53
44
|
if (format !== 'ttl' && format !== 'trig') {
|
|
54
45
|
return serialized;
|
|
55
46
|
}
|
|
56
47
|
const lines = serialized.split('\n');
|
|
57
48
|
const out: string[] = [];
|
|
58
49
|
for (let i = 0; i < lines.length; i += 1) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (line.trim().endsWith('.')) {
|
|
50
|
+
out.push(lines[i]);
|
|
51
|
+
if (lines[i].trim().endsWith('.')) {
|
|
62
52
|
const next = lines[i + 1];
|
|
63
53
|
if (next !== undefined && next.trim().length > 0) {
|
|
64
54
|
out.push('');
|
|
@@ -68,225 +58,244 @@ function prettyPrintRdf(serialized: string, format: RdfFormat): string {
|
|
|
68
58
|
return out.join('\n');
|
|
69
59
|
}
|
|
70
60
|
|
|
71
|
-
function
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
let statementMatch: RegExpExecArray | null;
|
|
75
|
-
while ((statementMatch = statementPattern.exec(serialized)) !== null) {
|
|
76
|
-
const tail = statementMatch[1] ?? '';
|
|
77
|
-
const iriPattern = /<([^>]+)>/g;
|
|
78
|
-
let iriMatch: RegExpExecArray | null;
|
|
79
|
-
while ((iriMatch = iriPattern.exec(tail)) !== null) {
|
|
80
|
-
const iri = iriMatch[1]?.trim();
|
|
81
|
-
if (iri) {
|
|
82
|
-
imports.add(iri);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// Handle compact single-line N-Triples cases defensively.
|
|
87
|
-
if (imports.size === 0 && serialized.includes(OWL_IMPORTS_IRI)) {
|
|
88
|
-
const linePattern = /<http:\/\/www\.w3\.org\/2002\/07\/owl#imports>\s+<([^>]+)>/g;
|
|
89
|
-
let lineMatch: RegExpExecArray | null;
|
|
90
|
-
while ((lineMatch = linePattern.exec(serialized)) !== null) {
|
|
91
|
-
const iri = lineMatch[1]?.trim();
|
|
92
|
-
if (iri) {
|
|
93
|
-
imports.add(iri);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return Array.from(imports);
|
|
61
|
+
function resolveEntailmentsPath(uri: string): string {
|
|
62
|
+
const trimmed = uri.trim();
|
|
63
|
+
return trimmed.startsWith('file://') ? fileURLToPath(trimmed) : trimmed;
|
|
98
64
|
}
|
|
99
65
|
|
|
100
|
-
function sortFilesLeafToRoot(
|
|
101
|
-
files
|
|
102
|
-
|
|
103
|
-
const filesByOntologyIri = new Map<string, AssertionsPayload['files'][number]>();
|
|
104
|
-
for (const file of files) {
|
|
105
|
-
filesByOntologyIri.set(file.ontologyIri, file);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const importsByOntologyIri = new Map<string, string[]>();
|
|
109
|
-
for (const file of files) {
|
|
110
|
-
importsByOntologyIri.set(file.ontologyIri, extractImports(file.content));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const sorted: AssertionsPayload['files'] = [];
|
|
66
|
+
function sortFilesLeafToRoot(files: ExportFile[]): ExportFile[] {
|
|
67
|
+
const byPath = new Map<string, ExportFile>(files.map(f => [f.path, f]));
|
|
68
|
+
const sorted: ExportFile[] = [];
|
|
114
69
|
const state = new Map<string, 0 | 1 | 2>();
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
if (current === 1) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
state.set(ontologyIri, 1);
|
|
125
|
-
for (const importedIri of importsByOntologyIri.get(ontologyIri) ?? []) {
|
|
126
|
-
if (filesByOntologyIri.has(importedIri)) {
|
|
127
|
-
visit(importedIri);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
state.set(ontologyIri, 2);
|
|
131
|
-
const file = filesByOntologyIri.get(ontologyIri);
|
|
132
|
-
if (file) {
|
|
133
|
-
sorted.push(file);
|
|
70
|
+
const visit = (p: string): void => {
|
|
71
|
+
if ((state.get(p) ?? 0) !== 0) return;
|
|
72
|
+
state.set(p, 1);
|
|
73
|
+
for (const imp of byPath.get(p)?.imports ?? []) {
|
|
74
|
+
visit(imp);
|
|
134
75
|
}
|
|
76
|
+
state.set(p, 2);
|
|
77
|
+
const f = byPath.get(p);
|
|
78
|
+
if (f) sorted.push(f);
|
|
135
79
|
};
|
|
80
|
+
for (const p of [...byPath.keys()].sort()) visit(p);
|
|
81
|
+
return sorted;
|
|
82
|
+
}
|
|
136
83
|
|
|
137
|
-
|
|
138
|
-
|
|
84
|
+
function collectClosure(start: string, byPath: Map<string, ExportFile>, into: Set<string>): void {
|
|
85
|
+
const stack = [start];
|
|
86
|
+
while (stack.length > 0) {
|
|
87
|
+
const p = stack.pop() as string;
|
|
88
|
+
if (into.has(p)) continue;
|
|
89
|
+
into.add(p);
|
|
90
|
+
for (const imp of byPath.get(p)?.imports ?? []) stack.push(imp);
|
|
139
91
|
}
|
|
140
|
-
return sorted;
|
|
141
92
|
}
|
|
142
93
|
|
|
143
|
-
function
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
94
|
+
function closureFiles(start: string, byPath: Map<string, ExportFile>): ExportFile[] {
|
|
95
|
+
const seen = new Set<string>();
|
|
96
|
+
collectClosure(start, byPath, seen);
|
|
97
|
+
const result: ExportFile[] = [];
|
|
98
|
+
for (const p of seen) {
|
|
99
|
+
const f = byPath.get(p);
|
|
100
|
+
if (f) result.push(f);
|
|
147
101
|
}
|
|
148
|
-
return
|
|
102
|
+
return result;
|
|
149
103
|
}
|
|
150
104
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Files imported by no other file (maximal nodes of the import graph).
|
|
107
|
+
* Checking only these establishes consistency of every file: OWL-DL
|
|
108
|
+
* consistency is monotonic, so a consistent import closure proves every
|
|
109
|
+
* imported (subset) ontology is consistent too. The roots' closures cover
|
|
110
|
+
* every file, so this is sufficient — and far cheaper than reasoning each
|
|
111
|
+
* file's closure independently.
|
|
112
|
+
*/
|
|
113
|
+
function computeConsistencyRoots(files: ExportFile[]): ExportFile[] {
|
|
114
|
+
const byPath = new Map<string, ExportFile>(files.map(f => [f.path, f]));
|
|
115
|
+
const imported = new Set<string>();
|
|
116
|
+
for (const f of files) {
|
|
117
|
+
for (const imp of f.imports) imported.add(imp);
|
|
161
118
|
}
|
|
119
|
+
const roots = files.filter(f => !imported.has(f.path));
|
|
120
|
+
// Guard against import cycles whose component is imported by nobody outside
|
|
121
|
+
// it: make sure every file is in some root's closure, else add it directly.
|
|
122
|
+
const covered = new Set<string>();
|
|
123
|
+
for (const root of roots) collectClosure(root.path, byPath, covered);
|
|
124
|
+
const uncovered = files.filter(f => !covered.has(f.path));
|
|
125
|
+
return sortFilesLeafToRoot([...roots, ...uncovered]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveTimeout(opts: ReasonOptions): number {
|
|
129
|
+
return typeof opts.timeout === 'number' && Number.isFinite(opts.timeout) && opts.timeout > 0
|
|
130
|
+
? Math.floor(opts.timeout)
|
|
131
|
+
: 120_000;
|
|
162
132
|
}
|
|
163
133
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
134
|
+
/**
|
|
135
|
+
* A root's import closure is inconsistent. Reason its closure file-by-file,
|
|
136
|
+
* leaf-to-root, to find the first file whose own closure is inconsistent —
|
|
137
|
+
* the precise point where the inconsistency is introduced.
|
|
138
|
+
*/
|
|
139
|
+
async function localizeInconsistency(
|
|
140
|
+
root: ExportFile,
|
|
141
|
+
byPath: Map<string, ExportFile>,
|
|
142
|
+
workDir: string,
|
|
143
|
+
opts: ReasonOptions,
|
|
144
|
+
): Promise<{ file: ExportFile; result: Record<string, unknown> } | undefined> {
|
|
145
|
+
const closure = sortFilesLeafToRoot(closureFiles(root.path, byPath));
|
|
146
|
+
for (const file of closure) {
|
|
147
|
+
const result = await checkConsistency({
|
|
148
|
+
input: path.join(workDir, file.path),
|
|
149
|
+
inputRoot: workDir,
|
|
150
|
+
explanations: opts.explanation === true,
|
|
151
|
+
uniqueNamesAssumption: opts.uniqueNamesAssumption !== false,
|
|
152
|
+
checkOnly: true,
|
|
153
|
+
profile: false,
|
|
154
|
+
timeout: resolveTimeout(opts),
|
|
155
|
+
});
|
|
156
|
+
if (!result.consistent) {
|
|
157
|
+
return { file, result: result as unknown as Record<string, unknown> };
|
|
158
|
+
}
|
|
170
159
|
}
|
|
171
|
-
return
|
|
160
|
+
return undefined;
|
|
172
161
|
}
|
|
173
162
|
|
|
174
163
|
export const reasonAction = async (opts: ReasonOptions): Promise<void> => {
|
|
175
164
|
const startedAt = Date.now();
|
|
176
|
-
const
|
|
165
|
+
const persistDir = typeof opts.owl === 'string' && opts.owl.trim().length > 0
|
|
166
|
+
? path.resolve(opts.owl.trim())
|
|
167
|
+
: undefined;
|
|
168
|
+
const format = opts.format ?? 'ttl';
|
|
177
169
|
const pretty = opts.pretty === true;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
170
|
+
// Always reason inside a private temp dir and only promote it to the user's
|
|
171
|
+
// output directory on full success, so a failed/inconsistent run never leaves
|
|
172
|
+
// partial or internally-inconsistent artifacts behind in persistDir.
|
|
173
|
+
const workDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oml-reason-'));
|
|
174
|
+
const exported = await restPost<ExportPayload>(
|
|
175
|
+
'/v0/export',
|
|
176
|
+
{ owl: workDir, format, pretty } as Record<string, unknown>,
|
|
181
177
|
opts.authToken,
|
|
182
178
|
);
|
|
183
|
-
if (!
|
|
179
|
+
if (!exported.success) {
|
|
180
|
+
await fs.rm(workDir, { recursive: true, force: true });
|
|
184
181
|
printLintDiagnostics({
|
|
185
182
|
success: false,
|
|
186
|
-
filesChecked: Number(
|
|
187
|
-
errors: Number(
|
|
188
|
-
warnings: Number(
|
|
189
|
-
problems:
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
failCli(chalk.red(summary));
|
|
183
|
+
filesChecked: Number(exported.filesChecked ?? 0),
|
|
184
|
+
errors: Number(exported.errors ?? 0),
|
|
185
|
+
warnings: Number(exported.warnings ?? 0),
|
|
186
|
+
problems: exported.problems,
|
|
187
|
+
});
|
|
188
|
+
failCli(chalk.red(exported.error?.trim() || 'reason failed.'));
|
|
193
189
|
}
|
|
194
190
|
printLintDiagnostics({
|
|
195
191
|
success: true,
|
|
196
|
-
filesChecked: Number(
|
|
197
|
-
errors: Number(
|
|
198
|
-
warnings: Number(
|
|
199
|
-
problems:
|
|
200
|
-
}
|
|
201
|
-
if (
|
|
192
|
+
filesChecked: Number(exported.filesChecked ?? 0),
|
|
193
|
+
errors: Number(exported.errors ?? 0),
|
|
194
|
+
warnings: Number(exported.warnings ?? 0),
|
|
195
|
+
problems: exported.problems,
|
|
196
|
+
});
|
|
197
|
+
if (exported.files.length === 0) {
|
|
198
|
+
await fs.rm(workDir, { recursive: true, force: true });
|
|
202
199
|
console.log(chalk.yellow(`No .oml files found under ${path.resolve('.')}.`));
|
|
203
200
|
return;
|
|
204
201
|
}
|
|
205
202
|
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
let firstInconsistent: { file:
|
|
215
|
-
let firstFailure: {
|
|
203
|
+
const byPath = new Map<string, ExportFile>(exported.files.map(f => [f.path, f]));
|
|
204
|
+
// Check-only runs reason just the import-graph roots (consistency is
|
|
205
|
+
// monotonic, so the roots' closures prove every file consistent). Persisting
|
|
206
|
+
// entailments needs per-file output, so it still reasons every file.
|
|
207
|
+
const checkOnly = persistDir === undefined;
|
|
208
|
+
const targets = checkOnly
|
|
209
|
+
? computeConsistencyRoots(exported.files)
|
|
210
|
+
: sortFilesLeafToRoot(exported.files);
|
|
211
|
+
let firstInconsistent: { file: ExportFile; result: Record<string, unknown> } | undefined;
|
|
212
|
+
let firstFailure: { path: string; error: string } | undefined;
|
|
216
213
|
let entailmentsProduced = 0;
|
|
217
214
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
215
|
+
// Reason all targets in a single reasoner process: one JVM and one shared
|
|
216
|
+
// parse cache for the whole batch, instead of spawning a process and
|
|
217
|
+
// re-parsing every import closure per file.
|
|
218
|
+
const batchResults = await checkConsistencyBatch({
|
|
219
|
+
inputs: targets.map(f => path.join(workDir, f.path)),
|
|
220
|
+
inputRoot: workDir,
|
|
221
|
+
// Skip tracing on the batch scan; explanations are produced during
|
|
222
|
+
// localization on the culprit file instead.
|
|
223
|
+
explanations: false,
|
|
224
|
+
uniqueNamesAssumption: opts.uniqueNamesAssumption !== false,
|
|
225
|
+
checkOnly,
|
|
226
|
+
// Hoist shared entailments when persisting; targets are already
|
|
227
|
+
// ordered leaf-to-root, which hoisting requires. Ignored for
|
|
228
|
+
// check-only runs (no entailments are materialized).
|
|
229
|
+
hoist: opts.hoist === true && !checkOnly,
|
|
230
|
+
profile: opts.profile === true,
|
|
231
|
+
timeout: Math.max(opts.timeout ?? 0, 1_800_000),
|
|
232
|
+
});
|
|
233
|
+
const resultByInput = new Map(batchResults.map(r => [r.input, r]));
|
|
234
|
+
|
|
235
|
+
// Walk targets in leaf-to-root order so the first problem reported is the
|
|
236
|
+
// earliest (most-imported) point of failure or inconsistency.
|
|
237
|
+
for (const file of targets) {
|
|
238
|
+
const input = path.join(workDir, file.path);
|
|
239
|
+
const result = resultByInput.get(input);
|
|
240
|
+
if (!result) {
|
|
241
|
+
firstFailure = { path: file.path, error: 'Reasoner produced no result for this file.' };
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
if (typeof result.error === 'string' && result.error.length > 0) {
|
|
245
|
+
firstFailure = { path: file.path, error: result.error };
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
if (!result.consistent) {
|
|
249
|
+
firstInconsistent = checkOnly
|
|
250
|
+
? (await localizeInconsistency(file, byPath, workDir, opts))
|
|
251
|
+
?? { file, result: result as unknown as Record<string, unknown> }
|
|
252
|
+
: { file, result: result as unknown as Record<string, unknown> };
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (persistDir) {
|
|
256
|
+
if (typeof result.entailmentsUri !== 'string' || result.entailmentsUri.trim().length === 0) {
|
|
257
|
+
firstFailure = { path: file.path, error: 'Reasoner returned a consistent result but did not provide entailments URI.' };
|
|
242
258
|
break;
|
|
243
259
|
}
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (pretty) {
|
|
249
|
-
const entailmentsPath = resolveEntailmentsPath(result.entailmentsUri);
|
|
250
|
-
const entailmentsContent = await fs.readFile(entailmentsPath, 'utf-8');
|
|
251
|
-
await fs.writeFile(entailmentsPath, prettyPrintRdf(entailmentsContent, outputFormat), 'utf-8');
|
|
252
|
-
}
|
|
253
|
-
entailmentsProduced += 1;
|
|
260
|
+
if (pretty) {
|
|
261
|
+
const entailmentsPath = resolveEntailmentsPath(result.entailmentsUri);
|
|
262
|
+
const entailmentsContent = await fs.readFile(entailmentsPath, 'utf-8');
|
|
263
|
+
await fs.writeFile(entailmentsPath, prettyPrintRdf(entailmentsContent, format), 'utf-8');
|
|
254
264
|
}
|
|
255
|
-
|
|
256
|
-
firstFailure = {
|
|
257
|
-
modelUri: file.modelUri,
|
|
258
|
-
error: error instanceof Error ? error.message : String(error),
|
|
259
|
-
};
|
|
260
|
-
break;
|
|
265
|
+
entailmentsProduced += 1;
|
|
261
266
|
}
|
|
262
267
|
}
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
await fs.
|
|
266
|
-
await fs.
|
|
268
|
+
// Promote temp output to the user's directory only on a fully successful run.
|
|
269
|
+
if (persistDir && !firstFailure && !firstInconsistent) {
|
|
270
|
+
await fs.rm(persistDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
271
|
+
await fs.mkdir(persistDir, { recursive: true });
|
|
272
|
+
await fs.cp(workDir, persistDir, { recursive: true, force: true });
|
|
267
273
|
}
|
|
268
|
-
|
|
274
|
+
} catch (error) {
|
|
275
|
+
firstFailure = firstFailure ?? {
|
|
276
|
+
path: '(batch)',
|
|
277
|
+
error: error instanceof Error ? error.message : String(error),
|
|
278
|
+
};
|
|
279
|
+
} finally {
|
|
280
|
+
await fs.rm(workDir, { recursive: true, force: true });
|
|
269
281
|
}
|
|
270
282
|
|
|
271
283
|
if (firstFailure) {
|
|
272
|
-
|
|
273
|
-
failCli(chalk.red(`reason failed: ${modelUri}: ${firstFailure.error}`));
|
|
284
|
+
failCli(chalk.red(`reason failed: ${firstFailure.path}: ${firstFailure.error}`));
|
|
274
285
|
}
|
|
275
286
|
if (firstInconsistent) {
|
|
276
|
-
const where =
|
|
277
|
-
|| firstInconsistent.file.ontologyIri.trim()
|
|
278
|
-
|| '<unknown>';
|
|
287
|
+
const where = firstInconsistent.file.path;
|
|
279
288
|
if (opts.explanation === true) {
|
|
280
289
|
failCli(chalk.red(`Inconsistency found in: ${where}\n${JSON.stringify(firstInconsistent.result, null, 2)}`));
|
|
281
290
|
}
|
|
282
291
|
failCli(chalk.red(`Inconsistency found in: ${where}`));
|
|
283
292
|
}
|
|
284
293
|
|
|
285
|
-
if (
|
|
294
|
+
if (persistDir) {
|
|
286
295
|
console.log(chalk.green(
|
|
287
|
-
`reason: ${
|
|
296
|
+
`reason: ${exported.files.length} ontology file(s) reasoned in ${path.relative(process.cwd(), persistDir) || persistDir} (${entailmentsProduced} entailment artifact file(s) written) [${formatDuration(Date.now() - startedAt)}]`
|
|
288
297
|
));
|
|
289
298
|
return;
|
|
290
299
|
}
|
|
291
|
-
console.log(chalk.green(`reason: ${
|
|
300
|
+
console.log(chalk.green(`reason: ${exported.files.length} ontology file(s) checked via ${targets.length} import root(s) [${formatDuration(Date.now() - startedAt)}]`));
|
|
292
301
|
};
|