@ontrails/trails 1.0.0-beta.15 → 1.0.0-beta.16
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/CHANGELOG.md +197 -2
- package/README.md +27 -0
- package/package.json +19 -8
- package/src/app.ts +15 -5
- package/src/cli.ts +303 -10
- package/src/completions.ts +240 -0
- package/src/load-app-mirror.ts +160 -0
- package/src/local-state-io.ts +153 -0
- package/src/project-writes.ts +320 -0
- package/src/run-collision.ts +125 -0
- package/src/run-completions-install.ts +179 -0
- package/src/run-example.ts +149 -0
- package/src/run-examples.ts +148 -0
- package/src/run-quiet.ts +75 -0
- package/src/run-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch.ts +432 -0
- package/src/scaffold-versions.generated.ts +12 -0
- package/src/trails/add-surface.ts +45 -23
- package/src/trails/add-trail.ts +27 -17
- package/src/trails/add-verify.ts +57 -17
- package/src/trails/completions-complete.ts +165 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/create-scaffold.ts +86 -33
- package/src/trails/create.ts +11 -3
- package/src/trails/dev-clean.ts +6 -1
- package/src/trails/dev-reset.ts +6 -1
- package/src/trails/dev-stats.ts +6 -1
- package/src/trails/dev-support.ts +29 -17
- package/src/trails/draft-promote.ts +289 -80
- package/src/trails/guide.ts +54 -34
- package/src/trails/load-app.ts +251 -56
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +482 -0
- package/src/trails/run-examples.ts +141 -0
- package/src/trails/run.ts +403 -0
- package/src/trails/survey.ts +506 -200
- package/src/trails/topo-activation.ts +385 -0
- package/src/trails/topo-compile.ts +55 -0
- package/src/trails/topo-history.ts +6 -1
- package/src/trails/topo-output-schemas.ts +175 -0
- package/src/trails/topo-pin.ts +19 -6
- package/src/trails/topo-read-support.ts +171 -228
- package/src/trails/topo-reports.ts +400 -25
- package/src/trails/topo-store-support.ts +43 -19
- package/src/trails/topo-support.ts +18 -28
- package/src/trails/topo-unpin.ts +6 -1
- package/src/trails/topo-verify.ts +18 -5
- package/src/trails/topo.ts +60 -23
- package/src/trails/warden-guide.ts +121 -0
- package/src/trails/warden.ts +137 -56
- package/src/versions.ts +3 -18
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/__tests__/examples.test.ts +0 -45
- package/dist/bin/trails.d.ts +0 -3
- package/dist/bin/trails.d.ts.map +0 -1
- package/dist/bin/trails.js +0 -4
- package/dist/bin/trails.js.map +0 -1
- package/dist/src/app.d.ts +0 -2
- package/dist/src/app.d.ts.map +0 -1
- package/dist/src/app.js +0 -22
- package/dist/src/app.js.map +0 -1
- package/dist/src/clack.d.ts +0 -9
- package/dist/src/clack.d.ts.map +0 -1
- package/dist/src/clack.js +0 -84
- package/dist/src/clack.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -14
- package/dist/src/cli.js.map +0 -1
- package/dist/src/trails/add-surface.d.ts +0 -13
- package/dist/src/trails/add-surface.d.ts.map +0 -1
- package/dist/src/trails/add-surface.js +0 -110
- package/dist/src/trails/add-surface.js.map +0 -1
- package/dist/src/trails/add-trail.d.ts +0 -12
- package/dist/src/trails/add-trail.d.ts.map +0 -1
- package/dist/src/trails/add-trail.js +0 -104
- package/dist/src/trails/add-trail.js.map +0 -1
- package/dist/src/trails/add-trailhead.d.ts +0 -13
- package/dist/src/trails/add-trailhead.d.ts.map +0 -1
- package/dist/src/trails/add-trailhead.js +0 -88
- package/dist/src/trails/add-trailhead.js.map +0 -1
- package/dist/src/trails/add-verify.d.ts +0 -10
- package/dist/src/trails/add-verify.d.ts.map +0 -1
- package/dist/src/trails/add-verify.js +0 -68
- package/dist/src/trails/add-verify.js.map +0 -1
- package/dist/src/trails/create-scaffold.d.ts +0 -15
- package/dist/src/trails/create-scaffold.d.ts.map +0 -1
- package/dist/src/trails/create-scaffold.js +0 -295
- package/dist/src/trails/create-scaffold.js.map +0 -1
- package/dist/src/trails/create.d.ts +0 -18
- package/dist/src/trails/create.d.ts.map +0 -1
- package/dist/src/trails/create.js +0 -126
- package/dist/src/trails/create.js.map +0 -1
- package/dist/src/trails/dev-clean.d.ts +0 -9
- package/dist/src/trails/dev-clean.d.ts.map +0 -1
- package/dist/src/trails/dev-clean.js +0 -66
- package/dist/src/trails/dev-clean.js.map +0 -1
- package/dist/src/trails/dev-reset.d.ts +0 -6
- package/dist/src/trails/dev-reset.d.ts.map +0 -1
- package/dist/src/trails/dev-reset.js +0 -39
- package/dist/src/trails/dev-reset.js.map +0 -1
- package/dist/src/trails/dev-stats.d.ts +0 -7
- package/dist/src/trails/dev-stats.d.ts.map +0 -1
- package/dist/src/trails/dev-stats.js +0 -61
- package/dist/src/trails/dev-stats.js.map +0 -1
- package/dist/src/trails/dev-support.d.ts +0 -64
- package/dist/src/trails/dev-support.d.ts.map +0 -1
- package/dist/src/trails/dev-support.js +0 -181
- package/dist/src/trails/dev-support.js.map +0 -1
- package/dist/src/trails/draft-promote.d.ts +0 -18
- package/dist/src/trails/draft-promote.d.ts.map +0 -1
- package/dist/src/trails/draft-promote.js +0 -400
- package/dist/src/trails/draft-promote.js.map +0 -1
- package/dist/src/trails/guide.d.ts +0 -21
- package/dist/src/trails/guide.d.ts.map +0 -1
- package/dist/src/trails/guide.js +0 -61
- package/dist/src/trails/guide.js.map +0 -1
- package/dist/src/trails/load-app.d.ts +0 -12
- package/dist/src/trails/load-app.d.ts.map +0 -1
- package/dist/src/trails/load-app.js +0 -415
- package/dist/src/trails/load-app.js.map +0 -1
- package/dist/src/trails/project.d.ts +0 -8
- package/dist/src/trails/project.d.ts.map +0 -1
- package/dist/src/trails/project.js +0 -54
- package/dist/src/trails/project.js.map +0 -1
- package/dist/src/trails/survey.d.ts +0 -18
- package/dist/src/trails/survey.d.ts.map +0 -1
- package/dist/src/trails/survey.js +0 -234
- package/dist/src/trails/survey.js.map +0 -1
- package/dist/src/trails/topo-constants.d.ts +0 -3
- package/dist/src/trails/topo-constants.d.ts.map +0 -1
- package/dist/src/trails/topo-constants.js +0 -3
- package/dist/src/trails/topo-constants.js.map +0 -1
- package/dist/src/trails/topo-export.d.ts +0 -19
- package/dist/src/trails/topo-export.d.ts.map +0 -1
- package/dist/src/trails/topo-export.js +0 -31
- package/dist/src/trails/topo-export.js.map +0 -1
- package/dist/src/trails/topo-history.d.ts +0 -20
- package/dist/src/trails/topo-history.d.ts.map +0 -1
- package/dist/src/trails/topo-history.js +0 -32
- package/dist/src/trails/topo-history.js.map +0 -1
- package/dist/src/trails/topo-pin.d.ts +0 -17
- package/dist/src/trails/topo-pin.d.ts.map +0 -1
- package/dist/src/trails/topo-pin.js +0 -31
- package/dist/src/trails/topo-pin.js.map +0 -1
- package/dist/src/trails/topo-read-support.d.ts +0 -58
- package/dist/src/trails/topo-read-support.d.ts.map +0 -1
- package/dist/src/trails/topo-read-support.js +0 -167
- package/dist/src/trails/topo-read-support.js.map +0 -1
- package/dist/src/trails/topo-reports.d.ts +0 -54
- package/dist/src/trails/topo-reports.d.ts.map +0 -1
- package/dist/src/trails/topo-reports.js +0 -128
- package/dist/src/trails/topo-reports.js.map +0 -1
- package/dist/src/trails/topo-show.d.ts +0 -23
- package/dist/src/trails/topo-show.d.ts.map +0 -1
- package/dist/src/trails/topo-show.js +0 -49
- package/dist/src/trails/topo-show.js.map +0 -1
- package/dist/src/trails/topo-store-support.d.ts +0 -13
- package/dist/src/trails/topo-store-support.d.ts.map +0 -1
- package/dist/src/trails/topo-store-support.js +0 -55
- package/dist/src/trails/topo-store-support.js.map +0 -1
- package/dist/src/trails/topo-support.d.ts +0 -76
- package/dist/src/trails/topo-support.d.ts.map +0 -1
- package/dist/src/trails/topo-support.js +0 -132
- package/dist/src/trails/topo-support.js.map +0 -1
- package/dist/src/trails/topo-unpin.d.ts +0 -20
- package/dist/src/trails/topo-unpin.d.ts.map +0 -1
- package/dist/src/trails/topo-unpin.js +0 -44
- package/dist/src/trails/topo-unpin.js.map +0 -1
- package/dist/src/trails/topo-verify.d.ts +0 -5
- package/dist/src/trails/topo-verify.d.ts.map +0 -1
- package/dist/src/trails/topo-verify.js +0 -24
- package/dist/src/trails/topo-verify.js.map +0 -1
- package/dist/src/trails/topo.d.ts +0 -5
- package/dist/src/trails/topo.d.ts.map +0 -1
- package/dist/src/trails/topo.js +0 -63
- package/dist/src/trails/topo.js.map +0 -1
- package/dist/src/trails/warden.d.ts +0 -20
- package/dist/src/trails/warden.d.ts.map +0 -1
- package/dist/src/trails/warden.js +0 -98
- package/dist/src/trails/warden.js.map +0 -1
- package/dist/src/versions.d.ts +0 -12
- package/dist/src/versions.d.ts.map +0 -1
- package/dist/src/versions.js +0 -23
- package/dist/src/versions.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/__tests__/add-trail.test.ts +0 -97
- package/src/__tests__/create.test.ts +0 -415
- package/src/__tests__/draft-promote.test.ts +0 -144
- package/src/__tests__/guide.test.ts +0 -96
- package/src/__tests__/load-app.test.ts +0 -419
- package/src/__tests__/survey.test.ts +0 -377
- package/src/__tests__/topo-dev.test.ts +0 -426
- package/src/__tests__/warden.test.ts +0 -74
- package/src/trails/topo-export.ts +0 -35
- package/src/trails/topo-show.ts +0 -54
- package/tsconfig.json +0 -9
- package/tsconfig.tests.json +0 -10
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-surface bridge for the `run.example` trail.
|
|
3
|
+
*
|
|
4
|
+
* `run.example` resolves the named example on the target trail, executes it
|
|
5
|
+
* through the full pipeline, and packages an actual-vs-expected comparison
|
|
6
|
+
* into a structured envelope on the trail's outer `Result.ok(...)`. This module
|
|
7
|
+
* owns the surface decision of how to render that envelope:
|
|
8
|
+
*
|
|
9
|
+
* - Text mode (default): a compact summary on match, an `input / expected /
|
|
10
|
+
* actual / diff` block on mismatch.
|
|
11
|
+
* - JSON / JSONL: emits the full {@link RunExampleComparison} envelope so
|
|
12
|
+
* downstream consumers can parse the comparison shape directly.
|
|
13
|
+
*
|
|
14
|
+
* Match/mismatch is a comparison outcome, not an execution error: the trail
|
|
15
|
+
* always returns `Result.ok(envelope)`. This helper maps mismatch onto a
|
|
16
|
+
* non-zero exit code by throwing a `ValidationError` (category `validation`,
|
|
17
|
+
* exit 1) so Commander's error path runs.
|
|
18
|
+
*
|
|
19
|
+
* Outer Err on the run trail (NotFound, Ambiguous, Validation) is unaffected
|
|
20
|
+
* by `run.example`: this helper defers to the default handler so existing
|
|
21
|
+
* exit-code mapping and recovery hooks stay intact.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ActionResultContext } from '@ontrails/cli';
|
|
25
|
+
import { deriveOutputMode, output } from '@ontrails/cli';
|
|
26
|
+
import { ValidationError } from '@ontrails/core';
|
|
27
|
+
|
|
28
|
+
import { runExampleComparisonSchema } from './trails/run-example.js';
|
|
29
|
+
import type { RunExampleComparison } from './trails/run-example.js';
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Detection
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const isExampleRunCtx = (ctx: ActionResultContext): boolean =>
|
|
36
|
+
ctx.trail.id === 'run.example';
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Text formatting
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
const formatJson = (value: unknown): string => {
|
|
43
|
+
try {
|
|
44
|
+
const encoded = JSON.stringify(value, null, 2);
|
|
45
|
+
return encoded === undefined ? String(value) : encoded;
|
|
46
|
+
} catch {
|
|
47
|
+
return String(value);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const formatMatchText = (envelope: RunExampleComparison): string =>
|
|
52
|
+
[
|
|
53
|
+
`OK ${envelope.trailId} :: ${envelope.exampleName}`,
|
|
54
|
+
`mode: ${envelope.mode}`,
|
|
55
|
+
'actual matches expected.',
|
|
56
|
+
].join('\n');
|
|
57
|
+
|
|
58
|
+
const formatMismatchText = (envelope: RunExampleComparison): string => {
|
|
59
|
+
const diffBlock =
|
|
60
|
+
envelope.diff !== undefined && envelope.diff.length > 0
|
|
61
|
+
? envelope.diff.map((line) => ` - ${line}`).join('\n')
|
|
62
|
+
: ' - <no diff lines>';
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
`MISMATCH ${envelope.trailId} :: ${envelope.exampleName}`,
|
|
66
|
+
`mode: ${envelope.mode}`,
|
|
67
|
+
'input:',
|
|
68
|
+
formatJson(envelope.input),
|
|
69
|
+
'expected:',
|
|
70
|
+
formatJson(envelope.expected),
|
|
71
|
+
'actual:',
|
|
72
|
+
formatJson(envelope.actual),
|
|
73
|
+
'diff:',
|
|
74
|
+
diffBlock,
|
|
75
|
+
].join('\n');
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Entry point
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Return value:
|
|
84
|
+
* - `false` — `run.example` did not apply; caller should fall through to its
|
|
85
|
+
* default handler.
|
|
86
|
+
* - `true` — `run.example` handled the result and wrote output. Caller should
|
|
87
|
+
* not invoke the default handler.
|
|
88
|
+
*
|
|
89
|
+
* Throws a {@link ValidationError} on mismatch so Commander's error path runs
|
|
90
|
+
* and the run trail surface exits with the validation category exit code.
|
|
91
|
+
*/
|
|
92
|
+
export const tryExampleRunOutput = (ctx: ActionResultContext): boolean => {
|
|
93
|
+
if (!isExampleRunCtx(ctx)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Outer Err on the run.example trail (NotFound, Ambiguous, Validation) is not
|
|
98
|
+
// in scope here: defer to the default handler so existing
|
|
99
|
+
// exit-code mapping and recovery hooks stay intact.
|
|
100
|
+
if (ctx.result.isErr()) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const envelope = runExampleComparisonSchema.safeParse(ctx.result.value);
|
|
105
|
+
if (!envelope.success) {
|
|
106
|
+
// Defensive fallback: the trail owns the output schema, so this branch is
|
|
107
|
+
// unreachable in practice. Defer to the default handler if anything else
|
|
108
|
+
// slips through.
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const comparison = envelope.data;
|
|
112
|
+
|
|
113
|
+
const { mode } = deriveOutputMode(ctx.flags, ctx.topoName);
|
|
114
|
+
|
|
115
|
+
if (mode === 'text') {
|
|
116
|
+
if (comparison.match) {
|
|
117
|
+
output(formatMatchText(comparison), mode);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
process.stderr.write(`${formatMismatchText(comparison)}\n`);
|
|
121
|
+
throw new ValidationError(
|
|
122
|
+
`Example '${comparison.exampleName}' on trail '${comparison.trailId}' did not match expected outcome.`,
|
|
123
|
+
{
|
|
124
|
+
context: {
|
|
125
|
+
exampleName: comparison.exampleName,
|
|
126
|
+
mode: comparison.mode,
|
|
127
|
+
trailId: comparison.trailId,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// JSON / JSONL: emit the full envelope so downstream consumers can parse
|
|
134
|
+
// the comparison shape directly.
|
|
135
|
+
output(comparison, mode);
|
|
136
|
+
if (!comparison.match) {
|
|
137
|
+
throw new ValidationError(
|
|
138
|
+
`Example '${comparison.exampleName}' on trail '${comparison.trailId}' did not match expected outcome.`,
|
|
139
|
+
{
|
|
140
|
+
context: {
|
|
141
|
+
exampleName: comparison.exampleName,
|
|
142
|
+
mode: comparison.mode,
|
|
143
|
+
trailId: comparison.trailId,
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-surface bridge for the `run.examples` trail.
|
|
3
|
+
*
|
|
4
|
+
* `run.examples` is a pure metadata read: the trail returns a
|
|
5
|
+
* {@link RunExamplesListing} on its outer Ok value.
|
|
6
|
+
* This module owns the surface decision of how to render that listing:
|
|
7
|
+
*
|
|
8
|
+
* - Text mode (default): a table-like list with `name`, truncated `input`,
|
|
9
|
+
* and outcome (`ok` / `error: <code>`). Empty listings print
|
|
10
|
+
* `No examples defined`.
|
|
11
|
+
* - JSON / JSONL: the structured `examples` array is emitted directly via
|
|
12
|
+
* the resolved output mode, so agents and downstream consumers can parse
|
|
13
|
+
* the full structured shape (`name`, `input`, `expected`, `expectedMatch`,
|
|
14
|
+
* `error`, `signals`, `kind`, `provenance`).
|
|
15
|
+
*
|
|
16
|
+
* Errors at the outer layer (NotFoundError, AmbiguousError, ValidationError)
|
|
17
|
+
* are unaffected by `run.examples`: we always defer to the supplied default
|
|
18
|
+
* handler so the existing exit-code mapping and recovery hooks stay intact.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ActionResultContext } from '@ontrails/cli';
|
|
22
|
+
import { deriveOutputMode, output } from '@ontrails/cli';
|
|
23
|
+
import type { StructuredTrailExample } from '@ontrails/core';
|
|
24
|
+
|
|
25
|
+
import { runExamplesListingSchema } from './trails/run-examples.js';
|
|
26
|
+
import type { RunExamplesListing } from './trails/run-examples.js';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Detection
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const isExamplesRunCtx = (ctx: ActionResultContext): boolean =>
|
|
33
|
+
ctx.trail.id === 'run.examples';
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Text formatting
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
const INPUT_PREVIEW_LIMIT = 60;
|
|
40
|
+
|
|
41
|
+
const truncate = (value: string, limit: number): string =>
|
|
42
|
+
value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
|
|
43
|
+
|
|
44
|
+
const formatInputPreview = (input: unknown): string => {
|
|
45
|
+
if (input === undefined) {
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
let encoded: string;
|
|
49
|
+
try {
|
|
50
|
+
encoded = JSON.stringify(input) ?? '';
|
|
51
|
+
} catch {
|
|
52
|
+
encoded = String(input);
|
|
53
|
+
}
|
|
54
|
+
return truncate(encoded, INPUT_PREVIEW_LIMIT);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const formatOutcome = (example: StructuredTrailExample): string => {
|
|
58
|
+
if (example.kind === 'error') {
|
|
59
|
+
const code = example.error;
|
|
60
|
+
return code === undefined || code.length === 0 ? 'error' : `error: ${code}`;
|
|
61
|
+
}
|
|
62
|
+
return 'ok';
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
interface ExampleRow {
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly input: string;
|
|
68
|
+
readonly outcome: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const buildRows = (
|
|
72
|
+
examples: readonly StructuredTrailExample[]
|
|
73
|
+
): readonly ExampleRow[] =>
|
|
74
|
+
examples.map((example) => ({
|
|
75
|
+
input: formatInputPreview(example.input),
|
|
76
|
+
name: example.name,
|
|
77
|
+
outcome: formatOutcome(example),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
const padRight = (value: string, width: number): string =>
|
|
81
|
+
value.length >= width ? value : value + ' '.repeat(width - value.length);
|
|
82
|
+
|
|
83
|
+
const formatTable = (rows: readonly ExampleRow[]): string => {
|
|
84
|
+
const headers = { input: 'INPUT', name: 'NAME', outcome: 'OUTCOME' };
|
|
85
|
+
const allRows: readonly ExampleRow[] = [headers, ...rows];
|
|
86
|
+
|
|
87
|
+
const widths = {
|
|
88
|
+
input: Math.max(...allRows.map((row) => row.input.length)),
|
|
89
|
+
name: Math.max(...allRows.map((row) => row.name.length)),
|
|
90
|
+
outcome: Math.max(...allRows.map((row) => row.outcome.length)),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const formatRow = (row: ExampleRow): string =>
|
|
94
|
+
`${padRight(row.name, widths.name)} ${padRight(row.input, widths.input)} ${row.outcome}`;
|
|
95
|
+
|
|
96
|
+
return allRows.map(formatRow).join('\n');
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const formatTextListing = (listing: RunExamplesListing): string => {
|
|
100
|
+
if (listing.examples.length === 0) {
|
|
101
|
+
return 'No examples defined';
|
|
102
|
+
}
|
|
103
|
+
return formatTable(buildRows(listing.examples));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Entry point
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Return value:
|
|
112
|
+
* - `false` — `run.examples` did not apply; caller should fall through to its
|
|
113
|
+
* default handler.
|
|
114
|
+
* - `true` — `run.examples` handled the result and wrote output. Caller should
|
|
115
|
+
* not invoke the default handler.
|
|
116
|
+
*/
|
|
117
|
+
export const tryExamplesRunOutput = (ctx: ActionResultContext): boolean => {
|
|
118
|
+
if (!isExamplesRunCtx(ctx)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Outer Err on `run.examples` (NotFound, Ambiguous, Validation) is not in
|
|
123
|
+
// scope for this renderer: defer to the default handler so existing
|
|
124
|
+
// exit-code mapping and recovery hooks stay intact.
|
|
125
|
+
if (ctx.result.isErr()) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const listing = runExamplesListingSchema.safeParse(ctx.result.value);
|
|
130
|
+
if (!listing.success) {
|
|
131
|
+
// Defensive fallback: the trail owns the output schema, so this branch is
|
|
132
|
+
// unreachable in practice. Defer to the default handler if anything else
|
|
133
|
+
// slips through.
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { mode } = deriveOutputMode(ctx.flags, ctx.topoName);
|
|
138
|
+
|
|
139
|
+
if (mode === 'text') {
|
|
140
|
+
output(formatTextListing(listing.data), mode);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// JSON / JSONL: emit the structured examples array directly so downstream
|
|
145
|
+
// consumers can parse the full structured shape per example.
|
|
146
|
+
output(listing.data.examples, mode);
|
|
147
|
+
return true;
|
|
148
|
+
};
|
package/src/run-quiet.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-surface bridge for the `run` trail's `--quiet` flag.
|
|
3
|
+
*
|
|
4
|
+
* The `run` trail returns an `inner-trail-result` envelope as the `value` of
|
|
5
|
+
* its own outer `Result.ok(...)`. The default CLI on-result handler unwraps
|
|
6
|
+
* once, leaving stdout as `{ "kind": "inner-trail-result", "trailId": "...",
|
|
7
|
+
* "value": ... }` — useful when a caller wants provenance, but noisy when the
|
|
8
|
+
* caller only wants the inner value to feed downstream pipes.
|
|
9
|
+
*
|
|
10
|
+
* `--quiet` strips that envelope:
|
|
11
|
+
*
|
|
12
|
+
* - Inner result envelope → write the inner value to stdout via the resolved
|
|
13
|
+
* output mode.
|
|
14
|
+
*
|
|
15
|
+
* Errors at the outer layer (the run trail itself failing — `NotFoundError`,
|
|
16
|
+
* `AmbiguousError`, `ValidationError` from input) are unaffected by `--quiet`:
|
|
17
|
+
* we always defer to the supplied default handler so collision-recovery and
|
|
18
|
+
* the existing error surface stay intact.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ActionResultContext } from '@ontrails/cli';
|
|
22
|
+
import { deriveOutputMode, output } from '@ontrails/cli';
|
|
23
|
+
|
|
24
|
+
import { INNER_TRAIL_RESULT_KIND } from './trails/run.js';
|
|
25
|
+
|
|
26
|
+
interface InnerTrailResultEnvelope {
|
|
27
|
+
readonly kind: typeof INNER_TRAIL_RESULT_KIND;
|
|
28
|
+
readonly trailId: string;
|
|
29
|
+
readonly value: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isInnerTrailResultEnvelope = (
|
|
33
|
+
value: unknown
|
|
34
|
+
): value is InnerTrailResultEnvelope =>
|
|
35
|
+
typeof value === 'object' &&
|
|
36
|
+
value !== null &&
|
|
37
|
+
(value as { readonly kind?: unknown }).kind === INNER_TRAIL_RESULT_KIND &&
|
|
38
|
+
typeof (value as { readonly trailId?: unknown }).trailId === 'string' &&
|
|
39
|
+
'value' in value;
|
|
40
|
+
|
|
41
|
+
const isQuietRunCtx = (ctx: ActionResultContext): boolean =>
|
|
42
|
+
ctx.trail.id === 'run' && ctx.flags['quiet'] === true;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Return value:
|
|
46
|
+
* - `false` — `--quiet` did not apply; caller should fall through to its
|
|
47
|
+
* default handler.
|
|
48
|
+
* - `true` — `--quiet` handled the result and wrote output. Caller should not
|
|
49
|
+
* invoke the default handler.
|
|
50
|
+
*
|
|
51
|
+
*/
|
|
52
|
+
export const tryQuietRunOutput = async (
|
|
53
|
+
ctx: ActionResultContext
|
|
54
|
+
): Promise<boolean> => {
|
|
55
|
+
if (!isQuietRunCtx(ctx)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Outer Err on the run trail (collision, not-found, validation) is not in
|
|
60
|
+
// scope for --quiet: defer to the default handler so existing exit-code
|
|
61
|
+
// mapping and recovery hooks stay intact.
|
|
62
|
+
if (ctx.result.isErr()) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const inner: unknown = ctx.result.value;
|
|
67
|
+
if (!isInnerTrailResultEnvelope(inner)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { mode } = deriveOutputMode(ctx.flags, ctx.topoName);
|
|
72
|
+
const value = inner.value === undefined ? null : inner.value;
|
|
73
|
+
output(value, mode);
|
|
74
|
+
return true;
|
|
75
|
+
};
|
package/src/run-trace.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-surface bridge for the `--trace` flag.
|
|
3
|
+
*
|
|
4
|
+
* `--trace` installs a per-invocation in-memory {@link TraceSink} so the
|
|
5
|
+
* intrinsic tracing pipeline in `@ontrails/core` records every trail-,
|
|
6
|
+
* span-, signal-, and activation-level event during the invocation. After
|
|
7
|
+
* the trail completes (success or failure), the records are rendered as a
|
|
8
|
+
* tree to stderr via `renderTraceTree` from `@ontrails/observe`. Under
|
|
9
|
+
* `--json`, the structured `TraceRecord[]` is also emitted on stdout as
|
|
10
|
+
* the `tracing` field of a Result envelope.
|
|
11
|
+
*
|
|
12
|
+
* Design notes:
|
|
13
|
+
*
|
|
14
|
+
* - The sink is **per-invocation**, not module-global. Each call to
|
|
15
|
+
* {@link installTraceSink} replaces the registry entry with a fresh
|
|
16
|
+
* `MemoryTraceSink` and returns a handle whose {@link TraceSession.finalize}
|
|
17
|
+
* restores the previous sink and returns the captured records.
|
|
18
|
+
* - Tracing output is split across streams: the tree always goes to stderr,
|
|
19
|
+
* structured records only enter stdout when both `--trace` and `--json`
|
|
20
|
+
* are set. Under `--quiet`, the inner trail value remains the only stdout
|
|
21
|
+
* payload (no envelope) per ADR-0044's pipe-friendly contract.
|
|
22
|
+
* - `--trace` on `run.examples` is a metadata read; the run trail short-circuits
|
|
23
|
+
* before any execution, so no trace tree is rendered.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { getTraceSink, registerTraceSink } from '@ontrails/core';
|
|
27
|
+
import type { TraceRecord, TraceSink } from '@ontrails/core';
|
|
28
|
+
import { createMemorySink, renderTraceTree } from '@ontrails/observe';
|
|
29
|
+
import type { MemoryTraceSink } from '@ontrails/observe';
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Argv detection
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Detect whether `--trace` appears in argv.
|
|
37
|
+
*
|
|
38
|
+
* Pre-parsed argv detection lets the CLI install the sink before
|
|
39
|
+
* `surface()` parses argv. The flag is also wired through the build
|
|
40
|
+
* pipeline as a meta flag, so trail input is unaffected.
|
|
41
|
+
*/
|
|
42
|
+
export const argvHasTraceFlag = (argv: readonly string[]): boolean =>
|
|
43
|
+
argv.includes('--trace');
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Sink session
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/** Handle returned by {@link installTraceSink}. */
|
|
50
|
+
export interface TraceSession {
|
|
51
|
+
/** The fresh in-memory sink that received records during this invocation. */
|
|
52
|
+
readonly sink: MemoryTraceSink;
|
|
53
|
+
/**
|
|
54
|
+
* Restore the previous trace sink and return a stable snapshot of the
|
|
55
|
+
* records collected during this session. Safe to call once; subsequent
|
|
56
|
+
* calls return an empty array.
|
|
57
|
+
*/
|
|
58
|
+
readonly finalize: () => readonly TraceRecord[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const restoreSink = (previous: TraceSink): void => {
|
|
62
|
+
// `registerTraceSink(undefined)` collapses back to `NOOP_SINK`, which is
|
|
63
|
+
// what we want when the prior sink was the default no-op singleton. For
|
|
64
|
+
// any other prior sink (e.g. an OTel adapter wired by the host), restore
|
|
65
|
+
// it directly so we do not accidentally drop the host's configured sink.
|
|
66
|
+
registerTraceSink(previous);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Register a fresh {@link MemoryTraceSink} as the active trace sink and
|
|
71
|
+
* return a handle that can later restore the previous sink.
|
|
72
|
+
*/
|
|
73
|
+
export const installTraceSink = (): TraceSession => {
|
|
74
|
+
const previous = getTraceSink();
|
|
75
|
+
const sink = createMemorySink();
|
|
76
|
+
registerTraceSink(sink);
|
|
77
|
+
|
|
78
|
+
let finalized = false;
|
|
79
|
+
return {
|
|
80
|
+
finalize: () => {
|
|
81
|
+
if (finalized) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
finalized = true;
|
|
85
|
+
const records = sink.records();
|
|
86
|
+
restoreSink(previous);
|
|
87
|
+
return records;
|
|
88
|
+
},
|
|
89
|
+
sink,
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Stderr rendering
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Render the captured records as a tree to stderr, followed by a newline.
|
|
99
|
+
*
|
|
100
|
+
* No-ops on an empty record list so that quiet trails (e.g. metadata reads
|
|
101
|
+
* that never invoke `executeTrail`) do not produce a stray blank line.
|
|
102
|
+
*/
|
|
103
|
+
export const writeTraceTreeToStderr = (
|
|
104
|
+
records: readonly TraceRecord[]
|
|
105
|
+
): void => {
|
|
106
|
+
if (records.length === 0) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const tree = renderTraceTree(records);
|
|
110
|
+
if (tree.length === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
process.stderr.write(`${tree}\n`);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// JSON envelope shaping
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Result-style envelope emitted on stdout under `--trace --json`.
|
|
122
|
+
*
|
|
123
|
+
* Mirrors the shape of `Result<T, E>` but adds a `tracing` field so a
|
|
124
|
+
* downstream consumer can deserialize the run outcome and the structured
|
|
125
|
+
* trace from a single document.
|
|
126
|
+
*/
|
|
127
|
+
export type TraceJsonEnvelope =
|
|
128
|
+
| {
|
|
129
|
+
readonly ok: true;
|
|
130
|
+
readonly value: unknown;
|
|
131
|
+
readonly tracing: readonly TraceRecord[];
|
|
132
|
+
}
|
|
133
|
+
| {
|
|
134
|
+
readonly ok: false;
|
|
135
|
+
readonly error: { readonly message: string; readonly name: string };
|
|
136
|
+
readonly tracing: readonly TraceRecord[];
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Minimal Result-shape contract used by the JSON envelope builder.
|
|
141
|
+
*
|
|
142
|
+
* The `value` and `error` properties may be present at the same time on a
|
|
143
|
+
* Result instance (the discriminated union narrows by `isOk` / `isErr`),
|
|
144
|
+
* so this interface keeps both optional and lets the builder branch without
|
|
145
|
+
* asserting either side.
|
|
146
|
+
*/
|
|
147
|
+
interface ResultLike {
|
|
148
|
+
readonly isOk: () => boolean;
|
|
149
|
+
readonly isErr: () => boolean;
|
|
150
|
+
readonly value?: unknown;
|
|
151
|
+
readonly error?: unknown;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const errorFromUnknown = (
|
|
155
|
+
value: unknown
|
|
156
|
+
): { readonly message: string; readonly name: string } => {
|
|
157
|
+
if (value instanceof Error) {
|
|
158
|
+
return { message: value.message, name: value.name };
|
|
159
|
+
}
|
|
160
|
+
return { message: String(value), name: 'Error' };
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Build the stdout envelope for `--trace --json`.
|
|
165
|
+
*
|
|
166
|
+
* On success, the inner value is taken straight from the trail's
|
|
167
|
+
* `Result.ok(...)` payload. On failure the envelope captures the error's
|
|
168
|
+
* `name` and `message` -- both safe to serialize and consistent with how
|
|
169
|
+
* other Trails surfaces project errors.
|
|
170
|
+
*/
|
|
171
|
+
export const buildTraceJsonEnvelope = (
|
|
172
|
+
result: ResultLike,
|
|
173
|
+
records: readonly TraceRecord[]
|
|
174
|
+
): TraceJsonEnvelope => {
|
|
175
|
+
if (result.isOk()) {
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
tracing: records,
|
|
179
|
+
value: result.value,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
error: errorFromUnknown(result.error),
|
|
184
|
+
ok: false,
|
|
185
|
+
tracing: records,
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Serialize a {@link TraceJsonEnvelope} as a single JSON document for stdout.
|
|
191
|
+
*
|
|
192
|
+
* Indented with two spaces to match the rest of the CLI's `--json`
|
|
193
|
+
* formatting (see `output()` in `@ontrails/cli`).
|
|
194
|
+
*/
|
|
195
|
+
export const formatTraceJsonEnvelope = (envelope: TraceJsonEnvelope): string =>
|
|
196
|
+
`${JSON.stringify(envelope, null, 2)}\n`;
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// onResult bridge
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
interface TryTraceCtx {
|
|
203
|
+
readonly flags: Record<string, unknown>;
|
|
204
|
+
readonly result: ResultLike;
|
|
205
|
+
readonly trail?: { readonly id: string } | undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const isJsonMode = (flags: Record<string, unknown>): boolean => {
|
|
209
|
+
if (flags['json'] === true) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
if (typeof flags['output'] === 'string' && flags['output'] === 'json') {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const shouldEmitTraceEnvelope = (flags: Record<string, unknown>): boolean => {
|
|
219
|
+
if (flags['trace'] !== true) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
if (flags['quiet'] === true) {
|
|
223
|
+
// `--quiet` is the explicit pipe-friendly mode. Adding the tracing
|
|
224
|
+
// envelope to stdout would defeat the contract -- defer to the
|
|
225
|
+
// existing quiet handler for stdout and only render the tree on
|
|
226
|
+
// stderr (handled outside this helper).
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (flags['jsonl'] === true) {
|
|
230
|
+
// `--jsonl` streams items per line; the structured envelope cannot be
|
|
231
|
+
// expressed without breaking the line-delimited contract.
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return isJsonMode(flags);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const traceOutputIsOwnedByRunFamily = (ctx: TryTraceCtx): boolean => {
|
|
238
|
+
if (ctx.trail?.id === 'run.example') {
|
|
239
|
+
// `run.example` owns stdout via its comparison envelope. Still render the
|
|
240
|
+
// trace tree to stderr, but do not override the example helper output.
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
if (ctx.trail?.id === 'run.examples') {
|
|
244
|
+
// Pure metadata read; no execution to trace.
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* If the invocation requested both `--trace` and `--json`, serialize the
|
|
252
|
+
* trace envelope to stdout and return `true`. Otherwise return `false` so
|
|
253
|
+
* the caller falls through to the regular on-result chain.
|
|
254
|
+
*
|
|
255
|
+
* The stderr tree is **not** rendered here -- it is rendered uniformly
|
|
256
|
+
* for every `--trace` invocation in the CLI entry-point's `finally`
|
|
257
|
+
* block, even when this helper short-circuits.
|
|
258
|
+
*/
|
|
259
|
+
export const tryTraceJsonOutput = (
|
|
260
|
+
ctx: TryTraceCtx,
|
|
261
|
+
session: TraceSession
|
|
262
|
+
): boolean => {
|
|
263
|
+
if (
|
|
264
|
+
!shouldEmitTraceEnvelope(ctx.flags) ||
|
|
265
|
+
!traceOutputIsOwnedByRunFamily(ctx)
|
|
266
|
+
) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
const records = session.sink.records();
|
|
270
|
+
const envelope = buildTraceJsonEnvelope(ctx.result, records);
|
|
271
|
+
process.stdout.write(formatTraceJsonEnvelope(envelope));
|
|
272
|
+
return true;
|
|
273
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ActionResultContext } from '@ontrails/cli';
|
|
2
|
+
|
|
3
|
+
interface WardenResultValue {
|
|
4
|
+
readonly formatted: string;
|
|
5
|
+
readonly passed?: boolean | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const isWardenResultValue = (value: unknown): value is WardenResultValue => {
|
|
9
|
+
if (typeof value !== 'object' || value === null) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const candidate = value as Record<string, unknown>;
|
|
13
|
+
return (
|
|
14
|
+
typeof candidate['formatted'] === 'string' &&
|
|
15
|
+
(candidate['passed'] === undefined ||
|
|
16
|
+
typeof candidate['passed'] === 'boolean')
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const tryWardenOutput = (ctx: ActionResultContext): boolean => {
|
|
21
|
+
if (
|
|
22
|
+
(ctx.trail.id !== 'warden' && ctx.trail.id !== 'warden.guide') ||
|
|
23
|
+
ctx.result.isErr()
|
|
24
|
+
) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const { value } = ctx.result;
|
|
28
|
+
if (!isWardenResultValue(value)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (value.formatted.length > 0) {
|
|
33
|
+
process.stdout.write(`${value.formatted}\n`);
|
|
34
|
+
}
|
|
35
|
+
if (typeof value.passed === 'boolean') {
|
|
36
|
+
process.exitCode = value.passed ? 0 : 1;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
};
|