@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.
@@ -14,12 +14,10 @@ export type ExportOptions = {
14
14
  authToken?: string;
15
15
  };
16
16
 
17
- type ExportPayload = {
17
+ type OwlPayload = {
18
18
  success: boolean;
19
- filesWritten: number;
20
19
  outputDir: string;
21
- format: 'ttl' | 'trig' | 'nt' | 'nq' | 'n3';
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<ExportPayload>(
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.filesWritten <= 0) {
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.filesWritten} OML file(s) exported in ${path.relative(process.cwd(), result.outputDir) || result.outputDir} [${formatDuration(Date.now() - startedAt)}]`
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
  };
@@ -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 LintPayload, type LintProblem } from './lint.js';
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?: RdfFormat | string;
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 AssertionsPayload = {
26
+ type ExportFile = {
27
+ path: string;
28
+ imports: string[];
29
+ };
30
+
31
+ type ExportPayload = {
28
32
  success: boolean;
29
33
  error?: string;
30
- format?: RdfFormat;
34
+ outputDir: string;
31
35
  filesChecked?: number;
32
36
  errors?: number;
33
37
  warnings?: number;
34
38
  problems?: LintProblem[];
35
- files: Array<{
36
- modelUri: string;
37
- ontologyIri: string;
38
- content: string;
39
- }>;
39
+ files: ExportFile[];
40
40
  };
41
41
 
42
- const OWL_IMPORTS_IRI = '<http://www.w3.org/2002/07/owl#imports>';
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
- const line = lines[i];
60
- out.push(line);
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 extractImports(serialized: string): string[] {
72
- const imports = new Set<string>();
73
- const statementPattern = /(?:<http:\/\/www\.w3\.org\/2002\/07\/owl#imports>|owl:imports)\s+([^;.]*)[;.]/gms;
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: AssertionsPayload['files'],
102
- ): AssertionsPayload['files'] {
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 sortedRoots = [...filesByOntologyIri.keys()].sort((a, b) => a.localeCompare(b));
116
- const visit = (ontologyIri: string): void => {
117
- const current = state.get(ontologyIri) ?? 0;
118
- if (current === 2) {
119
- return;
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
- for (const ontologyIri of sortedRoots) {
138
- visit(ontologyIri);
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 resolveEntailmentsPath(uri: string): string {
144
- const trimmed = uri.trim();
145
- if (trimmed.startsWith('file://')) {
146
- return fileURLToPath(trimmed);
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 trimmed;
102
+ return result;
149
103
  }
150
104
 
151
- function ontologyIriToTempPath(ontologyIri: string, format: RdfFormat): string {
152
- try {
153
- const iri = new URL(ontologyIri);
154
- const rawPath = iri.pathname.replace(/\/+$/, '');
155
- const segments = rawPath.split('/').filter(Boolean);
156
- const stem = segments.at(-1) || 'index';
157
- const dirSegs = iri.host ? [iri.host, ...segments.slice(0, -1)] : segments.slice(0, -1);
158
- return dirSegs.length > 0 ? path.join(...dirSegs, `${stem}.${format}`) : `${stem}.${format}`;
159
- } catch {
160
- return ontologyIri.replace(/[^a-zA-Z0-9_/-]/g, '_') + '.' + format;
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
- function modelUriToRelativePath(modelUri: string | undefined): string {
165
- const trimmed = (modelUri ?? '').trim();
166
- if (trimmed.startsWith('file://')) {
167
- const absolute = fileURLToPath(trimmed);
168
- const relative = path.relative(process.cwd(), absolute);
169
- return relative.length > 0 ? relative : path.basename(absolute);
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 trimmed;
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 outputFormat = normalizeRdfFormat(opts.format);
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
- const assertions = await restPost<AssertionsPayload>(
179
- '/v0/assertions',
180
- { format: outputFormat, pretty } as Record<string, unknown>,
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 (!assertions.success) {
179
+ if (!exported.success) {
180
+ await fs.rm(workDir, { recursive: true, force: true });
184
181
  printLintDiagnostics({
185
182
  success: false,
186
- filesChecked: Number(assertions.filesChecked ?? 0),
187
- errors: Number(assertions.errors ?? 0),
188
- warnings: Number(assertions.warnings ?? 0),
189
- problems: assertions.problems,
190
- } satisfies LintPayload);
191
- const summary = assertions.error?.trim() || 'reason failed.';
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(assertions.filesChecked ?? 0),
197
- errors: Number(assertions.errors ?? 0),
198
- warnings: Number(assertions.warnings ?? 0),
199
- problems: assertions.problems,
200
- } satisfies LintPayload);
201
- if (assertions.files.length === 0) {
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 outputDir = typeof opts.owl === 'string' && opts.owl.trim().length > 0
207
- ? path.resolve(opts.owl.trim())
208
- : undefined;
209
- const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'oml-reason-cli-'));
210
- if (outputDir) {
211
- await fs.rm(outputDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
212
- }
213
- const orderedFiles = sortFilesLeafToRoot(assertions.files);
214
- let firstInconsistent: { file: AssertionsPayload['files'][number]; result: Record<string, unknown> } | undefined;
215
- let firstFailure: { modelUri: string; error: string } | undefined;
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
- for (const file of orderedFiles) {
219
- const target = path.join(tempRoot, ontologyIriToTempPath(file.ontologyIri, outputFormat));
220
- await fs.mkdir(path.dirname(target), { recursive: true });
221
- await fs.writeFile(target, prettyPrintRdf(file.content, outputFormat), 'utf-8');
222
- }
223
- for (const file of orderedFiles) {
224
- const target = path.join(tempRoot, ontologyIriToTempPath(file.ontologyIri, outputFormat));
225
- try {
226
- const result = await checkConsistency({
227
- input: target,
228
- inputRoot: tempRoot,
229
- explanations: opts.explanation === true,
230
- uniqueNamesAssumption: opts.uniqueNamesAssumption !== false,
231
- checkOnly: outputDir === undefined,
232
- profile: opts.profile === true,
233
- timeout: typeof opts.timeout === 'number' && Number.isFinite(opts.timeout) && opts.timeout > 0
234
- ? Math.floor(opts.timeout)
235
- : 120_000,
236
- });
237
- if (!result.consistent) {
238
- firstInconsistent = {
239
- file,
240
- result: result as unknown as Record<string, unknown>,
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 (outputDir) {
245
- if (typeof result.entailmentsUri !== 'string' || result.entailmentsUri.trim().length === 0) {
246
- throw new Error('Reasoner returned a consistent result but did not provide entailments URI.');
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
- } catch (error) {
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
- } finally {
264
- if (outputDir && !firstFailure && !firstInconsistent) {
265
- await fs.mkdir(outputDir, { recursive: true });
266
- await fs.cp(tempRoot, outputDir, { recursive: true, force: true });
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
- await fs.rm(tempRoot, { recursive: true, force: true });
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
- const modelUri = String(firstFailure.modelUri ?? '').trim() || '<workspace>';
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 = modelUriToRelativePath(firstInconsistent.file.modelUri)
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 (outputDir) {
294
+ if (persistDir) {
286
295
  console.log(chalk.green(
287
- `reason: ${assertions.files.length} ontology file(s) reasoned in ${path.relative(process.cwd(), outputDir) || outputDir} (${entailmentsProduced} entailment artifact file(s) written) [${formatDuration(Date.now() - startedAt)}]`
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: ${assertions.files.length} ontology file(s) checked [${formatDuration(Date.now() - startedAt)}]`));
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
  };