@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.
Files changed (155) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/__tests__/examples.test.ts +29 -4
  4. package/dist/src/app.js +2 -2
  5. package/dist/src/app.js.map +1 -1
  6. package/dist/src/clack.d.ts +1 -1
  7. package/dist/src/clack.js +1 -1
  8. package/dist/src/cli.js +4 -3
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/trails/add-surface.d.ts +3 -3
  11. package/dist/src/trails/add-surface.d.ts.map +1 -1
  12. package/dist/src/trails/add-surface.js +46 -24
  13. package/dist/src/trails/add-surface.js.map +1 -1
  14. package/dist/src/trails/add-trail.d.ts +3 -1
  15. package/dist/src/trails/add-trail.d.ts.map +1 -1
  16. package/dist/src/trails/add-trail.js +38 -11
  17. package/dist/src/trails/add-trail.js.map +1 -1
  18. package/dist/src/trails/add-trailhead.d.ts +1 -1
  19. package/dist/src/trails/add-trailhead.d.ts.map +1 -1
  20. package/dist/src/trails/add-trailhead.js +4 -4
  21. package/dist/src/trails/add-verify.d.ts +1 -1
  22. package/dist/src/trails/add-verify.d.ts.map +1 -1
  23. package/dist/src/trails/add-verify.js +7 -6
  24. package/dist/src/trails/add-verify.js.map +1 -1
  25. package/dist/src/trails/create-scaffold.d.ts +1 -1
  26. package/dist/src/trails/create-scaffold.d.ts.map +1 -1
  27. package/dist/src/trails/create-scaffold.js +10 -3
  28. package/dist/src/trails/create-scaffold.js.map +1 -1
  29. package/dist/src/trails/create.d.ts +10 -14
  30. package/dist/src/trails/create.d.ts.map +1 -1
  31. package/dist/src/trails/create.js +42 -37
  32. package/dist/src/trails/create.js.map +1 -1
  33. package/dist/src/trails/dev-clean.d.ts +4 -4
  34. package/dist/src/trails/dev-clean.d.ts.map +1 -1
  35. package/dist/src/trails/dev-clean.js +22 -21
  36. package/dist/src/trails/dev-clean.js.map +1 -1
  37. package/dist/src/trails/dev-reset.d.ts +1 -1
  38. package/dist/src/trails/dev-reset.d.ts.map +1 -1
  39. package/dist/src/trails/dev-reset.js +3 -2
  40. package/dist/src/trails/dev-reset.js.map +1 -1
  41. package/dist/src/trails/dev-stats.d.ts +4 -4
  42. package/dist/src/trails/dev-stats.d.ts.map +1 -1
  43. package/dist/src/trails/dev-stats.js +19 -19
  44. package/dist/src/trails/dev-stats.js.map +1 -1
  45. package/dist/src/trails/dev-support.d.ts +17 -17
  46. package/dist/src/trails/dev-support.d.ts.map +1 -1
  47. package/dist/src/trails/dev-support.js +60 -57
  48. package/dist/src/trails/dev-support.js.map +1 -1
  49. package/dist/src/trails/draft-promote.d.ts +1 -1
  50. package/dist/src/trails/draft-promote.d.ts.map +1 -1
  51. package/dist/src/trails/draft-promote.js +30 -16
  52. package/dist/src/trails/draft-promote.js.map +1 -1
  53. package/dist/src/trails/guide.d.ts +2 -2
  54. package/dist/src/trails/guide.d.ts.map +1 -1
  55. package/dist/src/trails/guide.js +1 -4
  56. package/dist/src/trails/guide.js.map +1 -1
  57. package/dist/src/trails/load-app.d.ts +7 -1
  58. package/dist/src/trails/load-app.d.ts.map +1 -1
  59. package/dist/src/trails/load-app.js +374 -26
  60. package/dist/src/trails/load-app.js.map +1 -1
  61. package/dist/src/trails/survey.d.ts +5 -5
  62. package/dist/src/trails/survey.d.ts.map +1 -1
  63. package/dist/src/trails/survey.js +51 -29
  64. package/dist/src/trails/survey.js.map +1 -1
  65. package/dist/src/trails/topo-export.d.ts +5 -4
  66. package/dist/src/trails/topo-export.d.ts.map +1 -1
  67. package/dist/src/trails/topo-export.js +4 -7
  68. package/dist/src/trails/topo-export.js.map +1 -1
  69. package/dist/src/trails/topo-history.d.ts +6 -10
  70. package/dist/src/trails/topo-history.d.ts.map +1 -1
  71. package/dist/src/trails/topo-history.js +7 -8
  72. package/dist/src/trails/topo-history.js.map +1 -1
  73. package/dist/src/trails/topo-pin.d.ts +5 -9
  74. package/dist/src/trails/topo-pin.d.ts.map +1 -1
  75. package/dist/src/trails/topo-pin.js +5 -9
  76. package/dist/src/trails/topo-pin.js.map +1 -1
  77. package/dist/src/trails/topo-read-support.d.ts +12 -8
  78. package/dist/src/trails/topo-read-support.d.ts.map +1 -1
  79. package/dist/src/trails/topo-read-support.js +71 -82
  80. package/dist/src/trails/topo-read-support.js.map +1 -1
  81. package/dist/src/trails/topo-reports.d.ts +15 -11
  82. package/dist/src/trails/topo-reports.d.ts.map +1 -1
  83. package/dist/src/trails/topo-reports.js +40 -34
  84. package/dist/src/trails/topo-reports.js.map +1 -1
  85. package/dist/src/trails/topo-show.d.ts +4 -4
  86. package/dist/src/trails/topo-show.d.ts.map +1 -1
  87. package/dist/src/trails/topo-show.js +8 -12
  88. package/dist/src/trails/topo-show.js.map +1 -1
  89. package/dist/src/trails/topo-store-support.d.ts +2 -2
  90. package/dist/src/trails/topo-store-support.d.ts.map +1 -1
  91. package/dist/src/trails/topo-store-support.js +22 -22
  92. package/dist/src/trails/topo-store-support.js.map +1 -1
  93. package/dist/src/trails/topo-support.d.ts +19 -30
  94. package/dist/src/trails/topo-support.d.ts.map +1 -1
  95. package/dist/src/trails/topo-support.js +63 -96
  96. package/dist/src/trails/topo-support.js.map +1 -1
  97. package/dist/src/trails/topo-unpin.d.ts +9 -4
  98. package/dist/src/trails/topo-unpin.d.ts.map +1 -1
  99. package/dist/src/trails/topo-unpin.js +9 -4
  100. package/dist/src/trails/topo-unpin.js.map +1 -1
  101. package/dist/src/trails/topo-verify.d.ts +2 -2
  102. package/dist/src/trails/topo-verify.d.ts.map +1 -1
  103. package/dist/src/trails/topo-verify.js +1 -5
  104. package/dist/src/trails/topo-verify.js.map +1 -1
  105. package/dist/src/trails/topo.d.ts +2 -2
  106. package/dist/src/trails/topo.d.ts.map +1 -1
  107. package/dist/src/trails/topo.js +6 -10
  108. package/dist/src/trails/topo.js.map +1 -1
  109. package/dist/src/trails/warden.d.ts +3 -2
  110. package/dist/src/trails/warden.d.ts.map +1 -1
  111. package/dist/src/trails/warden.js +11 -2
  112. package/dist/src/trails/warden.js.map +1 -1
  113. package/dist/src/versions.d.ts +12 -0
  114. package/dist/src/versions.d.ts.map +1 -0
  115. package/dist/src/versions.js +23 -0
  116. package/dist/src/versions.js.map +1 -0
  117. package/dist/tsconfig.tsbuildinfo +1 -1
  118. package/package.json +8 -8
  119. package/src/__tests__/add-trail.test.ts +97 -0
  120. package/src/__tests__/create.test.ts +91 -27
  121. package/src/__tests__/guide.test.ts +10 -5
  122. package/src/__tests__/load-app.test.ts +365 -4
  123. package/src/__tests__/survey.test.ts +143 -67
  124. package/src/__tests__/topo-dev.test.ts +48 -46
  125. package/src/app.ts +2 -2
  126. package/src/clack.ts +1 -1
  127. package/src/cli.ts +4 -3
  128. package/src/trails/add-surface.ts +150 -0
  129. package/src/trails/add-trail.ts +46 -10
  130. package/src/trails/add-verify.ts +11 -6
  131. package/src/trails/create-scaffold.ts +16 -3
  132. package/src/trails/create.ts +76 -71
  133. package/src/trails/dev-clean.ts +25 -21
  134. package/src/trails/dev-reset.ts +3 -2
  135. package/src/trails/dev-stats.ts +22 -19
  136. package/src/trails/dev-support.ts +88 -86
  137. package/src/trails/draft-promote.ts +70 -35
  138. package/src/trails/guide.ts +1 -4
  139. package/src/trails/load-app.ts +511 -32
  140. package/src/trails/survey.ts +60 -35
  141. package/src/trails/topo-export.ts +5 -9
  142. package/src/trails/topo-history.ts +8 -10
  143. package/src/trails/topo-pin.ts +9 -13
  144. package/src/trails/topo-read-support.ts +103 -106
  145. package/src/trails/topo-reports.ts +54 -47
  146. package/src/trails/topo-show.ts +8 -12
  147. package/src/trails/topo-store-support.ts +36 -28
  148. package/src/trails/topo-support.ts +90 -134
  149. package/src/trails/topo-unpin.ts +11 -6
  150. package/src/trails/topo-verify.ts +1 -5
  151. package/src/trails/topo.ts +6 -10
  152. package/src/trails/warden.ts +12 -3
  153. package/src/versions.ts +43 -0
  154. package/tsconfig.tests.json +10 -0
  155. 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, provision, topo, trail } from '@ontrails/core';
11
+ import { ConflictError, Result, resource, topo, trail } from '@ontrails/core';
12
12
  import {
13
- generateTrailheadMap,
14
- hashTrailheadMap,
15
- diffTrailheadMaps,
13
+ deriveSurfaceMap,
14
+ deriveSurfaceMapHash,
15
+ deriveSurfaceMapDiff,
16
+ writeSurfaceMap,
16
17
  } from '@ontrails/schema';
17
- import type { TrailheadMap } from '@ontrails/schema';
18
+ import type { SurfaceMap } from '@ontrails/schema';
18
19
  import { z } from 'zod';
19
20
 
20
21
  import {
21
- generateBriefReport,
22
- generateSurveyList,
23
- generateTrailDetail,
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
- NotFoundError: ['search'],
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
- provisions: [
56
- provision('db.main', {
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 [dbProvision] = helloTrail.provisions;
70
- if (!dbProvision) {
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
- dbProvision,
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 = (dir: string): void => {
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, provision, topo, trail } from '@ontrails/core';
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
- provisions: [
100
- provision('db.main', {
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.provisions;
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
- export const app = topo('survey-fixture', { dbMain, hello });
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('generateTrailheadMap includes all trails', () => {
129
- const trailheadMap = generateTrailheadMap(app);
130
- expect(trailheadMap.entries.length).toBe(3);
131
- const ids = trailheadMap.entries.map((e) => e.id);
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('trailhead map entries have expected fields', () => {
138
- const trailheadMap = generateTrailheadMap(app);
139
- const hello = trailheadMap.entries.find((e) => e.id === '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?.provisions).toEqual(['db.main']);
169
+ expect(hello?.resources).toEqual(['db.main']);
146
170
  });
147
171
 
148
172
  test('JSON output is valid JSON', () => {
149
- const trailheadMap = generateTrailheadMap(app);
150
- const json = JSON.stringify(trailheadMap, null, 2);
151
- const parsed = JSON.parse(json) as TrailheadMap;
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('hashTrailheadMap produces stable hash', () => {
157
- const trailheadMap = generateTrailheadMap(app);
158
- const hash1 = hashTrailheadMap(trailheadMap);
159
- const hash2 = hashTrailheadMap(trailheadMap);
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('diffTrailheadMaps detects added trails', () => {
166
- const prev = generateTrailheadMap(topo('test', { hello: helloTrail }));
167
- const curr = generateTrailheadMap(app);
168
- const diff = diffTrailheadMaps(prev, curr);
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('diffTrailheadMaps detects removed trails', () => {
177
- const prev = generateTrailheadMap(app);
178
- const curr = generateTrailheadMap(topo('test', { hello: helloTrail }));
179
- const diff = diffTrailheadMaps(prev, curr);
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('diffTrailheadMaps returns empty for identical maps', () => {
188
- const trailheadMap = generateTrailheadMap(app);
189
- const diff = diffTrailheadMaps(trailheadMap, trailheadMap);
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 = generateBriefReport(app);
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 = generateBriefReport(app);
231
+ const report = deriveBriefReport(app);
208
232
  expect(report.trails).toBe(2);
209
233
  expect(report.signals).toBe(0);
210
- expect(report.provisions).toBe(1);
234
+ expect(report.resources).toBe(1);
211
235
  });
212
236
 
213
237
  test('detects features in use', () => {
214
- const report = generateBriefReport(app);
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.provisions).toBe(true);
243
+ expect(report.features.resources).toBe(true);
220
244
  });
221
245
 
222
246
  test('JSON output is valid', () => {
223
- const report = generateBriefReport(app);
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.provisions).toBe(1);
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 = generateBriefReport(emptyApp);
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.provisions).toBe(false);
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 provisions, crossings, and intent', () => {
244
- const detail = generateTrailDetail(helloTrail);
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.provisions).toEqual(['db.main']);
273
+ expect(parsed.resources).toEqual(['db.main']);
250
274
  });
251
275
  });
252
276
 
253
- describe('trails survey provisions section', () => {
254
- test('list output includes provision lifetime and health status', () => {
255
- const report = generateSurveyList(app);
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.provisions.find((entry) => entry.id === 'db.main');
281
+ const db = parsed.resources.find((entry) => entry.id === 'db.main');
258
282
 
259
- expect(parsed.provisionCount).toBe(1);
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: 'provision',
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', '_trailhead.json'))).toBe(true);
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/tracker';
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, provision, topo, trail } from '@ontrails/core';
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
- provisions: [
68
- provision('db.main', {
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.provisions;
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.provisionCount).toBe(1);
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.provisions).toEqual(['db.main']);
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', '_trailhead.json'))).toBe(true);
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
- writeFileSync(
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(legacySummary.lockExists).toBe(true);
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', 'trailhead.lock'), 'stale\n');
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
- provisionCount: 1,
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
- provisions: true,
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
- provisions: ['db.main'],
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.pin.name).toBe('before-auth');
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 save_id = ?'
260
+ 'SELECT COUNT(*) as count FROM topo_trails WHERE snapshot_id = ?'
268
261
  )
269
- .get(firstPin.save.id);
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 save_id = ?'
265
+ 'SELECT COUNT(*) as count FROM topo_trails WHERE snapshot_id = ?'
273
266
  )
274
- .get(firstExport.save.id);
267
+ .get(firstExport.snapshot.id);
275
268
  const projectedSaves = projectionDb
276
269
  .query<{ count: number }, []>(
277
- 'SELECT COUNT(DISTINCT save_id) as count FROM topo_trails'
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.pinCount).toBe(1);
330
- expect(history.saveCount).toBeGreaterThanOrEqual(3);
331
- expect(history.pins[0]?.saveId).toBe(firstPin.save.id);
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.saves.some((save) => save.id === secondExport.save.id)
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.pinCount).toBe(1);
340
- expect(stats.tracker.recordCount).toBe(2);
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({ dryRun: true, saves: 0, trackAgeMs: 0 }, {
344
- cwd: dir,
345
- } as never)
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.topoSaves).toBeGreaterThanOrEqual(2);
349
- expect(cleanPreview.removed.trackRecords).toBe(2);
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, saves: 0, trackAgeMs: 0, yes: true },
355
+ { dryRun: false, snapshots: 0, traceAgeMs: 0, yes: true },
354
356
  { cwd: dir } as never
355
357
  )
356
358
  );
357
- expect(cleanResult.removed.trackRecords).toBe(2);
358
- expect(cleanResult.remaining.pinCount).toBe(1);
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.pin?.name).toBe('before-auth');
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.topoSaves).toBe(0);
408
- expect(preview.removed.trackRecords).toBe(0);
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.topoSaves).toBe(0);
418
- expect(applied.removed.trackRecords).toBe(0);
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 addTrailhead from './trails/add-trailhead.js';
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
- addTrailhead,
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 gate so @ontrails/cli remains prompt-library agnostic.
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 { trailhead } from '@ontrails/cli/commander';
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
- trailhead(app, {
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: '0.1.0',
14
+ version: trailsPackageVersion,
14
15
  });