@ontrails/trails 1.0.0-beta.24 → 1.0.0-beta.29
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 +189 -0
- package/README.md +6 -5
- package/package.json +16 -14
- package/src/app.ts +42 -3
- package/src/cli.ts +17 -3
- package/src/local-state-io.ts +20 -0
- package/src/mcp-app.ts +10 -0
- package/src/mcp-options.ts +2 -3
- package/src/project-writes.ts +11 -11
- package/src/release/check.ts +41 -25
- package/src/release/index.ts +40 -0
- package/src/release/native-bun-registry.ts +282 -19
- package/src/release/pack-coherence.ts +457 -0
- package/src/release/policy.ts +1684 -0
- package/src/release/semver.ts +104 -0
- package/src/release/wayfinder-dogfood-smoke.ts +600 -66
- package/src/run-completions-install.ts +2 -2
- package/src/run-schema.ts +41 -0
- package/src/run-wayfind-outline.ts +166 -0
- package/src/trails/adapter-check.ts +1 -1
- package/src/trails/add-surface.ts +1 -1
- package/src/trails/add-verify.ts +3 -3
- package/src/trails/compile.ts +17 -25
- package/src/trails/create-scaffold.ts +14 -14
- package/src/trails/create.ts +5 -5
- package/src/trails/dev-support.ts +44 -24
- package/src/trails/doctor.ts +23 -2
- package/src/trails/draft-promote.ts +7 -7
- package/src/trails/guide.ts +15 -19
- package/src/trails/load-app.ts +58 -9
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/regrade.ts +72 -0
- package/src/trails/release-check.ts +1 -0
- package/src/trails/run-example.ts +21 -37
- package/src/trails/run-examples.ts +16 -32
- package/src/trails/run.ts +31 -44
- package/src/trails/survey.ts +76 -62
- package/src/trails/topo-output-schemas.ts +11 -0
- package/src/trails/topo-pin.ts +6 -20
- package/src/trails/topo-read-support.ts +91 -41
- package/src/trails/topo-reports.ts +4 -2
- package/src/trails/topo-store-support.ts +172 -39
- package/src/trails/topo-support.ts +1 -2
- package/src/trails/topo.ts +5 -19
- package/src/trails/validate.ts +9 -20
- package/src/trails/version-lifecycle-support.ts +10 -20
- package/src/trails/warden.ts +18 -0
- package/src/trails/wayfind.ts +1001 -0
package/src/trails/survey.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { basename, extname, join } from 'node:path';
|
|
9
9
|
|
|
10
|
-
import type { Topo } from '@ontrails/core';
|
|
10
|
+
import type { CliCommandAliasInput, Topo } from '@ontrails/core';
|
|
11
11
|
import {
|
|
12
12
|
deriveSafePath,
|
|
13
13
|
NotFoundError,
|
|
@@ -28,8 +28,7 @@ import { z } from 'zod';
|
|
|
28
28
|
|
|
29
29
|
import { writeIsolatedExampleJsonFile } from '../local-state-io.js';
|
|
30
30
|
|
|
31
|
-
import {
|
|
32
|
-
import { resolveTrailRootDir } from './root-dir.js';
|
|
31
|
+
import { withFreshAppLease, withOperatorRootDir } from './operator-context.js';
|
|
33
32
|
import {
|
|
34
33
|
buildCurrentTopoBrief,
|
|
35
34
|
buildCurrentTopoList,
|
|
@@ -435,7 +434,7 @@ const readPathTopoGraph = async (
|
|
|
435
434
|
const describeAgainstPathTarget = (against: string): string =>
|
|
436
435
|
basename(against) === 'topo.lock' || extname(against) === '.json'
|
|
437
436
|
? 'workspace-relative TopoGraph file'
|
|
438
|
-
: 'workspace-relative directory containing topo.lock';
|
|
437
|
+
: 'workspace-relative directory containing trails.lock or topo.lock';
|
|
439
438
|
|
|
440
439
|
const topoGraphNotFound = (against: string): NotFoundError =>
|
|
441
440
|
new NotFoundError(
|
|
@@ -449,7 +448,9 @@ const readAgainstTopoGraph = async (
|
|
|
449
448
|
against?: string | undefined
|
|
450
449
|
): Promise<Result<{ against: string; map: TopoGraph }, Error>> => {
|
|
451
450
|
if (against === undefined || against === 'saved') {
|
|
452
|
-
const map =
|
|
451
|
+
const map =
|
|
452
|
+
(await readTopoGraph({ dir: rootDir })) ??
|
|
453
|
+
(await readTopoGraph({ dir: join(rootDir, '.trails') }));
|
|
453
454
|
return map === null
|
|
454
455
|
? Result.err(
|
|
455
456
|
new NotFoundError(
|
|
@@ -505,9 +506,13 @@ const buildSurveyLookup = (
|
|
|
505
506
|
app: Topo,
|
|
506
507
|
entityId: string,
|
|
507
508
|
rootDir: string,
|
|
509
|
+
cliAliases:
|
|
510
|
+
| Readonly<Record<string, readonly CliCommandAliasInput[]>>
|
|
511
|
+
| undefined,
|
|
508
512
|
surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
|
|
509
513
|
): Result<object, Error> => {
|
|
510
514
|
const matches = buildCurrentTopoMatches(app, entityId, {
|
|
515
|
+
cliAliases,
|
|
511
516
|
rootDir,
|
|
512
517
|
surfaceLayerNames,
|
|
513
518
|
});
|
|
@@ -518,9 +523,13 @@ const buildSurveyTrailDetail = (
|
|
|
518
523
|
app: Topo,
|
|
519
524
|
id: string,
|
|
520
525
|
rootDir: string,
|
|
526
|
+
cliAliases:
|
|
527
|
+
| Readonly<Record<string, readonly CliCommandAliasInput[]>>
|
|
528
|
+
| undefined,
|
|
521
529
|
surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
|
|
522
530
|
): Result<object, Error> => {
|
|
523
531
|
const detail = buildCurrentTrailDetail(app, id, {
|
|
532
|
+
cliAliases,
|
|
524
533
|
rootDir,
|
|
525
534
|
surfaceLayerNames,
|
|
526
535
|
});
|
|
@@ -572,15 +581,24 @@ type SurveyHandler = (
|
|
|
572
581
|
app: Topo,
|
|
573
582
|
input: SurveyInput,
|
|
574
583
|
rootDir: string,
|
|
584
|
+
cliAliases:
|
|
585
|
+
| Readonly<Record<string, readonly CliCommandAliasInput[]>>
|
|
586
|
+
| undefined,
|
|
575
587
|
surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
|
|
576
588
|
) => Result<object, Error> | Promise<Result<object, Error>>;
|
|
577
589
|
|
|
578
590
|
/** Handlers keyed by survey mode. */
|
|
579
591
|
const surveyHandlers: Record<SurveyMode, SurveyHandler> = {
|
|
580
|
-
lookup: (app, input, rootDir, surfaceLayerNames) =>
|
|
592
|
+
lookup: (app, input, rootDir, cliAliases, surfaceLayerNames) =>
|
|
581
593
|
input.id === undefined || input.id === ''
|
|
582
594
|
? Result.err(new ValidationError('Survey lookup requires an id'))
|
|
583
|
-
: buildSurveyLookup(
|
|
595
|
+
: buildSurveyLookup(
|
|
596
|
+
app,
|
|
597
|
+
input.id,
|
|
598
|
+
rootDir,
|
|
599
|
+
cliAliases,
|
|
600
|
+
surfaceLayerNames
|
|
601
|
+
),
|
|
584
602
|
overview: (app, _input, rootDir) =>
|
|
585
603
|
Result.ok(buildCurrentTopoList(app, { rootDir })),
|
|
586
604
|
};
|
|
@@ -595,11 +613,20 @@ const dispatchSurvey = async (
|
|
|
595
613
|
app: Topo,
|
|
596
614
|
input: SurveyInput,
|
|
597
615
|
rootDir: string,
|
|
616
|
+
cliAliases:
|
|
617
|
+
| Readonly<Record<string, readonly CliCommandAliasInput[]>>
|
|
618
|
+
| undefined,
|
|
598
619
|
surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
|
|
599
620
|
): Promise<Result<SurveyEnvelope, Error>> => {
|
|
600
621
|
const mode = deriveSurveyMode(input);
|
|
601
622
|
const handler = surveyHandlers[mode];
|
|
602
|
-
const result = await handler(
|
|
623
|
+
const result = await handler(
|
|
624
|
+
app,
|
|
625
|
+
input,
|
|
626
|
+
rootDir,
|
|
627
|
+
cliAliases,
|
|
628
|
+
surfaceLayerNames
|
|
629
|
+
);
|
|
603
630
|
if (result.isErr()) {
|
|
604
631
|
return result;
|
|
605
632
|
}
|
|
@@ -615,19 +642,16 @@ const detailInputSchema = z.object({
|
|
|
615
642
|
const withFreshSurveyApp = async <T>(
|
|
616
643
|
input: { readonly module?: string | undefined },
|
|
617
644
|
rootDir: string,
|
|
618
|
-
consume: (
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
lease.release();
|
|
629
|
-
}
|
|
630
|
-
};
|
|
645
|
+
consume: (
|
|
646
|
+
app: Topo,
|
|
647
|
+
cliAliases:
|
|
648
|
+
| Readonly<Record<string, readonly CliCommandAliasInput[]>>
|
|
649
|
+
| undefined
|
|
650
|
+
) => Promise<Result<T, Error>> | Result<T, Error>
|
|
651
|
+
): Promise<Result<T, Error>> =>
|
|
652
|
+
withFreshAppLease(input.module, rootDir, (lease) =>
|
|
653
|
+
consume(lease.app, lease.cliAliases)
|
|
654
|
+
);
|
|
631
655
|
|
|
632
656
|
const withResolvedSurveyApp = async <T>(
|
|
633
657
|
input: {
|
|
@@ -637,16 +661,17 @@ const withResolvedSurveyApp = async <T>(
|
|
|
637
661
|
cwd: string | undefined,
|
|
638
662
|
consume: (
|
|
639
663
|
app: Topo,
|
|
640
|
-
rootDir: string
|
|
664
|
+
rootDir: string,
|
|
665
|
+
cliAliases:
|
|
666
|
+
| Readonly<Record<string, readonly CliCommandAliasInput[]>>
|
|
667
|
+
| undefined
|
|
641
668
|
) => Promise<Result<T, Error>> | Result<T, Error>
|
|
642
|
-
): Promise<Result<T, Error>> =>
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
return withFreshSurveyApp(input, rootDir, (app) => consume(app, rootDir));
|
|
649
|
-
};
|
|
669
|
+
): Promise<Result<T, Error>> =>
|
|
670
|
+
withOperatorRootDir(input, { cwd }, (rootDir) =>
|
|
671
|
+
withFreshSurveyApp(input, rootDir, (app, cliAliases) =>
|
|
672
|
+
consume(app, rootDir, cliAliases)
|
|
673
|
+
)
|
|
674
|
+
);
|
|
650
675
|
|
|
651
676
|
const moduleInputSchema = z.object({
|
|
652
677
|
module: z.string().optional().describe('Path to the app module'),
|
|
@@ -718,8 +743,14 @@ const surveyMatchOutput = z.discriminatedUnion('kind', [
|
|
|
718
743
|
export const surveyTrail = trail('survey', {
|
|
719
744
|
args: ['id'],
|
|
720
745
|
blaze: async (input, ctx) =>
|
|
721
|
-
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
|
|
722
|
-
dispatchSurvey(
|
|
746
|
+
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir, cliAliases) =>
|
|
747
|
+
dispatchSurvey(
|
|
748
|
+
app,
|
|
749
|
+
input,
|
|
750
|
+
rootDir,
|
|
751
|
+
cliAliases,
|
|
752
|
+
readSurfaceLayerNamesFromContext(ctx)
|
|
753
|
+
)
|
|
723
754
|
),
|
|
724
755
|
description: 'Full topo introspection',
|
|
725
756
|
examples: [
|
|
@@ -827,6 +858,7 @@ export const surveySurfacesTrail = trail('survey.surfaces', {
|
|
|
827
858
|
});
|
|
828
859
|
|
|
829
860
|
export const surveyDiffTrail = trail('survey.diff', {
|
|
861
|
+
args: ['target'],
|
|
830
862
|
blaze: async (input, ctx) =>
|
|
831
863
|
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
|
|
832
864
|
buildSurveyDiff(app, rootDir, input)
|
|
@@ -838,6 +870,16 @@ export const surveyDiffTrail = trail('survey.diff', {
|
|
|
838
870
|
input: createDiffExampleInput(),
|
|
839
871
|
name: 'Diff against baseline',
|
|
840
872
|
},
|
|
873
|
+
{
|
|
874
|
+
description: 'Show only breaking contract drift',
|
|
875
|
+
input: { ...createDiffExampleInput(), breaks: true },
|
|
876
|
+
name: 'Breaking changes',
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
description: 'Show graph-only force audit events',
|
|
880
|
+
input: { ...createDiffExampleInput(), forces: true },
|
|
881
|
+
name: 'Force audit events',
|
|
882
|
+
},
|
|
841
883
|
{
|
|
842
884
|
description: 'Reject an empty saved map target',
|
|
843
885
|
error: 'ValidationError',
|
|
@@ -859,43 +901,15 @@ export const surveyDiffTrail = trail('survey.diff', {
|
|
|
859
901
|
output: diffOutput,
|
|
860
902
|
});
|
|
861
903
|
|
|
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,
|
|
887
|
-
intent: 'read',
|
|
888
|
-
output: diffOutput,
|
|
889
|
-
});
|
|
890
|
-
|
|
891
904
|
export const surveyTrailDetailTrail = trail('survey.trail', {
|
|
892
905
|
args: ['id'],
|
|
893
906
|
blaze: async (input, ctx) =>
|
|
894
|
-
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
|
|
907
|
+
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir, cliAliases) =>
|
|
895
908
|
buildSurveyTrailDetail(
|
|
896
909
|
app,
|
|
897
910
|
input.id,
|
|
898
911
|
rootDir,
|
|
912
|
+
cliAliases,
|
|
899
913
|
readSurfaceLayerNamesFromContext(ctx)
|
|
900
914
|
)
|
|
901
915
|
),
|
|
@@ -178,6 +178,17 @@ export const trailDetailOutput = z.object({
|
|
|
178
178
|
cli: z
|
|
179
179
|
.object({
|
|
180
180
|
path: z.array(z.string()).readonly(),
|
|
181
|
+
routes: z
|
|
182
|
+
.array(
|
|
183
|
+
z.object({
|
|
184
|
+
kind: z.enum(['alias', 'canonical']),
|
|
185
|
+
path: z.array(z.string()).readonly(),
|
|
186
|
+
source: z.enum(['derived', 'surface', 'trail']),
|
|
187
|
+
target: z.string(),
|
|
188
|
+
})
|
|
189
|
+
)
|
|
190
|
+
.readonly()
|
|
191
|
+
.optional(),
|
|
181
192
|
})
|
|
182
193
|
.nullable(),
|
|
183
194
|
composedLayers: z.object({
|
package/src/trails/topo-pin.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Result, trail } from '@ontrails/core';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { resolveTrailRootDir } from './root-dir.js';
|
|
4
|
+
import { withFreshOperatorApp } from './operator-context.js';
|
|
6
5
|
import {
|
|
7
6
|
createIsolatedExampleInput,
|
|
8
7
|
pinCurrentTopoSnapshot,
|
|
@@ -10,25 +9,12 @@ import {
|
|
|
10
9
|
} from './topo-support.js';
|
|
11
10
|
|
|
12
11
|
export const topoPinTrail = trail('topo.pin', {
|
|
13
|
-
blaze: async (input, ctx) =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return rootDirResult;
|
|
17
|
-
}
|
|
18
|
-
const rootDir = rootDirResult.value;
|
|
19
|
-
const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
|
|
20
|
-
if (leaseResult.isErr()) {
|
|
21
|
-
return leaseResult;
|
|
22
|
-
}
|
|
23
|
-
const lease = leaseResult.value;
|
|
24
|
-
try {
|
|
25
|
-
return Result.ok(
|
|
12
|
+
blaze: async (input, ctx) =>
|
|
13
|
+
withFreshOperatorApp(input, ctx, ({ lease, rootDir }) =>
|
|
14
|
+
Result.ok(
|
|
26
15
|
pinCurrentTopoSnapshot(lease.app, { name: input.name, rootDir })
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
lease.release();
|
|
30
|
-
}
|
|
31
|
-
},
|
|
16
|
+
)
|
|
17
|
+
),
|
|
32
18
|
description: 'Pin the current topo under a durable name',
|
|
33
19
|
examples: [
|
|
34
20
|
{
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
|
|
11
|
-
import type { Topo, TrailContext } from '@ontrails/core';
|
|
11
|
+
import type { CliCommandAliasInput, Topo, TrailContext } from '@ontrails/core';
|
|
12
12
|
import {
|
|
13
13
|
ConflictError,
|
|
14
14
|
deriveTrailsDbPath,
|
|
@@ -24,9 +24,10 @@ import {
|
|
|
24
24
|
deriveTopoGraphHash,
|
|
25
25
|
readLockManifest,
|
|
26
26
|
readTopoGraph,
|
|
27
|
+
readTrailsLock,
|
|
27
28
|
stripTopoGraphForces,
|
|
28
29
|
} from '@ontrails/topographer';
|
|
29
|
-
import type { TopoGraph } from '@ontrails/topographer';
|
|
30
|
+
import type { LockManifest, TopoGraph } from '@ontrails/topographer';
|
|
30
31
|
|
|
31
32
|
import type {
|
|
32
33
|
BriefReport,
|
|
@@ -71,6 +72,9 @@ export interface CurrentTopoMatch {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export interface CurrentTopoReadOptions {
|
|
75
|
+
readonly cliAliases?:
|
|
76
|
+
| Readonly<Record<string, readonly CliCommandAliasInput[]>>
|
|
77
|
+
| undefined;
|
|
74
78
|
readonly rootDir?: string | undefined;
|
|
75
79
|
readonly surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined;
|
|
76
80
|
}
|
|
@@ -96,6 +100,46 @@ export const readSurfaceLayerNamesFromContext = (
|
|
|
96
100
|
const hasCommittedLock = (trailsDir: string): boolean =>
|
|
97
101
|
existsSync(join(trailsDir, 'trails.lock'));
|
|
98
102
|
|
|
103
|
+
const readCommittedLock = async (
|
|
104
|
+
rootDir: string
|
|
105
|
+
): Promise<{
|
|
106
|
+
readonly lockManifest: LockManifest;
|
|
107
|
+
readonly topoGraph: TopoGraph;
|
|
108
|
+
} | null> => {
|
|
109
|
+
const trailsLock = await readTrailsLock({ dir: rootDir });
|
|
110
|
+
if (trailsLock !== null) {
|
|
111
|
+
return {
|
|
112
|
+
lockManifest: {
|
|
113
|
+
artifacts: [
|
|
114
|
+
{
|
|
115
|
+
path: 'topo.lock',
|
|
116
|
+
role: 'topo',
|
|
117
|
+
sha256: trailsLock.topoGraphHash,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
scope: trailsLock.scope,
|
|
121
|
+
summary: trailsLock.summary,
|
|
122
|
+
version: 3,
|
|
123
|
+
},
|
|
124
|
+
topoGraph: trailsLock.topoGraph as TopoGraph,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const legacyDir = deriveTrailsDir({ rootDir });
|
|
129
|
+
const legacyManifest = await readLockManifest({ dir: legacyDir });
|
|
130
|
+
if (legacyManifest === null) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const legacyTopoGraph = await readTopoGraph({ dir: legacyDir });
|
|
134
|
+
if (legacyTopoGraph === null) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
lockManifest: legacyManifest,
|
|
139
|
+
topoGraph: legacyTopoGraph,
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
99
143
|
// ---------------------------------------------------------------------------
|
|
100
144
|
// Public read-only consumers
|
|
101
145
|
// ---------------------------------------------------------------------------
|
|
@@ -105,12 +149,13 @@ export const buildTopoSummary = (
|
|
|
105
149
|
options?: { readonly rootDir?: string }
|
|
106
150
|
): TopoSummaryReport => {
|
|
107
151
|
const rootDir = deriveRootDir(options?.rootDir);
|
|
108
|
-
const trailsDir = deriveTrailsDir({ rootDir });
|
|
109
152
|
return {
|
|
110
153
|
app: deriveBriefReport(app),
|
|
111
154
|
dbPath: deriveTrailsDbPath({ rootDir }),
|
|
112
155
|
list: deriveSurveyList(app),
|
|
113
|
-
lockExists:
|
|
156
|
+
lockExists:
|
|
157
|
+
hasCommittedLock(rootDir) ||
|
|
158
|
+
hasCommittedLock(deriveTrailsDir({ rootDir })),
|
|
114
159
|
lockPath: LOCK_PATH,
|
|
115
160
|
};
|
|
116
161
|
};
|
|
@@ -154,6 +199,7 @@ export const buildCurrentTrailDetail = (
|
|
|
154
199
|
? undefined
|
|
155
200
|
: deriveTrailDetail(trail, app, undefined, {
|
|
156
201
|
surfaceLayerNames: options?.surfaceLayerNames,
|
|
202
|
+
topoGraph: deriveTopoGraph(app, { cliAliases: options?.cliAliases }),
|
|
157
203
|
});
|
|
158
204
|
};
|
|
159
205
|
|
|
@@ -192,7 +238,7 @@ export const buildCurrentTopoMatches = (
|
|
|
192
238
|
(activationGraph ??= deriveActivationGraph(app));
|
|
193
239
|
let topoGraph: ReturnType<typeof deriveTopoGraph> | undefined;
|
|
194
240
|
const getTopoGraph = (): ReturnType<typeof deriveTopoGraph> =>
|
|
195
|
-
(topoGraph ??= deriveTopoGraph(app));
|
|
241
|
+
(topoGraph ??= deriveTopoGraph(app, { cliAliases: options?.cliAliases }));
|
|
196
242
|
|
|
197
243
|
const trail = app.get(id);
|
|
198
244
|
if (trail !== undefined) {
|
|
@@ -220,14 +266,17 @@ export const buildCurrentTopoMatches = (
|
|
|
220
266
|
|
|
221
267
|
export const validateCurrentTopo = async (
|
|
222
268
|
app: Topo,
|
|
223
|
-
options?: {
|
|
269
|
+
options?: {
|
|
270
|
+
readonly cliAliases?:
|
|
271
|
+
| Readonly<Record<string, readonly CliCommandAliasInput[]>>
|
|
272
|
+
| undefined;
|
|
273
|
+
readonly rootDir?: string;
|
|
274
|
+
}
|
|
224
275
|
): Promise<Result<TopoValidateReport, Error>> => {
|
|
225
276
|
const rootDir = deriveRootDir(options?.rootDir);
|
|
226
|
-
let
|
|
277
|
+
let committedLock: Awaited<ReturnType<typeof readCommittedLock>>;
|
|
227
278
|
try {
|
|
228
|
-
|
|
229
|
-
dir: deriveTrailsDir({ rootDir }),
|
|
230
|
-
});
|
|
279
|
+
committedLock = await readCommittedLock(rootDir);
|
|
231
280
|
} catch (error) {
|
|
232
281
|
const message =
|
|
233
282
|
error instanceof Error
|
|
@@ -240,7 +289,7 @@ export const validateCurrentTopo = async (
|
|
|
240
289
|
);
|
|
241
290
|
}
|
|
242
291
|
|
|
243
|
-
if (
|
|
292
|
+
if (committedLock === null) {
|
|
244
293
|
return Result.err(
|
|
245
294
|
new NotFoundError(
|
|
246
295
|
'No committed trails.lock found. Run `trails compile` first.'
|
|
@@ -248,7 +297,10 @@ export const validateCurrentTopo = async (
|
|
|
248
297
|
);
|
|
249
298
|
}
|
|
250
299
|
|
|
251
|
-
const currentExport = deriveCurrentTopoExport(app, {
|
|
300
|
+
const currentExport = deriveCurrentTopoExport(app, {
|
|
301
|
+
cliAliases: options?.cliAliases,
|
|
302
|
+
rootDir,
|
|
303
|
+
});
|
|
252
304
|
if (currentExport.isErr()) {
|
|
253
305
|
return currentExport;
|
|
254
306
|
}
|
|
@@ -256,7 +308,7 @@ export const validateCurrentTopo = async (
|
|
|
256
308
|
currentExport.value.topoGraphJson
|
|
257
309
|
) as TopoGraph;
|
|
258
310
|
const currentHash = currentExport.value.topoGraphHash;
|
|
259
|
-
const topoArtifact = lockManifest.artifacts.find(
|
|
311
|
+
const topoArtifact = committedLock.lockManifest.artifacts.find(
|
|
260
312
|
(artifact) => artifact.role === 'topo' && artifact.path === 'topo.lock'
|
|
261
313
|
);
|
|
262
314
|
if (topoArtifact === undefined) {
|
|
@@ -267,36 +319,34 @@ export const validateCurrentTopo = async (
|
|
|
267
319
|
);
|
|
268
320
|
}
|
|
269
321
|
|
|
322
|
+
const committedTopo = committedLock.topoGraph;
|
|
323
|
+
const committedHash = deriveTopoGraphHash(committedTopo);
|
|
324
|
+
if (committedHash !== topoArtifact.sha256) {
|
|
325
|
+
return Result.err(
|
|
326
|
+
new ValidationError(
|
|
327
|
+
'trails.lock graph hash does not match its embedded TopoGraph. Run `trails compile` to refresh it.'
|
|
328
|
+
)
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
270
332
|
if (topoArtifact.sha256 !== currentHash) {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
forceStrippedHash === currentHash
|
|
282
|
-
) {
|
|
283
|
-
return Result.ok({
|
|
284
|
-
committedHash: topoArtifact.sha256,
|
|
285
|
-
currentHash,
|
|
286
|
-
lockPath: LOCK_PATH,
|
|
287
|
-
stale: false,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
333
|
+
const forceStrippedHash = deriveTopoGraphHash(
|
|
334
|
+
stripTopoGraphForces(committedTopo)
|
|
335
|
+
);
|
|
336
|
+
if (forceStrippedHash === currentHash) {
|
|
337
|
+
return Result.ok({
|
|
338
|
+
committedHash: topoArtifact.sha256,
|
|
339
|
+
currentHash,
|
|
340
|
+
lockPath: LOCK_PATH,
|
|
341
|
+
stale: false,
|
|
342
|
+
});
|
|
290
343
|
}
|
|
291
|
-
const breakingSummary =
|
|
292
|
-
committedTopo
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
? ` Breaking changes detected: ${diff.breaking.length}.`
|
|
298
|
-
: '';
|
|
299
|
-
})();
|
|
344
|
+
const breakingSummary = (() => {
|
|
345
|
+
const diff = deriveTopoGraphDiff(committedTopo, currentTopo);
|
|
346
|
+
return diff.breaking.length > 0
|
|
347
|
+
? ` Breaking changes detected: ${diff.breaking.length}.`
|
|
348
|
+
: '';
|
|
349
|
+
})();
|
|
300
350
|
return Result.err(
|
|
301
351
|
new ConflictError(
|
|
302
352
|
`trails.lock is stale. Run \`trails compile\` to refresh it.${breakingSummary}`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DETOUR_MAX_ATTEMPTS_CAP,
|
|
3
|
-
|
|
3
|
+
deriveTrailCliCommandProjection,
|
|
4
4
|
filterSurfaceTrails,
|
|
5
5
|
isArchivedTrailVersionEntry,
|
|
6
6
|
zodToJsonSchema,
|
|
@@ -176,6 +176,7 @@ export interface TrailDetailReport {
|
|
|
176
176
|
readonly activationSources: readonly ActivationSourceReport[];
|
|
177
177
|
readonly cli: {
|
|
178
178
|
readonly path: readonly string[];
|
|
179
|
+
readonly routes?: NonNullable<TopoGraphEntry['cli']>['routes'];
|
|
179
180
|
} | null;
|
|
180
181
|
/**
|
|
181
182
|
* Composed layer names visible at the survey boundary.
|
|
@@ -560,7 +561,8 @@ export const deriveShippedSurfaceProjectionsForTrail = (
|
|
|
560
561
|
trail.id,
|
|
561
562
|
'trail'
|
|
562
563
|
);
|
|
563
|
-
const commandPath =
|
|
564
|
+
const commandPath =
|
|
565
|
+
entry?.cli?.path ?? deriveTrailCliCommandProjection(trail).path;
|
|
564
566
|
const httpMethod = deriveHttpMethod(trail.intent);
|
|
565
567
|
const httpPath = deriveHttpPath(trail.id);
|
|
566
568
|
const mcpToolName = deriveToolName(app.name, trail.id);
|