@ontrails/trails 1.0.0-beta.12 → 1.0.0-beta.13

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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `survey` trail -- Full topo introspection.
3
3
  *
4
- * Lists trails, shows detail for individual trails, generates surface maps,
4
+ * Lists trails, shows detail for individual trails, generates trailhead maps,
5
5
  * and diffs against previous versions.
6
6
  */
7
7
 
@@ -9,13 +9,13 @@ import type { Topo, Trail } from '@ontrails/core';
9
9
  import { Result, trail } from '@ontrails/core';
10
10
  import type { DiffResult } from '@ontrails/schema';
11
11
  import {
12
- diffSurfaceMaps,
12
+ diffTrailheadMaps,
13
13
  generateOpenApiSpec,
14
- generateSurfaceMap,
15
- hashSurfaceMap,
16
- readSurfaceMap,
17
- writeSurfaceLock,
18
- writeSurfaceMap,
14
+ generateTrailheadMap,
15
+ hashTrailheadMap,
16
+ readTrailheadMap,
17
+ writeTrailheadLock,
18
+ writeTrailheadMap,
19
19
  } from '@ontrails/schema';
20
20
  import { z } from 'zod';
21
21
 
@@ -30,15 +30,15 @@ export interface BriefReport {
30
30
  readonly version: string;
31
31
  readonly contractVersion: string;
32
32
  readonly features: {
33
- readonly services: boolean;
33
+ readonly provisions: boolean;
34
34
  readonly outputSchemas: boolean;
35
35
  readonly examples: boolean;
36
36
  readonly detours: boolean;
37
- readonly events: boolean;
37
+ readonly signals: boolean;
38
38
  };
39
39
  readonly trails: number;
40
- readonly events: number;
41
- readonly services: number;
40
+ readonly signals: number;
41
+ readonly provisions: number;
42
42
  }
43
43
 
44
44
  export interface SurveyListReport {
@@ -49,12 +49,12 @@ export interface SurveyListReport {
49
49
  readonly kind: string;
50
50
  readonly safety: string;
51
51
  }[];
52
- readonly serviceCount: number;
53
- readonly services: readonly {
52
+ readonly provisionCount: number;
53
+ readonly provisions: readonly {
54
54
  readonly description: string | null;
55
55
  readonly health: 'available' | 'none';
56
56
  readonly id: string;
57
- readonly kind: 'service';
57
+ readonly kind: 'provision';
58
58
  readonly lifetime: 'singleton';
59
59
  readonly usedBy: readonly string[];
60
60
  }[];
@@ -64,12 +64,12 @@ export interface TrailDetailReport {
64
64
  readonly description: string | null;
65
65
  readonly detours: Trail<unknown, unknown>['detours'] | null;
66
66
  readonly examples: readonly unknown[];
67
- readonly follow: readonly string[];
67
+ readonly crosses: readonly string[];
68
68
  readonly id: string;
69
69
  readonly intent: 'read' | 'write' | 'destroy';
70
70
  readonly kind: string;
71
71
  readonly safety: string;
72
- readonly services: readonly string[];
72
+ readonly provisions: readonly string[];
73
73
  }
74
74
 
75
75
  /** Check if a trail has a specific feature. */
@@ -87,7 +87,7 @@ const detectFeatures = (
87
87
  hasDetours: boolean;
88
88
  hasExamples: boolean;
89
89
  hasOutputSchemas: boolean;
90
- hasServices: boolean;
90
+ hasProvisions: boolean;
91
91
  } => {
92
92
  const trails = [...app.trails.values()].map(
93
93
  (item) => item as unknown as Record<string, unknown>
@@ -96,30 +96,31 @@ const detectFeatures = (
96
96
  hasDetours: trails.some((r) => trailHas(r, 'detours')),
97
97
  hasExamples: trails.some((r) => trailHas(r, 'examples')),
98
98
  hasOutputSchemas: trails.some((r) => trailHas(r, 'output')),
99
- hasServices: trails.some(
99
+ hasProvisions: trails.some(
100
100
  (r) =>
101
- Array.isArray(r['services']) && (r['services'] as unknown[]).length > 0
101
+ Array.isArray(r['provisions']) &&
102
+ (r['provisions'] as unknown[]).length > 0
102
103
  ),
103
104
  };
104
105
  };
105
106
 
106
107
  /** Generate a compact capability report for the given topo. */
107
108
  export const generateBriefReport = (app: Topo): BriefReport => {
108
- const { hasDetours, hasExamples, hasOutputSchemas, hasServices } =
109
+ const { hasDetours, hasExamples, hasOutputSchemas, hasProvisions } =
109
110
  detectFeatures(app);
110
111
 
111
112
  return {
112
113
  contractVersion: '2026-03',
113
- events: app.events.size,
114
114
  features: {
115
115
  detours: hasDetours,
116
- events: app.events.size > 0,
117
116
  examples: hasExamples,
118
117
  outputSchemas: hasOutputSchemas,
119
- services: hasServices,
118
+ provisions: hasProvisions,
119
+ signals: app.signals.size > 0,
120
120
  },
121
121
  name: app.name,
122
- services: app.services.size,
122
+ provisions: app.provisions.size,
123
+ signals: app.signals.size,
123
124
  trails: app.trails.size,
124
125
  version: '0.1.0',
125
126
  };
@@ -141,16 +142,16 @@ const safetyLabel = (entry: {
141
142
  return '-';
142
143
  };
143
144
 
144
- const buildServiceUsage = (
145
+ const buildProvisionUsage = (
145
146
  app: Topo
146
147
  ): ReadonlyMap<string, readonly string[]> => {
147
148
  const usage = new Map<string, string[]>();
148
149
 
149
150
  for (const trailDef of app.list()) {
150
- for (const declaredService of trailDef.services) {
151
- const users = usage.get(declaredService.id) ?? [];
151
+ for (const declaredProvision of trailDef.provisions) {
152
+ const users = usage.get(declaredProvision.id) ?? [];
152
153
  users.push(trailDef.id);
153
- usage.set(declaredService.id, users);
154
+ usage.set(declaredProvision.id, users);
154
155
  }
155
156
  }
156
157
 
@@ -159,22 +160,22 @@ const buildServiceUsage = (
159
160
  );
160
161
  };
161
162
 
162
- const serviceHealthStatus = (service: {
163
+ const provisionHealthStatus = (provision: {
163
164
  health?: unknown;
164
165
  }): 'available' | 'none' =>
165
- service.health === undefined ? 'none' : 'available';
166
+ provision.health === undefined ? 'none' : 'available';
166
167
 
167
- const formatServiceList = (app: Topo): SurveyListReport['services'] => {
168
- const usage = buildServiceUsage(app);
168
+ const formatProvisionList = (app: Topo): SurveyListReport['provisions'] => {
169
+ const usage = buildProvisionUsage(app);
169
170
  return app
170
- .listServices()
171
- .map((service) => ({
172
- description: service.description ?? null,
173
- health: serviceHealthStatus(service),
174
- id: service.id,
175
- kind: service.kind,
171
+ .listProvisions()
172
+ .map((provision) => ({
173
+ description: provision.description ?? null,
174
+ health: provisionHealthStatus(provision),
175
+ id: provision.id,
176
+ kind: provision.kind,
176
177
  lifetime: 'singleton' as const,
177
- usedBy: usage.get(service.id) ?? [],
178
+ usedBy: usage.get(provision.id) ?? [],
178
179
  }))
179
180
  .toSorted((a, b) => a.id.localeCompare(b.id));
180
181
  };
@@ -199,13 +200,13 @@ export const generateSurveyList = (app: Topo): SurveyListReport => {
199
200
  };
200
201
  });
201
202
 
202
- const services = formatServiceList(app);
203
+ const provisions = formatProvisionList(app);
203
204
 
204
205
  return {
205
206
  count: items.length,
206
207
  entries,
207
- serviceCount: services.length,
208
- services,
208
+ provisionCount: provisions.length,
209
+ provisions,
209
210
  };
210
211
  };
211
212
 
@@ -213,8 +214,8 @@ export const generateSurveyList = (app: Topo): SurveyListReport => {
213
214
  * Build a human-readable detail view for a single trail.
214
215
  *
215
216
  * Overlaps with `trailToEntry` in `@ontrails/schema` which builds the
216
- * surface-map entry. The two serve different audiences (human display vs
217
- * machine-diffable surface map) so they are kept separate.
217
+ * trailhead-map entry. The two serve different audiences (human display vs
218
+ * machine-diffable trailhead map) so they are kept separate.
218
219
  */
219
220
  export const generateTrailDetail = (
220
221
  item: Trail<unknown, unknown>
@@ -224,27 +225,27 @@ export const generateTrailDetail = (
224
225
  );
225
226
 
226
227
  return {
228
+ crosses: item.crosses.toSorted(),
227
229
  description: item.description ?? null,
228
230
  detours: item.detours ?? null,
229
231
  examples: item.examples ?? [],
230
- follow: item.follow.toSorted(),
231
232
  id: item.id,
232
233
  intent: item.intent,
233
234
  kind: item.kind,
235
+ provisions: item.provisions.map((provision) => provision.id).toSorted(),
234
236
  safety,
235
- services: item.services.map((service) => service.id).toSorted(),
236
237
  };
237
238
  };
238
239
 
239
- const formatServiceDetail = (app: Topo, serviceId: string): object => {
240
- const item = app.getService(serviceId);
241
- const usedBy = buildServiceUsage(app).get(serviceId) ?? [];
240
+ const formatProvisionDetail = (app: Topo, provisionId: string): object => {
241
+ const item = app.getProvision(provisionId);
242
+ const usedBy = buildProvisionUsage(app).get(provisionId) ?? [];
242
243
 
243
244
  return {
244
245
  description: item?.description ?? null,
245
- health: item ? serviceHealthStatus(item) : 'none',
246
- id: serviceId,
247
- kind: 'service',
246
+ health: item ? provisionHealthStatus(item) : 'none',
247
+ id: provisionId,
248
+ kind: 'provision',
248
249
  lifetime: 'singleton',
249
250
  usedBy,
250
251
  };
@@ -261,17 +262,17 @@ const buildSurveyDiff = async (
261
262
  app: Topo,
262
263
  breakingOnly: boolean
263
264
  ): Promise<Result<object, Error>> => {
264
- const currentMap = generateSurfaceMap(app);
265
- const previousMap = await readSurfaceMap();
265
+ const currentMap = generateTrailheadMap(app);
266
+ const previousMap = await readTrailheadMap();
266
267
  if (!previousMap) {
267
268
  return Result.err(
268
269
  new Error(
269
- 'No previous surface map found. Run `trails survey generate` first.'
270
+ 'No previous trailhead map found. Run `trails survey generate` first.'
270
271
  )
271
272
  );
272
273
  }
273
274
 
274
- const diff = diffSurfaceMaps(previousMap, currentMap);
275
+ const diff = diffTrailheadMaps(previousMap, currentMap);
275
276
  return Result.ok(
276
277
  breakingOnly
277
278
  ? formatDiff({
@@ -292,19 +293,19 @@ const buildSurveyDetail = (
292
293
  if (item) {
293
294
  return Result.ok(generateTrailDetail(item as Trail<unknown, unknown>));
294
295
  }
295
- if (app.getService(trailId)) {
296
- return Result.ok(formatServiceDetail(app, trailId));
296
+ if (app.getProvision(trailId)) {
297
+ return Result.ok(formatProvisionDetail(app, trailId));
297
298
  }
298
- return Result.err(new Error(`Trail or service not found: ${trailId}`));
299
+ return Result.err(new Error(`Trail or provision not found: ${trailId}`));
299
300
  };
300
301
 
301
302
  const buildSurveyGenerate = async (
302
303
  app: Topo
303
304
  ): Promise<Result<object, Error>> => {
304
- const surfaceMap = generateSurfaceMap(app);
305
- const mapPath = await writeSurfaceMap(surfaceMap);
306
- const hash = hashSurfaceMap(surfaceMap);
307
- const lockPath = await writeSurfaceLock(hash);
305
+ const trailheadMap = generateTrailheadMap(app);
306
+ const mapPath = await writeTrailheadMap(trailheadMap);
307
+ const hash = hashTrailheadMap(trailheadMap);
308
+ const lockPath = await writeTrailheadLock(hash);
308
309
  return Result.ok({ hash, lockPath, mapPath });
309
310
  };
310
311
 
@@ -362,21 +363,25 @@ const dispatchSurvey = (
362
363
  // ---------------------------------------------------------------------------
363
364
 
364
365
  export const surveyTrail = trail('survey', {
366
+ blaze: async (input, ctx) => {
367
+ const app = await loadApp(input.module, ctx.cwd ?? '.');
368
+ return dispatchSurvey(app, input);
369
+ },
365
370
  description: 'Full topo introspection',
366
371
  examples: [
367
372
  {
368
- description: 'Lists all registered trails with safety and surface info',
369
- input: {},
373
+ description: 'Lists all registered trails with safety and trailhead info',
374
+ input: { module: './src/app.ts' },
370
375
  name: 'List all trails',
371
376
  },
372
377
  {
373
378
  description: 'Quick capability summary with counts and feature flags',
374
- input: { brief: true },
379
+ input: { brief: true, module: './src/app.ts' },
375
380
  name: 'Brief capability report',
376
381
  },
377
382
  {
378
383
  description: 'Generate an OpenAPI 3.1 specification for the topo',
379
- input: { openapi: true },
384
+ input: { module: './src/app.ts', openapi: true },
380
385
  name: 'OpenAPI spec',
381
386
  },
382
387
  ],
@@ -390,7 +395,7 @@ export const surveyTrail = trail('survey', {
390
395
  generate: z
391
396
  .boolean()
392
397
  .default(false)
393
- .describe('Generate surface map and lock file'),
398
+ .describe('Generate trailhead map and lock file'),
394
399
  module: z
395
400
  .string()
396
401
  .default('./src/app.ts')
@@ -410,13 +415,13 @@ export const surveyTrail = trail('survey', {
410
415
  safety: z.string(),
411
416
  })
412
417
  ),
413
- serviceCount: z.number(),
414
- services: z.array(
418
+ provisionCount: z.number(),
419
+ provisions: z.array(
415
420
  z.object({
416
421
  description: z.string().nullable(),
417
422
  health: z.enum(['available', 'none']),
418
423
  id: z.string(),
419
- kind: z.literal('service'),
424
+ kind: z.literal('provision'),
420
425
  lifetime: z.literal('singleton'),
421
426
  usedBy: z.array(z.string()),
422
427
  })
@@ -424,16 +429,16 @@ export const surveyTrail = trail('survey', {
424
429
  }),
425
430
  z.object({
426
431
  contractVersion: z.string(),
427
- events: z.number(),
428
432
  features: z.object({
429
433
  detours: z.boolean(),
430
- events: z.boolean(),
431
434
  examples: z.boolean(),
432
435
  outputSchemas: z.boolean(),
433
- services: z.boolean(),
436
+ provisions: z.boolean(),
437
+ signals: z.boolean(),
434
438
  }),
435
439
  name: z.string(),
436
- services: z.number(),
440
+ provisions: z.number(),
441
+ signals: z.number(),
437
442
  trails: z.number(),
438
443
  version: z.string(),
439
444
  }),
@@ -444,21 +449,21 @@ export const surveyTrail = trail('survey', {
444
449
  warnings: z.array(z.unknown()),
445
450
  }),
446
451
  z.object({
452
+ crosses: z.array(z.string()),
447
453
  description: z.unknown().nullable(),
448
454
  detours: z.unknown().nullable(),
449
455
  examples: z.array(z.unknown()),
450
- follow: z.array(z.string()),
451
456
  id: z.string(),
452
457
  intent: z.enum(['read', 'write', 'destroy']),
453
458
  kind: z.string(),
459
+ provisions: z.array(z.string()),
454
460
  safety: z.string(),
455
- services: z.array(z.string()),
456
461
  }),
457
462
  z.object({
458
463
  description: z.string().nullable(),
459
464
  health: z.enum(['available', 'none']),
460
465
  id: z.string(),
461
- kind: z.literal('service'),
466
+ kind: z.literal('provision'),
462
467
  lifetime: z.literal('singleton'),
463
468
  usedBy: z.array(z.string()),
464
469
  }),
@@ -488,8 +493,4 @@ export const surveyTrail = trail('survey', {
488
493
  .optional(),
489
494
  }),
490
495
  ]),
491
- run: async (input, ctx) => {
492
- const app = await loadApp(input.module, ctx.cwd ?? '.');
493
- return dispatchSurvey(app, input);
494
- },
495
496
  });
@@ -21,6 +21,38 @@ import { loadApp } from './load-app.js';
21
21
  // ---------------------------------------------------------------------------
22
22
 
23
23
  export const wardenTrail = trail('warden', {
24
+ blaze: async (input, ctx) => {
25
+ const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
26
+ // oxlint-disable-next-line prefer-await-to-then -- catch converts rejection to undefined cleanly
27
+ const topo = await loadApp('./src/app.ts', rootDir).catch(
28
+ (): undefined => undefined
29
+ );
30
+
31
+ const report = await runWarden({
32
+ driftOnly: input.driftOnly,
33
+ lintOnly: input.lintOnly,
34
+ rootDir,
35
+ topo,
36
+ });
37
+
38
+ const formatters: Record<string, (r: typeof report) => string> = {
39
+ github: formatGitHubAnnotations,
40
+ json: formatJson,
41
+ summary: formatSummary,
42
+ text: formatWardenReport,
43
+ };
44
+ const formatter = formatters[input.format] ?? formatWardenReport;
45
+ const formatted = formatter(report);
46
+
47
+ return Result.ok({
48
+ diagnostics: report.diagnostics,
49
+ drift: report.drift,
50
+ errorCount: report.errorCount,
51
+ formatted,
52
+ passed: report.passed,
53
+ warnCount: report.warnCount,
54
+ });
55
+ },
24
56
  description: 'Run governance checks (lint + drift)',
25
57
  examples: [
26
58
  {
@@ -69,36 +101,4 @@ export const wardenTrail = trail('warden', {
69
101
  passed: z.boolean(),
70
102
  warnCount: z.number(),
71
103
  }),
72
- run: async (input, ctx) => {
73
- const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
74
- // oxlint-disable-next-line prefer-await-to-then -- catch converts rejection to undefined cleanly
75
- const topo = await loadApp('./src/app.ts', rootDir).catch(
76
- (): undefined => undefined
77
- );
78
-
79
- const report = await runWarden({
80
- driftOnly: input.driftOnly,
81
- lintOnly: input.lintOnly,
82
- rootDir,
83
- topo,
84
- });
85
-
86
- const formatters: Record<string, (r: typeof report) => string> = {
87
- github: formatGitHubAnnotations,
88
- json: formatJson,
89
- summary: formatSummary,
90
- text: formatWardenReport,
91
- };
92
- const formatter = formatters[input.format] ?? formatWardenReport;
93
- const formatted = formatter(report);
94
-
95
- return Result.ok({
96
- diagnostics: report.diagnostics,
97
- drift: report.drift,
98
- errorCount: report.errorCount,
99
- formatted,
100
- passed: report.passed,
101
- warnCount: report.warnCount,
102
- });
103
- },
104
104
  });