@ontrails/trails 1.0.0-beta.17 → 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.
- package/CHANGELOG.md +139 -0
- package/README.md +7 -10
- package/package.json +13 -12
- package/src/app.ts +14 -4
- package/src/cli.ts +16 -0
- package/src/lifecycle-source-io.ts +33 -0
- package/src/project-writes.ts +62 -5
- package/src/retired-topo-command.ts +36 -0
- package/src/run-adapter-check.ts +76 -0
- package/src/run-collision.ts +1 -0
- package/src/trails/adapter-check.ts +244 -0
- package/src/trails/add-surface.ts +18 -18
- package/src/trails/add-trail.ts +3 -2
- package/src/trails/add-verify.ts +30 -6
- package/src/trails/{topo-compile.ts → compile.ts} +16 -8
- package/src/trails/completions-complete.ts +1 -1
- package/src/trails/create-adapter.ts +1084 -0
- package/src/trails/create-scaffold.ts +243 -29
- package/src/trails/create.ts +118 -17
- package/src/trails/deprecate.ts +59 -0
- package/src/trails/dev-clean.ts +2 -2
- package/src/trails/dev-reset.ts +2 -2
- package/src/trails/dev-stats.ts +1 -1
- package/src/trails/doctor.ts +56 -0
- package/src/trails/draft-promote.ts +1 -0
- package/src/trails/guide.ts +2 -2
- package/src/trails/revise.ts +53 -0
- package/src/trails/run-example.ts +12 -7
- package/src/trails/run-examples.ts +3 -3
- package/src/trails/run.ts +7 -4
- package/src/trails/survey.ts +332 -25
- package/src/trails/topo-history.ts +1 -1
- package/src/trails/topo-output-schemas.ts +30 -1
- package/src/trails/topo-pin.ts +3 -2
- package/src/trails/topo-read-support.ts +49 -8
- package/src/trails/topo-reports.ts +39 -22
- package/src/trails/topo-store-support.ts +62 -16
- package/src/trails/topo-support.ts +1 -1
- package/src/trails/topo-unpin.ts +2 -2
- package/src/trails/topo.ts +2 -2
- package/src/trails/{topo-verify.ts → validate.ts} +7 -7
- package/src/trails/version-lifecycle-support.ts +945 -0
- package/src/trails/warden-guide.ts +8 -0
- package/src/trails/warden.ts +18 -2
- package/src/versions.ts +4 -1
package/src/trails/survey.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
createTopoStore,
|
|
21
21
|
deriveTopoGraphDiff,
|
|
22
22
|
deriveTopoGraph,
|
|
23
|
+
resolveTopoGraphVersionReference,
|
|
23
24
|
TOPO_GRAPH_SCHEMA_VERSION,
|
|
24
25
|
readTopoGraph,
|
|
25
26
|
} from '@ontrails/topographer';
|
|
@@ -84,6 +85,21 @@ interface SurveyDiffReport {
|
|
|
84
85
|
readonly warnings: readonly DiffEntry[];
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
interface DiffInput {
|
|
89
|
+
readonly against?: string | undefined;
|
|
90
|
+
readonly breakingOnly?: boolean | undefined;
|
|
91
|
+
readonly breaks?: boolean | undefined;
|
|
92
|
+
readonly forces?: boolean | undefined;
|
|
93
|
+
readonly module?: string | undefined;
|
|
94
|
+
readonly rootDir?: string | undefined;
|
|
95
|
+
readonly target?: string | undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface ParsedDiffTarget {
|
|
99
|
+
readonly id: string;
|
|
100
|
+
readonly versions?: ReadonlySet<number> | undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
87
103
|
const formatDiff = (diff: DiffResult, against: string): SurveyDiffReport => ({
|
|
88
104
|
against,
|
|
89
105
|
breaking: diff.breaking,
|
|
@@ -93,6 +109,252 @@ const formatDiff = (diff: DiffResult, against: string): SurveyDiffReport => ({
|
|
|
93
109
|
warnings: diff.warnings,
|
|
94
110
|
});
|
|
95
111
|
|
|
112
|
+
const partitionDiffEntries = (entries: readonly DiffEntry[]): DiffResult => {
|
|
113
|
+
const sorted = [...entries].toSorted((left, right) =>
|
|
114
|
+
left.id.localeCompare(right.id)
|
|
115
|
+
);
|
|
116
|
+
const breaking = sorted.filter((entry) => entry.severity === 'breaking');
|
|
117
|
+
const warnings = sorted.filter((entry) => entry.severity === 'warning');
|
|
118
|
+
const info = sorted.filter((entry) => entry.severity === 'info');
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
breaking,
|
|
122
|
+
entries: sorted,
|
|
123
|
+
hasBreaking: breaking.length > 0,
|
|
124
|
+
info,
|
|
125
|
+
warnings,
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const parseVersionRange = (
|
|
130
|
+
reference: string
|
|
131
|
+
): ReadonlySet<number> | undefined => {
|
|
132
|
+
const match = /^(\d+)\.\.(\d+)$/.exec(reference);
|
|
133
|
+
if (match === null) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const start = Number(match[1]);
|
|
137
|
+
const end = Number(match[2]);
|
|
138
|
+
if (start < 1 || end < start) {
|
|
139
|
+
throw new ValidationError(
|
|
140
|
+
`Diff version range must use ascending positive versions: ${reference}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return new Set(
|
|
145
|
+
Array.from({ length: end - start + 1 }, (_value, index) => start + index)
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const findDiffTargetEntry = (
|
|
150
|
+
previous: TopoGraph,
|
|
151
|
+
current: TopoGraph,
|
|
152
|
+
id: string
|
|
153
|
+
) =>
|
|
154
|
+
current.entries.find((entry) => entry.id === id) ??
|
|
155
|
+
previous.entries.find((entry) => entry.id === id);
|
|
156
|
+
|
|
157
|
+
const parseDiffTarget = (
|
|
158
|
+
previous: TopoGraph,
|
|
159
|
+
current: TopoGraph,
|
|
160
|
+
target: string | undefined
|
|
161
|
+
): Result<ParsedDiffTarget | undefined, Error> => {
|
|
162
|
+
if (target === undefined || target.length === 0) {
|
|
163
|
+
return Result.ok();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const separator = target.lastIndexOf('@');
|
|
167
|
+
const id = separator === -1 ? target : target.slice(0, separator);
|
|
168
|
+
const reference =
|
|
169
|
+
separator === -1 ? undefined : target.slice(separator + 1).trim();
|
|
170
|
+
if (id.length === 0 || reference === '') {
|
|
171
|
+
return Result.err(
|
|
172
|
+
new ValidationError('Diff target must use trail.id or trail.id@version')
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const entry = findDiffTargetEntry(previous, current, id);
|
|
177
|
+
if (entry === undefined) {
|
|
178
|
+
return Result.err(new NotFoundError(`Trail not found for diff: ${id}`));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (reference === undefined) {
|
|
182
|
+
return Result.ok({ id });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const range = parseVersionRange(reference);
|
|
187
|
+
if (range !== undefined) {
|
|
188
|
+
return Result.ok({ id, versions: range });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return Result.ok({
|
|
192
|
+
id,
|
|
193
|
+
versions: new Set([
|
|
194
|
+
resolveTopoGraphVersionReference(entry, reference).version,
|
|
195
|
+
]),
|
|
196
|
+
});
|
|
197
|
+
} catch (error: unknown) {
|
|
198
|
+
return Result.err(
|
|
199
|
+
error instanceof Error ? error : new Error(String(error))
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const detailVersions = (detail: string): readonly number[] => {
|
|
205
|
+
const match = /^(?:Live version|Version) (\d+)\b/.exec(detail);
|
|
206
|
+
if (match !== null) {
|
|
207
|
+
return [Number(match[1])];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const supportMatch = /^Supported versions (?:added|removed): (.+)$/.exec(
|
|
211
|
+
detail
|
|
212
|
+
);
|
|
213
|
+
if (supportMatch === null) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (supportMatch[1] ?? '')
|
|
218
|
+
.split(',')
|
|
219
|
+
.map((part) => Number(part.trim()))
|
|
220
|
+
.filter((version) => Number.isInteger(version) && version > 0);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
type DiffSeverity = DiffEntry['severity'];
|
|
224
|
+
|
|
225
|
+
const severityRank: Record<DiffSeverity, number> = {
|
|
226
|
+
breaking: 2,
|
|
227
|
+
info: 0,
|
|
228
|
+
warning: 1,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const higherSeverity = (
|
|
232
|
+
left: DiffSeverity,
|
|
233
|
+
right: DiffSeverity
|
|
234
|
+
): DiffSeverity => (severityRank[right] > severityRank[left] ? right : left);
|
|
235
|
+
|
|
236
|
+
const versionStatus = (detail: string): string | undefined =>
|
|
237
|
+
/^Version \d+ (?:added|removed) \(([^)]+)\)$/.exec(detail)?.[1];
|
|
238
|
+
|
|
239
|
+
const visibleDetailSeverity = (detail: string): DiffSeverity => {
|
|
240
|
+
if (detail.startsWith('Force event ')) {
|
|
241
|
+
return 'warning';
|
|
242
|
+
}
|
|
243
|
+
if (detail.startsWith('Supported versions removed: ')) {
|
|
244
|
+
return 'breaking';
|
|
245
|
+
}
|
|
246
|
+
if (detail.startsWith('Supported versions added: ')) {
|
|
247
|
+
return 'info';
|
|
248
|
+
}
|
|
249
|
+
if (
|
|
250
|
+
/^Live version \d+ (?:added without examples|example coverage removed)$/.test(
|
|
251
|
+
detail
|
|
252
|
+
)
|
|
253
|
+
) {
|
|
254
|
+
return 'warning';
|
|
255
|
+
}
|
|
256
|
+
if (detail.startsWith('Live version ') && detail.includes(' examples: ')) {
|
|
257
|
+
return 'info';
|
|
258
|
+
}
|
|
259
|
+
if (/^Version \d+ status changed: .+ -> archived$/.test(detail)) {
|
|
260
|
+
return 'warning';
|
|
261
|
+
}
|
|
262
|
+
if (detail.startsWith('Version ') && detail.includes(' status changed: ')) {
|
|
263
|
+
return 'info';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const status = versionStatus(detail);
|
|
267
|
+
if (detail.startsWith('Version ') && detail.includes(' removed (')) {
|
|
268
|
+
return status === 'archived' ? 'warning' : 'breaking';
|
|
269
|
+
}
|
|
270
|
+
if (detail.startsWith('Version ') && detail.includes(' added (')) {
|
|
271
|
+
return status === 'archived' ? 'info' : 'warning';
|
|
272
|
+
}
|
|
273
|
+
if (
|
|
274
|
+
/^Version \d+ (?:kind changed:|Required (?:input|contour) field ".+" added|(?:Input|Output|Contour) field ".+" (?:removed|type changed:|changed from optional to required))/.test(
|
|
275
|
+
detail
|
|
276
|
+
)
|
|
277
|
+
) {
|
|
278
|
+
return 'breaking';
|
|
279
|
+
}
|
|
280
|
+
if (
|
|
281
|
+
/^Version \d+ (?:marker changed:|Optional (?:input|contour) field ".+" added|Output field ".+" added)/.test(
|
|
282
|
+
detail
|
|
283
|
+
)
|
|
284
|
+
) {
|
|
285
|
+
return 'info';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return 'info';
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const visibleDetailsSeverity = (details: readonly string[]): DiffSeverity => {
|
|
292
|
+
let severity: DiffSeverity = 'info';
|
|
293
|
+
for (const detail of details) {
|
|
294
|
+
severity = higherSeverity(severity, visibleDetailSeverity(detail));
|
|
295
|
+
}
|
|
296
|
+
return severity;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const detailsChanged = (
|
|
300
|
+
previous: readonly string[],
|
|
301
|
+
next: readonly string[]
|
|
302
|
+
): boolean =>
|
|
303
|
+
previous.length !== next.length ||
|
|
304
|
+
previous.some((detail, index) => detail !== next[index]);
|
|
305
|
+
|
|
306
|
+
const filterDetails = (
|
|
307
|
+
details: readonly string[],
|
|
308
|
+
target: ParsedDiffTarget | undefined,
|
|
309
|
+
forcesOnly: boolean
|
|
310
|
+
): readonly string[] => {
|
|
311
|
+
const visible = forcesOnly
|
|
312
|
+
? details.filter((detail) => detail.startsWith('Force event '))
|
|
313
|
+
: [...details];
|
|
314
|
+
if (target?.versions === undefined || forcesOnly) {
|
|
315
|
+
return visible;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return visible.filter((detail) => {
|
|
319
|
+
const versions = detailVersions(detail);
|
|
320
|
+
return versions.some((version) => target.versions?.has(version));
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const filterDiff = (
|
|
325
|
+
diff: DiffResult,
|
|
326
|
+
target: ParsedDiffTarget | undefined,
|
|
327
|
+
options: Pick<DiffInput, 'breakingOnly' | 'breaks' | 'forces'>
|
|
328
|
+
): DiffResult => {
|
|
329
|
+
const entries = diff.entries.flatMap((entry): DiffEntry[] => {
|
|
330
|
+
if (target !== undefined && entry.id !== target.id) {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
const details = filterDetails(
|
|
334
|
+
entry.details,
|
|
335
|
+
target,
|
|
336
|
+
options.forces === true
|
|
337
|
+
);
|
|
338
|
+
if (details.length === 0) {
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
return [
|
|
342
|
+
{
|
|
343
|
+
...entry,
|
|
344
|
+
details,
|
|
345
|
+
severity: detailsChanged(entry.details, details)
|
|
346
|
+
? visibleDetailsSeverity(details)
|
|
347
|
+
: entry.severity,
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const partitioned = partitionDiffEntries(entries);
|
|
353
|
+
return options.breakingOnly === true || options.breaks === true
|
|
354
|
+
? partitionDiffEntries(partitioned.breaking)
|
|
355
|
+
: partitioned;
|
|
356
|
+
};
|
|
357
|
+
|
|
96
358
|
const createDiffExampleInput = (): {
|
|
97
359
|
readonly against: string;
|
|
98
360
|
readonly module: string;
|
|
@@ -191,7 +453,7 @@ const readAgainstTopoGraph = async (
|
|
|
191
453
|
return map === null
|
|
192
454
|
? Result.err(
|
|
193
455
|
new NotFoundError(
|
|
194
|
-
'No saved TopoGraph found. Run `trails
|
|
456
|
+
'No saved TopoGraph found. Run `trails compile` first.'
|
|
195
457
|
)
|
|
196
458
|
)
|
|
197
459
|
: Result.ok({ against: 'saved', map });
|
|
@@ -218,21 +480,25 @@ const readAgainstTopoGraph = async (
|
|
|
218
480
|
const buildSurveyDiff = async (
|
|
219
481
|
app: Topo,
|
|
220
482
|
rootDir: string,
|
|
221
|
-
|
|
222
|
-
against?: string | undefined
|
|
483
|
+
input: DiffInput
|
|
223
484
|
): Promise<Result<SurveyDiffReport, Error>> => {
|
|
224
485
|
const currentMap = deriveTopoGraph(app);
|
|
225
|
-
const previous = await readAgainstTopoGraph(rootDir, against);
|
|
486
|
+
const previous = await readAgainstTopoGraph(rootDir, input.against);
|
|
226
487
|
if (previous.isErr()) {
|
|
227
488
|
return previous;
|
|
228
489
|
}
|
|
229
490
|
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
491
|
+
const target = parseDiffTarget(previous.value.map, currentMap, input.target);
|
|
492
|
+
if (target.isErr()) {
|
|
493
|
+
return target;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const diff = filterDiff(
|
|
497
|
+
deriveTopoGraphDiff(previous.value.map, currentMap),
|
|
498
|
+
target.value,
|
|
499
|
+
input
|
|
235
500
|
);
|
|
501
|
+
return Result.ok(formatDiff(diff, previous.value.against));
|
|
236
502
|
};
|
|
237
503
|
|
|
238
504
|
const buildSurveyLookup = (
|
|
@@ -404,6 +670,32 @@ const diffOutput = z.object({
|
|
|
404
670
|
warnings: z.array(diffEntryOutput),
|
|
405
671
|
});
|
|
406
672
|
|
|
673
|
+
const diffInputSchema = z.object({
|
|
674
|
+
against: z
|
|
675
|
+
.string()
|
|
676
|
+
.min(1)
|
|
677
|
+
.optional()
|
|
678
|
+
.describe(
|
|
679
|
+
'Saved TopoGraph target: "saved", a workspace path (topo.lock, .json file, or directory with topo.lock), then a pin/snapshot id'
|
|
680
|
+
),
|
|
681
|
+
breakingOnly: z
|
|
682
|
+
.boolean()
|
|
683
|
+
.default(false)
|
|
684
|
+
.describe('Legacy alias for --breaks; only show breaking changes'),
|
|
685
|
+
breaks: z.boolean().default(false).describe('Only show breaking changes'),
|
|
686
|
+
forces: z
|
|
687
|
+
.boolean()
|
|
688
|
+
.default(false)
|
|
689
|
+
.describe('Only show graph force audit events'),
|
|
690
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
691
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
692
|
+
target: z
|
|
693
|
+
.string()
|
|
694
|
+
.min(1)
|
|
695
|
+
.optional()
|
|
696
|
+
.describe('Trail or trail version target, such as user.create@1..2'),
|
|
697
|
+
});
|
|
698
|
+
|
|
407
699
|
const surveyMatchOutput = z.discriminatedUnion('kind', [
|
|
408
700
|
z.object({
|
|
409
701
|
detail: trailDetailOutput,
|
|
@@ -537,7 +829,7 @@ export const surveySurfacesTrail = trail('survey.surfaces', {
|
|
|
537
829
|
export const surveyDiffTrail = trail('survey.diff', {
|
|
538
830
|
blaze: async (input, ctx) =>
|
|
539
831
|
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
|
|
540
|
-
buildSurveyDiff(app, rootDir, input
|
|
832
|
+
buildSurveyDiff(app, rootDir, input)
|
|
541
833
|
),
|
|
542
834
|
description: 'Diff the current topo against a saved TopoGraph',
|
|
543
835
|
examples: [
|
|
@@ -562,21 +854,36 @@ export const surveyDiffTrail = trail('survey.diff', {
|
|
|
562
854
|
name: 'Reject empty breaking-only target',
|
|
563
855
|
},
|
|
564
856
|
],
|
|
565
|
-
input:
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
857
|
+
input: diffInputSchema,
|
|
858
|
+
intent: 'read',
|
|
859
|
+
output: diffOutput,
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
export const diffTrail = trail('diff', {
|
|
863
|
+
args: ['target'],
|
|
864
|
+
blaze: async (input, ctx) =>
|
|
865
|
+
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
|
|
866
|
+
buildSurveyDiff(app, rootDir, input)
|
|
867
|
+
),
|
|
868
|
+
description: 'Diff the current topo against a saved TopoGraph',
|
|
869
|
+
examples: [
|
|
870
|
+
{
|
|
871
|
+
description: 'Compare current topo to a saved TopoGraph directory',
|
|
872
|
+
input: createDiffExampleInput(),
|
|
873
|
+
name: 'Diff against baseline',
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
description: 'Show only breaking contract drift',
|
|
877
|
+
input: { ...createDiffExampleInput(), breaks: true },
|
|
878
|
+
name: 'Breaking changes',
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
description: 'Show graph-only force audit events',
|
|
882
|
+
input: { ...createDiffExampleInput(), forces: true },
|
|
883
|
+
name: 'Force audit events',
|
|
884
|
+
},
|
|
885
|
+
],
|
|
886
|
+
input: diffInputSchema,
|
|
580
887
|
intent: 'read',
|
|
581
888
|
output: diffOutput,
|
|
582
889
|
});
|
|
@@ -13,7 +13,7 @@ export const topoHistoryTrail = trail('topo.history', {
|
|
|
13
13
|
blaze: (input, ctx) => {
|
|
14
14
|
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
15
15
|
if (rootDirResult.isErr()) {
|
|
16
|
-
return
|
|
16
|
+
return rootDirResult;
|
|
17
17
|
}
|
|
18
18
|
const rootDir = rootDirResult.value;
|
|
19
19
|
return Result.ok(listTopoHistory({ limit: input.limit, rootDir }));
|
|
@@ -137,6 +137,32 @@ export const shippedSurfaceInventoryOutput = z.object({
|
|
|
137
137
|
.readonly(),
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
const trailVersionEntryOutput = z.object({
|
|
141
|
+
composes: z.array(z.string()).readonly().optional(),
|
|
142
|
+
detours: z
|
|
143
|
+
.array(
|
|
144
|
+
z.object({
|
|
145
|
+
maxAttempts: z.number(),
|
|
146
|
+
on: z.string(),
|
|
147
|
+
})
|
|
148
|
+
)
|
|
149
|
+
.readonly()
|
|
150
|
+
.optional(),
|
|
151
|
+
exampleCount: z.number(),
|
|
152
|
+
examples: z.array(z.unknown()).readonly().optional(),
|
|
153
|
+
input: jsonSchemaOutput,
|
|
154
|
+
kind: z.enum(['revision', 'fork']),
|
|
155
|
+
marker: z.string(),
|
|
156
|
+
output: jsonSchemaOutput,
|
|
157
|
+
resources: z.array(z.string()).readonly().optional(),
|
|
158
|
+
status: z
|
|
159
|
+
.object({
|
|
160
|
+
state: z.enum(['deprecated', 'archived']),
|
|
161
|
+
})
|
|
162
|
+
.catchall(z.unknown())
|
|
163
|
+
.optional(),
|
|
164
|
+
});
|
|
165
|
+
|
|
140
166
|
export const trailDetailOutput = z.object({
|
|
141
167
|
activatedBy: z.array(z.string()).readonly(),
|
|
142
168
|
activates: z.array(z.string()).readonly(),
|
|
@@ -163,9 +189,9 @@ export const trailDetailOutput = z.object({
|
|
|
163
189
|
topo: z.array(z.string()).readonly(),
|
|
164
190
|
trail: z.array(z.string()).readonly(),
|
|
165
191
|
}),
|
|
192
|
+
composes: z.array(z.string()).readonly(),
|
|
166
193
|
contourDetails: z.array(contourDetailOutput).readonly(),
|
|
167
194
|
contours: z.array(z.string()).readonly(),
|
|
168
|
-
crosses: z.array(z.string()).readonly(),
|
|
169
195
|
description: z.string().nullable(),
|
|
170
196
|
detours: z
|
|
171
197
|
.array(
|
|
@@ -190,8 +216,11 @@ export const trailDetailOutput = z.object({
|
|
|
190
216
|
pattern: z.string().nullable(),
|
|
191
217
|
resources: z.array(z.string()).readonly(),
|
|
192
218
|
safety: z.string(),
|
|
219
|
+
supports: z.array(z.number()).readonly(),
|
|
193
220
|
surfaceProjections: z.array(surfaceProjectionOutput).readonly(),
|
|
194
221
|
surfaces: z.array(z.string()).readonly(),
|
|
222
|
+
version: z.number().nullable(),
|
|
223
|
+
versions: z.record(z.string(), trailVersionEntryOutput),
|
|
195
224
|
});
|
|
196
225
|
|
|
197
226
|
export const resourceDetailOutput = z.object({
|
package/src/trails/topo-pin.ts
CHANGED
|
@@ -13,12 +13,12 @@ export const topoPinTrail = trail('topo.pin', {
|
|
|
13
13
|
blaze: async (input, ctx) => {
|
|
14
14
|
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
15
15
|
if (rootDirResult.isErr()) {
|
|
16
|
-
return
|
|
16
|
+
return rootDirResult;
|
|
17
17
|
}
|
|
18
18
|
const rootDir = rootDirResult.value;
|
|
19
19
|
const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
|
|
20
20
|
if (leaseResult.isErr()) {
|
|
21
|
-
return
|
|
21
|
+
return leaseResult;
|
|
22
22
|
}
|
|
23
23
|
const lease = leaseResult.value;
|
|
24
24
|
try {
|
|
@@ -48,4 +48,5 @@ export const topoPinTrail = trail('topo.pin', {
|
|
|
48
48
|
output: z.object({
|
|
49
49
|
snapshot: topoSnapshotOutput,
|
|
50
50
|
}),
|
|
51
|
+
permit: { scopes: ['topo:write'] },
|
|
51
52
|
});
|
|
@@ -18,7 +18,15 @@ import {
|
|
|
18
18
|
SURFACE_LAYER_NAMES_KEY,
|
|
19
19
|
ValidationError,
|
|
20
20
|
} from '@ontrails/core';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
deriveTopoGraph,
|
|
23
|
+
deriveTopoGraphDiff,
|
|
24
|
+
deriveTopoGraphHash,
|
|
25
|
+
readLockManifest,
|
|
26
|
+
readTopoGraph,
|
|
27
|
+
stripTopoGraphForces,
|
|
28
|
+
} from '@ontrails/topographer';
|
|
29
|
+
import type { TopoGraph } from '@ontrails/topographer';
|
|
22
30
|
|
|
23
31
|
import type {
|
|
24
32
|
BriefReport,
|
|
@@ -28,6 +36,7 @@ import type {
|
|
|
28
36
|
TrailDetailReport,
|
|
29
37
|
} from './topo-reports.js';
|
|
30
38
|
import {
|
|
39
|
+
countTrailExamples,
|
|
31
40
|
deriveBriefReport,
|
|
32
41
|
deriveResourceDetail,
|
|
33
42
|
deriveSignalDetail,
|
|
@@ -36,7 +45,7 @@ import {
|
|
|
36
45
|
} from './topo-reports.js';
|
|
37
46
|
import type { ActivationGraphReport } from './topo-activation.js';
|
|
38
47
|
import { deriveActivationGraph } from './topo-activation.js';
|
|
39
|
-
import type { TopoSummaryReport,
|
|
48
|
+
import type { TopoSummaryReport, TopoValidateReport } from './topo-support.js';
|
|
40
49
|
import { deriveRootDir, LOCK_PATH } from './topo-support.js';
|
|
41
50
|
import { deriveCurrentTopoExport } from './topo-store-support.js';
|
|
42
51
|
|
|
@@ -129,7 +138,7 @@ export const buildCurrentGuideEntries = (
|
|
|
129
138
|
.list()
|
|
130
139
|
.map((trail) => ({
|
|
131
140
|
description: trail.description ?? '(no description)',
|
|
132
|
-
exampleCount: trail
|
|
141
|
+
exampleCount: countTrailExamples(trail),
|
|
133
142
|
id: trail.id,
|
|
134
143
|
kind: 'trail' as const,
|
|
135
144
|
}))
|
|
@@ -209,10 +218,10 @@ export const buildCurrentTopoMatches = (
|
|
|
209
218
|
return matches;
|
|
210
219
|
};
|
|
211
220
|
|
|
212
|
-
export const
|
|
221
|
+
export const validateCurrentTopo = async (
|
|
213
222
|
app: Topo,
|
|
214
223
|
options?: { readonly rootDir?: string }
|
|
215
|
-
): Promise<Result<
|
|
224
|
+
): Promise<Result<TopoValidateReport, Error>> => {
|
|
216
225
|
const rootDir = deriveRootDir(options?.rootDir);
|
|
217
226
|
let lockManifest: Awaited<ReturnType<typeof readLockManifest>>;
|
|
218
227
|
try {
|
|
@@ -234,7 +243,7 @@ export const verifyCurrentTopo = async (
|
|
|
234
243
|
if (lockManifest === null) {
|
|
235
244
|
return Result.err(
|
|
236
245
|
new NotFoundError(
|
|
237
|
-
'No committed trails.lock found. Run `trails
|
|
246
|
+
'No committed trails.lock found. Run `trails compile` first.'
|
|
238
247
|
)
|
|
239
248
|
);
|
|
240
249
|
}
|
|
@@ -243,6 +252,9 @@ export const verifyCurrentTopo = async (
|
|
|
243
252
|
if (currentExport.isErr()) {
|
|
244
253
|
return currentExport;
|
|
245
254
|
}
|
|
255
|
+
const currentTopo = JSON.parse(
|
|
256
|
+
currentExport.value.topoGraphJson
|
|
257
|
+
) as TopoGraph;
|
|
246
258
|
const currentHash = currentExport.value.topoGraphHash;
|
|
247
259
|
const topoArtifact = lockManifest.artifacts.find(
|
|
248
260
|
(artifact) => artifact.role === 'topo' && artifact.path === 'topo.lock'
|
|
@@ -250,15 +262,44 @@ export const verifyCurrentTopo = async (
|
|
|
250
262
|
if (topoArtifact === undefined) {
|
|
251
263
|
return Result.err(
|
|
252
264
|
new NotFoundError(
|
|
253
|
-
'No topo.lock artifact found in trails.lock. Run `trails
|
|
265
|
+
'No topo.lock artifact found in trails.lock. Run `trails compile` first.'
|
|
254
266
|
)
|
|
255
267
|
);
|
|
256
268
|
}
|
|
257
269
|
|
|
258
270
|
if (topoArtifact.sha256 !== currentHash) {
|
|
271
|
+
const committedTopo = await readTopoGraph({
|
|
272
|
+
dir: deriveTrailsDir({ rootDir }),
|
|
273
|
+
});
|
|
274
|
+
if (committedTopo !== null) {
|
|
275
|
+
const committedHash = deriveTopoGraphHash(committedTopo);
|
|
276
|
+
const forceStrippedHash = deriveTopoGraphHash(
|
|
277
|
+
stripTopoGraphForces(committedTopo)
|
|
278
|
+
);
|
|
279
|
+
if (
|
|
280
|
+
committedHash === topoArtifact.sha256 &&
|
|
281
|
+
forceStrippedHash === currentHash
|
|
282
|
+
) {
|
|
283
|
+
return Result.ok({
|
|
284
|
+
committedHash: topoArtifact.sha256,
|
|
285
|
+
currentHash,
|
|
286
|
+
lockPath: LOCK_PATH,
|
|
287
|
+
stale: false,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const breakingSummary =
|
|
292
|
+
committedTopo === null
|
|
293
|
+
? ''
|
|
294
|
+
: (() => {
|
|
295
|
+
const diff = deriveTopoGraphDiff(committedTopo, currentTopo);
|
|
296
|
+
return diff.breaking.length > 0
|
|
297
|
+
? ` Breaking changes detected: ${diff.breaking.length}.`
|
|
298
|
+
: '';
|
|
299
|
+
})();
|
|
259
300
|
return Result.err(
|
|
260
301
|
new ConflictError(
|
|
261
|
-
|
|
302
|
+
`trails.lock is stale. Run \`trails compile\` to refresh it.${breakingSummary}`
|
|
262
303
|
)
|
|
263
304
|
);
|
|
264
305
|
}
|