@ontrails/trails 1.0.0-beta.18 → 1.0.0-beta.19

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 (45) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +7 -10
  3. package/package.json +13 -12
  4. package/src/app.ts +14 -4
  5. package/src/cli.ts +16 -0
  6. package/src/lifecycle-source-io.ts +33 -0
  7. package/src/project-writes.ts +62 -5
  8. package/src/retired-topo-command.ts +36 -0
  9. package/src/run-adapter-check.ts +76 -0
  10. package/src/run-collision.ts +1 -0
  11. package/src/trails/adapter-check.ts +244 -0
  12. package/src/trails/add-surface.ts +18 -18
  13. package/src/trails/add-trail.ts +3 -2
  14. package/src/trails/add-verify.ts +30 -6
  15. package/src/trails/{topo-compile.ts → compile.ts} +16 -8
  16. package/src/trails/completions-complete.ts +1 -1
  17. package/src/trails/create-adapter.ts +1084 -0
  18. package/src/trails/create-scaffold.ts +243 -29
  19. package/src/trails/create.ts +118 -17
  20. package/src/trails/deprecate.ts +59 -0
  21. package/src/trails/dev-clean.ts +2 -2
  22. package/src/trails/dev-reset.ts +2 -2
  23. package/src/trails/dev-stats.ts +1 -1
  24. package/src/trails/doctor.ts +56 -0
  25. package/src/trails/draft-promote.ts +1 -0
  26. package/src/trails/guide.ts +2 -2
  27. package/src/trails/revise.ts +53 -0
  28. package/src/trails/run-example.ts +12 -7
  29. package/src/trails/run-examples.ts +3 -3
  30. package/src/trails/run.ts +7 -4
  31. package/src/trails/survey.ts +332 -25
  32. package/src/trails/topo-history.ts +1 -1
  33. package/src/trails/topo-output-schemas.ts +30 -1
  34. package/src/trails/topo-pin.ts +3 -2
  35. package/src/trails/topo-read-support.ts +49 -8
  36. package/src/trails/topo-reports.ts +39 -22
  37. package/src/trails/topo-store-support.ts +62 -16
  38. package/src/trails/topo-support.ts +1 -1
  39. package/src/trails/topo-unpin.ts +2 -2
  40. package/src/trails/topo.ts +2 -2
  41. package/src/trails/{topo-verify.ts → validate.ts} +7 -7
  42. package/src/trails/version-lifecycle-support.ts +945 -0
  43. package/src/trails/warden-guide.ts +8 -0
  44. package/src/trails/warden.ts +18 -2
  45. package/src/versions.ts +4 -1
@@ -2,6 +2,7 @@ import {
2
2
  DETOUR_MAX_ATTEMPTS_CAP,
3
3
  deriveCliPath,
4
4
  filterSurfaceTrails,
5
+ isArchivedTrailVersionEntry,
5
6
  zodToJsonSchema,
6
7
  } from '@ontrails/core';
7
8
  import type { AnyTrail, Signal, Topo } from '@ontrails/core';
@@ -16,6 +17,7 @@ import type {
16
17
  TopoGraphEntry,
17
18
  TopoGraphFieldOverride,
18
19
  TopoGraphLayerReference,
20
+ TopoGraphVersionEntry,
19
21
  } from '@ontrails/topographer';
20
22
  import { z } from 'zod';
21
23
 
@@ -196,7 +198,7 @@ export interface TrailDetailReport {
196
198
  | null;
197
199
  readonly examples: readonly unknown[];
198
200
  readonly fieldOverrides: readonly TopoGraphFieldOverride[];
199
- readonly crosses: readonly string[];
201
+ readonly composes: readonly string[];
200
202
  readonly fires: readonly string[];
201
203
  readonly governance: Readonly<Record<string, unknown>> | null;
202
204
  readonly id: string;
@@ -211,6 +213,9 @@ export interface TrailDetailReport {
211
213
  readonly resources: readonly string[];
212
214
  readonly surfaceProjections: readonly ShippedSurfaceProjection[];
213
215
  readonly surfaces: readonly string[];
216
+ readonly supports: readonly number[];
217
+ readonly version: number | null;
218
+ readonly versions: Readonly<Record<string, TopoGraphVersionEntry>>;
214
219
  }
215
220
 
216
221
  export interface SignalDetailReport {
@@ -231,13 +236,20 @@ export interface SignalDetailReport {
231
236
  readonly producers: readonly string[];
232
237
  }
233
238
 
234
- const trailHas = (raw: Record<string, unknown>, key: string): boolean => {
235
- if (key === 'examples' || key === 'detours') {
236
- return Array.isArray(raw[key]) && (raw[key] as unknown[]).length > 0;
239
+ const countLiveTrailVersionExamples = (trail: AnyTrail): number => {
240
+ let count = 0;
241
+ for (const entry of Object.values(trail.versions ?? {})) {
242
+ if (isArchivedTrailVersionEntry(entry)) {
243
+ continue;
244
+ }
245
+ count += entry.examples?.length ?? 0;
237
246
  }
238
- return Boolean(raw[key]);
247
+ return count;
239
248
  };
240
249
 
250
+ export const countTrailExamples = (trail: AnyTrail): number =>
251
+ (trail.examples?.length ?? 0) + countLiveTrailVersionExamples(trail);
252
+
241
253
  const detectFeatures = (
242
254
  app: Topo
243
255
  ): {
@@ -246,18 +258,12 @@ const detectFeatures = (
246
258
  hasOutputSchemas: boolean;
247
259
  hasResources: boolean;
248
260
  } => {
249
- const trails = [...app.trails.values()].map(
250
- (item) => item as unknown as Record<string, unknown>
251
- );
261
+ const trails = [...app.trails.values()];
252
262
  return {
253
- hasDetours: trails.some((r) => trailHas(r, 'detours')),
254
- hasExamples: trails.some((r) => trailHas(r, 'examples')),
255
- hasOutputSchemas: trails.some((r) => trailHas(r, 'output')),
256
- hasResources: trails.some(
257
- (r) =>
258
- Array.isArray(r['resources']) &&
259
- (r['resources'] as unknown[]).length > 0
260
- ),
263
+ hasDetours: trails.some((trail) => trail.detours.length > 0),
264
+ hasExamples: trails.some((trail) => countTrailExamples(trail) > 0),
265
+ hasOutputSchemas: trails.some((trail) => trail.output !== undefined),
266
+ hasResources: trails.some((trail) => trail.resources.length > 0),
261
267
  };
262
268
  };
263
269
 
@@ -386,11 +392,7 @@ export const deriveSurveyList = (app: Topo): SurveyListReport => {
386
392
  const safety = safetyLabel(
387
393
  item as unknown as { intent?: 'read' | 'write' | 'destroy' }
388
394
  );
389
- const examples = Array.isArray(
390
- (item as unknown as { examples?: unknown[] }).examples
391
- )
392
- ? (item as unknown as { examples: unknown[] }).examples.length
393
- : 0;
395
+ const examples = countTrailExamples(item);
394
396
 
395
397
  return {
396
398
  activatedBy: trailActivation.activatedBy,
@@ -657,6 +659,14 @@ const deriveResolvedSurfaceProjections = (
657
659
  : deriveShippedSurfaceProjectionsForTrail(app, trail, topoGraph);
658
660
  };
659
661
 
662
+ const deriveResolvedTrailVersionDetail = (
663
+ topoEntry: TopoGraphEntry | undefined
664
+ ): Pick<TrailDetailReport, 'supports' | 'version' | 'versions'> => ({
665
+ supports: topoEntry?.supports ?? [],
666
+ version: topoEntry?.version ?? null,
667
+ versions: topoEntry?.versions ?? {},
668
+ });
669
+
660
670
  const deriveResolvedTrailGraphDetail = (
661
671
  app: Topo | undefined,
662
672
  trailId: string,
@@ -676,6 +686,9 @@ const deriveResolvedTrailGraphDetail = (
676
686
  | 'output'
677
687
  | 'surfaceProjections'
678
688
  | 'surfaces'
689
+ | 'supports'
690
+ | 'version'
691
+ | 'versions'
679
692
  > => {
680
693
  const topoGraph =
681
694
  topoGraphOverride ?? (app === undefined ? undefined : deriveTopoGraph(app));
@@ -714,6 +727,7 @@ const deriveResolvedTrailGraphDetail = (
714
727
  topoGraph
715
728
  ),
716
729
  surfaces: topoEntry?.surfaces ?? [],
730
+ ...deriveResolvedTrailVersionDetail(topoEntry),
717
731
  };
718
732
  };
719
733
 
@@ -765,9 +779,9 @@ export const deriveTrailDetail = (
765
779
  topo: topoLayerNames,
766
780
  trail: trailLayerNames,
767
781
  },
782
+ composes: item.composes.toSorted(),
768
783
  contourDetails: graphDetail.contourDetails,
769
784
  contours: graphDetail.contours,
770
- crosses: item.crosses.toSorted(),
771
785
  description: item.description ?? null,
772
786
  detours: formatTrailDetours(item),
773
787
  examples: item.examples ?? [],
@@ -784,7 +798,10 @@ export const deriveTrailDetail = (
784
798
  pattern: item.pattern ?? null,
785
799
  resources: item.resources.map((resource) => resource.id).toSorted(),
786
800
  safety,
801
+ supports: graphDetail.supports,
787
802
  surfaceProjections: graphDetail.surfaceProjections,
788
803
  surfaces: graphDetail.surfaces,
804
+ version: graphDetail.version,
805
+ versions: graphDetail.versions,
789
806
  };
790
807
  };
@@ -9,6 +9,7 @@ import { Database } from 'bun:sqlite';
9
9
 
10
10
  import type { Topo } from '@ontrails/core';
11
11
  import {
12
+ ConflictError,
12
13
  deriveTrailsDir,
13
14
  InternalError,
14
15
  openWriteTrailsDb,
@@ -20,7 +21,15 @@ import type {
20
21
  TopoSnapshot,
21
22
  } from '@ontrails/topographer';
22
23
  import type { StoredTopoExport } from '@ontrails/topographer/backend-support';
23
- import { writeLockManifest, writeTopoGraph } from '@ontrails/topographer';
24
+ import {
25
+ annotateTopoGraphForces,
26
+ carryForwardTopoGraphForces,
27
+ deriveTopoGraphDiff,
28
+ deriveTopoGraphHash,
29
+ readTopoGraph,
30
+ writeLockManifest,
31
+ writeTopoGraph,
32
+ } from '@ontrails/topographer';
24
33
  import {
25
34
  createStoredTopoSnapshot,
26
35
  getStoredTopoExport,
@@ -85,19 +94,46 @@ export const deriveCurrentTopoExport = (
85
94
 
86
95
  const writeStoredExportArtifacts = async (
87
96
  storedExport: StoredTopoExport,
88
- trailsDir: string
97
+ trailsDir: string,
98
+ options?: { readonly force?: boolean | undefined }
89
99
  ): Promise<Pick<TopoExportReport, 'hash' | 'lockPath' | 'topoPath'>> => {
90
- const topoPath = await writeTopoGraph(
91
- JSON.parse(storedExport.topoGraphJson) as TopoGraph,
92
- { dir: trailsDir }
93
- );
94
- const lockPath = await writeLockManifest(
95
- JSON.parse(storedExport.lockManifestJson) as LockManifest,
96
- { dir: trailsDir }
97
- );
100
+ const previousTopo = await readTopoGraph({ dir: trailsDir });
101
+ const nextTopo = JSON.parse(storedExport.topoGraphJson) as TopoGraph;
102
+ const diff =
103
+ previousTopo === null
104
+ ? undefined
105
+ : deriveTopoGraphDiff(previousTopo, nextTopo);
106
+ if (diff !== undefined && diff.breaking.length > 0 && !options?.force) {
107
+ throw new ConflictError(
108
+ `Topo contains ${diff.breaking.length} breaking change(s). Add a version entry, revert the change, or rerun with --force.`
109
+ );
110
+ }
111
+
112
+ const topoGraphBase =
113
+ previousTopo === null
114
+ ? nextTopo
115
+ : carryForwardTopoGraphForces(previousTopo, nextTopo);
116
+ const topoGraph =
117
+ diff === undefined || diff.breaking.length === 0
118
+ ? topoGraphBase
119
+ : annotateTopoGraphForces(topoGraphBase, diff.breaking);
120
+ const hash = deriveTopoGraphHash(topoGraph);
121
+ const lockManifest = {
122
+ ...(JSON.parse(storedExport.lockManifestJson) as LockManifest),
123
+ artifacts: [
124
+ {
125
+ path: 'topo.lock',
126
+ role: 'topo',
127
+ sha256: hash,
128
+ },
129
+ ],
130
+ } satisfies LockManifest;
131
+
132
+ const topoPath = await writeTopoGraph(topoGraph, { dir: trailsDir });
133
+ const lockPath = await writeLockManifest(lockManifest, { dir: trailsDir });
98
134
 
99
135
  return {
100
- hash: storedExport.topoGraphHash,
136
+ hash,
101
137
  lockPath,
102
138
  topoPath,
103
139
  };
@@ -105,7 +141,7 @@ const writeStoredExportArtifacts = async (
105
141
 
106
142
  export const exportCurrentTopo = async (
107
143
  app: Topo,
108
- options?: { readonly rootDir?: string }
144
+ options?: { readonly force?: boolean | undefined; readonly rootDir?: string }
109
145
  ): Promise<Result<TopoExportReport, Error>> => {
110
146
  const rootDir = deriveRootDir(options?.rootDir);
111
147
  const db = openWriteTrailsDb({ rootDir });
@@ -117,10 +153,20 @@ export const exportCurrentTopo = async (
117
153
  }
118
154
 
119
155
  const { snapshot, storedExport } = persisted.value;
120
- const artifacts = await writeStoredExportArtifacts(
121
- storedExport,
122
- deriveTrailsDir({ rootDir })
123
- );
156
+ let artifacts: Pick<TopoExportReport, 'hash' | 'lockPath' | 'topoPath'>;
157
+ try {
158
+ artifacts = await writeStoredExportArtifacts(
159
+ storedExport,
160
+ deriveTrailsDir({ rootDir }),
161
+ { force: options?.force }
162
+ );
163
+ } catch (error: unknown) {
164
+ return Result.err(
165
+ error instanceof Error
166
+ ? error
167
+ : new InternalError('Unable to write topo artifacts')
168
+ );
169
+ }
124
170
  return Result.ok({ ...artifacts, snapshot });
125
171
  } finally {
126
172
  db.close();
@@ -62,7 +62,7 @@ export interface TopoExportReport {
62
62
  readonly topoPath: string;
63
63
  }
64
64
 
65
- export interface TopoVerifyReport {
65
+ export interface TopoValidateReport {
66
66
  readonly committedHash: string;
67
67
  readonly currentHash: string;
68
68
  readonly lockPath: string;
@@ -20,7 +20,7 @@ export const topoUnpinTrail = trail('topo.unpin', {
20
20
 
21
21
  const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
22
22
  if (rootDirResult.isErr()) {
23
- return Result.err(rootDirResult.error);
23
+ return rootDirResult;
24
24
  }
25
25
  const rootDir = rootDirResult.value;
26
26
  return Result.ok(
@@ -57,5 +57,5 @@ export const topoUnpinTrail = trail('topo.unpin', {
57
57
  removed: z.boolean(),
58
58
  snapshot: topoSnapshotOutput.optional(),
59
59
  }),
60
- permit: { scopes: ['topo:delete'] },
60
+ permit: { scopes: ['topo:write'] },
61
61
  });
@@ -76,12 +76,12 @@ export const topoTrail = trail('topo', {
76
76
  blaze: async (input, ctx) => {
77
77
  const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
78
78
  if (rootDirResult.isErr()) {
79
- return Result.err(rootDirResult.error);
79
+ return rootDirResult;
80
80
  }
81
81
  const rootDir = rootDirResult.value;
82
82
  const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
83
83
  if (leaseResult.isErr()) {
84
- return Result.err(leaseResult.error);
84
+ return leaseResult;
85
85
  }
86
86
  const lease = leaseResult.value;
87
87
  try {
@@ -1,29 +1,29 @@
1
- import { Result, trail } from '@ontrails/core';
1
+ import { trail } from '@ontrails/core';
2
2
  import { z } from 'zod';
3
3
 
4
4
  import { tryLoadFreshAppLease } from './load-app.js';
5
5
  import { resolveTrailRootDir } from './root-dir.js';
6
- import { verifyCurrentTopo } from './topo-read-support.js';
6
+ import { validateCurrentTopo } from './topo-read-support.js';
7
7
 
8
- export const topoVerifyTrail = trail('topo.verify', {
8
+ export const validateTrail = trail('validate', {
9
9
  blaze: async (input, ctx) => {
10
10
  const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
11
11
  if (rootDirResult.isErr()) {
12
- return Result.err(rootDirResult.error);
12
+ return rootDirResult;
13
13
  }
14
14
  const rootDir = rootDirResult.value;
15
15
  const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
16
16
  if (leaseResult.isErr()) {
17
- return Result.err(leaseResult.error);
17
+ return leaseResult;
18
18
  }
19
19
  const lease = leaseResult.value;
20
20
  try {
21
- return await verifyCurrentTopo(lease.app, { rootDir });
21
+ return await validateCurrentTopo(lease.app, { rootDir });
22
22
  } finally {
23
23
  lease.release();
24
24
  }
25
25
  },
26
- description: 'Verify that the committed lockfile matches the current topo',
26
+ description: 'Validate that committed topo artifacts match the current topo',
27
27
  input: z.object({
28
28
  module: z.string().optional().describe('Path to the app module'),
29
29
  rootDir: z.string().optional().describe('Workspace root directory'),