@ontrails/topography 1.0.0-beta.41
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 +543 -0
- package/README.md +196 -0
- package/package.json +33 -0
- package/src/activation-report.ts +385 -0
- package/src/backend-support.ts +11 -0
- package/src/derive.ts +690 -0
- package/src/diff.ts +1088 -0
- package/src/forces.ts +125 -0
- package/src/hash.ts +55 -0
- package/src/index.ts +272 -0
- package/src/internal/topo-snapshots.ts +682 -0
- package/src/internal/topo-store-read.ts +1102 -0
- package/src/internal/topo-store.ts +1651 -0
- package/src/io.ts +280 -0
- package/src/library-projection.ts +133 -0
- package/src/overlays.ts +155 -0
- package/src/permit.ts +19 -0
- package/src/source-fingerprint.ts +103 -0
- package/src/surface-bindings.ts +101 -0
- package/src/topo-store.ts +716 -0
- package/src/types.ts +749 -0
- package/src/versioning.ts +280 -0
- package/src/wayfind/error-facts.ts +363 -0
- package/src/wayfind/filters.ts +430 -0
- package/src/wayfind/loader.ts +443 -0
- package/src/wayfind/navigation.ts +265 -0
- package/src/wayfind/provenance.ts +152 -0
- package/src/wayfind/queries.ts +1656 -0
- package/src/wayfind/relations.ts +344 -0
- package/src/workspace-topos.ts +483 -0
|
@@ -0,0 +1,1656 @@
|
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { adapterTargetPlacements, checkAdapters } from '@ontrails/adapter-kit';
|
|
4
|
+
import type { AdapterFact } from '@ontrails/adapter-kit';
|
|
5
|
+
import {
|
|
6
|
+
AmbiguousError,
|
|
7
|
+
DerivationError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
Result,
|
|
10
|
+
ValidationError,
|
|
11
|
+
errorCategories,
|
|
12
|
+
surfaceNames,
|
|
13
|
+
topo,
|
|
14
|
+
trail,
|
|
15
|
+
} from '@ontrails/core';
|
|
16
|
+
import type { TrailsError } from '@ontrails/core';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
|
|
19
|
+
import { deriveTopoGraphDiff } from '../diff.js';
|
|
20
|
+
import type { TopoGraph, TopoGraphEntry } from '../types.js';
|
|
21
|
+
import {
|
|
22
|
+
filterWayfinderEntityRefs,
|
|
23
|
+
wayfinderEntityFilterSchema,
|
|
24
|
+
} from './filters.js';
|
|
25
|
+
import type {
|
|
26
|
+
WayfinderEntityFilterInput,
|
|
27
|
+
WayfinderEntityKind,
|
|
28
|
+
} from './filters.js';
|
|
29
|
+
import { loadWayfinderArtifacts } from './loader.js';
|
|
30
|
+
import type {
|
|
31
|
+
WayfinderArtifactLoad,
|
|
32
|
+
WayfinderArtifactLoaderOptions,
|
|
33
|
+
} from './loader.js';
|
|
34
|
+
import { deriveTrailErrorFacts } from './error-facts.js';
|
|
35
|
+
import {
|
|
36
|
+
resolveWayfinderPopulation,
|
|
37
|
+
resolveWayfinderRelations,
|
|
38
|
+
} from './navigation.js';
|
|
39
|
+
import { wayfinderDriftFromArtifactStatus } from './provenance.js';
|
|
40
|
+
import {
|
|
41
|
+
diffResult,
|
|
42
|
+
impactNodeSchema,
|
|
43
|
+
relationEdgeSchema,
|
|
44
|
+
relationGroupSchema,
|
|
45
|
+
} from './relations.js';
|
|
46
|
+
const artifactSourceSchema = z.object({
|
|
47
|
+
kind: z.enum(['lockManifest', 'topoGraph', 'topoStore']),
|
|
48
|
+
path: z.string().optional(),
|
|
49
|
+
schemaVersion: z.number().optional(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const envelopeSchema = z.object({
|
|
53
|
+
drift: z.object({
|
|
54
|
+
artifacts: z
|
|
55
|
+
.array(z.enum(['lockManifest', 'topoGraph', 'topoStore']))
|
|
56
|
+
.readonly()
|
|
57
|
+
.optional(),
|
|
58
|
+
reasons: z.array(z.record(z.string(), z.unknown())).readonly().optional(),
|
|
59
|
+
status: z.enum(['absent', 'aligned', 'drifted']),
|
|
60
|
+
}),
|
|
61
|
+
source: artifactSourceSchema,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const sourceInputSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
dir: z.string().optional().describe('Directory containing trails.lock'),
|
|
67
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
68
|
+
trailsDbPath: z.string().optional().describe('Path to trails.db'),
|
|
69
|
+
})
|
|
70
|
+
.strict();
|
|
71
|
+
|
|
72
|
+
const filteredInputSchema = sourceInputSchema.extend({
|
|
73
|
+
filters: wayfinderEntityFilterSchema.optional(),
|
|
74
|
+
limit: z.number().int().positive().max(500).default(100),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const adapterFactKindSchema = z.enum([
|
|
78
|
+
'available',
|
|
79
|
+
'configured',
|
|
80
|
+
'observed',
|
|
81
|
+
'used',
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const adapterFactsInputSchema = z
|
|
85
|
+
.object({
|
|
86
|
+
filters: z
|
|
87
|
+
.object({
|
|
88
|
+
kind: adapterFactKindSchema.optional(),
|
|
89
|
+
packageName: z.string().optional(),
|
|
90
|
+
target: z.string().optional(),
|
|
91
|
+
})
|
|
92
|
+
.optional(),
|
|
93
|
+
limit: z.number().int().positive().max(500).default(100),
|
|
94
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
95
|
+
})
|
|
96
|
+
.strict();
|
|
97
|
+
|
|
98
|
+
const overlayInputSchema = sourceInputSchema.extend({
|
|
99
|
+
namespace: z
|
|
100
|
+
.string()
|
|
101
|
+
.min(1)
|
|
102
|
+
.describe('Overlay namespace to read from the saved graph'),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const inspectKindSchema = z.enum([
|
|
106
|
+
'entity',
|
|
107
|
+
'trailhead',
|
|
108
|
+
'resource',
|
|
109
|
+
'signal',
|
|
110
|
+
'surface',
|
|
111
|
+
'trail',
|
|
112
|
+
'version',
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const inspectInputSchema = sourceInputSchema.extend({
|
|
116
|
+
id: z.string().min(1).describe('Entity ID to inspect'),
|
|
117
|
+
kind: inspectKindSchema.optional().describe('Optional entity kind'),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const contractInputSchema = inspectInputSchema.extend({
|
|
121
|
+
version: z.number().int().positive().optional(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const relationInputSchema = inspectInputSchema.extend({
|
|
125
|
+
filters: wayfinderEntityFilterSchema.optional(),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const impactDirectionSchema = z
|
|
129
|
+
.enum(['downstream', 'upstream', 'both'])
|
|
130
|
+
.default('downstream');
|
|
131
|
+
|
|
132
|
+
const impactInputSchema = inspectInputSchema.extend({
|
|
133
|
+
direction: impactDirectionSchema,
|
|
134
|
+
filters: wayfinderEntityFilterSchema.optional(),
|
|
135
|
+
limit: z.number().int().positive().max(500).default(100),
|
|
136
|
+
maxDepth: z.number().int().positive().max(10).default(2),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
type ImpactDirection = z.output<typeof impactDirectionSchema>;
|
|
140
|
+
|
|
141
|
+
const relationModeFromImpactDirection = (direction: ImpactDirection) => {
|
|
142
|
+
if (direction === 'both') {
|
|
143
|
+
return 'related';
|
|
144
|
+
}
|
|
145
|
+
return direction === 'upstream' ? 'deps' : 'impact';
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const diffInputSchema = sourceInputSchema
|
|
149
|
+
.extend({
|
|
150
|
+
againstDir: z
|
|
151
|
+
.string()
|
|
152
|
+
.optional()
|
|
153
|
+
.describe('Baseline artifact directory containing trails.lock'),
|
|
154
|
+
againstRootDir: z
|
|
155
|
+
.string()
|
|
156
|
+
.optional()
|
|
157
|
+
.describe('Baseline workspace root directory'),
|
|
158
|
+
againstTrailsDbPath: z
|
|
159
|
+
.string()
|
|
160
|
+
.optional()
|
|
161
|
+
.describe('Baseline trails.db path'),
|
|
162
|
+
})
|
|
163
|
+
.strict()
|
|
164
|
+
.refine(
|
|
165
|
+
(input) =>
|
|
166
|
+
input.againstDir !== undefined || input.againstRootDir !== undefined,
|
|
167
|
+
{
|
|
168
|
+
message: 'Provide againstDir or againstRootDir for the baseline graph.',
|
|
169
|
+
path: ['againstDir'],
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
.refine(
|
|
173
|
+
(input) =>
|
|
174
|
+
input.againstDir === undefined || input.againstRootDir === undefined,
|
|
175
|
+
{
|
|
176
|
+
message: 'Provide only one of againstDir or againstRootDir.',
|
|
177
|
+
path: ['againstDir'],
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const refOutputSchema = z.object({
|
|
182
|
+
id: z.string(),
|
|
183
|
+
kind: inspectKindSchema,
|
|
184
|
+
trailId: z.string().optional(),
|
|
185
|
+
versionKey: z.string().optional(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const entrySummarySchema = z.object({
|
|
189
|
+
exampleCount: z.number(),
|
|
190
|
+
id: z.string(),
|
|
191
|
+
surfaces: z.array(z.string()).readonly(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const cliRouteSchema = z.object({
|
|
195
|
+
kind: z.enum(['alias', 'canonical']),
|
|
196
|
+
path: z.array(z.string()).readonly(),
|
|
197
|
+
source: z.enum(['derived', 'surface', 'trail']),
|
|
198
|
+
target: z.string(),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const cliProjectionSchema = z
|
|
202
|
+
.object({
|
|
203
|
+
path: z.array(z.string()).readonly(),
|
|
204
|
+
routes: z.array(cliRouteSchema).readonly().optional(),
|
|
205
|
+
})
|
|
206
|
+
.nullable();
|
|
207
|
+
|
|
208
|
+
const trailSummarySchema = entrySummarySchema.extend({
|
|
209
|
+
cli: cliProjectionSchema,
|
|
210
|
+
composes: z.array(z.string()).readonly(),
|
|
211
|
+
intent: z.enum(['destroy', 'read', 'write']),
|
|
212
|
+
kind: z.literal('trail'),
|
|
213
|
+
resources: z.array(z.string()).readonly(),
|
|
214
|
+
signals: z.array(z.string()).readonly(),
|
|
215
|
+
version: z.number().nullable(),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const entitySummarySchema = entrySummarySchema.extend({
|
|
219
|
+
identity: z.string().optional(),
|
|
220
|
+
kind: z.literal('entity'),
|
|
221
|
+
references: z.array(z.unknown()).readonly(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const resourceSummarySchema = entrySummarySchema.extend({
|
|
225
|
+
kind: z.literal('resource'),
|
|
226
|
+
usedBy: z.array(z.string()).readonly(),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const signalSummarySchema = entrySummarySchema.extend({
|
|
230
|
+
consumers: z.array(z.string()).readonly(),
|
|
231
|
+
kind: z.literal('signal'),
|
|
232
|
+
producers: z.array(z.string()).readonly(),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const surfaceSummarySchema = z.object({
|
|
236
|
+
id: z.string(),
|
|
237
|
+
trailheads: z.array(z.string()).readonly(),
|
|
238
|
+
trails: z.array(z.string()).readonly(),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const trailheadSummarySchema = z.object({
|
|
242
|
+
description: z.string(),
|
|
243
|
+
id: z.string(),
|
|
244
|
+
memberIds: z.array(z.string()).readonly(),
|
|
245
|
+
surfaces: z.array(z.string()).readonly(),
|
|
246
|
+
visibility: z.enum(['internal', 'public']).optional(),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const versionSummarySchema = z.object({
|
|
250
|
+
current: z.boolean(),
|
|
251
|
+
exampleCount: z.number(),
|
|
252
|
+
id: z.string(),
|
|
253
|
+
kind: z.enum(['current', 'fork', 'revision']),
|
|
254
|
+
marker: z.string().optional(),
|
|
255
|
+
resources: z.array(z.string()).readonly(),
|
|
256
|
+
status: z.unknown().optional(),
|
|
257
|
+
trailId: z.string(),
|
|
258
|
+
version: z.number(),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const exampleSummarySchema = z.object({
|
|
262
|
+
example: z.unknown(),
|
|
263
|
+
index: z.number(),
|
|
264
|
+
source: z.enum(['entry', 'version']),
|
|
265
|
+
targetId: z.string(),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const errorFactsCompletenessSchema = z.discriminatedUnion('status', [
|
|
269
|
+
z.object({
|
|
270
|
+
reason: z.literal('authored-facts-exhausted'),
|
|
271
|
+
status: z.literal('complete'),
|
|
272
|
+
}),
|
|
273
|
+
z.object({
|
|
274
|
+
reason: z.enum(['inferred-facts-supplied', 'observed-facts-supplied']),
|
|
275
|
+
status: z.literal('partial'),
|
|
276
|
+
}),
|
|
277
|
+
z.object({
|
|
278
|
+
reason: z.enum(['no-exhaustive-emitted-error-contract', 'not-evaluated']),
|
|
279
|
+
status: z.literal('unknown'),
|
|
280
|
+
}),
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
const errorSurfaceProjectionSchema = z.object({
|
|
284
|
+
category: z.enum(errorCategories),
|
|
285
|
+
code: z.number(),
|
|
286
|
+
name: z.string(),
|
|
287
|
+
retryable: z.boolean(),
|
|
288
|
+
surface: z.enum(surfaceNames),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const errorTaxonomyProjectionSchema = z.object({
|
|
292
|
+
category: z.enum(errorCategories).optional(),
|
|
293
|
+
dynamicCategory: z
|
|
294
|
+
.object({
|
|
295
|
+
inheritsCategoryFrom: z.literal('wrapped-error'),
|
|
296
|
+
})
|
|
297
|
+
.optional(),
|
|
298
|
+
known: z.boolean(),
|
|
299
|
+
name: z.string(),
|
|
300
|
+
retryable: z.boolean().optional(),
|
|
301
|
+
surfaces: z.array(errorSurfaceProjectionSchema).readonly(),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const errorFactProvenanceSchema = z.object({
|
|
305
|
+
detail: z.string().optional(),
|
|
306
|
+
detourIndex: z.number().optional(),
|
|
307
|
+
exampleName: z.string().optional(),
|
|
308
|
+
source: z.enum([
|
|
309
|
+
'runtime-observation',
|
|
310
|
+
'static-inference',
|
|
311
|
+
'trail.detours',
|
|
312
|
+
'trail.examples',
|
|
313
|
+
'trail.versions.detours',
|
|
314
|
+
'trail.versions.examples',
|
|
315
|
+
]),
|
|
316
|
+
trailId: z.string(),
|
|
317
|
+
version: z.number().optional(),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const errorFactSchema = z.object({
|
|
321
|
+
completeness: errorFactsCompletenessSchema,
|
|
322
|
+
kind: z.enum(['documented', 'handled', 'inferred', 'observed']),
|
|
323
|
+
provenance: errorFactProvenanceSchema,
|
|
324
|
+
taxonomy: errorTaxonomyProjectionSchema,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const trailErrorFactsSchema = z.object({
|
|
328
|
+
completeness: z.object({
|
|
329
|
+
documented: errorFactsCompletenessSchema,
|
|
330
|
+
emitted: errorFactsCompletenessSchema,
|
|
331
|
+
handled: errorFactsCompletenessSchema,
|
|
332
|
+
inferred: errorFactsCompletenessSchema,
|
|
333
|
+
observed: errorFactsCompletenessSchema,
|
|
334
|
+
}),
|
|
335
|
+
facts: z.array(errorFactSchema).readonly(),
|
|
336
|
+
trailId: z.string(),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const describeOutputSchema = envelopeSchema.extend({
|
|
340
|
+
entity: z.record(z.string(), z.unknown()),
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const contractOutputSchema = envelopeSchema.extend({
|
|
344
|
+
contract: z.record(z.string(), z.unknown()),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const nearbyOutputSchema = envelopeSchema.extend({
|
|
348
|
+
edges: z.array(relationEdgeSchema).readonly(),
|
|
349
|
+
relations: z.array(relationGroupSchema).readonly(),
|
|
350
|
+
target: refOutputSchema,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const impactOutputSchema = envelopeSchema.extend({
|
|
354
|
+
direction: z.enum(['downstream', 'upstream', 'both']),
|
|
355
|
+
edges: z.array(relationEdgeSchema).readonly(),
|
|
356
|
+
maxDepth: z.number(),
|
|
357
|
+
nodes: z.array(impactNodeSchema).readonly(),
|
|
358
|
+
target: refOutputSchema,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const diffEntryOutputSchema = z.object({
|
|
362
|
+
change: z.enum(['added', 'removed', 'modified']),
|
|
363
|
+
details: z.array(z.string()).readonly(),
|
|
364
|
+
id: z.string(),
|
|
365
|
+
kind: z.enum(['entity', 'trailhead', 'resource', 'signal', 'trail']),
|
|
366
|
+
severity: z.enum(['info', 'warning', 'breaking']),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const diffResultOutputSchema = z.object({
|
|
370
|
+
breaking: z.array(diffEntryOutputSchema).readonly(),
|
|
371
|
+
entries: z.array(diffEntryOutputSchema).readonly(),
|
|
372
|
+
hasBreaking: z.boolean(),
|
|
373
|
+
info: z.array(diffEntryOutputSchema).readonly(),
|
|
374
|
+
warnings: z.array(diffEntryOutputSchema).readonly(),
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const diffOutputSchema = envelopeSchema.extend({
|
|
378
|
+
against: envelopeSchema,
|
|
379
|
+
diff: diffResultOutputSchema,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const errorsOutputSchema = envelopeSchema.extend({
|
|
383
|
+
errors: z.array(trailErrorFactsSchema).readonly(),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const adapterPlacementSchema = z.enum(adapterTargetPlacements);
|
|
387
|
+
|
|
388
|
+
const adapterFactProvenanceSchema = z.object({
|
|
389
|
+
packageJsonPath: z.string().optional(),
|
|
390
|
+
paths: z.array(z.string()).readonly().optional(),
|
|
391
|
+
source: z.enum([
|
|
392
|
+
'adapter-package-manifest',
|
|
393
|
+
'conformance-test',
|
|
394
|
+
'owner-package-manifest',
|
|
395
|
+
'runtime-observation',
|
|
396
|
+
]),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const adapterFactSchema = z.object({
|
|
400
|
+
adapterType: z.string().optional(),
|
|
401
|
+
key: z.string(),
|
|
402
|
+
kind: adapterFactKindSchema,
|
|
403
|
+
ownerPackage: z.string().optional(),
|
|
404
|
+
packageName: z.string().optional(),
|
|
405
|
+
placement: adapterPlacementSchema.optional(),
|
|
406
|
+
placements: z.array(adapterPlacementSchema).readonly().optional(),
|
|
407
|
+
provenance: adapterFactProvenanceSchema,
|
|
408
|
+
target: z.string(),
|
|
409
|
+
targetKey: z.string().optional(),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const adapterFactCountsSchema = z.object({
|
|
413
|
+
available: z.number(),
|
|
414
|
+
configured: z.number(),
|
|
415
|
+
diagnostics: z.number(),
|
|
416
|
+
observed: z.number(),
|
|
417
|
+
used: z.number(),
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const adapterDiagnosticSchema = z.object({
|
|
421
|
+
code: z.string(),
|
|
422
|
+
message: z.string(),
|
|
423
|
+
packageJsonPath: z.string(),
|
|
424
|
+
packageName: z.string().optional(),
|
|
425
|
+
placement: adapterPlacementSchema.optional(),
|
|
426
|
+
severity: z.enum(['error', 'warn']),
|
|
427
|
+
target: z.string().optional(),
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const adaptersOutputSchema = z.object({
|
|
431
|
+
adapters: z.array(adapterFactSchema).readonly(),
|
|
432
|
+
counts: adapterFactCountsSchema,
|
|
433
|
+
diagnostics: z.array(adapterDiagnosticSchema).readonly(),
|
|
434
|
+
rootDir: z.string(),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
type SourceInput = z.output<typeof sourceInputSchema>;
|
|
438
|
+
type AdapterFactsInput = z.output<typeof adapterFactsInputSchema>;
|
|
439
|
+
type InspectInput = z.output<typeof inspectInputSchema>;
|
|
440
|
+
type ContractInput = z.output<typeof contractInputSchema>;
|
|
441
|
+
type DiffInput = z.output<typeof diffInputSchema>;
|
|
442
|
+
|
|
443
|
+
interface LoadedWayfinderGraph {
|
|
444
|
+
readonly graph: TopoGraph;
|
|
445
|
+
readonly load: WayfinderArtifactLoad;
|
|
446
|
+
readonly source: {
|
|
447
|
+
readonly kind: 'topoGraph';
|
|
448
|
+
readonly path: string;
|
|
449
|
+
readonly schemaVersion: number;
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const toLoaderOptions = (
|
|
454
|
+
input: SourceInput,
|
|
455
|
+
cwd: string | undefined
|
|
456
|
+
): WayfinderArtifactLoaderOptions => {
|
|
457
|
+
const rootDir = input.rootDir ?? (input.dir === undefined ? cwd : undefined);
|
|
458
|
+
return {
|
|
459
|
+
...(input.dir === undefined ? {} : { dir: input.dir }),
|
|
460
|
+
...(rootDir === undefined ? {} : { rootDir }),
|
|
461
|
+
...(input.trailsDbPath === undefined ? {} : { path: input.trailsDbPath }),
|
|
462
|
+
};
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const topoGraphSourcePath = (
|
|
466
|
+
input: SourceInput,
|
|
467
|
+
cwd: string | undefined
|
|
468
|
+
): string | undefined => {
|
|
469
|
+
if (input.dir !== undefined) {
|
|
470
|
+
return join(input.dir, 'trails.lock');
|
|
471
|
+
}
|
|
472
|
+
const rootDir = input.rootDir ?? cwd;
|
|
473
|
+
return rootDir === undefined ? undefined : join(rootDir, 'trails.lock');
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const loadGraph = async (
|
|
477
|
+
input: SourceInput,
|
|
478
|
+
cwd: string | undefined
|
|
479
|
+
): Promise<Result<LoadedWayfinderGraph, TrailsError>> => {
|
|
480
|
+
let load: WayfinderArtifactLoad;
|
|
481
|
+
try {
|
|
482
|
+
load = await loadWayfinderArtifacts(toLoaderOptions(input, cwd));
|
|
483
|
+
} catch (error) {
|
|
484
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
485
|
+
return Result.err(
|
|
486
|
+
new DerivationError('Unable to load Wayfinder artifacts.', {
|
|
487
|
+
cause,
|
|
488
|
+
context: {
|
|
489
|
+
artifact: 'topoGraph',
|
|
490
|
+
path: topoGraphSourcePath(input, cwd) ?? 'trails.lock',
|
|
491
|
+
},
|
|
492
|
+
})
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
if (
|
|
496
|
+
load.artifactStatus.status === 'schema-version-drift' &&
|
|
497
|
+
load.artifactStatus.artifact === 'topoGraph'
|
|
498
|
+
) {
|
|
499
|
+
return Result.err(
|
|
500
|
+
new DerivationError(load.artifactStatus.message, {
|
|
501
|
+
context: {
|
|
502
|
+
artifact: load.artifactStatus.artifact,
|
|
503
|
+
artifactStatus: load.artifactStatus.status,
|
|
504
|
+
},
|
|
505
|
+
})
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (load.topoGraph === null) {
|
|
509
|
+
return Result.err(
|
|
510
|
+
new NotFoundError(
|
|
511
|
+
'No wayfinder TopoGraph artifact found. Run `trails compile` first.'
|
|
512
|
+
)
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return Result.ok({
|
|
516
|
+
graph: load.topoGraph,
|
|
517
|
+
load,
|
|
518
|
+
source: {
|
|
519
|
+
kind: 'topoGraph',
|
|
520
|
+
path: topoGraphSourcePath(input, cwd) ?? 'trails.lock',
|
|
521
|
+
schemaVersion: load.topoGraph.topoGraphSchemaVersion,
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const adapterFactsRootDir = (
|
|
527
|
+
input: AdapterFactsInput,
|
|
528
|
+
cwd: string | undefined
|
|
529
|
+
): Result<string, ValidationError> => {
|
|
530
|
+
const rootDir = input.rootDir ?? cwd;
|
|
531
|
+
return rootDir === undefined
|
|
532
|
+
? Result.err(
|
|
533
|
+
new ValidationError(
|
|
534
|
+
'Provide rootDir or run wayfind.adapters from a workspace directory.'
|
|
535
|
+
)
|
|
536
|
+
)
|
|
537
|
+
: Result.ok(resolve(rootDir));
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const filteredAdapterFacts = (
|
|
541
|
+
input: AdapterFactsInput,
|
|
542
|
+
cwd: string | undefined
|
|
543
|
+
): Result<z.output<typeof adaptersOutputSchema>, ValidationError> => {
|
|
544
|
+
const rootDir = adapterFactsRootDir(input, cwd);
|
|
545
|
+
if (rootDir.isErr()) {
|
|
546
|
+
return rootDir;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const report = checkAdapters(rootDir.value);
|
|
550
|
+
const facts = report.facts
|
|
551
|
+
.filter(
|
|
552
|
+
(fact: AdapterFact) =>
|
|
553
|
+
(input.filters?.kind === undefined ||
|
|
554
|
+
fact.kind === input.filters.kind) &&
|
|
555
|
+
(input.filters?.target === undefined ||
|
|
556
|
+
fact.target === input.filters.target) &&
|
|
557
|
+
(input.filters?.packageName === undefined ||
|
|
558
|
+
fact.packageName === input.filters.packageName)
|
|
559
|
+
)
|
|
560
|
+
.slice(0, input.limit);
|
|
561
|
+
|
|
562
|
+
return Result.ok({
|
|
563
|
+
adapters: facts,
|
|
564
|
+
counts: {
|
|
565
|
+
available: report.facts.filter(
|
|
566
|
+
(fact: AdapterFact) => fact.kind === 'available'
|
|
567
|
+
).length,
|
|
568
|
+
configured: report.facts.filter(
|
|
569
|
+
(fact: AdapterFact) => fact.kind === 'configured'
|
|
570
|
+
).length,
|
|
571
|
+
diagnostics: report.diagnostics.length,
|
|
572
|
+
observed: report.facts.filter(
|
|
573
|
+
(fact: AdapterFact) => fact.kind === 'observed'
|
|
574
|
+
).length,
|
|
575
|
+
used: report.facts.filter((fact: AdapterFact) => fact.kind === 'used')
|
|
576
|
+
.length,
|
|
577
|
+
},
|
|
578
|
+
diagnostics: [...report.diagnostics],
|
|
579
|
+
rootDir: rootDir.value,
|
|
580
|
+
});
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const envelope = (
|
|
584
|
+
loaded: LoadedWayfinderGraph
|
|
585
|
+
): z.output<typeof envelopeSchema> => ({
|
|
586
|
+
drift: wayfinderDriftFromArtifactStatus(loaded.load.artifactStatus),
|
|
587
|
+
source: loaded.source,
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const entrySignals = (entry: TopoGraphEntry): readonly string[] =>
|
|
591
|
+
[
|
|
592
|
+
...(entry.fires ?? []),
|
|
593
|
+
...(entry.on ?? []),
|
|
594
|
+
...(entry.from ?? []),
|
|
595
|
+
...(entry.producers ?? []),
|
|
596
|
+
...(entry.consumers ?? []),
|
|
597
|
+
].toSorted();
|
|
598
|
+
|
|
599
|
+
const entryExamples = (entry: TopoGraphEntry): readonly unknown[] =>
|
|
600
|
+
entry.examples ?? [];
|
|
601
|
+
|
|
602
|
+
const entryById = (
|
|
603
|
+
graph: TopoGraph,
|
|
604
|
+
id: string,
|
|
605
|
+
kind?: TopoGraphEntry['kind']
|
|
606
|
+
): TopoGraphEntry | undefined =>
|
|
607
|
+
graph.entries.find(
|
|
608
|
+
(entry) => entry.id === id && (kind === undefined || entry.kind === kind)
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
const usedByResource = (
|
|
612
|
+
graph: TopoGraph,
|
|
613
|
+
resourceId: string
|
|
614
|
+
): readonly string[] =>
|
|
615
|
+
graph.entries
|
|
616
|
+
.filter(
|
|
617
|
+
(entry) =>
|
|
618
|
+
entry.kind === 'trail' && (entry.resources ?? []).includes(resourceId)
|
|
619
|
+
)
|
|
620
|
+
.map((entry) => entry.id)
|
|
621
|
+
.toSorted();
|
|
622
|
+
|
|
623
|
+
const trailSummaries = (graph: TopoGraph) =>
|
|
624
|
+
graph.entries
|
|
625
|
+
.filter((entry) => entry.kind === 'trail')
|
|
626
|
+
.map((entry) => ({
|
|
627
|
+
cli: entry.cli ?? null,
|
|
628
|
+
composes: entry.composes ?? [],
|
|
629
|
+
exampleCount: entry.exampleCount,
|
|
630
|
+
id: entry.id,
|
|
631
|
+
intent: entry.intent ?? 'write',
|
|
632
|
+
kind: 'trail' as const,
|
|
633
|
+
resources: entry.resources ?? [],
|
|
634
|
+
signals: entrySignals(entry),
|
|
635
|
+
surfaces: entry.surfaces,
|
|
636
|
+
version: entry.version ?? null,
|
|
637
|
+
}));
|
|
638
|
+
|
|
639
|
+
const entitySummaries = (graph: TopoGraph) =>
|
|
640
|
+
graph.entries
|
|
641
|
+
.filter((entry) => entry.kind === 'entity')
|
|
642
|
+
.map((entry) => ({
|
|
643
|
+
exampleCount: entry.exampleCount,
|
|
644
|
+
id: entry.id,
|
|
645
|
+
identity: entry.identity,
|
|
646
|
+
kind: 'entity' as const,
|
|
647
|
+
references: entry.references ?? [],
|
|
648
|
+
surfaces: entry.surfaces,
|
|
649
|
+
}));
|
|
650
|
+
|
|
651
|
+
const resourceSummaries = (graph: TopoGraph) =>
|
|
652
|
+
graph.entries
|
|
653
|
+
.filter((entry) => entry.kind === 'resource')
|
|
654
|
+
.map((entry) => ({
|
|
655
|
+
exampleCount: entry.exampleCount,
|
|
656
|
+
id: entry.id,
|
|
657
|
+
kind: 'resource' as const,
|
|
658
|
+
surfaces: entry.surfaces,
|
|
659
|
+
usedBy: usedByResource(graph, entry.id),
|
|
660
|
+
}));
|
|
661
|
+
|
|
662
|
+
const signalSummaries = (graph: TopoGraph) =>
|
|
663
|
+
graph.entries
|
|
664
|
+
.filter((entry) => entry.kind === 'signal')
|
|
665
|
+
.map((entry) => ({
|
|
666
|
+
consumers: entry.consumers ?? [],
|
|
667
|
+
exampleCount: entry.exampleCount,
|
|
668
|
+
id: entry.id,
|
|
669
|
+
kind: 'signal' as const,
|
|
670
|
+
producers: entry.producers ?? [],
|
|
671
|
+
surfaces: entry.surfaces,
|
|
672
|
+
}));
|
|
673
|
+
|
|
674
|
+
const surfaceSummaries = (graph: TopoGraph) => {
|
|
675
|
+
const surfaceIds = new Set<string>();
|
|
676
|
+
for (const entry of graph.entries) {
|
|
677
|
+
for (const surface of entry.surfaces) {
|
|
678
|
+
surfaceIds.add(surface);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
for (const trailhead of graph.trailheads ?? []) {
|
|
682
|
+
for (const surface of trailhead.surfaces) {
|
|
683
|
+
surfaceIds.add(surface);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return [...surfaceIds].toSorted().map((surface) => ({
|
|
687
|
+
id: surface,
|
|
688
|
+
trailheads: (graph.trailheads ?? [])
|
|
689
|
+
.filter((trailhead) => trailhead.surfaces.includes(surface))
|
|
690
|
+
.map((trailhead) => trailhead.id)
|
|
691
|
+
.toSorted(),
|
|
692
|
+
trails: filterWayfinderEntityRefs(graph, {
|
|
693
|
+
kind: 'trail',
|
|
694
|
+
surface,
|
|
695
|
+
}).map((ref) => ref.id),
|
|
696
|
+
}));
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const trailheadSummaries = (graph: TopoGraph) =>
|
|
700
|
+
(graph.trailheads ?? []).map((trailhead) => ({
|
|
701
|
+
description: trailhead.description,
|
|
702
|
+
id: trailhead.id,
|
|
703
|
+
memberIds: trailhead.memberIds,
|
|
704
|
+
surfaces: trailhead.surfaces,
|
|
705
|
+
visibility: trailhead.visibility,
|
|
706
|
+
}));
|
|
707
|
+
|
|
708
|
+
const versionSummaries = (graph: TopoGraph) =>
|
|
709
|
+
graph.entries
|
|
710
|
+
.filter((entry) => entry.kind === 'trail' && entry.version !== undefined)
|
|
711
|
+
.flatMap((entry) => {
|
|
712
|
+
const current = {
|
|
713
|
+
current: true,
|
|
714
|
+
exampleCount: entry.exampleCount,
|
|
715
|
+
id: `${entry.id}@${entry.version as number}`,
|
|
716
|
+
kind: 'current' as const,
|
|
717
|
+
marker: entry.marker,
|
|
718
|
+
resources: entry.resources ?? [],
|
|
719
|
+
trailId: entry.id,
|
|
720
|
+
version: entry.version as number,
|
|
721
|
+
};
|
|
722
|
+
const historical = Object.entries(entry.versions ?? {}).map(
|
|
723
|
+
([versionKey, version]) => ({
|
|
724
|
+
current: false,
|
|
725
|
+
exampleCount: version.exampleCount,
|
|
726
|
+
id: `${entry.id}@${versionKey}`,
|
|
727
|
+
kind: version.kind,
|
|
728
|
+
marker: version.marker,
|
|
729
|
+
resources: version.resources ?? [],
|
|
730
|
+
status: version.status,
|
|
731
|
+
trailId: entry.id,
|
|
732
|
+
version: Number(versionKey),
|
|
733
|
+
})
|
|
734
|
+
);
|
|
735
|
+
return [current, ...historical].toSorted(
|
|
736
|
+
(a, b) => a.trailId.localeCompare(b.trailId) || a.version - b.version
|
|
737
|
+
);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const exampleSummaries = (graph: TopoGraph) =>
|
|
741
|
+
graph.entries.flatMap((entry) => {
|
|
742
|
+
const current = entryExamples(entry).map((example, index) => ({
|
|
743
|
+
example,
|
|
744
|
+
index,
|
|
745
|
+
source: 'entry' as const,
|
|
746
|
+
targetId: entry.id,
|
|
747
|
+
}));
|
|
748
|
+
const versions = Object.entries(entry.versions ?? {}).flatMap(
|
|
749
|
+
([versionKey, version]) =>
|
|
750
|
+
(version.examples ?? []).map((example, index) => ({
|
|
751
|
+
example,
|
|
752
|
+
index,
|
|
753
|
+
source: 'version' as const,
|
|
754
|
+
targetId: `${entry.id}@${versionKey}`,
|
|
755
|
+
}))
|
|
756
|
+
);
|
|
757
|
+
return [...current, ...versions];
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
const kindFilter = (
|
|
761
|
+
kind: WayfinderEntityKind,
|
|
762
|
+
filters: WayfinderEntityFilterInput | undefined
|
|
763
|
+
): WayfinderEntityFilterInput => ({ ...filters, kind });
|
|
764
|
+
|
|
765
|
+
const allowsTrailKind = (
|
|
766
|
+
filters: WayfinderEntityFilterInput | undefined
|
|
767
|
+
): boolean => {
|
|
768
|
+
const kind = filters?.kind;
|
|
769
|
+
return (
|
|
770
|
+
kind === undefined ||
|
|
771
|
+
kind === 'trail' ||
|
|
772
|
+
(Array.isArray(kind) && kind.includes('trail'))
|
|
773
|
+
);
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const allowsVersionKind = (
|
|
777
|
+
filters: WayfinderEntityFilterInput | undefined
|
|
778
|
+
): boolean => {
|
|
779
|
+
const kind = filters?.kind;
|
|
780
|
+
return (
|
|
781
|
+
kind === undefined ||
|
|
782
|
+
kind === 'version' ||
|
|
783
|
+
(Array.isArray(kind) && kind.includes('version'))
|
|
784
|
+
);
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
const allowsExampleWidening = (
|
|
788
|
+
filters: WayfinderEntityFilterInput | undefined
|
|
789
|
+
): boolean => filters?.exampleCoverage !== false;
|
|
790
|
+
|
|
791
|
+
const filteredExampleSummaries = (
|
|
792
|
+
graph: TopoGraph,
|
|
793
|
+
filters: WayfinderEntityFilterInput | undefined,
|
|
794
|
+
limit: number
|
|
795
|
+
) => {
|
|
796
|
+
if (filters === undefined || Object.keys(filters).length === 0) {
|
|
797
|
+
return exampleSummaries(graph).slice(0, limit);
|
|
798
|
+
}
|
|
799
|
+
const ids = new Set(
|
|
800
|
+
filterWayfinderEntityRefs(graph, filters).map((ref) => ref.id)
|
|
801
|
+
);
|
|
802
|
+
const trailIds = new Set(
|
|
803
|
+
allowsTrailKind(filters)
|
|
804
|
+
? filterWayfinderEntityRefs(graph, kindFilter('trail', filters)).map(
|
|
805
|
+
(ref) => ref.id
|
|
806
|
+
)
|
|
807
|
+
: []
|
|
808
|
+
);
|
|
809
|
+
const versionIds = new Set(
|
|
810
|
+
allowsVersionKind(filters)
|
|
811
|
+
? filterWayfinderEntityRefs(graph, kindFilter('version', filters)).map(
|
|
812
|
+
(ref) => ref.id
|
|
813
|
+
)
|
|
814
|
+
: []
|
|
815
|
+
);
|
|
816
|
+
const currentVersionTrailIds = new Set(
|
|
817
|
+
graph.entries
|
|
818
|
+
.filter(
|
|
819
|
+
(entry) =>
|
|
820
|
+
entry.kind === 'trail' &&
|
|
821
|
+
entry.version !== undefined &&
|
|
822
|
+
versionIds.has(`${entry.id}@${entry.version}`)
|
|
823
|
+
)
|
|
824
|
+
.map((entry) => entry.id)
|
|
825
|
+
);
|
|
826
|
+
const historicalVersionTrailIds = new Map(
|
|
827
|
+
graph.entries.flatMap((entry) =>
|
|
828
|
+
entry.kind === 'trail'
|
|
829
|
+
? Object.keys(entry.versions ?? {}).map((versionKey) => [
|
|
830
|
+
`${entry.id}@${versionKey}`,
|
|
831
|
+
entry.id,
|
|
832
|
+
])
|
|
833
|
+
: []
|
|
834
|
+
)
|
|
835
|
+
);
|
|
836
|
+
return exampleSummaries(graph)
|
|
837
|
+
.filter(
|
|
838
|
+
(example) =>
|
|
839
|
+
ids.has(example.targetId) ||
|
|
840
|
+
(example.source === 'entry' &&
|
|
841
|
+
currentVersionTrailIds.has(example.targetId)) ||
|
|
842
|
+
(example.source === 'version' &&
|
|
843
|
+
allowsExampleWidening(filters) &&
|
|
844
|
+
trailIds.has(historicalVersionTrailIds.get(example.targetId) ?? ''))
|
|
845
|
+
)
|
|
846
|
+
.slice(0, limit);
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const filteredVersionSummaries = (
|
|
850
|
+
graph: TopoGraph,
|
|
851
|
+
filters: WayfinderEntityFilterInput | undefined,
|
|
852
|
+
limit: number
|
|
853
|
+
) => {
|
|
854
|
+
const summaries = versionSummaries(graph);
|
|
855
|
+
if (filters === undefined || Object.keys(filters).length === 0) {
|
|
856
|
+
return summaries.slice(0, limit);
|
|
857
|
+
}
|
|
858
|
+
const ids = new Set(
|
|
859
|
+
filterWayfinderEntityRefs(graph, kindFilter('version', filters)).map(
|
|
860
|
+
(ref) => ref.id
|
|
861
|
+
)
|
|
862
|
+
);
|
|
863
|
+
const trailIds = new Set(
|
|
864
|
+
filterWayfinderEntityRefs(graph, kindFilter('trail', filters)).map(
|
|
865
|
+
(ref) => ref.id
|
|
866
|
+
)
|
|
867
|
+
);
|
|
868
|
+
return summaries
|
|
869
|
+
.filter(
|
|
870
|
+
(summary) =>
|
|
871
|
+
ids.has(summary.id) ||
|
|
872
|
+
(summary.current && trailIds.has(summary.trailId))
|
|
873
|
+
)
|
|
874
|
+
.slice(0, limit);
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const againstInput = (input: DiffInput): SourceInput => ({
|
|
878
|
+
...(input.againstDir === undefined ? {} : { dir: input.againstDir }),
|
|
879
|
+
...(input.againstRootDir === undefined
|
|
880
|
+
? {}
|
|
881
|
+
: { rootDir: input.againstRootDir }),
|
|
882
|
+
...(input.againstTrailsDbPath === undefined
|
|
883
|
+
? {}
|
|
884
|
+
: { trailsDbPath: input.againstTrailsDbPath }),
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
const diffBaselineError = (input: DiffInput): ValidationError | undefined => {
|
|
888
|
+
if (input.againstDir === undefined && input.againstRootDir === undefined) {
|
|
889
|
+
return new ValidationError(
|
|
890
|
+
'Provide againstDir or againstRootDir for the baseline graph.'
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
if (input.againstDir !== undefined && input.againstRootDir !== undefined) {
|
|
894
|
+
return new ValidationError(
|
|
895
|
+
'Provide only one of againstDir or againstRootDir.'
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
return undefined;
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
const describeSurface = (graph: TopoGraph, id: string) => {
|
|
902
|
+
const surface = surfaceSummaries(graph).find(
|
|
903
|
+
(candidate) => candidate.id === id
|
|
904
|
+
);
|
|
905
|
+
return surface === undefined ? undefined : { ...surface, kind: 'surface' };
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
const describeTrailhead = (graph: TopoGraph, id: string) => {
|
|
909
|
+
const trailhead = trailheadSummaries(graph).find(
|
|
910
|
+
(candidate) => candidate.id === id
|
|
911
|
+
);
|
|
912
|
+
return trailhead === undefined
|
|
913
|
+
? undefined
|
|
914
|
+
: { ...trailhead, kind: 'trailhead' };
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
const describeVersion = (graph: TopoGraph, id: string) => {
|
|
918
|
+
const version = versionSummaries(graph).find(
|
|
919
|
+
(candidate) => candidate.id === id
|
|
920
|
+
);
|
|
921
|
+
return version === undefined ? undefined : { ...version, kind: 'version' };
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
type ResolvedEntity = Readonly<Record<string, unknown>>;
|
|
925
|
+
|
|
926
|
+
interface ResolvedEntityCandidate {
|
|
927
|
+
readonly kind: WayfinderEntityKind;
|
|
928
|
+
readonly value: ResolvedEntity;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const ambiguousId = (
|
|
932
|
+
id: string,
|
|
933
|
+
candidates: readonly ResolvedEntityCandidate[]
|
|
934
|
+
): AmbiguousError =>
|
|
935
|
+
new AmbiguousError(
|
|
936
|
+
`Wayfinder id "${id}" matched multiple entity kinds: ${candidates
|
|
937
|
+
.map((candidate) => candidate.kind)
|
|
938
|
+
.join(', ')}. Pass kind to disambiguate.`
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
const singleCandidate = (
|
|
942
|
+
id: string,
|
|
943
|
+
candidates: readonly ResolvedEntityCandidate[]
|
|
944
|
+
): Result<ResolvedEntity | undefined, AmbiguousError> => {
|
|
945
|
+
if (candidates.length > 1) {
|
|
946
|
+
return Result.err(ambiguousId(id, candidates));
|
|
947
|
+
}
|
|
948
|
+
return Result.ok(candidates[0]?.value);
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
const describeCandidates = (
|
|
952
|
+
graph: TopoGraph,
|
|
953
|
+
id: string
|
|
954
|
+
): readonly ResolvedEntityCandidate[] => {
|
|
955
|
+
const candidates: ResolvedEntityCandidate[] = [];
|
|
956
|
+
const entry = entryById(graph, id);
|
|
957
|
+
if (entry !== undefined) {
|
|
958
|
+
candidates.push({
|
|
959
|
+
kind: entry.kind,
|
|
960
|
+
value: entry as unknown as ResolvedEntity,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
const trailhead = describeTrailhead(graph, id);
|
|
964
|
+
if (trailhead !== undefined) {
|
|
965
|
+
candidates.push({ kind: 'trailhead', value: trailhead });
|
|
966
|
+
}
|
|
967
|
+
const surface = describeSurface(graph, id);
|
|
968
|
+
if (surface !== undefined) {
|
|
969
|
+
candidates.push({ kind: 'surface', value: surface });
|
|
970
|
+
}
|
|
971
|
+
const version = describeVersion(graph, id);
|
|
972
|
+
if (version !== undefined) {
|
|
973
|
+
candidates.push({ kind: 'version', value: version });
|
|
974
|
+
}
|
|
975
|
+
return candidates;
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
const describeEntry = (
|
|
979
|
+
graph: TopoGraph,
|
|
980
|
+
input: InspectInput
|
|
981
|
+
): Result<ResolvedEntity | undefined, AmbiguousError> => {
|
|
982
|
+
if (input.kind === 'surface') {
|
|
983
|
+
return Result.ok(describeSurface(graph, input.id));
|
|
984
|
+
}
|
|
985
|
+
if (input.kind === 'trailhead') {
|
|
986
|
+
return Result.ok(describeTrailhead(graph, input.id));
|
|
987
|
+
}
|
|
988
|
+
if (input.kind === 'version') {
|
|
989
|
+
return Result.ok(describeVersion(graph, input.id));
|
|
990
|
+
}
|
|
991
|
+
if (input.kind === undefined) {
|
|
992
|
+
return singleCandidate(input.id, describeCandidates(graph, input.id));
|
|
993
|
+
}
|
|
994
|
+
const entry = entryById(graph, input.id, input.kind);
|
|
995
|
+
return Result.ok(
|
|
996
|
+
entry === undefined ? undefined : (entry as unknown as ResolvedEntity)
|
|
997
|
+
);
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
const contractVersionFor = (
|
|
1001
|
+
graph: TopoGraph,
|
|
1002
|
+
input: ContractInput
|
|
1003
|
+
): Readonly<Record<string, unknown>> | undefined => {
|
|
1004
|
+
const versionId =
|
|
1005
|
+
input.version === undefined ? input.id : `${input.id}@${input.version}`;
|
|
1006
|
+
const described = describeVersion(graph, versionId);
|
|
1007
|
+
if (described === undefined) {
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
const entry = entryById(graph, described.trailId, 'trail');
|
|
1011
|
+
const version = entry?.versions?.[String(described.version)];
|
|
1012
|
+
return {
|
|
1013
|
+
id: described.trailId,
|
|
1014
|
+
input: version?.input ?? entry?.input ?? null,
|
|
1015
|
+
kind: 'version',
|
|
1016
|
+
output: version?.output ?? entry?.output ?? null,
|
|
1017
|
+
resources: version?.resources ?? entry?.resources ?? [],
|
|
1018
|
+
version: described.version,
|
|
1019
|
+
};
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
const contractEntryKind = (
|
|
1023
|
+
kind: ContractInput['kind']
|
|
1024
|
+
): TopoGraphEntry['kind'] | undefined =>
|
|
1025
|
+
kind === undefined ||
|
|
1026
|
+
kind === 'trailhead' ||
|
|
1027
|
+
kind === 'surface' ||
|
|
1028
|
+
kind === 'version'
|
|
1029
|
+
? undefined
|
|
1030
|
+
: kind;
|
|
1031
|
+
|
|
1032
|
+
const contractEntry = (
|
|
1033
|
+
entry: TopoGraphEntry
|
|
1034
|
+
): Readonly<Record<string, unknown>> => ({
|
|
1035
|
+
cli: entry.cli ?? null,
|
|
1036
|
+
id: entry.id,
|
|
1037
|
+
input: entry.input ?? null,
|
|
1038
|
+
kind: entry.kind,
|
|
1039
|
+
output: entry.output ?? null,
|
|
1040
|
+
payload: entry.payload ?? null,
|
|
1041
|
+
resources: entry.resources ?? [],
|
|
1042
|
+
schema: entry.schema ?? null,
|
|
1043
|
+
version: entry.version ?? null,
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
const contractSurfaceOrTrailhead = (
|
|
1047
|
+
graph: TopoGraph,
|
|
1048
|
+
input: ContractInput
|
|
1049
|
+
): ResolvedEntity | undefined => {
|
|
1050
|
+
if (input.kind === 'trailhead') {
|
|
1051
|
+
return describeTrailhead(graph, input.id);
|
|
1052
|
+
}
|
|
1053
|
+
if (input.kind === 'surface') {
|
|
1054
|
+
return describeSurface(graph, input.id);
|
|
1055
|
+
}
|
|
1056
|
+
return undefined;
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
const contractFor = (
|
|
1060
|
+
graph: TopoGraph,
|
|
1061
|
+
input: ContractInput
|
|
1062
|
+
): Result<ResolvedEntity | undefined, AmbiguousError> => {
|
|
1063
|
+
if (input.kind === 'version' || input.version !== undefined) {
|
|
1064
|
+
return Result.ok(contractVersionFor(graph, input));
|
|
1065
|
+
}
|
|
1066
|
+
if (input.kind === 'surface' || input.kind === 'trailhead') {
|
|
1067
|
+
return Result.ok(contractSurfaceOrTrailhead(graph, input));
|
|
1068
|
+
}
|
|
1069
|
+
if (input.kind === undefined) {
|
|
1070
|
+
const candidates: ResolvedEntityCandidate[] = [];
|
|
1071
|
+
const entry = entryById(graph, input.id);
|
|
1072
|
+
if (entry !== undefined) {
|
|
1073
|
+
candidates.push({ kind: entry.kind, value: contractEntry(entry) });
|
|
1074
|
+
}
|
|
1075
|
+
const trailhead = describeTrailhead(graph, input.id);
|
|
1076
|
+
if (trailhead !== undefined) {
|
|
1077
|
+
candidates.push({ kind: 'trailhead', value: trailhead });
|
|
1078
|
+
}
|
|
1079
|
+
const surface = describeSurface(graph, input.id);
|
|
1080
|
+
if (surface !== undefined) {
|
|
1081
|
+
candidates.push({ kind: 'surface', value: surface });
|
|
1082
|
+
}
|
|
1083
|
+
const version = contractVersionFor(graph, { ...input, kind: 'version' });
|
|
1084
|
+
if (version !== undefined) {
|
|
1085
|
+
candidates.push({ kind: 'version', value: version });
|
|
1086
|
+
}
|
|
1087
|
+
return singleCandidate(input.id, candidates);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const entry = entryById(graph, input.id, contractEntryKind(input.kind));
|
|
1091
|
+
if (entry === undefined) {
|
|
1092
|
+
return Result.ok(contractSurfaceOrTrailhead(graph, input));
|
|
1093
|
+
}
|
|
1094
|
+
return Result.ok(contractEntry(entry));
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
const withGraph = async <TValue>(
|
|
1098
|
+
input: SourceInput,
|
|
1099
|
+
cwd: string | undefined,
|
|
1100
|
+
project: (loaded: LoadedWayfinderGraph) => TValue
|
|
1101
|
+
): Promise<Result<TValue, TrailsError>> => {
|
|
1102
|
+
const loaded = await loadGraph(input, cwd);
|
|
1103
|
+
if (loaded.isErr()) {
|
|
1104
|
+
return loaded;
|
|
1105
|
+
}
|
|
1106
|
+
return Result.ok(project(loaded.value));
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
const notFound = (kind: string, id: string): NotFoundError =>
|
|
1110
|
+
new NotFoundError(`No Wayfinder ${kind} found for "${id}".`);
|
|
1111
|
+
|
|
1112
|
+
const filteredIds = (
|
|
1113
|
+
graph: TopoGraph,
|
|
1114
|
+
kind: WayfinderEntityKind,
|
|
1115
|
+
filters: WayfinderEntityFilterInput | undefined,
|
|
1116
|
+
limit: number
|
|
1117
|
+
): ReadonlySet<string> =>
|
|
1118
|
+
new Set(
|
|
1119
|
+
resolveWayfinderPopulation(graph, {
|
|
1120
|
+
filters,
|
|
1121
|
+
kind,
|
|
1122
|
+
limit,
|
|
1123
|
+
}).map((ref) => ref.id)
|
|
1124
|
+
);
|
|
1125
|
+
|
|
1126
|
+
const filteredErrorFacts = (
|
|
1127
|
+
graph: TopoGraph,
|
|
1128
|
+
filters: WayfinderEntityFilterInput | undefined,
|
|
1129
|
+
limit: number
|
|
1130
|
+
) => {
|
|
1131
|
+
const ids = filteredIds(graph, 'trail', filters, limit);
|
|
1132
|
+
return deriveTrailErrorFacts(graph)
|
|
1133
|
+
.filter((entry) => ids.has(entry.trailId))
|
|
1134
|
+
.slice(0, limit);
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
export const wayfindOverviewTrail = trail('wayfind.overview', {
|
|
1138
|
+
description: 'Summarize the saved Wayfinder topo graph',
|
|
1139
|
+
examples: [{ input: {}, name: 'Overview' }],
|
|
1140
|
+
implementation: async (input, ctx) =>
|
|
1141
|
+
withGraph(input, ctx.cwd, (loaded) => {
|
|
1142
|
+
const { graph } = loaded;
|
|
1143
|
+
return {
|
|
1144
|
+
...envelope(loaded),
|
|
1145
|
+
counts: {
|
|
1146
|
+
entities: entitySummaries(graph).length,
|
|
1147
|
+
examples: exampleSummaries(graph).length,
|
|
1148
|
+
resources: resourceSummaries(graph).length,
|
|
1149
|
+
signals: signalSummaries(graph).length,
|
|
1150
|
+
surfaces: surfaceSummaries(graph).length,
|
|
1151
|
+
trailheads: trailheadSummaries(graph).length,
|
|
1152
|
+
trails: trailSummaries(graph).length,
|
|
1153
|
+
versions: versionSummaries(graph).length,
|
|
1154
|
+
},
|
|
1155
|
+
generatedAt: graph.generatedAt ?? null,
|
|
1156
|
+
workspace:
|
|
1157
|
+
graph.workspace === undefined
|
|
1158
|
+
? null
|
|
1159
|
+
: {
|
|
1160
|
+
collisionCount: graph.workspace.collisions?.length ?? 0,
|
|
1161
|
+
trailCount: Object.keys(graph.workspace.trails).length,
|
|
1162
|
+
},
|
|
1163
|
+
};
|
|
1164
|
+
}),
|
|
1165
|
+
input: sourceInputSchema,
|
|
1166
|
+
intent: 'read',
|
|
1167
|
+
output: envelopeSchema.extend({
|
|
1168
|
+
counts: z.object({
|
|
1169
|
+
entities: z.number(),
|
|
1170
|
+
examples: z.number(),
|
|
1171
|
+
resources: z.number(),
|
|
1172
|
+
signals: z.number(),
|
|
1173
|
+
surfaces: z.number(),
|
|
1174
|
+
trailheads: z.number(),
|
|
1175
|
+
trails: z.number(),
|
|
1176
|
+
versions: z.number(),
|
|
1177
|
+
}),
|
|
1178
|
+
generatedAt: z.string().nullable(),
|
|
1179
|
+
workspace: z
|
|
1180
|
+
.object({
|
|
1181
|
+
collisionCount: z.number(),
|
|
1182
|
+
trailCount: z.number(),
|
|
1183
|
+
})
|
|
1184
|
+
.nullable(),
|
|
1185
|
+
}),
|
|
1186
|
+
visibility: 'internal',
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
export const wayfindSearchTrail = trail('wayfind.search', {
|
|
1190
|
+
description: 'Find topo graph entities with typed filters',
|
|
1191
|
+
examples: [{ input: { filters: { kind: 'trail' } }, name: 'Find trails' }],
|
|
1192
|
+
implementation: async (input, ctx) =>
|
|
1193
|
+
withGraph(input, ctx.cwd, (loaded) => ({
|
|
1194
|
+
...envelope(loaded),
|
|
1195
|
+
matches: resolveWayfinderPopulation(loaded.graph, {
|
|
1196
|
+
filters: input.filters,
|
|
1197
|
+
limit: input.limit,
|
|
1198
|
+
}).map((ref) => ({
|
|
1199
|
+
id: ref.id,
|
|
1200
|
+
kind: ref.kind,
|
|
1201
|
+
...(ref.trailId === undefined ? {} : { trailId: ref.trailId }),
|
|
1202
|
+
...(ref.versionKey === undefined ? {} : { versionKey: ref.versionKey }),
|
|
1203
|
+
})),
|
|
1204
|
+
})),
|
|
1205
|
+
input: filteredInputSchema,
|
|
1206
|
+
intent: 'read',
|
|
1207
|
+
output: envelopeSchema.extend({
|
|
1208
|
+
matches: z.array(refOutputSchema).readonly(),
|
|
1209
|
+
}),
|
|
1210
|
+
visibility: 'internal',
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
export const wayfindTrailsTrail = trail('wayfind.trails', {
|
|
1214
|
+
description: 'List saved trail contracts',
|
|
1215
|
+
examples: [{ input: {}, name: 'List trails' }],
|
|
1216
|
+
implementation: async (input, ctx) =>
|
|
1217
|
+
withGraph(input, ctx.cwd, (loaded) => {
|
|
1218
|
+
const ids = filteredIds(
|
|
1219
|
+
loaded.graph,
|
|
1220
|
+
'trail',
|
|
1221
|
+
input.filters,
|
|
1222
|
+
input.limit
|
|
1223
|
+
);
|
|
1224
|
+
return {
|
|
1225
|
+
...envelope(loaded),
|
|
1226
|
+
trails: trailSummaries(loaded.graph).filter((entry) =>
|
|
1227
|
+
ids.has(entry.id)
|
|
1228
|
+
),
|
|
1229
|
+
};
|
|
1230
|
+
}),
|
|
1231
|
+
input: filteredInputSchema,
|
|
1232
|
+
intent: 'read',
|
|
1233
|
+
output: envelopeSchema.extend({
|
|
1234
|
+
trails: z.array(trailSummarySchema).readonly(),
|
|
1235
|
+
}),
|
|
1236
|
+
visibility: 'internal',
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
export const wayfindEntitiesTrail = trail('wayfind.entities', {
|
|
1240
|
+
description: 'List saved entity contracts',
|
|
1241
|
+
examples: [{ input: {}, name: 'List entities' }],
|
|
1242
|
+
implementation: async (input, ctx) =>
|
|
1243
|
+
withGraph(input, ctx.cwd, (loaded) => {
|
|
1244
|
+
const ids = filteredIds(
|
|
1245
|
+
loaded.graph,
|
|
1246
|
+
'entity',
|
|
1247
|
+
input.filters,
|
|
1248
|
+
input.limit
|
|
1249
|
+
);
|
|
1250
|
+
return {
|
|
1251
|
+
...envelope(loaded),
|
|
1252
|
+
entities: entitySummaries(loaded.graph).filter((entry) =>
|
|
1253
|
+
ids.has(entry.id)
|
|
1254
|
+
),
|
|
1255
|
+
};
|
|
1256
|
+
}),
|
|
1257
|
+
input: filteredInputSchema,
|
|
1258
|
+
intent: 'read',
|
|
1259
|
+
output: envelopeSchema.extend({
|
|
1260
|
+
entities: z.array(entitySummarySchema).readonly(),
|
|
1261
|
+
}),
|
|
1262
|
+
visibility: 'internal',
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
export const wayfindResourcesTrail = trail('wayfind.resources', {
|
|
1266
|
+
description: 'List saved resource contracts and usage',
|
|
1267
|
+
examples: [{ input: {}, name: 'List resources' }],
|
|
1268
|
+
implementation: async (input, ctx) =>
|
|
1269
|
+
withGraph(input, ctx.cwd, (loaded) => {
|
|
1270
|
+
const ids = filteredIds(
|
|
1271
|
+
loaded.graph,
|
|
1272
|
+
'resource',
|
|
1273
|
+
input.filters,
|
|
1274
|
+
input.limit
|
|
1275
|
+
);
|
|
1276
|
+
return {
|
|
1277
|
+
...envelope(loaded),
|
|
1278
|
+
resources: resourceSummaries(loaded.graph).filter((entry) =>
|
|
1279
|
+
ids.has(entry.id)
|
|
1280
|
+
),
|
|
1281
|
+
};
|
|
1282
|
+
}),
|
|
1283
|
+
input: filteredInputSchema,
|
|
1284
|
+
intent: 'read',
|
|
1285
|
+
output: envelopeSchema.extend({
|
|
1286
|
+
resources: z.array(resourceSummarySchema).readonly(),
|
|
1287
|
+
}),
|
|
1288
|
+
visibility: 'internal',
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
export const wayfindSignalsTrail = trail('wayfind.signals', {
|
|
1292
|
+
description: 'List saved signal contracts and graph usage',
|
|
1293
|
+
examples: [{ input: {}, name: 'List signals' }],
|
|
1294
|
+
implementation: async (input, ctx) =>
|
|
1295
|
+
withGraph(input, ctx.cwd, (loaded) => {
|
|
1296
|
+
const ids = filteredIds(
|
|
1297
|
+
loaded.graph,
|
|
1298
|
+
'signal',
|
|
1299
|
+
input.filters,
|
|
1300
|
+
input.limit
|
|
1301
|
+
);
|
|
1302
|
+
return {
|
|
1303
|
+
...envelope(loaded),
|
|
1304
|
+
signals: signalSummaries(loaded.graph).filter((entry) =>
|
|
1305
|
+
ids.has(entry.id)
|
|
1306
|
+
),
|
|
1307
|
+
};
|
|
1308
|
+
}),
|
|
1309
|
+
input: filteredInputSchema,
|
|
1310
|
+
intent: 'read',
|
|
1311
|
+
output: envelopeSchema.extend({
|
|
1312
|
+
signals: z.array(signalSummarySchema).readonly(),
|
|
1313
|
+
}),
|
|
1314
|
+
visibility: 'internal',
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
export const wayfindSurfacesTrail = trail('wayfind.surfaces', {
|
|
1318
|
+
description: 'List saved direct and trailhead-rendered surfaces',
|
|
1319
|
+
examples: [{ input: {}, name: 'List surfaces' }],
|
|
1320
|
+
implementation: async (input, ctx) =>
|
|
1321
|
+
withGraph(input, ctx.cwd, (loaded) => {
|
|
1322
|
+
const ids = filteredIds(
|
|
1323
|
+
loaded.graph,
|
|
1324
|
+
'surface',
|
|
1325
|
+
input.filters,
|
|
1326
|
+
input.limit
|
|
1327
|
+
);
|
|
1328
|
+
return {
|
|
1329
|
+
...envelope(loaded),
|
|
1330
|
+
surfaces: surfaceSummaries(loaded.graph).filter((entry) =>
|
|
1331
|
+
ids.has(entry.id)
|
|
1332
|
+
),
|
|
1333
|
+
};
|
|
1334
|
+
}),
|
|
1335
|
+
input: filteredInputSchema,
|
|
1336
|
+
intent: 'read',
|
|
1337
|
+
output: envelopeSchema.extend({
|
|
1338
|
+
surfaces: z.array(surfaceSummarySchema).readonly(),
|
|
1339
|
+
}),
|
|
1340
|
+
visibility: 'internal',
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
export const wayfindTrailheadsTrail = trail('wayfind.trailheads', {
|
|
1344
|
+
description: 'List saved trailhead membership',
|
|
1345
|
+
examples: [{ input: {}, name: 'List trailheads' }],
|
|
1346
|
+
implementation: async (input, ctx) =>
|
|
1347
|
+
withGraph(input, ctx.cwd, (loaded) => {
|
|
1348
|
+
const ids = filteredIds(
|
|
1349
|
+
loaded.graph,
|
|
1350
|
+
'trailhead',
|
|
1351
|
+
input.filters,
|
|
1352
|
+
input.limit
|
|
1353
|
+
);
|
|
1354
|
+
return {
|
|
1355
|
+
...envelope(loaded),
|
|
1356
|
+
trailheads: trailheadSummaries(loaded.graph).filter((entry) =>
|
|
1357
|
+
ids.has(entry.id)
|
|
1358
|
+
),
|
|
1359
|
+
};
|
|
1360
|
+
}),
|
|
1361
|
+
input: filteredInputSchema,
|
|
1362
|
+
intent: 'read',
|
|
1363
|
+
output: envelopeSchema.extend({
|
|
1364
|
+
trailheads: z.array(trailheadSummarySchema).readonly(),
|
|
1365
|
+
}),
|
|
1366
|
+
visibility: 'internal',
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
export const wayfindVersionsTrail = trail('wayfind.versions', {
|
|
1370
|
+
description: 'List saved trail version contracts',
|
|
1371
|
+
examples: [{ input: {}, name: 'List versions' }],
|
|
1372
|
+
implementation: async (input, ctx) =>
|
|
1373
|
+
withGraph(input, ctx.cwd, (loaded) => ({
|
|
1374
|
+
...envelope(loaded),
|
|
1375
|
+
versions: filteredVersionSummaries(
|
|
1376
|
+
loaded.graph,
|
|
1377
|
+
input.filters,
|
|
1378
|
+
input.limit
|
|
1379
|
+
),
|
|
1380
|
+
})),
|
|
1381
|
+
input: filteredInputSchema,
|
|
1382
|
+
intent: 'read',
|
|
1383
|
+
output: envelopeSchema.extend({
|
|
1384
|
+
versions: z.array(versionSummarySchema).readonly(),
|
|
1385
|
+
}),
|
|
1386
|
+
visibility: 'internal',
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
export const wayfindExamplesTrail = trail('wayfind.examples', {
|
|
1390
|
+
description: 'List saved examples without executing trails',
|
|
1391
|
+
examples: [{ input: {}, name: 'List examples' }],
|
|
1392
|
+
implementation: async (input, ctx) =>
|
|
1393
|
+
withGraph(input, ctx.cwd, (loaded) => ({
|
|
1394
|
+
...envelope(loaded),
|
|
1395
|
+
examples: filteredExampleSummaries(
|
|
1396
|
+
loaded.graph,
|
|
1397
|
+
input.filters,
|
|
1398
|
+
input.limit
|
|
1399
|
+
),
|
|
1400
|
+
})),
|
|
1401
|
+
input: filteredInputSchema,
|
|
1402
|
+
intent: 'read',
|
|
1403
|
+
output: envelopeSchema.extend({
|
|
1404
|
+
examples: z.array(exampleSummarySchema).readonly(),
|
|
1405
|
+
}),
|
|
1406
|
+
visibility: 'internal',
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
export const wayfindErrorsTrail = trail('wayfind.errors', {
|
|
1410
|
+
description: 'List saved trail error facts with provenance',
|
|
1411
|
+
examples: [{ input: {}, name: 'List trail error facts' }],
|
|
1412
|
+
implementation: async (input, ctx) =>
|
|
1413
|
+
withGraph(input, ctx.cwd, (loaded) => ({
|
|
1414
|
+
...envelope(loaded),
|
|
1415
|
+
errors: filteredErrorFacts(loaded.graph, input.filters, input.limit),
|
|
1416
|
+
})),
|
|
1417
|
+
input: filteredInputSchema,
|
|
1418
|
+
intent: 'read',
|
|
1419
|
+
output: errorsOutputSchema,
|
|
1420
|
+
visibility: 'internal',
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
export const wayfindAdaptersTrail = trail('wayfind.adapters', {
|
|
1424
|
+
description: 'List adapter facts with package and conformance provenance',
|
|
1425
|
+
examples: [{ input: {}, name: 'List adapter facts' }],
|
|
1426
|
+
implementation: (input, ctx) => filteredAdapterFacts(input, ctx.cwd),
|
|
1427
|
+
input: adapterFactsInputSchema,
|
|
1428
|
+
intent: 'read',
|
|
1429
|
+
output: adaptersOutputSchema,
|
|
1430
|
+
visibility: 'internal',
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
export const wayfindOverlayTrail = trail('wayfind.overlay', {
|
|
1434
|
+
description: 'Read a namespaced fact overlay from the saved graph',
|
|
1435
|
+
examples: [
|
|
1436
|
+
{ input: { namespace: 'cloudflare' }, name: 'Read cloudflare lock facts' },
|
|
1437
|
+
],
|
|
1438
|
+
implementation: async (input, ctx) => {
|
|
1439
|
+
const loaded = await loadGraph(input, ctx.cwd);
|
|
1440
|
+
if (loaded.isErr()) {
|
|
1441
|
+
return loaded;
|
|
1442
|
+
}
|
|
1443
|
+
const overlays = loaded.value.graph.overlays ?? {};
|
|
1444
|
+
const namespaces = Object.keys(overlays).toSorted();
|
|
1445
|
+
if (!Object.hasOwn(overlays, input.namespace)) {
|
|
1446
|
+
return Result.err(
|
|
1447
|
+
new NotFoundError(
|
|
1448
|
+
`No lock overlay named "${input.namespace}". Available overlays: ${namespaces.length === 0 ? 'none' : namespaces.join(', ')}. Adapters contribute overlays via trailsOverlays; run \`trails compile\` to refresh the lock.`
|
|
1449
|
+
)
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
return Result.ok({
|
|
1453
|
+
...envelope(loaded.value),
|
|
1454
|
+
facts: overlays[input.namespace],
|
|
1455
|
+
namespace: input.namespace,
|
|
1456
|
+
namespaces,
|
|
1457
|
+
});
|
|
1458
|
+
},
|
|
1459
|
+
input: overlayInputSchema,
|
|
1460
|
+
intent: 'read',
|
|
1461
|
+
output: envelopeSchema.extend({
|
|
1462
|
+
facts: z.unknown(),
|
|
1463
|
+
namespace: z.string(),
|
|
1464
|
+
namespaces: z.array(z.string()).readonly(),
|
|
1465
|
+
}),
|
|
1466
|
+
visibility: 'internal',
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
export const wayfindDescribeTrail = trail('wayfind.describe', {
|
|
1470
|
+
args: ['id'],
|
|
1471
|
+
description: 'Inspect one saved topo graph entity',
|
|
1472
|
+
examples: [{ input: { id: 'user.create' }, name: 'Describe entity' }],
|
|
1473
|
+
implementation: async (input, ctx) => {
|
|
1474
|
+
const loaded = await loadGraph(input, ctx.cwd);
|
|
1475
|
+
if (loaded.isErr()) {
|
|
1476
|
+
return loaded;
|
|
1477
|
+
}
|
|
1478
|
+
const entity = describeEntry(loaded.value.graph, input);
|
|
1479
|
+
if (entity.isErr()) {
|
|
1480
|
+
return entity;
|
|
1481
|
+
}
|
|
1482
|
+
if (entity.value === undefined) {
|
|
1483
|
+
return Result.err(notFound(input.kind ?? 'entity', input.id));
|
|
1484
|
+
}
|
|
1485
|
+
return Result.ok({ ...envelope(loaded.value), entity: entity.value });
|
|
1486
|
+
},
|
|
1487
|
+
input: inspectInputSchema,
|
|
1488
|
+
intent: 'read',
|
|
1489
|
+
output: describeOutputSchema,
|
|
1490
|
+
visibility: 'internal',
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
export const wayfindContractTrail = trail('wayfind.contract', {
|
|
1494
|
+
args: ['id'],
|
|
1495
|
+
description: 'Inspect one saved input/output contract',
|
|
1496
|
+
examples: [{ input: { id: 'user.create' }, name: 'Inspect contract' }],
|
|
1497
|
+
implementation: async (input, ctx) => {
|
|
1498
|
+
const loaded = await loadGraph(input, ctx.cwd);
|
|
1499
|
+
if (loaded.isErr()) {
|
|
1500
|
+
return loaded;
|
|
1501
|
+
}
|
|
1502
|
+
const contract = contractFor(loaded.value.graph, input);
|
|
1503
|
+
if (contract.isErr()) {
|
|
1504
|
+
return contract;
|
|
1505
|
+
}
|
|
1506
|
+
if (contract.value === undefined) {
|
|
1507
|
+
return Result.err(notFound(input.kind ?? 'contract', input.id));
|
|
1508
|
+
}
|
|
1509
|
+
return Result.ok({ ...envelope(loaded.value), contract: contract.value });
|
|
1510
|
+
},
|
|
1511
|
+
input: contractInputSchema,
|
|
1512
|
+
intent: 'read',
|
|
1513
|
+
output: contractOutputSchema,
|
|
1514
|
+
visibility: 'internal',
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
export const wayfindNearbyTrail = trail('wayfind.nearby', {
|
|
1518
|
+
args: ['id'],
|
|
1519
|
+
description: 'Inspect direct graph relationships around one topo entity',
|
|
1520
|
+
examples: [{ input: { id: 'user.create' }, name: 'Nearby graph context' }],
|
|
1521
|
+
implementation: async (input, ctx) => {
|
|
1522
|
+
const loaded = await loadGraph(input, ctx.cwd);
|
|
1523
|
+
if (loaded.isErr()) {
|
|
1524
|
+
return loaded;
|
|
1525
|
+
}
|
|
1526
|
+
const resolved = resolveWayfinderRelations(loaded.value.graph, {
|
|
1527
|
+
filters: input.filters,
|
|
1528
|
+
id: input.id,
|
|
1529
|
+
kind: input.kind,
|
|
1530
|
+
limit: 100,
|
|
1531
|
+
maxDepth: 1,
|
|
1532
|
+
mode: 'related',
|
|
1533
|
+
view: 'groups',
|
|
1534
|
+
});
|
|
1535
|
+
if (resolved.isErr()) {
|
|
1536
|
+
return resolved;
|
|
1537
|
+
}
|
|
1538
|
+
if (resolved.value === undefined) {
|
|
1539
|
+
return Result.err(notFound(input.kind ?? 'entity', input.id));
|
|
1540
|
+
}
|
|
1541
|
+
return Result.ok({
|
|
1542
|
+
...envelope(loaded.value),
|
|
1543
|
+
edges: resolved.value.edges,
|
|
1544
|
+
relations: resolved.value.groups,
|
|
1545
|
+
target: resolved.value.target,
|
|
1546
|
+
});
|
|
1547
|
+
},
|
|
1548
|
+
input: relationInputSchema,
|
|
1549
|
+
intent: 'read',
|
|
1550
|
+
output: nearbyOutputSchema,
|
|
1551
|
+
visibility: 'internal',
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
export const wayfindImpactTrail = trail('wayfind.impact', {
|
|
1555
|
+
args: ['id'],
|
|
1556
|
+
description: 'Traverse multi-hop graph impact from one topo entity',
|
|
1557
|
+
examples: [
|
|
1558
|
+
{
|
|
1559
|
+
input: { direction: 'downstream', id: 'db.main', kind: 'resource' },
|
|
1560
|
+
name: 'Resource impact',
|
|
1561
|
+
},
|
|
1562
|
+
],
|
|
1563
|
+
implementation: async (input, ctx) => {
|
|
1564
|
+
const impactInput = {
|
|
1565
|
+
...input,
|
|
1566
|
+
direction: input.direction ?? 'downstream',
|
|
1567
|
+
limit: input.limit ?? 100,
|
|
1568
|
+
maxDepth: input.maxDepth ?? 2,
|
|
1569
|
+
};
|
|
1570
|
+
const loaded = await loadGraph(input, ctx.cwd);
|
|
1571
|
+
if (loaded.isErr()) {
|
|
1572
|
+
return loaded;
|
|
1573
|
+
}
|
|
1574
|
+
const resolved = resolveWayfinderRelations(loaded.value.graph, {
|
|
1575
|
+
filters: input.filters,
|
|
1576
|
+
id: input.id,
|
|
1577
|
+
kind: input.kind,
|
|
1578
|
+
limit: impactInput.limit,
|
|
1579
|
+
maxDepth: impactInput.maxDepth,
|
|
1580
|
+
mode: relationModeFromImpactDirection(impactInput.direction),
|
|
1581
|
+
});
|
|
1582
|
+
if (resolved.isErr()) {
|
|
1583
|
+
return resolved;
|
|
1584
|
+
}
|
|
1585
|
+
if (resolved.value === undefined) {
|
|
1586
|
+
return Result.err(notFound(input.kind ?? 'entity', input.id));
|
|
1587
|
+
}
|
|
1588
|
+
return Result.ok({
|
|
1589
|
+
...envelope(loaded.value),
|
|
1590
|
+
direction: impactInput.direction,
|
|
1591
|
+
edges: resolved.value.edges,
|
|
1592
|
+
maxDepth: impactInput.maxDepth,
|
|
1593
|
+
nodes: resolved.value.nodes,
|
|
1594
|
+
target: resolved.value.target,
|
|
1595
|
+
});
|
|
1596
|
+
},
|
|
1597
|
+
input: impactInputSchema,
|
|
1598
|
+
intent: 'read',
|
|
1599
|
+
output: impactOutputSchema,
|
|
1600
|
+
visibility: 'internal',
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
export const wayfindDiffTrail = trail('wayfind.diff', {
|
|
1604
|
+
description: 'Diff two saved Wayfinder topo graph artifacts',
|
|
1605
|
+
examples: [
|
|
1606
|
+
{
|
|
1607
|
+
input: { againstDir: '.trails-baseline' },
|
|
1608
|
+
name: 'Diff against saved artifacts',
|
|
1609
|
+
},
|
|
1610
|
+
],
|
|
1611
|
+
implementation: async (input, ctx) => {
|
|
1612
|
+
const baselineError = diffBaselineError(input);
|
|
1613
|
+
if (baselineError !== undefined) {
|
|
1614
|
+
return Result.err(baselineError);
|
|
1615
|
+
}
|
|
1616
|
+
const current = await loadGraph(input, ctx.cwd);
|
|
1617
|
+
if (current.isErr()) {
|
|
1618
|
+
return current;
|
|
1619
|
+
}
|
|
1620
|
+
const baseline = await loadGraph(againstInput(input), ctx.cwd);
|
|
1621
|
+
if (baseline.isErr()) {
|
|
1622
|
+
return baseline;
|
|
1623
|
+
}
|
|
1624
|
+
const diff = deriveTopoGraphDiff(baseline.value.graph, current.value.graph);
|
|
1625
|
+
return Result.ok({
|
|
1626
|
+
...envelope(current.value),
|
|
1627
|
+
against: envelope(baseline.value),
|
|
1628
|
+
diff: diffResult(diff),
|
|
1629
|
+
});
|
|
1630
|
+
},
|
|
1631
|
+
input: diffInputSchema,
|
|
1632
|
+
intent: 'read',
|
|
1633
|
+
output: diffOutputSchema,
|
|
1634
|
+
visibility: 'internal',
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
export const wayfinderTopo = topo('wayfinder', {
|
|
1638
|
+
wayfindAdaptersTrail,
|
|
1639
|
+
wayfindContractTrail,
|
|
1640
|
+
wayfindDescribeTrail,
|
|
1641
|
+
wayfindDiffTrail,
|
|
1642
|
+
wayfindEntitiesTrail,
|
|
1643
|
+
wayfindErrorsTrail,
|
|
1644
|
+
wayfindExamplesTrail,
|
|
1645
|
+
wayfindImpactTrail,
|
|
1646
|
+
wayfindNearbyTrail,
|
|
1647
|
+
wayfindOverlayTrail,
|
|
1648
|
+
wayfindOverviewTrail,
|
|
1649
|
+
wayfindResourcesTrail,
|
|
1650
|
+
wayfindSearchTrail,
|
|
1651
|
+
wayfindSignalsTrail,
|
|
1652
|
+
wayfindSurfacesTrail,
|
|
1653
|
+
wayfindTrailheadsTrail,
|
|
1654
|
+
wayfindTrailsTrail,
|
|
1655
|
+
wayfindVersionsTrail,
|
|
1656
|
+
});
|