@ontrails/trails 1.0.0-beta.4 → 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 +1104 -4
- package/README.md +27 -0
- package/package.json +31 -7
- package/src/app.ts +147 -3
- package/src/cli.ts +345 -11
- package/src/completions.ts +240 -0
- package/src/lifecycle-source-io.ts +33 -0
- package/src/load-app-mirror.ts +202 -0
- package/src/local-state-io.ts +173 -0
- package/src/mcp-app.ts +42 -0
- package/src/mcp-options.ts +91 -0
- package/src/mcp.ts +8 -0
- package/src/project-writes.ts +377 -0
- package/src/regrade/config.ts +152 -0
- package/src/regrade/history.ts +430 -0
- package/src/regrade/live-api-preserve.ts +8 -0
- package/src/regrade/plan-artifact.ts +285 -0
- package/src/release/bindings.ts +39 -0
- package/src/release/check.ts +844 -0
- package/src/release/config.ts +63 -0
- package/src/release/contract-facts.ts +425 -0
- package/src/release/index.ts +142 -0
- package/src/release/lock-roundtrip-smoke.ts +230 -0
- package/src/release/native-bun-publish.ts +651 -0
- package/src/release/native-bun-registry.ts +765 -0
- package/src/release/notes-cli.ts +171 -0
- package/src/release/notes.ts +390 -0
- package/src/release/pack-coherence.ts +455 -0
- package/src/release/packed-artifacts-smoke.ts +236 -0
- package/src/release/policy.ts +1687 -0
- package/src/release/semver.ts +104 -0
- package/src/release/smoke.ts +56 -0
- package/src/release/wayfinder-dogfood-smoke.ts +760 -0
- package/src/retired-topo-command.ts +36 -0
- package/src/run-adapter-check.ts +76 -0
- package/src/run-collision.ts +126 -0
- package/src/run-completions-install.ts +179 -0
- package/src/run-example.ts +149 -0
- package/src/run-examples.ts +148 -0
- package/src/run-quiet.ts +75 -0
- package/src/run-release-check.ts +74 -0
- package/src/run-schema.ts +74 -0
- package/src/run-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch.ts +432 -0
- package/src/run-wayfind-outline.ts +170 -0
- package/src/scaffold-version-sync.ts +183 -0
- package/src/scaffold-versions.generated.ts +12 -0
- package/src/trails/adapter-check.ts +244 -0
- package/src/trails/add-surface.ts +99 -45
- package/src/trails/add-trail.ts +84 -37
- package/src/trails/add-verify.ts +100 -30
- package/src/trails/compile.ts +58 -0
- package/src/trails/completions-complete.ts +165 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/create-adapter.ts +785 -0
- package/src/trails/create-scaffold.ts +401 -106
- package/src/trails/create-versions.ts +62 -0
- package/src/trails/create.ts +186 -72
- package/src/trails/deprecate.ts +59 -0
- package/src/trails/dev-clean.ts +82 -0
- package/src/trails/dev-reset.ts +50 -0
- package/src/trails/dev-stats.ts +72 -0
- package/src/trails/dev-support.ts +360 -0
- package/src/trails/doctor.ts +77 -0
- package/src/trails/draft-promote.ts +949 -0
- package/src/trails/guide.ts +70 -68
- package/src/trails/load-app.ts +1123 -15
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/project.ts +17 -3
- package/src/trails/regrade.ts +3091 -0
- package/src/trails/release-check.ts +105 -0
- package/src/trails/release-smoke.ts +49 -0
- package/src/trails/revise.ts +53 -0
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +475 -0
- package/src/trails/run-examples.ts +129 -0
- package/src/trails/run.ts +434 -0
- package/src/trails/scaffold-json.ts +58 -0
- package/src/trails/survey.ts +877 -214
- package/src/trails/topo-activation.ts +14 -0
- package/src/trails/topo-constants.ts +2 -0
- package/src/trails/topo-history.ts +47 -0
- package/src/trails/topo-output-schemas.ts +259 -0
- package/src/trails/topo-pin.ts +38 -0
- package/src/trails/topo-read-support.ts +368 -0
- package/src/trails/topo-reports.ts +809 -0
- package/src/trails/topo-store-support.ts +325 -0
- package/src/trails/topo-support.ts +220 -0
- package/src/trails/topo-unpin.ts +61 -0
- package/src/trails/topo.ts +92 -0
- package/src/trails/validate.ts +27 -0
- package/src/trails/version-lifecycle-support.ts +936 -0
- package/src/trails/warden-guide.ts +134 -0
- package/src/trails/warden.ts +198 -58
- package/src/trails/wayfind-outline.ts +876 -0
- package/src/trails/wayfind.ts +1053 -0
- package/src/versions.ts +31 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/__tests__/examples.test.ts +0 -6
- package/dist/bin/trails.d.ts +0 -3
- package/dist/bin/trails.d.ts.map +0 -1
- package/dist/bin/trails.js +0 -4
- package/dist/bin/trails.js.map +0 -1
- package/dist/src/app.d.ts +0 -2
- package/dist/src/app.d.ts.map +0 -1
- package/dist/src/app.js +0 -11
- package/dist/src/app.js.map +0 -1
- package/dist/src/clack.d.ts +0 -9
- package/dist/src/clack.d.ts.map +0 -1
- package/dist/src/clack.js +0 -84
- package/dist/src/clack.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -13
- package/dist/src/cli.js.map +0 -1
- package/dist/src/trails/add-surface.d.ts +0 -13
- package/dist/src/trails/add-surface.d.ts.map +0 -1
- package/dist/src/trails/add-surface.js +0 -88
- package/dist/src/trails/add-surface.js.map +0 -1
- package/dist/src/trails/add-trail.d.ts +0 -10
- package/dist/src/trails/add-trail.d.ts.map +0 -1
- package/dist/src/trails/add-trail.js +0 -77
- package/dist/src/trails/add-trail.js.map +0 -1
- package/dist/src/trails/add-verify.d.ts +0 -10
- package/dist/src/trails/add-verify.d.ts.map +0 -1
- package/dist/src/trails/add-verify.js +0 -67
- package/dist/src/trails/add-verify.js.map +0 -1
- package/dist/src/trails/create-scaffold.d.ts +0 -15
- package/dist/src/trails/create-scaffold.d.ts.map +0 -1
- package/dist/src/trails/create-scaffold.js +0 -288
- package/dist/src/trails/create-scaffold.js.map +0 -1
- package/dist/src/trails/create.d.ts +0 -22
- package/dist/src/trails/create.d.ts.map +0 -1
- package/dist/src/trails/create.js +0 -121
- package/dist/src/trails/create.js.map +0 -1
- package/dist/src/trails/guide.d.ts +0 -11
- package/dist/src/trails/guide.d.ts.map +0 -1
- package/dist/src/trails/guide.js +0 -80
- package/dist/src/trails/guide.js.map +0 -1
- package/dist/src/trails/load-app.d.ts +0 -4
- package/dist/src/trails/load-app.d.ts.map +0 -1
- package/dist/src/trails/load-app.js +0 -24
- package/dist/src/trails/load-app.js.map +0 -1
- package/dist/src/trails/project.d.ts +0 -8
- package/dist/src/trails/project.d.ts.map +0 -1
- package/dist/src/trails/project.js +0 -43
- package/dist/src/trails/project.js.map +0 -1
- package/dist/src/trails/survey.d.ts +0 -31
- package/dist/src/trails/survey.d.ts.map +0 -1
- package/dist/src/trails/survey.js +0 -221
- package/dist/src/trails/survey.js.map +0 -1
- package/dist/src/trails/warden.d.ts +0 -19
- package/dist/src/trails/warden.d.ts.map +0 -1
- package/dist/src/trails/warden.js +0 -88
- package/dist/src/trails/warden.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/__tests__/create.test.ts +0 -349
- package/src/__tests__/guide.test.ts +0 -91
- package/src/__tests__/load-app.test.ts +0 -15
- package/src/__tests__/survey.test.ts +0 -159
- package/src/__tests__/warden.test.ts +0 -74
- package/tsconfig.json +0 -9
package/src/trails/survey.ts
CHANGED
|
@@ -1,300 +1,963 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `survey` trail -- Full topo introspection.
|
|
3
3
|
*
|
|
4
|
-
* Lists trails,
|
|
5
|
-
*
|
|
4
|
+
* Lists trails, looks up trails/resources/signals, and diffs against previous
|
|
5
|
+
* versions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
import type {
|
|
8
|
+
import { basename, extname, join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
import type { Topo } from '@ontrails/core';
|
|
11
|
+
import {
|
|
12
|
+
deriveSafePath,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
Result,
|
|
15
|
+
trail,
|
|
16
|
+
ValidationError,
|
|
17
|
+
} from '@ontrails/core';
|
|
18
|
+
import type {
|
|
19
|
+
DiffEntry,
|
|
20
|
+
DiffResult,
|
|
21
|
+
TopoGraph,
|
|
22
|
+
TopoGraphOverlayRegistration,
|
|
23
|
+
} from '@ontrails/topography';
|
|
11
24
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from '@ontrails/
|
|
25
|
+
createTopoStore,
|
|
26
|
+
deriveTopoGraphDiff,
|
|
27
|
+
deriveTopoGraph,
|
|
28
|
+
resolveTopoGraphVersionReference,
|
|
29
|
+
TOPO_GRAPH_SCHEMA_VERSION,
|
|
30
|
+
readTopoGraph,
|
|
31
|
+
} from '@ontrails/topography';
|
|
19
32
|
import { z } from 'zod';
|
|
20
33
|
|
|
21
|
-
import {
|
|
34
|
+
import { writeIsolatedExampleJsonFile } from '../local-state-io.js';
|
|
35
|
+
|
|
36
|
+
import { withFreshAppLease, withOperatorRootDir } from './operator-context.js';
|
|
37
|
+
import {
|
|
38
|
+
deriveCurrentTopoBrief,
|
|
39
|
+
deriveCurrentTopoList,
|
|
40
|
+
deriveCurrentTopoMatches,
|
|
41
|
+
deriveCurrentTrailDetail,
|
|
42
|
+
deriveCurrentResourceDetail,
|
|
43
|
+
deriveCurrentSignalDetail,
|
|
44
|
+
readSurfaceLayerNamesFromContext,
|
|
45
|
+
} from './topo-read-support.js';
|
|
46
|
+
import {
|
|
47
|
+
activationOverviewOutput,
|
|
48
|
+
resourceDetailOutput,
|
|
49
|
+
shippedSurfaceInventoryOutput,
|
|
50
|
+
signalDetailOutput,
|
|
51
|
+
trailDetailOutput,
|
|
52
|
+
} from './topo-output-schemas.js';
|
|
53
|
+
import { createIsolatedExampleInput } from './topo-support.js';
|
|
54
|
+
import {
|
|
55
|
+
briefReportSchema,
|
|
56
|
+
deriveShippedSurfaceProjectionInventory,
|
|
57
|
+
} from './topo-reports.js';
|
|
58
|
+
import type { SurfaceLayerNames } from './topo-reports.js';
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
briefReportSchema,
|
|
62
|
+
deriveBriefReport,
|
|
63
|
+
deriveResourceDetail,
|
|
64
|
+
deriveShippedSurfaceProjectionInventory,
|
|
65
|
+
deriveSignalDetail,
|
|
66
|
+
deriveSurveyList,
|
|
67
|
+
deriveTrailDetail,
|
|
68
|
+
} from './topo-reports.js';
|
|
69
|
+
export type {
|
|
70
|
+
BriefReport,
|
|
71
|
+
ShippedSurfaceInventoryReport,
|
|
72
|
+
ShippedSurfaceProjection,
|
|
73
|
+
SignalDetailReport,
|
|
74
|
+
SurfaceLayerNames,
|
|
75
|
+
SurveyListReport,
|
|
76
|
+
TrailDetailReport,
|
|
77
|
+
} from './topo-reports.js';
|
|
22
78
|
|
|
23
79
|
// ---------------------------------------------------------------------------
|
|
24
|
-
//
|
|
80
|
+
// Survey diff helpers
|
|
25
81
|
// ---------------------------------------------------------------------------
|
|
26
82
|
|
|
27
|
-
|
|
28
|
-
readonly
|
|
29
|
-
readonly
|
|
30
|
-
readonly
|
|
31
|
-
readonly
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
readonly detours: boolean;
|
|
35
|
-
readonly events: boolean;
|
|
36
|
-
};
|
|
37
|
-
readonly trails: number;
|
|
38
|
-
readonly events: number;
|
|
83
|
+
interface SurveyDiffReport {
|
|
84
|
+
readonly against: string;
|
|
85
|
+
readonly breaking: readonly DiffEntry[];
|
|
86
|
+
readonly hasBreaking: boolean;
|
|
87
|
+
readonly info: readonly DiffEntry[];
|
|
88
|
+
readonly mode: 'diff';
|
|
89
|
+
readonly warnings: readonly DiffEntry[];
|
|
39
90
|
}
|
|
40
91
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
92
|
+
interface DiffInput {
|
|
93
|
+
readonly against?: string | undefined;
|
|
94
|
+
readonly breakingOnly?: boolean | undefined;
|
|
95
|
+
readonly breaks?: boolean | undefined;
|
|
96
|
+
readonly forces?: boolean | undefined;
|
|
97
|
+
readonly module?: string | undefined;
|
|
98
|
+
readonly rootDir?: string | undefined;
|
|
99
|
+
readonly target?: string | undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface ParsedDiffTarget {
|
|
103
|
+
readonly id: string;
|
|
104
|
+
readonly versions?: ReadonlySet<number> | undefined;
|
|
105
|
+
}
|
|
48
106
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
107
|
+
const formatDiff = (diff: DiffResult, against: string): SurveyDiffReport => ({
|
|
108
|
+
against,
|
|
109
|
+
breaking: diff.breaking,
|
|
110
|
+
hasBreaking: diff.hasBreaking,
|
|
111
|
+
info: diff.info,
|
|
112
|
+
mode: 'diff',
|
|
113
|
+
warnings: diff.warnings,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const partitionDiffEntries = (entries: readonly DiffEntry[]): DiffResult => {
|
|
117
|
+
const sorted = [...entries].toSorted((left, right) =>
|
|
118
|
+
left.id.localeCompare(right.id)
|
|
55
119
|
);
|
|
120
|
+
const breaking = sorted.filter((entry) => entry.severity === 'breaking');
|
|
121
|
+
const warnings = sorted.filter((entry) => entry.severity === 'warning');
|
|
122
|
+
const info = sorted.filter((entry) => entry.severity === 'info');
|
|
123
|
+
|
|
56
124
|
return {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
125
|
+
breaking,
|
|
126
|
+
entries: sorted,
|
|
127
|
+
hasBreaking: breaking.length > 0,
|
|
128
|
+
info,
|
|
129
|
+
warnings,
|
|
60
130
|
};
|
|
61
131
|
};
|
|
62
132
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
133
|
+
const parseVersionRange = (
|
|
134
|
+
reference: string
|
|
135
|
+
): ReadonlySet<number> | undefined => {
|
|
136
|
+
const match = /^(\d+)\.\.(\d+)$/.exec(reference);
|
|
137
|
+
if (match === null) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const start = Number(match[1]);
|
|
141
|
+
const end = Number(match[2]);
|
|
142
|
+
if (start < 1 || end < start) {
|
|
143
|
+
throw new ValidationError(
|
|
144
|
+
`Diff version range must use ascending positive versions: ${reference}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
66
147
|
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
features: {
|
|
71
|
-
detours: hasDetours,
|
|
72
|
-
events: app.events.size > 0,
|
|
73
|
-
examples: hasExamples,
|
|
74
|
-
outputSchemas: hasOutputSchemas,
|
|
75
|
-
},
|
|
76
|
-
name: app.name,
|
|
77
|
-
trails: app.trails.size,
|
|
78
|
-
version: '0.1.0',
|
|
79
|
-
};
|
|
148
|
+
return new Set(
|
|
149
|
+
Array.from({ length: end - start + 1 }, (_value, index) => start + index)
|
|
150
|
+
);
|
|
80
151
|
};
|
|
81
152
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
153
|
+
const findDiffTargetEntry = (
|
|
154
|
+
previous: TopoGraph,
|
|
155
|
+
current: TopoGraph,
|
|
156
|
+
id: string
|
|
157
|
+
) =>
|
|
158
|
+
current.entries.find((entry) => entry.id === id) ??
|
|
159
|
+
previous.entries.find((entry) => entry.id === id);
|
|
160
|
+
|
|
161
|
+
const parseDiffTarget = (
|
|
162
|
+
previous: TopoGraph,
|
|
163
|
+
current: TopoGraph,
|
|
164
|
+
target: string | undefined
|
|
165
|
+
): Result<ParsedDiffTarget | undefined, Error> => {
|
|
166
|
+
if (target === undefined || target.length === 0) {
|
|
167
|
+
return Result.ok();
|
|
168
|
+
}
|
|
85
169
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
170
|
+
const separator = target.lastIndexOf('@');
|
|
171
|
+
const id = separator === -1 ? target : target.slice(0, separator);
|
|
172
|
+
const reference =
|
|
173
|
+
separator === -1 ? undefined : target.slice(separator + 1).trim();
|
|
174
|
+
if (id.length === 0 || reference === '') {
|
|
175
|
+
return Result.err(
|
|
176
|
+
new ValidationError('Diff target must use trail.id or trail.id@version')
|
|
177
|
+
);
|
|
91
178
|
}
|
|
92
|
-
|
|
93
|
-
|
|
179
|
+
|
|
180
|
+
const entry = findDiffTargetEntry(previous, current, id);
|
|
181
|
+
if (entry === undefined) {
|
|
182
|
+
return Result.err(new NotFoundError(`Trail not found for diff: ${id}`));
|
|
94
183
|
}
|
|
95
|
-
return '-';
|
|
96
|
-
};
|
|
97
184
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
185
|
+
if (reference === undefined) {
|
|
186
|
+
return Result.ok({ id });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const range = parseVersionRange(reference);
|
|
191
|
+
if (range !== undefined) {
|
|
192
|
+
return Result.ok({ id, versions: range });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return Result.ok({
|
|
196
|
+
id,
|
|
197
|
+
versions: new Set([
|
|
198
|
+
resolveTopoGraphVersionReference(entry, reference).version,
|
|
199
|
+
]),
|
|
200
|
+
});
|
|
201
|
+
} catch (error: unknown) {
|
|
202
|
+
return Result.err(
|
|
203
|
+
error instanceof Error ? error : new Error(String(error))
|
|
103
204
|
);
|
|
104
|
-
|
|
105
|
-
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const detailVersions = (detail: string): readonly number[] => {
|
|
209
|
+
const match = /^(?:Live version|Version) (\d+)\b/.exec(detail);
|
|
210
|
+
if (match !== null) {
|
|
211
|
+
return [Number(match[1])];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const supportMatch = /^Supported versions (?:added|removed): (.+)$/.exec(
|
|
215
|
+
detail
|
|
216
|
+
);
|
|
217
|
+
if (supportMatch === null) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return (supportMatch[1] ?? '')
|
|
222
|
+
.split(',')
|
|
223
|
+
.map((part) => Number(part.trim()))
|
|
224
|
+
.filter((version) => Number.isInteger(version) && version > 0);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
type DiffSeverity = DiffEntry['severity'];
|
|
228
|
+
|
|
229
|
+
const severityRank: Record<DiffSeverity, number> = {
|
|
230
|
+
breaking: 2,
|
|
231
|
+
info: 0,
|
|
232
|
+
warning: 1,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const higherSeverity = (
|
|
236
|
+
left: DiffSeverity,
|
|
237
|
+
right: DiffSeverity
|
|
238
|
+
): DiffSeverity => (severityRank[right] > severityRank[left] ? right : left);
|
|
239
|
+
|
|
240
|
+
const versionStatus = (detail: string): string | undefined =>
|
|
241
|
+
/^Version \d+ (?:added|removed) \(([^)]+)\)$/.exec(detail)?.[1];
|
|
242
|
+
|
|
243
|
+
const visibleDetailSeverity = (detail: string): DiffSeverity => {
|
|
244
|
+
if (detail.startsWith('Force event ')) {
|
|
245
|
+
return 'warning';
|
|
246
|
+
}
|
|
247
|
+
if (detail.startsWith('Supported versions removed: ')) {
|
|
248
|
+
return 'breaking';
|
|
249
|
+
}
|
|
250
|
+
if (detail.startsWith('Supported versions added: ')) {
|
|
251
|
+
return 'info';
|
|
252
|
+
}
|
|
253
|
+
if (
|
|
254
|
+
/^Live version \d+ (?:added without examples|example coverage removed)$/.test(
|
|
255
|
+
detail
|
|
106
256
|
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
257
|
+
) {
|
|
258
|
+
return 'warning';
|
|
259
|
+
}
|
|
260
|
+
if (detail.startsWith('Live version ') && detail.includes(' examples: ')) {
|
|
261
|
+
return 'info';
|
|
262
|
+
}
|
|
263
|
+
if (/^Version \d+ status changed: .+ -> archived$/.test(detail)) {
|
|
264
|
+
return 'warning';
|
|
265
|
+
}
|
|
266
|
+
if (detail.startsWith('Version ') && detail.includes(' status changed: ')) {
|
|
267
|
+
return 'info';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const status = versionStatus(detail);
|
|
271
|
+
if (detail.startsWith('Version ') && detail.includes(' removed (')) {
|
|
272
|
+
return status === 'archived' ? 'warning' : 'breaking';
|
|
273
|
+
}
|
|
274
|
+
if (detail.startsWith('Version ') && detail.includes(' added (')) {
|
|
275
|
+
return status === 'archived' ? 'info' : 'warning';
|
|
276
|
+
}
|
|
277
|
+
if (
|
|
278
|
+
/^Version \d+ (?:kind changed:|Required (?:input|entity) field ".+" added|(?:Input|Output|Entity) field ".+" (?:removed|type changed:|changed from optional to required))/.test(
|
|
279
|
+
detail
|
|
280
|
+
)
|
|
281
|
+
) {
|
|
282
|
+
return 'breaking';
|
|
283
|
+
}
|
|
284
|
+
if (
|
|
285
|
+
/^Version \d+ (?:marker changed:|Optional (?:input|entity) field ".+" added|Output field ".+" added)/.test(
|
|
286
|
+
detail
|
|
287
|
+
)
|
|
288
|
+
) {
|
|
289
|
+
return 'info';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return 'info';
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const visibleDetailsSeverity = (details: readonly string[]): DiffSeverity => {
|
|
296
|
+
let severity: DiffSeverity = 'info';
|
|
297
|
+
for (const detail of details) {
|
|
298
|
+
severity = higherSeverity(severity, visibleDetailSeverity(detail));
|
|
299
|
+
}
|
|
300
|
+
return severity;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const detailsChanged = (
|
|
304
|
+
previous: readonly string[],
|
|
305
|
+
next: readonly string[]
|
|
306
|
+
): boolean =>
|
|
307
|
+
previous.length !== next.length ||
|
|
308
|
+
previous.some((detail, index) => detail !== next[index]);
|
|
309
|
+
|
|
310
|
+
const filterDetails = (
|
|
311
|
+
details: readonly string[],
|
|
312
|
+
target: ParsedDiffTarget | undefined,
|
|
313
|
+
forcesOnly: boolean
|
|
314
|
+
): readonly string[] => {
|
|
315
|
+
const visible = forcesOnly
|
|
316
|
+
? details.filter((detail) => detail.startsWith('Force event '))
|
|
317
|
+
: [...details];
|
|
318
|
+
if (target?.versions === undefined || forcesOnly) {
|
|
319
|
+
return visible;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return visible.filter((detail) => {
|
|
323
|
+
const versions = detailVersions(detail);
|
|
324
|
+
return versions.some((version) => target.versions?.has(version));
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const filterDiff = (
|
|
329
|
+
diff: DiffResult,
|
|
330
|
+
target: ParsedDiffTarget | undefined,
|
|
331
|
+
options: Pick<DiffInput, 'breakingOnly' | 'breaks' | 'forces'>
|
|
332
|
+
): DiffResult => {
|
|
333
|
+
const entries = diff.entries.flatMap((entry): DiffEntry[] => {
|
|
334
|
+
if (target !== undefined && entry.id !== target.id) {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
const details = filterDetails(
|
|
338
|
+
entry.details,
|
|
339
|
+
target,
|
|
340
|
+
options.forces === true
|
|
341
|
+
);
|
|
342
|
+
if (details.length === 0) {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
return [
|
|
346
|
+
{
|
|
347
|
+
...entry,
|
|
348
|
+
details,
|
|
349
|
+
severity: detailsChanged(entry.details, details)
|
|
350
|
+
? visibleDetailsSeverity(details)
|
|
351
|
+
: entry.severity,
|
|
352
|
+
},
|
|
353
|
+
];
|
|
116
354
|
});
|
|
117
355
|
|
|
118
|
-
|
|
356
|
+
const partitioned = partitionDiffEntries(entries);
|
|
357
|
+
return options.breakingOnly === true || options.breaks === true
|
|
358
|
+
? partitionDiffEntries(partitioned.breaking)
|
|
359
|
+
: partitioned;
|
|
119
360
|
};
|
|
120
361
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
362
|
+
const createDiffExampleInput = (): {
|
|
363
|
+
readonly against: string;
|
|
364
|
+
readonly module: string;
|
|
365
|
+
readonly rootDir: string;
|
|
366
|
+
} => {
|
|
367
|
+
const input = createIsolatedExampleInput('survey-diff');
|
|
368
|
+
writeIsolatedExampleJsonFile(input.rootDir, 'baseline/topo.lock', {
|
|
369
|
+
activationGraph: {
|
|
370
|
+
edgeCount: 0,
|
|
371
|
+
edges: [],
|
|
372
|
+
sourceCount: 0,
|
|
373
|
+
sourceKeys: [],
|
|
374
|
+
trailIds: [],
|
|
375
|
+
},
|
|
376
|
+
activationSources: {},
|
|
377
|
+
entries: [],
|
|
378
|
+
generatedAt: '2026-01-01T00:00:00.000Z',
|
|
379
|
+
topoGraphSchemaVersion: TOPO_GRAPH_SCHEMA_VERSION,
|
|
380
|
+
} satisfies TopoGraph);
|
|
381
|
+
return { ...input, against: 'baseline' };
|
|
382
|
+
};
|
|
132
383
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
384
|
+
const isNotFound = (error: unknown): boolean =>
|
|
385
|
+
typeof error === 'object' &&
|
|
386
|
+
error !== null &&
|
|
387
|
+
(error as NodeJS.ErrnoException).code === 'ENOENT';
|
|
388
|
+
|
|
389
|
+
const readTopoGraphFile = async (
|
|
390
|
+
filePath: string
|
|
391
|
+
): Promise<TopoGraph | null> => {
|
|
392
|
+
try {
|
|
393
|
+
return (await Bun.file(filePath).json()) as TopoGraph;
|
|
394
|
+
} catch (error: unknown) {
|
|
395
|
+
if (isNotFound(error)) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
141
400
|
};
|
|
142
401
|
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
});
|
|
402
|
+
const readStoredTopoGraph = (
|
|
403
|
+
rootDir: string,
|
|
404
|
+
against: string
|
|
405
|
+
): TopoGraph | undefined => {
|
|
406
|
+
try {
|
|
407
|
+
const store = createTopoStore({ rootDir });
|
|
408
|
+
const stored =
|
|
409
|
+
store.exports.get({ pin: against }) ??
|
|
410
|
+
store.exports.get({ snapshotId: against });
|
|
411
|
+
return stored === undefined
|
|
412
|
+
? undefined
|
|
413
|
+
: (JSON.parse(stored.topoGraphJson) as TopoGraph);
|
|
414
|
+
} catch (error: unknown) {
|
|
415
|
+
if (error instanceof NotFoundError) {
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const readPathTopoGraph = async (
|
|
423
|
+
rootDir: string,
|
|
424
|
+
against: string
|
|
425
|
+
): Promise<Result<TopoGraph | null, Error>> => {
|
|
426
|
+
const safePath = deriveSafePath(rootDir, against);
|
|
427
|
+
if (safePath.isErr()) {
|
|
428
|
+
return safePath;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return Result.ok(
|
|
432
|
+
basename(safePath.value) === 'topo.lock' ||
|
|
433
|
+
extname(safePath.value) === '.json'
|
|
434
|
+
? await readTopoGraphFile(safePath.value)
|
|
435
|
+
: await readTopoGraph({ dir: safePath.value })
|
|
436
|
+
);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const describeAgainstPathTarget = (against: string): string =>
|
|
440
|
+
basename(against) === 'topo.lock' || extname(against) === '.json'
|
|
441
|
+
? 'workspace-relative TopoGraph file'
|
|
442
|
+
: 'workspace-relative directory containing trails.lock or topo.lock';
|
|
443
|
+
|
|
444
|
+
const topoGraphNotFound = (against: string): NotFoundError =>
|
|
445
|
+
new NotFoundError(
|
|
446
|
+
`No TopoGraph found for: ${against}. Tried ${describeAgainstPathTarget(
|
|
447
|
+
against
|
|
448
|
+
)}, then topo-store pin and snapshot references.`
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const readAgainstTopoGraph = async (
|
|
452
|
+
rootDir: string,
|
|
453
|
+
against?: string | undefined
|
|
454
|
+
): Promise<Result<{ against: string; map: TopoGraph }, Error>> => {
|
|
455
|
+
if (against === undefined || against === 'saved') {
|
|
456
|
+
const map =
|
|
457
|
+
(await readTopoGraph({ dir: rootDir })) ??
|
|
458
|
+
(await readTopoGraph({ dir: join(rootDir, '.trails') }));
|
|
459
|
+
return map === null
|
|
460
|
+
? Result.err(
|
|
461
|
+
new NotFoundError(
|
|
462
|
+
'No saved TopoGraph found. Run `trails compile` first.'
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
: Result.ok({ against: 'saved', map });
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Treat explicit filesystem targets as the most local user intent; stored
|
|
469
|
+
// pins and snapshot ids are fallback references when no path exists.
|
|
470
|
+
const pathMap = await readPathTopoGraph(rootDir, against);
|
|
471
|
+
if (pathMap.isErr()) {
|
|
472
|
+
return pathMap;
|
|
473
|
+
}
|
|
474
|
+
if (pathMap.value !== null) {
|
|
475
|
+
return Result.ok({ against, map: pathMap.value });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const storedMap = readStoredTopoGraph(rootDir, against);
|
|
479
|
+
if (storedMap !== undefined) {
|
|
480
|
+
return Result.ok({ against, map: storedMap });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return Result.err(topoGraphNotFound(against));
|
|
484
|
+
};
|
|
149
485
|
|
|
150
486
|
const buildSurveyDiff = async (
|
|
151
487
|
app: Topo,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
'No previous surface map found. Run `trails survey generate` first.'
|
|
160
|
-
)
|
|
161
|
-
);
|
|
488
|
+
rootDir: string,
|
|
489
|
+
input: DiffInput
|
|
490
|
+
): Promise<Result<SurveyDiffReport, Error>> => {
|
|
491
|
+
const currentMap = deriveTopoGraph(app);
|
|
492
|
+
const previous = await readAgainstTopoGraph(rootDir, input.against);
|
|
493
|
+
if (previous.isErr()) {
|
|
494
|
+
return previous;
|
|
162
495
|
}
|
|
163
496
|
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
: formatDiff(diff)
|
|
497
|
+
const target = parseDiffTarget(previous.value.map, currentMap, input.target);
|
|
498
|
+
if (target.isErr()) {
|
|
499
|
+
return target;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const diff = filterDiff(
|
|
503
|
+
deriveTopoGraphDiff(previous.value.map, currentMap),
|
|
504
|
+
target.value,
|
|
505
|
+
input
|
|
174
506
|
);
|
|
507
|
+
return Result.ok(formatDiff(diff, previous.value.against));
|
|
175
508
|
};
|
|
176
509
|
|
|
177
|
-
const
|
|
510
|
+
const buildSurveyLookup = (
|
|
178
511
|
app: Topo,
|
|
179
|
-
|
|
512
|
+
entityId: string,
|
|
513
|
+
rootDir: string,
|
|
514
|
+
overlays: readonly TopoGraphOverlayRegistration[] | undefined,
|
|
515
|
+
surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
|
|
180
516
|
): Result<object, Error> => {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
517
|
+
const matches = deriveCurrentTopoMatches(app, entityId, {
|
|
518
|
+
overlays,
|
|
519
|
+
rootDir,
|
|
520
|
+
surfaceLayerNames,
|
|
521
|
+
});
|
|
522
|
+
return Result.ok({ matches });
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const buildSurveyTrailDetail = (
|
|
526
|
+
app: Topo,
|
|
527
|
+
id: string,
|
|
528
|
+
rootDir: string,
|
|
529
|
+
overlays: readonly TopoGraphOverlayRegistration[] | undefined,
|
|
530
|
+
surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
|
|
531
|
+
): Result<object, Error> => {
|
|
532
|
+
const detail = deriveCurrentTrailDetail(app, id, {
|
|
533
|
+
overlays,
|
|
534
|
+
rootDir,
|
|
535
|
+
surfaceLayerNames,
|
|
536
|
+
});
|
|
537
|
+
return detail === undefined
|
|
538
|
+
? Result.err(new NotFoundError(`Trail not found: ${id}`))
|
|
539
|
+
: Result.ok(detail);
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const buildSurveyResourceDetail = (
|
|
543
|
+
app: Topo,
|
|
544
|
+
id: string,
|
|
545
|
+
rootDir: string
|
|
546
|
+
): Result<object, Error> => {
|
|
547
|
+
const detail = deriveCurrentResourceDetail(app, id, { rootDir });
|
|
548
|
+
return detail === undefined
|
|
549
|
+
? Result.err(new NotFoundError(`Resource not found: ${id}`))
|
|
550
|
+
: Result.ok(detail);
|
|
186
551
|
};
|
|
187
552
|
|
|
188
|
-
const
|
|
189
|
-
app: Topo
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
553
|
+
const buildSurveySignalDetail = (
|
|
554
|
+
app: Topo,
|
|
555
|
+
id: string,
|
|
556
|
+
rootDir: string
|
|
557
|
+
): Result<object, Error> => {
|
|
558
|
+
const detail = deriveCurrentSignalDetail(app, id, { rootDir });
|
|
559
|
+
return detail === undefined
|
|
560
|
+
? Result.err(new NotFoundError(`Signal not found: ${id}`))
|
|
561
|
+
: Result.ok(detail);
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const buildSurveySurfaceInventory = (app: Topo): Result<object, Error> =>
|
|
565
|
+
Result.ok(deriveShippedSurfaceProjectionInventory(app));
|
|
566
|
+
|
|
567
|
+
interface SurveyInput {
|
|
568
|
+
id?: string | undefined;
|
|
569
|
+
module?: string | undefined;
|
|
570
|
+
rootDir?: string | undefined;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
type SurveyMode = 'lookup' | 'overview';
|
|
574
|
+
|
|
575
|
+
type SurveyEnvelope = { readonly mode: SurveyMode } & Record<string, unknown>;
|
|
576
|
+
|
|
577
|
+
/** Determine which survey mode was requested, falling back to 'overview'. */
|
|
578
|
+
const deriveSurveyMode = (input: SurveyInput): SurveyMode =>
|
|
579
|
+
input.id === undefined || input.id === '' ? 'overview' : 'lookup';
|
|
580
|
+
|
|
581
|
+
type SurveyHandler = (
|
|
582
|
+
app: Topo,
|
|
583
|
+
input: SurveyInput,
|
|
584
|
+
rootDir: string,
|
|
585
|
+
overlays: readonly TopoGraphOverlayRegistration[] | undefined,
|
|
586
|
+
surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
|
|
587
|
+
) => Result<object, Error> | Promise<Result<object, Error>>;
|
|
588
|
+
|
|
589
|
+
/** Handlers keyed by survey mode. */
|
|
590
|
+
const surveyHandlers: Record<SurveyMode, SurveyHandler> = {
|
|
591
|
+
lookup: (app, input, rootDir, overlays, surfaceLayerNames) =>
|
|
592
|
+
input.id === undefined || input.id === ''
|
|
593
|
+
? Result.err(new ValidationError('Survey lookup requires an id'))
|
|
594
|
+
: buildSurveyLookup(app, input.id, rootDir, overlays, surfaceLayerNames),
|
|
595
|
+
overview: (app, _input, rootDir) =>
|
|
596
|
+
Result.ok(deriveCurrentTopoList(app, { rootDir })),
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const envelopeSurveyValue = (
|
|
600
|
+
mode: SurveyMode,
|
|
601
|
+
value: object
|
|
602
|
+
): SurveyEnvelope => ({ ...value, mode });
|
|
603
|
+
|
|
604
|
+
/** Dispatch to the appropriate survey sub-command based on input flags. */
|
|
605
|
+
const dispatchSurvey = async (
|
|
606
|
+
app: Topo,
|
|
607
|
+
input: SurveyInput,
|
|
608
|
+
rootDir: string,
|
|
609
|
+
overlays: readonly TopoGraphOverlayRegistration[] | undefined,
|
|
610
|
+
surfaceLayerNames?: Partial<SurfaceLayerNames> | undefined
|
|
611
|
+
): Promise<Result<SurveyEnvelope, Error>> => {
|
|
612
|
+
const mode = deriveSurveyMode(input);
|
|
613
|
+
const handler = surveyHandlers[mode];
|
|
614
|
+
const result = await handler(
|
|
615
|
+
app,
|
|
616
|
+
input,
|
|
617
|
+
rootDir,
|
|
618
|
+
overlays,
|
|
619
|
+
surfaceLayerNames
|
|
620
|
+
);
|
|
621
|
+
if (result.isErr()) {
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
return Result.ok(envelopeSurveyValue(mode, result.value));
|
|
196
625
|
};
|
|
197
626
|
|
|
627
|
+
const detailInputSchema = z.object({
|
|
628
|
+
id: z.string().describe('Trail, resource, or signal ID'),
|
|
629
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
630
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const withFreshSurveyApp = async <T>(
|
|
634
|
+
input: { readonly module?: string | undefined },
|
|
635
|
+
rootDir: string,
|
|
636
|
+
consume: (
|
|
637
|
+
app: Topo,
|
|
638
|
+
overlays: readonly TopoGraphOverlayRegistration[] | undefined
|
|
639
|
+
) => Promise<Result<T, Error>> | Result<T, Error>
|
|
640
|
+
): Promise<Result<T, Error>> =>
|
|
641
|
+
withFreshAppLease(input.module, rootDir, (lease) =>
|
|
642
|
+
consume(lease.app, lease.overlays)
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
const withResolvedSurveyApp = async <T>(
|
|
646
|
+
input: {
|
|
647
|
+
readonly module?: string | undefined;
|
|
648
|
+
readonly rootDir?: string | undefined;
|
|
649
|
+
},
|
|
650
|
+
cwd: string | undefined,
|
|
651
|
+
consume: (
|
|
652
|
+
app: Topo,
|
|
653
|
+
rootDir: string,
|
|
654
|
+
overlays: readonly TopoGraphOverlayRegistration[] | undefined
|
|
655
|
+
) => Promise<Result<T, Error>> | Result<T, Error>
|
|
656
|
+
): Promise<Result<T, Error>> =>
|
|
657
|
+
withOperatorRootDir(input, { cwd }, (rootDir) =>
|
|
658
|
+
withFreshSurveyApp(input, rootDir, (app, overlays) =>
|
|
659
|
+
consume(app, rootDir, overlays)
|
|
660
|
+
)
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
const moduleInputSchema = z.object({
|
|
664
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
665
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
const diffEntryOutput = z.object({
|
|
669
|
+
change: z.enum(['added', 'removed', 'modified']),
|
|
670
|
+
details: z.array(z.string()).readonly(),
|
|
671
|
+
id: z.string(),
|
|
672
|
+
kind: z.enum(['entity', 'trail', 'signal', 'resource']),
|
|
673
|
+
severity: z.enum(['info', 'warning', 'breaking']),
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
const diffOutput = z.object({
|
|
677
|
+
against: z.string(),
|
|
678
|
+
breaking: z.array(diffEntryOutput),
|
|
679
|
+
hasBreaking: z.boolean(),
|
|
680
|
+
info: z.array(diffEntryOutput),
|
|
681
|
+
mode: z.literal('diff'),
|
|
682
|
+
warnings: z.array(diffEntryOutput),
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const diffInputSchema = z.object({
|
|
686
|
+
against: z
|
|
687
|
+
.string()
|
|
688
|
+
.min(1)
|
|
689
|
+
.optional()
|
|
690
|
+
.describe(
|
|
691
|
+
'Saved TopoGraph target: "saved", a workspace path (topo.lock, .json file, or directory with topo.lock), then a pin/snapshot id'
|
|
692
|
+
),
|
|
693
|
+
breakingOnly: z
|
|
694
|
+
.boolean()
|
|
695
|
+
.default(false)
|
|
696
|
+
.describe('Legacy alias for --breaks; only show breaking changes'),
|
|
697
|
+
breaks: z.boolean().default(false).describe('Only show breaking changes'),
|
|
698
|
+
forces: z
|
|
699
|
+
.boolean()
|
|
700
|
+
.default(false)
|
|
701
|
+
.describe('Only show graph force audit events'),
|
|
702
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
703
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
704
|
+
target: z
|
|
705
|
+
.string()
|
|
706
|
+
.min(1)
|
|
707
|
+
.optional()
|
|
708
|
+
.describe('Trail or trail version target, such as user.create@1..2'),
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const surveyMatchOutput = z.discriminatedUnion('kind', [
|
|
712
|
+
z.object({
|
|
713
|
+
detail: trailDetailOutput,
|
|
714
|
+
kind: z.literal('trail'),
|
|
715
|
+
}),
|
|
716
|
+
z.object({
|
|
717
|
+
detail: resourceDetailOutput,
|
|
718
|
+
kind: z.literal('resource'),
|
|
719
|
+
}),
|
|
720
|
+
z.object({
|
|
721
|
+
detail: signalDetailOutput,
|
|
722
|
+
kind: z.literal('signal'),
|
|
723
|
+
}),
|
|
724
|
+
]);
|
|
725
|
+
|
|
198
726
|
// ---------------------------------------------------------------------------
|
|
199
727
|
// Trail definition
|
|
200
728
|
// ---------------------------------------------------------------------------
|
|
201
729
|
|
|
202
730
|
export const surveyTrail = trail('survey', {
|
|
731
|
+
args: ['id'],
|
|
203
732
|
description: 'Full topo introspection',
|
|
204
733
|
examples: [
|
|
205
734
|
{
|
|
206
|
-
description: '
|
|
207
|
-
input:
|
|
208
|
-
name: '
|
|
735
|
+
description: 'Show all registered trails, resources, and signals',
|
|
736
|
+
input: createIsolatedExampleInput('survey-overview'),
|
|
737
|
+
name: 'Overview',
|
|
209
738
|
},
|
|
210
739
|
{
|
|
211
|
-
description: '
|
|
212
|
-
input: {
|
|
213
|
-
name: '
|
|
740
|
+
description: 'Find every trail, resource, or signal with a matching ID',
|
|
741
|
+
input: { ...createIsolatedExampleInput('survey-lookup'), id: 'survey' },
|
|
742
|
+
name: 'Lookup by ID',
|
|
214
743
|
},
|
|
215
744
|
],
|
|
745
|
+
implementation: async (input, ctx) =>
|
|
746
|
+
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir, overlays) =>
|
|
747
|
+
dispatchSurvey(
|
|
748
|
+
app,
|
|
749
|
+
input,
|
|
750
|
+
rootDir,
|
|
751
|
+
overlays,
|
|
752
|
+
readSurfaceLayerNamesFromContext(ctx)
|
|
753
|
+
)
|
|
754
|
+
),
|
|
216
755
|
input: z.object({
|
|
217
|
-
|
|
218
|
-
.boolean()
|
|
219
|
-
.default(false)
|
|
220
|
-
.describe('Only show breaking changes'),
|
|
221
|
-
brief: z.boolean().default(false).describe('Quick capability summary'),
|
|
222
|
-
diff: z.string().optional().describe('Diff against a git ref'),
|
|
223
|
-
generate: z
|
|
224
|
-
.boolean()
|
|
225
|
-
.default(false)
|
|
226
|
-
.describe('Generate surface map and lock file'),
|
|
227
|
-
module: z
|
|
756
|
+
id: z
|
|
228
757
|
.string()
|
|
229
|
-
.
|
|
230
|
-
.describe('
|
|
231
|
-
|
|
758
|
+
.optional()
|
|
759
|
+
.describe('Trail, resource, or signal ID to look up'),
|
|
760
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
761
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
232
762
|
}),
|
|
233
763
|
intent: 'read',
|
|
234
|
-
output: z.
|
|
764
|
+
output: z.discriminatedUnion('mode', [
|
|
235
765
|
z.object({
|
|
766
|
+
activation: activationOverviewOutput,
|
|
236
767
|
count: z.number(),
|
|
237
768
|
entries: z.array(
|
|
238
769
|
z.object({
|
|
770
|
+
activatedBy: z.array(z.string()).readonly(),
|
|
771
|
+
activates: z.array(z.string()).readonly(),
|
|
239
772
|
examples: z.number(),
|
|
240
773
|
id: z.string(),
|
|
241
774
|
kind: z.string(),
|
|
242
775
|
safety: z.string(),
|
|
243
776
|
})
|
|
244
777
|
),
|
|
778
|
+
mode: z.literal('overview'),
|
|
779
|
+
resourceCount: z.number(),
|
|
780
|
+
resources: z.array(
|
|
781
|
+
z.object({
|
|
782
|
+
description: z.string().nullable(),
|
|
783
|
+
health: z.enum(['available', 'none']),
|
|
784
|
+
id: z.string(),
|
|
785
|
+
kind: z.literal('resource'),
|
|
786
|
+
lifetime: z.literal('singleton'),
|
|
787
|
+
usedBy: z.array(z.string()),
|
|
788
|
+
})
|
|
789
|
+
),
|
|
790
|
+
signalCount: z.number(),
|
|
791
|
+
signals: z.array(
|
|
792
|
+
z.object({
|
|
793
|
+
consumers: z.array(z.string()).readonly(),
|
|
794
|
+
description: z.string().nullable(),
|
|
795
|
+
examples: z.number(),
|
|
796
|
+
from: z.array(z.string()).readonly(),
|
|
797
|
+
id: z.string(),
|
|
798
|
+
kind: z.literal('signal'),
|
|
799
|
+
payloadSchema: z.boolean(),
|
|
800
|
+
producers: z.array(z.string()).readonly(),
|
|
801
|
+
})
|
|
802
|
+
),
|
|
245
803
|
}),
|
|
246
804
|
z.object({
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
features: z.object({
|
|
250
|
-
detours: z.boolean(),
|
|
251
|
-
events: z.boolean(),
|
|
252
|
-
examples: z.boolean(),
|
|
253
|
-
outputSchemas: z.boolean(),
|
|
254
|
-
}),
|
|
255
|
-
name: z.string(),
|
|
256
|
-
trails: z.number(),
|
|
257
|
-
version: z.string(),
|
|
258
|
-
}),
|
|
259
|
-
z.object({
|
|
260
|
-
breaking: z.array(z.unknown()),
|
|
261
|
-
hasBreaking: z.boolean(),
|
|
262
|
-
info: z.array(z.unknown()),
|
|
263
|
-
warnings: z.array(z.unknown()),
|
|
264
|
-
}),
|
|
265
|
-
z.object({
|
|
266
|
-
description: z.unknown().nullable(),
|
|
267
|
-
detours: z.unknown().nullable(),
|
|
268
|
-
examples: z.array(z.unknown()),
|
|
269
|
-
id: z.string(),
|
|
270
|
-
kind: z.string(),
|
|
271
|
-
safety: z.string(),
|
|
272
|
-
}),
|
|
273
|
-
z.object({
|
|
274
|
-
hash: z.string(),
|
|
275
|
-
lockPath: z.string(),
|
|
276
|
-
mapPath: z.string(),
|
|
805
|
+
matches: z.array(surveyMatchOutput),
|
|
806
|
+
mode: z.literal('lookup'),
|
|
277
807
|
}),
|
|
278
808
|
]),
|
|
279
|
-
|
|
280
|
-
const app = await loadApp(input.module, ctx.cwd ?? '.');
|
|
809
|
+
});
|
|
281
810
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
811
|
+
export const surveyBriefTrail = trail('survey.brief', {
|
|
812
|
+
description: 'Summarize topo capabilities',
|
|
813
|
+
examples: [
|
|
814
|
+
{
|
|
815
|
+
description: 'Show counts and feature flags',
|
|
816
|
+
input: createIsolatedExampleInput('survey-brief'),
|
|
817
|
+
name: 'Brief capability report',
|
|
818
|
+
},
|
|
819
|
+
],
|
|
820
|
+
implementation: async (input, ctx) =>
|
|
821
|
+
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
|
|
822
|
+
Result.ok(deriveCurrentTopoBrief(app, { rootDir }))
|
|
823
|
+
),
|
|
824
|
+
input: moduleInputSchema,
|
|
825
|
+
intent: 'read',
|
|
826
|
+
output: briefReportSchema,
|
|
827
|
+
});
|
|
285
828
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
829
|
+
export const surveySurfacesTrail = trail('survey.surfaces', {
|
|
830
|
+
description: 'Inventory shipped surface projections',
|
|
831
|
+
examples: [
|
|
832
|
+
{
|
|
833
|
+
description: 'Show CLI, MCP, and HTTP projections for public trails',
|
|
834
|
+
input: createIsolatedExampleInput('survey-surfaces'),
|
|
835
|
+
name: 'Shipped surface inventory',
|
|
836
|
+
},
|
|
837
|
+
],
|
|
838
|
+
implementation: async (input, ctx) =>
|
|
839
|
+
withResolvedSurveyApp(input, ctx.cwd, (app) =>
|
|
840
|
+
buildSurveySurfaceInventory(app)
|
|
841
|
+
),
|
|
842
|
+
input: moduleInputSchema,
|
|
843
|
+
intent: 'read',
|
|
844
|
+
output: shippedSurfaceInventoryOutput,
|
|
845
|
+
});
|
|
289
846
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
847
|
+
export const surveyDiffTrail = trail('survey.diff', {
|
|
848
|
+
args: ['target'],
|
|
849
|
+
description: 'Diff the current topo against a saved TopoGraph',
|
|
850
|
+
examples: [
|
|
851
|
+
{
|
|
852
|
+
description: 'Compare current topo to a saved TopoGraph directory',
|
|
853
|
+
input: createDiffExampleInput(),
|
|
854
|
+
name: 'Diff against baseline',
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
description: 'Show only breaking contract drift',
|
|
858
|
+
input: { ...createDiffExampleInput(), breaks: true },
|
|
859
|
+
name: 'Breaking changes',
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
description: 'Show graph-only force audit events',
|
|
863
|
+
input: { ...createDiffExampleInput(), forces: true },
|
|
864
|
+
name: 'Force audit events',
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
description: 'Reject an empty saved map target',
|
|
868
|
+
error: 'ValidationError',
|
|
869
|
+
input: { against: '' },
|
|
870
|
+
name: 'Reject empty diff target',
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
description: 'Reject an empty target before filtering breaking drift',
|
|
874
|
+
error: 'ValidationError',
|
|
875
|
+
input: {
|
|
876
|
+
against: '',
|
|
877
|
+
breakingOnly: true,
|
|
878
|
+
},
|
|
879
|
+
name: 'Reject empty breaking-only target',
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
implementation: async (input, ctx) =>
|
|
883
|
+
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
|
|
884
|
+
buildSurveyDiff(app, rootDir, input)
|
|
885
|
+
),
|
|
886
|
+
input: diffInputSchema,
|
|
887
|
+
intent: 'read',
|
|
888
|
+
output: diffOutput,
|
|
889
|
+
});
|
|
293
890
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
891
|
+
export const surveyTrailDetailTrail = trail('survey.trail', {
|
|
892
|
+
args: ['id'],
|
|
893
|
+
description: 'Inspect one trail by ID',
|
|
894
|
+
examples: [
|
|
895
|
+
{
|
|
896
|
+
description: 'Show trail contract detail',
|
|
897
|
+
input: {
|
|
898
|
+
...createIsolatedExampleInput('survey-trail-detail'),
|
|
899
|
+
id: 'survey',
|
|
900
|
+
},
|
|
901
|
+
name: 'Trail detail',
|
|
902
|
+
},
|
|
903
|
+
],
|
|
904
|
+
implementation: async (input, ctx) =>
|
|
905
|
+
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir, overlays) =>
|
|
906
|
+
buildSurveyTrailDetail(
|
|
907
|
+
app,
|
|
908
|
+
input.id,
|
|
909
|
+
rootDir,
|
|
910
|
+
overlays,
|
|
911
|
+
readSurfaceLayerNamesFromContext(ctx)
|
|
912
|
+
)
|
|
913
|
+
),
|
|
914
|
+
input: detailInputSchema,
|
|
915
|
+
intent: 'read',
|
|
916
|
+
output: trailDetailOutput,
|
|
917
|
+
});
|
|
297
918
|
|
|
298
|
-
|
|
299
|
-
|
|
919
|
+
export const surveyResourceTrail = trail('survey.resource', {
|
|
920
|
+
args: ['id'],
|
|
921
|
+
description: 'Inspect one resource by ID',
|
|
922
|
+
examples: [
|
|
923
|
+
{
|
|
924
|
+
description: 'Show resource usage detail',
|
|
925
|
+
error: 'NotFoundError',
|
|
926
|
+
input: {
|
|
927
|
+
...createIsolatedExampleInput('survey-resource-detail'),
|
|
928
|
+
id: 'db.main',
|
|
929
|
+
},
|
|
930
|
+
name: 'Resource detail',
|
|
931
|
+
},
|
|
932
|
+
],
|
|
933
|
+
implementation: async (input, ctx) =>
|
|
934
|
+
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
|
|
935
|
+
buildSurveyResourceDetail(app, input.id, rootDir)
|
|
936
|
+
),
|
|
937
|
+
input: detailInputSchema,
|
|
938
|
+
intent: 'read',
|
|
939
|
+
output: resourceDetailOutput,
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
export const surveySignalTrail = trail('survey.signal', {
|
|
943
|
+
args: ['id'],
|
|
944
|
+
description: 'Inspect one signal by ID',
|
|
945
|
+
examples: [
|
|
946
|
+
{
|
|
947
|
+
description: 'Show signal producer and consumer detail',
|
|
948
|
+
error: 'NotFoundError',
|
|
949
|
+
input: {
|
|
950
|
+
...createIsolatedExampleInput('survey-signal-detail'),
|
|
951
|
+
id: 'hello.greeted',
|
|
952
|
+
},
|
|
953
|
+
name: 'Signal detail',
|
|
954
|
+
},
|
|
955
|
+
],
|
|
956
|
+
implementation: async (input, ctx) =>
|
|
957
|
+
withResolvedSurveyApp(input, ctx.cwd, (app, rootDir) =>
|
|
958
|
+
buildSurveySignalDetail(app, input.id, rootDir)
|
|
959
|
+
),
|
|
960
|
+
input: detailInputSchema,
|
|
961
|
+
intent: 'read',
|
|
962
|
+
output: signalDetailOutput,
|
|
300
963
|
});
|