@ontrails/trails 1.0.0-beta.14 → 1.0.0-beta.15
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +13 -0
- package/__tests__/examples.test.ts +29 -4
- package/dist/src/app.js +2 -2
- package/dist/src/app.js.map +1 -1
- package/dist/src/clack.d.ts +1 -1
- package/dist/src/clack.js +1 -1
- package/dist/src/cli.js +4 -3
- package/dist/src/cli.js.map +1 -1
- package/dist/src/trails/add-surface.d.ts +3 -3
- package/dist/src/trails/add-surface.d.ts.map +1 -1
- package/dist/src/trails/add-surface.js +46 -24
- package/dist/src/trails/add-surface.js.map +1 -1
- package/dist/src/trails/add-trail.d.ts +3 -1
- package/dist/src/trails/add-trail.d.ts.map +1 -1
- package/dist/src/trails/add-trail.js +38 -11
- package/dist/src/trails/add-trail.js.map +1 -1
- package/dist/src/trails/add-trailhead.d.ts +1 -1
- package/dist/src/trails/add-trailhead.d.ts.map +1 -1
- package/dist/src/trails/add-trailhead.js +4 -4
- package/dist/src/trails/add-verify.d.ts +1 -1
- package/dist/src/trails/add-verify.d.ts.map +1 -1
- package/dist/src/trails/add-verify.js +7 -6
- package/dist/src/trails/add-verify.js.map +1 -1
- package/dist/src/trails/create-scaffold.d.ts +1 -1
- package/dist/src/trails/create-scaffold.d.ts.map +1 -1
- package/dist/src/trails/create-scaffold.js +10 -3
- package/dist/src/trails/create-scaffold.js.map +1 -1
- package/dist/src/trails/create.d.ts +10 -14
- package/dist/src/trails/create.d.ts.map +1 -1
- package/dist/src/trails/create.js +42 -37
- package/dist/src/trails/create.js.map +1 -1
- package/dist/src/trails/dev-clean.d.ts +4 -4
- package/dist/src/trails/dev-clean.d.ts.map +1 -1
- package/dist/src/trails/dev-clean.js +22 -21
- package/dist/src/trails/dev-clean.js.map +1 -1
- package/dist/src/trails/dev-reset.d.ts +1 -1
- package/dist/src/trails/dev-reset.d.ts.map +1 -1
- package/dist/src/trails/dev-reset.js +3 -2
- package/dist/src/trails/dev-reset.js.map +1 -1
- package/dist/src/trails/dev-stats.d.ts +4 -4
- package/dist/src/trails/dev-stats.d.ts.map +1 -1
- package/dist/src/trails/dev-stats.js +19 -19
- package/dist/src/trails/dev-stats.js.map +1 -1
- package/dist/src/trails/dev-support.d.ts +17 -17
- package/dist/src/trails/dev-support.d.ts.map +1 -1
- package/dist/src/trails/dev-support.js +60 -57
- package/dist/src/trails/dev-support.js.map +1 -1
- package/dist/src/trails/draft-promote.d.ts +1 -1
- package/dist/src/trails/draft-promote.d.ts.map +1 -1
- package/dist/src/trails/draft-promote.js +30 -16
- package/dist/src/trails/draft-promote.js.map +1 -1
- package/dist/src/trails/guide.d.ts +2 -2
- package/dist/src/trails/guide.d.ts.map +1 -1
- package/dist/src/trails/guide.js +1 -4
- package/dist/src/trails/guide.js.map +1 -1
- package/dist/src/trails/load-app.d.ts +7 -1
- package/dist/src/trails/load-app.d.ts.map +1 -1
- package/dist/src/trails/load-app.js +374 -26
- package/dist/src/trails/load-app.js.map +1 -1
- package/dist/src/trails/survey.d.ts +5 -5
- package/dist/src/trails/survey.d.ts.map +1 -1
- package/dist/src/trails/survey.js +51 -29
- package/dist/src/trails/survey.js.map +1 -1
- package/dist/src/trails/topo-export.d.ts +5 -4
- package/dist/src/trails/topo-export.d.ts.map +1 -1
- package/dist/src/trails/topo-export.js +4 -7
- package/dist/src/trails/topo-export.js.map +1 -1
- package/dist/src/trails/topo-history.d.ts +6 -10
- package/dist/src/trails/topo-history.d.ts.map +1 -1
- package/dist/src/trails/topo-history.js +7 -8
- package/dist/src/trails/topo-history.js.map +1 -1
- package/dist/src/trails/topo-pin.d.ts +5 -9
- package/dist/src/trails/topo-pin.d.ts.map +1 -1
- package/dist/src/trails/topo-pin.js +5 -9
- package/dist/src/trails/topo-pin.js.map +1 -1
- package/dist/src/trails/topo-read-support.d.ts +12 -8
- package/dist/src/trails/topo-read-support.d.ts.map +1 -1
- package/dist/src/trails/topo-read-support.js +71 -82
- package/dist/src/trails/topo-read-support.js.map +1 -1
- package/dist/src/trails/topo-reports.d.ts +15 -11
- package/dist/src/trails/topo-reports.d.ts.map +1 -1
- package/dist/src/trails/topo-reports.js +40 -34
- package/dist/src/trails/topo-reports.js.map +1 -1
- package/dist/src/trails/topo-show.d.ts +4 -4
- package/dist/src/trails/topo-show.d.ts.map +1 -1
- package/dist/src/trails/topo-show.js +8 -12
- package/dist/src/trails/topo-show.js.map +1 -1
- package/dist/src/trails/topo-store-support.d.ts +2 -2
- package/dist/src/trails/topo-store-support.d.ts.map +1 -1
- package/dist/src/trails/topo-store-support.js +22 -22
- package/dist/src/trails/topo-store-support.js.map +1 -1
- package/dist/src/trails/topo-support.d.ts +19 -30
- package/dist/src/trails/topo-support.d.ts.map +1 -1
- package/dist/src/trails/topo-support.js +63 -96
- package/dist/src/trails/topo-support.js.map +1 -1
- package/dist/src/trails/topo-unpin.d.ts +9 -4
- package/dist/src/trails/topo-unpin.d.ts.map +1 -1
- package/dist/src/trails/topo-unpin.js +9 -4
- package/dist/src/trails/topo-unpin.js.map +1 -1
- package/dist/src/trails/topo-verify.d.ts +2 -2
- package/dist/src/trails/topo-verify.d.ts.map +1 -1
- package/dist/src/trails/topo-verify.js +1 -5
- package/dist/src/trails/topo-verify.js.map +1 -1
- package/dist/src/trails/topo.d.ts +2 -2
- package/dist/src/trails/topo.d.ts.map +1 -1
- package/dist/src/trails/topo.js +6 -10
- package/dist/src/trails/topo.js.map +1 -1
- package/dist/src/trails/warden.d.ts +3 -2
- package/dist/src/trails/warden.d.ts.map +1 -1
- package/dist/src/trails/warden.js +11 -2
- package/dist/src/trails/warden.js.map +1 -1
- package/dist/src/versions.d.ts +12 -0
- package/dist/src/versions.d.ts.map +1 -0
- package/dist/src/versions.js +23 -0
- package/dist/src/versions.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/__tests__/add-trail.test.ts +97 -0
- package/src/__tests__/create.test.ts +91 -27
- package/src/__tests__/guide.test.ts +10 -5
- package/src/__tests__/load-app.test.ts +365 -4
- package/src/__tests__/survey.test.ts +143 -67
- package/src/__tests__/topo-dev.test.ts +48 -46
- package/src/app.ts +2 -2
- package/src/clack.ts +1 -1
- package/src/cli.ts +4 -3
- package/src/trails/add-surface.ts +150 -0
- package/src/trails/add-trail.ts +46 -10
- package/src/trails/add-verify.ts +11 -6
- package/src/trails/create-scaffold.ts +16 -3
- package/src/trails/create.ts +76 -71
- package/src/trails/dev-clean.ts +25 -21
- package/src/trails/dev-reset.ts +3 -2
- package/src/trails/dev-stats.ts +22 -19
- package/src/trails/dev-support.ts +88 -86
- package/src/trails/draft-promote.ts +70 -35
- package/src/trails/guide.ts +1 -4
- package/src/trails/load-app.ts +511 -32
- package/src/trails/survey.ts +60 -35
- package/src/trails/topo-export.ts +5 -9
- package/src/trails/topo-history.ts +8 -10
- package/src/trails/topo-pin.ts +9 -13
- package/src/trails/topo-read-support.ts +103 -106
- package/src/trails/topo-reports.ts +54 -47
- package/src/trails/topo-show.ts +8 -12
- package/src/trails/topo-store-support.ts +36 -28
- package/src/trails/topo-support.ts +90 -134
- package/src/trails/topo-unpin.ts +11 -6
- package/src/trails/topo-verify.ts +1 -5
- package/src/trails/topo.ts +6 -10
- package/src/trails/warden.ts +12 -3
- package/src/versions.ts +43 -0
- package/tsconfig.tests.json +10 -0
- package/src/trails/add-trailhead.ts +0 -121
|
@@ -8,21 +8,23 @@ import {
|
|
|
8
8
|
} from 'node:fs';
|
|
9
9
|
import { join, resolve } from 'node:path';
|
|
10
10
|
|
|
11
|
-
import { Result,
|
|
11
|
+
import { ConflictError, Result, resource, topo, trail } from '@ontrails/core';
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
deriveSurfaceMap,
|
|
14
|
+
deriveSurfaceMapHash,
|
|
15
|
+
deriveSurfaceMapDiff,
|
|
16
|
+
writeSurfaceMap,
|
|
16
17
|
} from '@ontrails/schema';
|
|
17
|
-
import type {
|
|
18
|
+
import type { SurfaceMap } from '@ontrails/schema';
|
|
18
19
|
import { z } from 'zod';
|
|
19
20
|
|
|
20
21
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
deriveBriefReport,
|
|
23
|
+
deriveSurveyList,
|
|
24
|
+
deriveTrailDetail,
|
|
24
25
|
surveyTrail,
|
|
25
26
|
} from '../trails/survey.js';
|
|
27
|
+
import { loadApp } from '../trails/load-app.js';
|
|
26
28
|
import type {
|
|
27
29
|
BriefReport,
|
|
28
30
|
SurveyListReport,
|
|
@@ -39,9 +41,13 @@ const helloTrail = trail('hello', {
|
|
|
39
41
|
return Result.ok({ message: `Hello, ${name}!` });
|
|
40
42
|
},
|
|
41
43
|
description: 'Say hello',
|
|
42
|
-
detours:
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
detours: [
|
|
45
|
+
{
|
|
46
|
+
on: ConflictError,
|
|
47
|
+
/* oxlint-disable-next-line require-await -- test stub */
|
|
48
|
+
recover: async () => Result.ok({ message: 'recovered' }),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
45
51
|
examples: [
|
|
46
52
|
{
|
|
47
53
|
expected: { message: 'Hello, world!' },
|
|
@@ -52,8 +58,8 @@ const helloTrail = trail('hello', {
|
|
|
52
58
|
input: z.object({ name: z.string().optional() }),
|
|
53
59
|
intent: 'read',
|
|
54
60
|
output: z.object({ message: z.string() }),
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
resources: [
|
|
62
|
+
resource('db.main', {
|
|
57
63
|
create: () => Result.ok({ source: 'factory' }),
|
|
58
64
|
}),
|
|
59
65
|
],
|
|
@@ -66,14 +72,14 @@ const byeTrail = trail('bye', {
|
|
|
66
72
|
output: z.object({ message: z.string() }),
|
|
67
73
|
});
|
|
68
74
|
|
|
69
|
-
const [
|
|
70
|
-
if (!
|
|
75
|
+
const [dbResource] = helloTrail.resources;
|
|
76
|
+
if (!dbResource) {
|
|
71
77
|
throw new Error('Expected helloTrail to declare db.main');
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
const app = topo('test-app', {
|
|
75
81
|
bye: byeTrail,
|
|
76
|
-
|
|
82
|
+
dbResource,
|
|
77
83
|
hello: helloTrail,
|
|
78
84
|
});
|
|
79
85
|
|
|
@@ -84,11 +90,27 @@ const expectOk = <T>(result: Result<T, Error>): T => {
|
|
|
84
90
|
return result.value;
|
|
85
91
|
};
|
|
86
92
|
|
|
87
|
-
const writeSurveyAppFixture = (
|
|
93
|
+
const writeSurveyAppFixture = (
|
|
94
|
+
dir: string,
|
|
95
|
+
options?: { withBye?: boolean }
|
|
96
|
+
) => {
|
|
88
97
|
mkdirSync(join(dir, 'src'), { recursive: true });
|
|
98
|
+
const byeSource = options?.withBye
|
|
99
|
+
? `
|
|
100
|
+
const bye = trail('bye', {
|
|
101
|
+
blaze: async (input) => Result.ok({ message: \`Bye, \${input.name ?? 'world'}!\` }),
|
|
102
|
+
input: z.object({ name: z.string().optional() }),
|
|
103
|
+
intent: 'read',
|
|
104
|
+
output: z.object({ message: z.string() }),
|
|
105
|
+
});
|
|
106
|
+
`
|
|
107
|
+
: '';
|
|
108
|
+
const topoMembers = options?.withBye
|
|
109
|
+
? '{ bye, dbMain, hello }'
|
|
110
|
+
: '{ dbMain, hello }';
|
|
89
111
|
writeFileSync(
|
|
90
112
|
join(dir, 'src', 'app.ts'),
|
|
91
|
-
`import { Result,
|
|
113
|
+
`import { Result, resource, topo, trail } from '@ontrails/core';
|
|
92
114
|
import { z } from 'zod';
|
|
93
115
|
|
|
94
116
|
const hello = trail('hello', {
|
|
@@ -96,19 +118,21 @@ const hello = trail('hello', {
|
|
|
96
118
|
input: z.object({ name: z.string().optional() }),
|
|
97
119
|
intent: 'read',
|
|
98
120
|
output: z.object({ message: z.string() }),
|
|
99
|
-
|
|
100
|
-
|
|
121
|
+
resources: [
|
|
122
|
+
resource('db.main', {
|
|
101
123
|
create: () => Result.ok({ source: 'factory' }),
|
|
102
124
|
}),
|
|
103
125
|
],
|
|
104
126
|
});
|
|
105
127
|
|
|
106
|
-
const [dbMain] = hello.
|
|
128
|
+
const [dbMain] = hello.resources;
|
|
107
129
|
if (!dbMain) {
|
|
108
130
|
throw new Error('expected hello to declare db.main');
|
|
109
131
|
}
|
|
110
132
|
|
|
111
|
-
|
|
133
|
+
${byeSource}
|
|
134
|
+
|
|
135
|
+
export const app = topo('survey-fixture', ${topoMembers});
|
|
112
136
|
`
|
|
113
137
|
);
|
|
114
138
|
};
|
|
@@ -125,47 +149,47 @@ const repoTempDir = (): string =>
|
|
|
125
149
|
// ---------------------------------------------------------------------------
|
|
126
150
|
|
|
127
151
|
describe('trails survey', () => {
|
|
128
|
-
test('
|
|
129
|
-
const
|
|
130
|
-
expect(
|
|
131
|
-
const ids =
|
|
152
|
+
test('deriveSurfaceMap includes all trails', () => {
|
|
153
|
+
const surfaceMap = deriveSurfaceMap(app);
|
|
154
|
+
expect(surfaceMap.entries.length).toBe(3);
|
|
155
|
+
const ids = surfaceMap.entries.map((e) => e.id);
|
|
132
156
|
expect(ids).toContain('hello');
|
|
133
157
|
expect(ids).toContain('bye');
|
|
134
158
|
expect(ids).toContain('db.main');
|
|
135
159
|
});
|
|
136
160
|
|
|
137
|
-
test('
|
|
138
|
-
const
|
|
139
|
-
const hello =
|
|
161
|
+
test('surface map entries have expected fields', () => {
|
|
162
|
+
const surfaceMap = deriveSurfaceMap(app);
|
|
163
|
+
const hello = surfaceMap.entries.find((e) => e.id === 'hello');
|
|
140
164
|
expect(hello).toBeDefined();
|
|
141
165
|
expect(hello?.cli?.path).toEqual(['hello']);
|
|
142
166
|
expect(hello?.kind).toBe('trail');
|
|
143
167
|
expect(hello?.intent).toBe('read');
|
|
144
168
|
expect(hello?.exampleCount).toBe(1);
|
|
145
|
-
expect(hello?.
|
|
169
|
+
expect(hello?.resources).toEqual(['db.main']);
|
|
146
170
|
});
|
|
147
171
|
|
|
148
172
|
test('JSON output is valid JSON', () => {
|
|
149
|
-
const
|
|
150
|
-
const json = JSON.stringify(
|
|
151
|
-
const parsed = JSON.parse(json) as
|
|
173
|
+
const surfaceMap = deriveSurfaceMap(app);
|
|
174
|
+
const json = JSON.stringify(surfaceMap, null, 2);
|
|
175
|
+
const parsed = JSON.parse(json) as SurfaceMap;
|
|
152
176
|
expect(parsed.version).toBe('1.0');
|
|
153
177
|
expect(parsed.entries.length).toBe(3);
|
|
154
178
|
});
|
|
155
179
|
|
|
156
|
-
test('
|
|
157
|
-
const
|
|
158
|
-
const hash1 =
|
|
159
|
-
const hash2 =
|
|
180
|
+
test('deriveSurfaceMapHash produces stable hash', () => {
|
|
181
|
+
const surfaceMap = deriveSurfaceMap(app);
|
|
182
|
+
const hash1 = deriveSurfaceMapHash(surfaceMap);
|
|
183
|
+
const hash2 = deriveSurfaceMapHash(surfaceMap);
|
|
160
184
|
expect(hash1).toBe(hash2);
|
|
161
185
|
// SHA-256 hex
|
|
162
186
|
expect(hash1.length).toBe(64);
|
|
163
187
|
});
|
|
164
188
|
|
|
165
|
-
test('
|
|
166
|
-
const prev =
|
|
167
|
-
const curr =
|
|
168
|
-
const diff =
|
|
189
|
+
test('deriveSurfaceMapDiff detects added trails', () => {
|
|
190
|
+
const prev = deriveSurfaceMap(topo('test', { hello: helloTrail }));
|
|
191
|
+
const curr = deriveSurfaceMap(app);
|
|
192
|
+
const diff = deriveSurfaceMapDiff(prev, curr);
|
|
169
193
|
|
|
170
194
|
expect(diff.info.length).toBeGreaterThan(0);
|
|
171
195
|
const addedBye = diff.info.find((e) => e.id === 'bye');
|
|
@@ -173,10 +197,10 @@ describe('trails survey', () => {
|
|
|
173
197
|
expect(addedBye?.change).toBe('added');
|
|
174
198
|
});
|
|
175
199
|
|
|
176
|
-
test('
|
|
177
|
-
const prev =
|
|
178
|
-
const curr =
|
|
179
|
-
const diff =
|
|
200
|
+
test('deriveSurfaceMapDiff detects removed trails', () => {
|
|
201
|
+
const prev = deriveSurfaceMap(app);
|
|
202
|
+
const curr = deriveSurfaceMap(topo('test', { hello: helloTrail }));
|
|
203
|
+
const diff = deriveSurfaceMapDiff(prev, curr);
|
|
180
204
|
|
|
181
205
|
expect(diff.hasBreaking).toBe(true);
|
|
182
206
|
const removedBye = diff.breaking.find((e) => e.id === 'bye');
|
|
@@ -184,9 +208,9 @@ describe('trails survey', () => {
|
|
|
184
208
|
expect(removedBye?.change).toBe('removed');
|
|
185
209
|
});
|
|
186
210
|
|
|
187
|
-
test('
|
|
188
|
-
const
|
|
189
|
-
const diff =
|
|
211
|
+
test('deriveSurfaceMapDiff returns empty for identical maps', () => {
|
|
212
|
+
const surfaceMap = deriveSurfaceMap(app);
|
|
213
|
+
const diff = deriveSurfaceMapDiff(surfaceMap, surfaceMap);
|
|
190
214
|
expect(diff.entries.length).toBe(0);
|
|
191
215
|
expect(diff.hasBreaking).toBe(false);
|
|
192
216
|
});
|
|
@@ -198,70 +222,70 @@ describe('trails survey', () => {
|
|
|
198
222
|
|
|
199
223
|
describe('trails survey --brief', () => {
|
|
200
224
|
test('produces a valid capability report', () => {
|
|
201
|
-
const report =
|
|
225
|
+
const report = deriveBriefReport(app);
|
|
202
226
|
expect(report.name).toBe('test-app');
|
|
203
227
|
expect(report.contractVersion).toBe('2026-03');
|
|
204
228
|
});
|
|
205
229
|
|
|
206
230
|
test('report includes correct trail count', () => {
|
|
207
|
-
const report =
|
|
231
|
+
const report = deriveBriefReport(app);
|
|
208
232
|
expect(report.trails).toBe(2);
|
|
209
233
|
expect(report.signals).toBe(0);
|
|
210
|
-
expect(report.
|
|
234
|
+
expect(report.resources).toBe(1);
|
|
211
235
|
});
|
|
212
236
|
|
|
213
237
|
test('detects features in use', () => {
|
|
214
|
-
const report =
|
|
238
|
+
const report = deriveBriefReport(app);
|
|
215
239
|
expect(report.features.outputSchemas).toBe(true);
|
|
216
240
|
expect(report.features.examples).toBe(true);
|
|
217
241
|
expect(report.features.detours).toBe(true);
|
|
218
242
|
expect(report.features.signals).toBe(false);
|
|
219
|
-
expect(report.features.
|
|
243
|
+
expect(report.features.resources).toBe(true);
|
|
220
244
|
});
|
|
221
245
|
|
|
222
246
|
test('JSON output is valid', () => {
|
|
223
|
-
const report =
|
|
247
|
+
const report = deriveBriefReport(app);
|
|
224
248
|
const json = JSON.stringify(report, null, 2);
|
|
225
249
|
const parsed = JSON.parse(json) as BriefReport;
|
|
226
250
|
expect(parsed.name).toBe('test-app');
|
|
227
251
|
expect(parsed.trails).toBe(2);
|
|
228
|
-
expect(parsed.
|
|
252
|
+
expect(parsed.resources).toBe(1);
|
|
229
253
|
});
|
|
230
254
|
|
|
231
255
|
test('empty app reports zero features', () => {
|
|
232
256
|
const emptyApp = topo('empty', {});
|
|
233
|
-
const report =
|
|
257
|
+
const report = deriveBriefReport(emptyApp);
|
|
234
258
|
expect(report.trails).toBe(0);
|
|
235
259
|
expect(report.features.outputSchemas).toBe(false);
|
|
236
260
|
expect(report.features.examples).toBe(false);
|
|
237
261
|
expect(report.features.detours).toBe(false);
|
|
238
|
-
expect(report.features.
|
|
262
|
+
expect(report.features.resources).toBe(false);
|
|
239
263
|
});
|
|
240
264
|
});
|
|
241
265
|
|
|
242
266
|
describe('trails survey detail', () => {
|
|
243
|
-
test('trail detail includes declared
|
|
244
|
-
const detail =
|
|
267
|
+
test('trail detail includes declared resources, crossings, and intent', () => {
|
|
268
|
+
const detail = deriveTrailDetail(helloTrail);
|
|
245
269
|
const parsed = structuredClone(detail) as TrailDetailReport;
|
|
246
270
|
|
|
247
271
|
expect(parsed.crosses).toEqual([]);
|
|
248
272
|
expect(parsed.intent).toBe('read');
|
|
249
|
-
expect(parsed.
|
|
273
|
+
expect(parsed.resources).toEqual(['db.main']);
|
|
250
274
|
});
|
|
251
275
|
});
|
|
252
276
|
|
|
253
|
-
describe('trails survey
|
|
254
|
-
test('list output includes
|
|
255
|
-
const report =
|
|
277
|
+
describe('trails survey resources section', () => {
|
|
278
|
+
test('list output includes resource lifetime and health status', () => {
|
|
279
|
+
const report = deriveSurveyList(app);
|
|
256
280
|
const parsed = structuredClone(report) as SurveyListReport;
|
|
257
|
-
const db = parsed.
|
|
281
|
+
const db = parsed.resources.find((entry) => entry.id === 'db.main');
|
|
258
282
|
|
|
259
|
-
expect(parsed.
|
|
283
|
+
expect(parsed.resourceCount).toBe(1);
|
|
260
284
|
expect(db).toEqual({
|
|
261
285
|
description: null,
|
|
262
286
|
health: 'none',
|
|
263
287
|
id: 'db.main',
|
|
264
|
-
kind: '
|
|
288
|
+
kind: 'resource',
|
|
265
289
|
lifetime: 'singleton',
|
|
266
290
|
usedBy: ['hello'],
|
|
267
291
|
});
|
|
@@ -286,7 +310,7 @@ describe('trails survey generate', () => {
|
|
|
286
310
|
};
|
|
287
311
|
|
|
288
312
|
expect(generated.hash).toHaveLength(64);
|
|
289
|
-
expect(existsSync(join(dir, '.trails', '
|
|
313
|
+
expect(existsSync(join(dir, '.trails', '_surface.json'))).toBe(true);
|
|
290
314
|
expect(existsSync(join(dir, '.trails', 'trails.lock'))).toBe(true);
|
|
291
315
|
expect(
|
|
292
316
|
JSON.parse(readFileSync(join(dir, '.trails', 'trails.lock'), 'utf8'))
|
|
@@ -299,3 +323,55 @@ describe('trails survey generate', () => {
|
|
|
299
323
|
}
|
|
300
324
|
});
|
|
301
325
|
});
|
|
326
|
+
|
|
327
|
+
describe('trails survey diffSaved', () => {
|
|
328
|
+
test('returns an error when no saved surface map exists yet', async () => {
|
|
329
|
+
const dir = repoTempDir();
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
writeSurveyAppFixture(dir);
|
|
333
|
+
|
|
334
|
+
const result = await surveyTrail.blaze(
|
|
335
|
+
{ diffSaved: true, module: './src/app.ts' },
|
|
336
|
+
{ cwd: dir } as never
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
expect(result.isErr()).toBe(true);
|
|
340
|
+
expect(result.error.message).toContain('Run `trails topo export` first');
|
|
341
|
+
} finally {
|
|
342
|
+
rmSync(dir, { force: true, recursive: true });
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('diffs against the saved local surface map', async () => {
|
|
347
|
+
const dir = repoTempDir();
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
writeSurveyAppFixture(dir);
|
|
351
|
+
const baselineApp = await loadApp('./src/app.ts', dir);
|
|
352
|
+
await writeSurfaceMap(deriveSurfaceMap(baselineApp), {
|
|
353
|
+
dir: join(dir, '.trails'),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
writeSurveyAppFixture(dir, { withBye: true });
|
|
357
|
+
|
|
358
|
+
const result = await surveyTrail.blaze(
|
|
359
|
+
{ diffSaved: true, module: './src/app.ts' },
|
|
360
|
+
{ cwd: dir } as never
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
expect(result.isOk()).toBe(true);
|
|
364
|
+
expect(result.value).toMatchObject({
|
|
365
|
+
hasBreaking: false,
|
|
366
|
+
info: [
|
|
367
|
+
expect.objectContaining({
|
|
368
|
+
change: 'added',
|
|
369
|
+
id: 'bye',
|
|
370
|
+
}),
|
|
371
|
+
],
|
|
372
|
+
});
|
|
373
|
+
} finally {
|
|
374
|
+
rmSync(dir, { force: true, recursive: true });
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
@@ -12,7 +12,7 @@ import { join, resolve } from 'node:path';
|
|
|
12
12
|
|
|
13
13
|
import type { Result } from '@ontrails/core';
|
|
14
14
|
import { openReadTrailsDb } from '@ontrails/core/internal/trails-db';
|
|
15
|
-
import { createDevStore } from '@ontrails/
|
|
15
|
+
import { createDevStore } from '@ontrails/tracing';
|
|
16
16
|
|
|
17
17
|
import { devCleanTrail } from '../trails/dev-clean.js';
|
|
18
18
|
import { devResetTrail } from '../trails/dev-reset.js';
|
|
@@ -54,7 +54,7 @@ const writeAppFixture = (dir: string): void => {
|
|
|
54
54
|
mkdirSync(join(dir, 'src'), { recursive: true });
|
|
55
55
|
writeFileSync(
|
|
56
56
|
join(dir, 'src', 'app.ts'),
|
|
57
|
-
`import { Result,
|
|
57
|
+
`import { Result, resource, topo, trail } from '@ontrails/core';
|
|
58
58
|
import { z } from 'zod';
|
|
59
59
|
|
|
60
60
|
const hello = trail('hello', {
|
|
@@ -64,8 +64,8 @@ const hello = trail('hello', {
|
|
|
64
64
|
input: z.object({ name: z.string().optional() }),
|
|
65
65
|
intent: 'read',
|
|
66
66
|
output: z.object({ message: z.string() }),
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
resources: [
|
|
68
|
+
resource('db.main', {
|
|
69
69
|
create: () => Result.ok({ source: 'factory' }),
|
|
70
70
|
}),
|
|
71
71
|
],
|
|
@@ -78,7 +78,7 @@ const goodbye = trail('goodbye', {
|
|
|
78
78
|
output: z.object({ ok: z.boolean() }),
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
const [dbMain] = hello.
|
|
81
|
+
const [dbMain] = hello.resources;
|
|
82
82
|
if (!dbMain) {
|
|
83
83
|
throw new Error('expected hello to declare db.main');
|
|
84
84
|
}
|
|
@@ -100,7 +100,7 @@ describe('topo and dev trails', () => {
|
|
|
100
100
|
);
|
|
101
101
|
expect(summary.app.name).toBe('fixture-app');
|
|
102
102
|
expect(summary.list.count).toBe(2);
|
|
103
|
-
expect(summary.list.
|
|
103
|
+
expect(summary.list.resourceCount).toBe(1);
|
|
104
104
|
expect(summary.lockExists).toBe(false);
|
|
105
105
|
|
|
106
106
|
const detail = expectOk(
|
|
@@ -109,13 +109,13 @@ describe('topo and dev trails', () => {
|
|
|
109
109
|
} as never)
|
|
110
110
|
);
|
|
111
111
|
expect(detail.id).toBe('hello');
|
|
112
|
-
expect(detail.
|
|
112
|
+
expect(detail.resources).toEqual(['db.main']);
|
|
113
113
|
|
|
114
114
|
const exportResult = expectOk(
|
|
115
115
|
await topoExportTrail.blaze(moduleInput, { cwd: dir } as never)
|
|
116
116
|
);
|
|
117
117
|
expect(exportResult.hash).toHaveLength(64);
|
|
118
|
-
expect(existsSync(join(dir, '.trails', '
|
|
118
|
+
expect(existsSync(join(dir, '.trails', '_surface.json'))).toBe(true);
|
|
119
119
|
expect(existsSync(join(dir, '.trails', 'trails.lock'))).toBe(true);
|
|
120
120
|
expect(
|
|
121
121
|
JSON.parse(readFileSync(join(dir, '.trails', 'trails.lock'), 'utf8'))
|
|
@@ -124,23 +124,17 @@ describe('topo and dev trails', () => {
|
|
|
124
124
|
version: 1,
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
join(dir, '.trails', 'trailhead.lock'),
|
|
129
|
-
readFileSync(join(dir, '.trails', 'trails.lock'), 'utf8')
|
|
130
|
-
);
|
|
131
|
-
rmSync(join(dir, '.trails', 'trails.lock'));
|
|
132
|
-
|
|
133
|
-
const legacySummary = expectOk(
|
|
127
|
+
const summaryAfterExport = expectOk(
|
|
134
128
|
await topoTrail.blaze(moduleInput, { cwd: dir } as never)
|
|
135
129
|
);
|
|
136
|
-
expect(
|
|
130
|
+
expect(summaryAfterExport.lockExists).toBe(true);
|
|
137
131
|
|
|
138
132
|
const verifyResult = expectOk(
|
|
139
133
|
await topoVerifyTrail.blaze(moduleInput, { cwd: dir } as never)
|
|
140
134
|
);
|
|
141
135
|
expect(verifyResult.stale).toBe(false);
|
|
142
136
|
|
|
143
|
-
writeFileSync(join(dir, '.trails', '
|
|
137
|
+
writeFileSync(join(dir, '.trails', 'trails.lock'), 'stale\n');
|
|
144
138
|
const verifyError = expectErr(
|
|
145
139
|
await topoVerifyTrail.blaze(moduleInput, { cwd: dir } as never)
|
|
146
140
|
);
|
|
@@ -163,7 +157,7 @@ describe('topo and dev trails', () => {
|
|
|
163
157
|
);
|
|
164
158
|
expect(surveyList).toMatchObject({
|
|
165
159
|
count: 2,
|
|
166
|
-
|
|
160
|
+
resourceCount: 1,
|
|
167
161
|
});
|
|
168
162
|
|
|
169
163
|
const surveyBrief = expectOk(
|
|
@@ -175,7 +169,7 @@ describe('topo and dev trails', () => {
|
|
|
175
169
|
features: {
|
|
176
170
|
examples: true,
|
|
177
171
|
outputSchemas: true,
|
|
178
|
-
|
|
172
|
+
resources: true,
|
|
179
173
|
},
|
|
180
174
|
name: 'fixture-app',
|
|
181
175
|
trails: 2,
|
|
@@ -188,7 +182,7 @@ describe('topo and dev trails', () => {
|
|
|
188
182
|
);
|
|
189
183
|
expect(surveyDetail).toMatchObject({
|
|
190
184
|
id: 'hello',
|
|
191
|
-
|
|
185
|
+
resources: ['db.main'],
|
|
192
186
|
});
|
|
193
187
|
|
|
194
188
|
const guideList = expectOk(
|
|
@@ -243,8 +237,7 @@ describe('topo and dev trails', () => {
|
|
|
243
237
|
cwd: dir,
|
|
244
238
|
} as never)
|
|
245
239
|
);
|
|
246
|
-
expect(firstPin.
|
|
247
|
-
expect(firstPin.pin.saveId).toBe(firstPin.save.id);
|
|
240
|
+
expect(firstPin.snapshot.pinnedAs).toBe('before-auth');
|
|
248
241
|
|
|
249
242
|
const firstExport = expectOk(
|
|
250
243
|
await topoExportTrail.blaze(moduleInput, { cwd: dir } as never)
|
|
@@ -264,17 +257,17 @@ describe('topo and dev trails', () => {
|
|
|
264
257
|
try {
|
|
265
258
|
const pinnedRows = projectionDb
|
|
266
259
|
.query<{ count: number }, [string]>(
|
|
267
|
-
'SELECT COUNT(*) as count FROM topo_trails WHERE
|
|
260
|
+
'SELECT COUNT(*) as count FROM topo_trails WHERE snapshot_id = ?'
|
|
268
261
|
)
|
|
269
|
-
.get(firstPin.
|
|
262
|
+
.get(firstPin.snapshot.id);
|
|
270
263
|
const exportedRows = projectionDb
|
|
271
264
|
.query<{ count: number }, [string]>(
|
|
272
|
-
'SELECT COUNT(*) as count FROM topo_trails WHERE
|
|
265
|
+
'SELECT COUNT(*) as count FROM topo_trails WHERE snapshot_id = ?'
|
|
273
266
|
)
|
|
274
|
-
.get(firstExport.
|
|
267
|
+
.get(firstExport.snapshot.id);
|
|
275
268
|
const projectedSaves = projectionDb
|
|
276
269
|
.query<{ count: number }, []>(
|
|
277
|
-
'SELECT COUNT(DISTINCT
|
|
270
|
+
'SELECT COUNT(DISTINCT snapshot_id) as count FROM topo_trails'
|
|
278
271
|
)
|
|
279
272
|
.get();
|
|
280
273
|
const cachedSchemas = projectionDb
|
|
@@ -326,36 +319,45 @@ describe('topo and dev trails', () => {
|
|
|
326
319
|
const history = expectOk(
|
|
327
320
|
await topoHistoryTrail.blaze({}, { cwd: dir } as never)
|
|
328
321
|
);
|
|
329
|
-
expect(history.
|
|
330
|
-
expect(history.
|
|
331
|
-
expect(
|
|
322
|
+
expect(history.pinnedCount).toBe(1);
|
|
323
|
+
expect(history.snapshotCount).toBeGreaterThanOrEqual(3);
|
|
324
|
+
expect(
|
|
325
|
+
history.snapshots.some(
|
|
326
|
+
(snapshot) => snapshot.id === firstPin.snapshot.id
|
|
327
|
+
)
|
|
328
|
+
).toBe(true);
|
|
332
329
|
expect(
|
|
333
|
-
history.
|
|
330
|
+
history.snapshots.some(
|
|
331
|
+
(snapshot) => snapshot.id === secondExport.snapshot.id
|
|
332
|
+
)
|
|
334
333
|
).toBe(true);
|
|
335
334
|
|
|
336
335
|
const stats = expectOk(
|
|
337
336
|
await devStatsTrail.blaze({}, { cwd: dir } as never)
|
|
338
337
|
);
|
|
339
|
-
expect(stats.topo.
|
|
340
|
-
expect(stats.
|
|
338
|
+
expect(stats.topo.pinnedCount).toBe(1);
|
|
339
|
+
expect(stats.tracing.recordCount).toBe(2);
|
|
341
340
|
|
|
342
341
|
const cleanPreview = expectOk(
|
|
343
|
-
await devCleanTrail.blaze(
|
|
344
|
-
|
|
345
|
-
|
|
342
|
+
await devCleanTrail.blaze(
|
|
343
|
+
{ dryRun: true, snapshots: 0, traceAgeMs: 0 },
|
|
344
|
+
{
|
|
345
|
+
cwd: dir,
|
|
346
|
+
} as never
|
|
347
|
+
)
|
|
346
348
|
);
|
|
347
349
|
expect(cleanPreview.dryRun).toBe(true);
|
|
348
|
-
expect(cleanPreview.removed.
|
|
349
|
-
expect(cleanPreview.removed.
|
|
350
|
+
expect(cleanPreview.removed.topoSnapshots).toBeGreaterThanOrEqual(2);
|
|
351
|
+
expect(cleanPreview.removed.traceRecords).toBe(2);
|
|
350
352
|
|
|
351
353
|
const cleanResult = expectOk(
|
|
352
354
|
await devCleanTrail.blaze(
|
|
353
|
-
{ dryRun: false,
|
|
355
|
+
{ dryRun: false, snapshots: 0, traceAgeMs: 0, yes: true },
|
|
354
356
|
{ cwd: dir } as never
|
|
355
357
|
)
|
|
356
358
|
);
|
|
357
|
-
expect(cleanResult.removed.
|
|
358
|
-
expect(cleanResult.remaining.
|
|
359
|
+
expect(cleanResult.removed.traceRecords).toBe(2);
|
|
360
|
+
expect(cleanResult.remaining.pinnedCount).toBe(1);
|
|
359
361
|
|
|
360
362
|
const unpinPreview = expectOk(
|
|
361
363
|
await topoUnpinTrail.blaze({ dryRun: true, name: 'before-auth' }, {
|
|
@@ -363,7 +365,7 @@ describe('topo and dev trails', () => {
|
|
|
363
365
|
} as never)
|
|
364
366
|
);
|
|
365
367
|
expect(unpinPreview.dryRun).toBe(true);
|
|
366
|
-
expect(unpinPreview.
|
|
368
|
+
expect(unpinPreview.snapshot?.pinnedAs).toBe('before-auth');
|
|
367
369
|
|
|
368
370
|
const unpinResult = expectOk(
|
|
369
371
|
await topoUnpinTrail.blaze(
|
|
@@ -404,8 +406,8 @@ describe('topo and dev trails', () => {
|
|
|
404
406
|
await devCleanTrail.blaze({ dryRun: true }, { cwd: dir } as never)
|
|
405
407
|
);
|
|
406
408
|
expect(preview.dryRun).toBe(true);
|
|
407
|
-
expect(preview.removed.
|
|
408
|
-
expect(preview.removed.
|
|
409
|
+
expect(preview.removed.topoSnapshots).toBe(0);
|
|
410
|
+
expect(preview.removed.traceRecords).toBe(0);
|
|
409
411
|
expect(existsSync(join(dir, '.trails', 'trails.db'))).toBe(false);
|
|
410
412
|
|
|
411
413
|
const applied = expectOk(
|
|
@@ -414,8 +416,8 @@ describe('topo and dev trails', () => {
|
|
|
414
416
|
} as never)
|
|
415
417
|
);
|
|
416
418
|
expect(applied.dryRun).toBe(false);
|
|
417
|
-
expect(applied.removed.
|
|
418
|
-
expect(applied.removed.
|
|
419
|
+
expect(applied.removed.topoSnapshots).toBe(0);
|
|
420
|
+
expect(applied.removed.traceRecords).toBe(0);
|
|
419
421
|
expect(existsSync(join(dir, '.trails', 'trails.db'))).toBe(false);
|
|
420
422
|
} finally {
|
|
421
423
|
rmSync(dir, { force: true, recursive: true });
|
package/src/app.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { topo } from '@ontrails/core';
|
|
2
2
|
|
|
3
|
-
import * as
|
|
3
|
+
import * as addSurface from './trails/add-surface.js';
|
|
4
4
|
import * as addTrail from './trails/add-trail.js';
|
|
5
5
|
import * as addVerify from './trails/add-verify.js';
|
|
6
6
|
import * as create from './trails/create.js';
|
|
@@ -38,7 +38,7 @@ export const app = topo(
|
|
|
38
38
|
warden,
|
|
39
39
|
create,
|
|
40
40
|
createScaffold,
|
|
41
|
-
|
|
41
|
+
addSurface,
|
|
42
42
|
addVerify,
|
|
43
43
|
addTrail
|
|
44
44
|
);
|
package/src/clack.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Clack-backed input resolver for the Trails CLI.
|
|
3
3
|
*
|
|
4
|
-
* This stays at the app
|
|
4
|
+
* This stays at the app layer so @ontrails/cli remains prompt-library agnostic.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Field, InputResolver, ResolveInputOptions } from '@ontrails/cli';
|
package/src/cli.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { outputModePreset } from '@ontrails/cli';
|
|
2
|
-
import {
|
|
2
|
+
import { surface } from '@ontrails/cli/commander';
|
|
3
3
|
|
|
4
4
|
import { app } from './app.js';
|
|
5
5
|
import { resolveInputWithClack } from './clack.js';
|
|
6
|
+
import { trailsPackageVersion } from './versions.js';
|
|
6
7
|
|
|
7
8
|
// oxlint-disable-next-line require-hook -- CLI entry point
|
|
8
|
-
|
|
9
|
+
await surface(app, {
|
|
9
10
|
description: 'Agent-native, contract-first TypeScript framework',
|
|
10
11
|
name: 'trails',
|
|
11
12
|
presets: [outputModePreset()],
|
|
12
13
|
resolveInput: resolveInputWithClack,
|
|
13
|
-
version:
|
|
14
|
+
version: trailsPackageVersion,
|
|
14
15
|
});
|