@ontrails/trails 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +1906 -0
  2. package/README.md +48 -0
  3. package/bin/trails.ts +3 -0
  4. package/package.json +57 -0
  5. package/src/app.ts +167 -0
  6. package/src/clack.ts +111 -0
  7. package/src/cli.ts +308 -0
  8. package/src/completions.ts +431 -0
  9. package/src/lifecycle-source-io.ts +33 -0
  10. package/src/load-app-mirror.ts +202 -0
  11. package/src/local-state-io.ts +129 -0
  12. package/src/mcp-app.ts +42 -0
  13. package/src/mcp-options.ts +92 -0
  14. package/src/mcp.ts +8 -0
  15. package/src/project-writes.ts +377 -0
  16. package/src/regrade/audit.ts +571 -0
  17. package/src/regrade/config.ts +152 -0
  18. package/src/regrade/history.ts +636 -0
  19. package/src/regrade/lifecycle.ts +76 -0
  20. package/src/regrade/live-api-preserve.ts +123 -0
  21. package/src/regrade/plan-artifact.ts +515 -0
  22. package/src/regrade/plan-derivation.ts +301 -0
  23. package/src/regrade/prepared-run.ts +259 -0
  24. package/src/regrade/receipt-history.ts +446 -0
  25. package/src/regrade/source-transaction.ts +185 -0
  26. package/src/release/bindings.ts +58 -0
  27. package/src/release/changeset-packages.ts +99 -0
  28. package/src/release/check.ts +1191 -0
  29. package/src/release/cli-bundle.ts +575 -0
  30. package/src/release/config.ts +73 -0
  31. package/src/release/contract-facts.ts +425 -0
  32. package/src/release/homebrew.ts +221 -0
  33. package/src/release/index.ts +180 -0
  34. package/src/release/lock-roundtrip-smoke.ts +255 -0
  35. package/src/release/lock-roundtrip-workspace.ts +107 -0
  36. package/src/release/native-bun-publish.ts +964 -0
  37. package/src/release/native-bun-registry.ts +848 -0
  38. package/src/release/notes-cli.ts +171 -0
  39. package/src/release/notes.ts +390 -0
  40. package/src/release/pack-coherence.ts +455 -0
  41. package/src/release/package-route-facts.ts +146 -0
  42. package/src/release/packed-artifacts-smoke.ts +236 -0
  43. package/src/release/policy.ts +1780 -0
  44. package/src/release/semver.ts +104 -0
  45. package/src/release/smoke.ts +56 -0
  46. package/src/release/stable-version-release.ts +80 -0
  47. package/src/release/wayfinder-dogfood-smoke.ts +762 -0
  48. package/src/release/zero-line-transition.ts +68 -0
  49. package/src/retired-topo-command.ts +36 -0
  50. package/src/run-adapter-check.ts +76 -0
  51. package/src/run-argv.ts +133 -0
  52. package/src/run-collision.ts +126 -0
  53. package/src/run-completions-install.ts +179 -0
  54. package/src/run-example.ts +149 -0
  55. package/src/run-examples.ts +148 -0
  56. package/src/run-quiet.ts +75 -0
  57. package/src/run-regrade-progress.ts +47 -0
  58. package/src/run-release-check.ts +74 -0
  59. package/src/run-schema.ts +74 -0
  60. package/src/run-trace.ts +273 -0
  61. package/src/run-warden.ts +39 -0
  62. package/src/run-watch-project.ts +52 -0
  63. package/src/run-watch.ts +381 -0
  64. package/src/run-wayfind-outline.ts +170 -0
  65. package/src/scaffold-version-sync.ts +183 -0
  66. package/src/scaffold-versions.generated.ts +12 -0
  67. package/src/trails/adapter-check.ts +244 -0
  68. package/src/trails/add-surface.ts +816 -0
  69. package/src/trails/add-trail.ts +141 -0
  70. package/src/trails/add-verify.ts +252 -0
  71. package/src/trails/compile.ts +118 -0
  72. package/src/trails/completions-complete.ts +236 -0
  73. package/src/trails/completions.ts +47 -0
  74. package/src/trails/config-explain.ts +43 -0
  75. package/src/trails/create-adapter.ts +785 -0
  76. package/src/trails/create-scaffold.ts +1215 -0
  77. package/src/trails/create-versions.ts +62 -0
  78. package/src/trails/create.ts +652 -0
  79. package/src/trails/deprecate.ts +59 -0
  80. package/src/trails/dev-clean.ts +80 -0
  81. package/src/trails/dev-reset.ts +48 -0
  82. package/src/trails/dev-stats.ts +71 -0
  83. package/src/trails/dev-support.ts +360 -0
  84. package/src/trails/doctor.ts +77 -0
  85. package/src/trails/draft-promote.ts +949 -0
  86. package/src/trails/guide.ts +106 -0
  87. package/src/trails/load-app.ts +1145 -0
  88. package/src/trails/operator-context.ts +66 -0
  89. package/src/trails/project-context-output.ts +304 -0
  90. package/src/trails/project-context.ts +613 -0
  91. package/src/trails/project.ts +65 -0
  92. package/src/trails/regrade.ts +4951 -0
  93. package/src/trails/release-check.ts +113 -0
  94. package/src/trails/release-smoke.ts +49 -0
  95. package/src/trails/revise.ts +53 -0
  96. package/src/trails/root-dir.ts +21 -0
  97. package/src/trails/run-example.ts +592 -0
  98. package/src/trails/run-examples.ts +149 -0
  99. package/src/trails/run.ts +496 -0
  100. package/src/trails/scaffold-json.ts +60 -0
  101. package/src/trails/scaffold-topo-identity.ts +479 -0
  102. package/src/trails/survey.ts +990 -0
  103. package/src/trails/topo-activation.ts +14 -0
  104. package/src/trails/topo-constants.ts +2 -0
  105. package/src/trails/topo-history.ts +47 -0
  106. package/src/trails/topo-output-schemas.ts +259 -0
  107. package/src/trails/topo-pin.ts +38 -0
  108. package/src/trails/topo-read-support.ts +368 -0
  109. package/src/trails/topo-reports.ts +809 -0
  110. package/src/trails/topo-store-support.ts +323 -0
  111. package/src/trails/topo-support.ts +247 -0
  112. package/src/trails/topo-unpin.ts +61 -0
  113. package/src/trails/topo.ts +92 -0
  114. package/src/trails/validate.ts +348 -0
  115. package/src/trails/version-lifecycle-support.ts +936 -0
  116. package/src/trails/warden-guide.ts +134 -0
  117. package/src/trails/warden.ts +598 -0
  118. package/src/trails/wayfind-diff.ts +716 -0
  119. package/src/trails/wayfind-outline.ts +876 -0
  120. package/src/trails/wayfind.ts +1319 -0
  121. package/src/versions.ts +31 -0
@@ -0,0 +1,92 @@
1
+ import { Result, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { withFreshOperatorApp } from './operator-context.js';
5
+ import { activationOverviewOutput } from './topo-output-schemas.js';
6
+ import { deriveTopoSummary } from './topo-read-support.js';
7
+ import { createCurrentAppExampleInput } from './topo-support.js';
8
+
9
+ const summaryOutput = z.object({
10
+ app: z.object({
11
+ contractVersion: z.string(),
12
+ features: z.object({
13
+ detours: z.boolean(),
14
+ examples: z.boolean(),
15
+ outputSchemas: z.boolean(),
16
+ resources: z.boolean(),
17
+ signals: z.boolean(),
18
+ }),
19
+ name: z.string(),
20
+ resources: z.number(),
21
+ signals: z.number(),
22
+ trails: z.number(),
23
+ version: z.string(),
24
+ }),
25
+ dbPath: z.string(),
26
+ list: z.object({
27
+ activation: activationOverviewOutput,
28
+ count: z.number(),
29
+ entries: z
30
+ .array(
31
+ z.object({
32
+ activatedBy: z.array(z.string()).readonly(),
33
+ activates: z.array(z.string()).readonly(),
34
+ examples: z.number(),
35
+ id: z.string(),
36
+ kind: z.string(),
37
+ safety: z.string(),
38
+ })
39
+ )
40
+ .readonly(),
41
+ resourceCount: z.number(),
42
+ resources: z
43
+ .array(
44
+ z.object({
45
+ description: z.string().nullable(),
46
+ health: z.enum(['available', 'none']),
47
+ id: z.string(),
48
+ kind: z.literal('resource'),
49
+ lifetime: z.literal('singleton'),
50
+ usedBy: z.array(z.string()).readonly(),
51
+ })
52
+ )
53
+ .readonly(),
54
+ signalCount: z.number(),
55
+ signals: z
56
+ .array(
57
+ z.object({
58
+ consumers: z.array(z.string()).readonly(),
59
+ description: z.string().nullable(),
60
+ examples: z.number(),
61
+ from: z.array(z.string()).readonly(),
62
+ id: z.string(),
63
+ kind: z.literal('signal'),
64
+ payloadSchema: z.boolean(),
65
+ producers: z.array(z.string()).readonly(),
66
+ })
67
+ )
68
+ .readonly(),
69
+ }),
70
+ lockExists: z.boolean(),
71
+ lockPath: z.string(),
72
+ });
73
+
74
+ export const topoTrail = trail('topo', {
75
+ description: 'Show the current topo summary and entry list',
76
+ examples: [
77
+ {
78
+ input: createCurrentAppExampleInput(),
79
+ name: 'Show the current topo summary',
80
+ },
81
+ ],
82
+ implementation: async (input, ctx) =>
83
+ withFreshOperatorApp(input, ctx, ({ lease, rootDir }) =>
84
+ Result.ok(deriveTopoSummary(lease.app, { rootDir }))
85
+ ),
86
+ input: z.object({
87
+ module: z.string().optional().describe('Path to the app module'),
88
+ rootDir: z.string().optional().describe('Workspace root directory'),
89
+ }),
90
+ intent: 'read',
91
+ output: summaryOutput,
92
+ });
@@ -0,0 +1,348 @@
1
+ import { Result, trail, ValidationError } from '@ontrails/core';
2
+ import type { Result as TrailResult } from '@ontrails/core';
3
+ import { deriveWorkspaceView } from '@ontrails/topography';
4
+ import { z } from 'zod';
5
+
6
+ import { withFreshAppLease } from './operator-context.js';
7
+ import type {
8
+ OperatorAppProjectContext,
9
+ OperatorProjectApp,
10
+ OperatorWorkspaceProjectContext,
11
+ } from './project-context.js';
12
+ import {
13
+ assertConfiguredAppBinding,
14
+ assertObservableProjectApps,
15
+ resolveOperatorAppModuleContext,
16
+ resolveOperatorProjectContext,
17
+ } from './project-context.js';
18
+ import {
19
+ configuredAppProjectSelection,
20
+ configuredAppProjectSelectionSchema,
21
+ standaloneAppProjectSelection,
22
+ standaloneAppProjectSelectionSchema,
23
+ workspaceProjectSelection,
24
+ workspaceProjectSelectionSchema,
25
+ } from './project-context-output.js';
26
+ import { validateCurrentTopo } from './topo-read-support.js';
27
+
28
+ const validateTrailInputSchema = z.object({
29
+ app: z.string().optional().describe('Configured workspace app ID'),
30
+ module: z.string().optional().describe('Path to the app module'),
31
+ rootDir: z.string().optional().describe('Workspace root directory'),
32
+ });
33
+
34
+ type ValidateTrailInput = z.output<typeof validateTrailInputSchema>;
35
+
36
+ const appValidationOutputShape = {
37
+ committedHash: z.string(),
38
+ currentHash: z.string(),
39
+ lockPath: z.string(),
40
+ stale: z.literal(false),
41
+ } as const;
42
+
43
+ const configuredAppValidationOutputSchema = z.object({
44
+ ...appValidationOutputShape,
45
+ project: configuredAppProjectSelectionSchema,
46
+ selectedExtent: z.literal('configured-app'),
47
+ });
48
+
49
+ const standaloneAppValidationOutputSchema = z.object({
50
+ ...appValidationOutputShape,
51
+ project: standaloneAppProjectSelectionSchema,
52
+ selectedExtent: z.literal('standalone-app'),
53
+ });
54
+
55
+ const workspaceAppValidationSchema = z.object({
56
+ appId: z.string(),
57
+ committedHash: z.string(),
58
+ currentHash: z.string(),
59
+ lockPath: z.string(),
60
+ stale: z.literal(false),
61
+ });
62
+
63
+ const workspaceValidationOutputSchema = z.object({
64
+ apps: z.array(workspaceAppValidationSchema),
65
+ project: workspaceProjectSelectionSchema,
66
+ selectedExtent: z.literal('workspace'),
67
+ stale: z.literal(false),
68
+ workspaceViewHash: z.string(),
69
+ });
70
+
71
+ const validateTrailOutputSchema = z.discriminatedUnion('selectedExtent', [
72
+ configuredAppValidationOutputSchema,
73
+ standaloneAppValidationOutputSchema,
74
+ workspaceValidationOutputSchema,
75
+ ]);
76
+
77
+ type ValidateTrailOutput = z.output<typeof validateTrailOutputSchema>;
78
+ type AppValidationOutput = z.output<
79
+ | typeof configuredAppValidationOutputSchema
80
+ | typeof standaloneAppValidationOutputSchema
81
+ >;
82
+ type WorkspaceAppValidation = z.output<typeof workspaceAppValidationSchema>;
83
+
84
+ const forbiddenWorkspaceAggregateLocks = (view: {
85
+ readonly evidence: {
86
+ readonly unownedLocks: readonly { readonly kind: string }[];
87
+ };
88
+ }): readonly { readonly kind: string }[] =>
89
+ view.evidence.unownedLocks.filter(
90
+ (lock) => lock.kind === 'forbidden-workspace-aggregate'
91
+ );
92
+
93
+ const appContext = (
94
+ workspace: OperatorWorkspaceProjectContext,
95
+ app: OperatorProjectApp
96
+ ): OperatorAppProjectContext => ({
97
+ app,
98
+ boundaryDir: workspace.boundaryDir,
99
+ identity: workspace.identity,
100
+ projectRoot: workspace.projectRoot,
101
+ selectedExtent: 'configured-app',
102
+ selectionProvenance: workspace.selectionProvenance,
103
+ });
104
+
105
+ const assertValidatedConfiguredAppBinding = async (
106
+ context: OperatorAppProjectContext,
107
+ committedHash: string
108
+ ): Promise<TrailResult<void, ValidationError>> => {
109
+ const expectedAppId = context.app.id;
110
+ if (expectedAppId === undefined) {
111
+ return Result.ok();
112
+ }
113
+ try {
114
+ const view = await deriveWorkspaceView({
115
+ currentAppGraphHashes: { [expectedAppId]: committedHash },
116
+ identity: context.identity,
117
+ selectedAppIds: [expectedAppId],
118
+ });
119
+ const observation = view.evidence.apps.find(
120
+ (app) => app.id === expectedAppId
121
+ );
122
+ const forbiddenAggregateLocks = forbiddenWorkspaceAggregateLocks(view);
123
+ if (
124
+ observation?.binding === 'matched' &&
125
+ observation.freshness === 'fresh' &&
126
+ observation.status === 'available' &&
127
+ forbiddenAggregateLocks.length === 0
128
+ ) {
129
+ return Result.ok();
130
+ }
131
+ return Result.err(
132
+ new ValidationError(
133
+ `Configured app "${expectedAppId}" is bound to a contradictory saved lock. ${observation?.detail ?? 'No matching lock observation is available.'}`,
134
+ {
135
+ context: {
136
+ actualAppId: observation?.actualAppId,
137
+ expectedAppId,
138
+ freshness: observation?.freshness,
139
+ lockPath: context.app.lockPath,
140
+ projectRoot: context.projectRoot,
141
+ reason: 'invalid-binding',
142
+ status: observation?.status,
143
+ unownedLocks: view.evidence.unownedLocks,
144
+ },
145
+ }
146
+ )
147
+ );
148
+ } catch (error) {
149
+ return Result.err(
150
+ new ValidationError(
151
+ `Unable to prove the saved lock binding for configured app "${expectedAppId}".`,
152
+ {
153
+ ...(error instanceof Error ? { cause: error } : {}),
154
+ context: {
155
+ detail: error instanceof Error ? error.message : String(error),
156
+ expectedAppId,
157
+ lockPath: context.app.lockPath,
158
+ projectRoot: context.projectRoot,
159
+ reason: 'invalid-binding',
160
+ },
161
+ }
162
+ )
163
+ );
164
+ }
165
+ };
166
+
167
+ const validateAppProject = async (
168
+ context: OperatorAppProjectContext,
169
+ options: { readonly validateArtifactBinding?: boolean } = {}
170
+ ): Promise<TrailResult<AppValidationOutput, Error>> => {
171
+ const moduleContext = resolveOperatorAppModuleContext(context);
172
+ if (moduleContext.isErr()) {
173
+ return moduleContext;
174
+ }
175
+ const selectedContext = moduleContext.value;
176
+ return withFreshAppLease<AppValidationOutput>(
177
+ selectedContext.app.modulePath,
178
+ selectedContext.app.rootDir,
179
+ async (lease) => {
180
+ const binding = assertConfiguredAppBinding(
181
+ selectedContext,
182
+ lease.app.name
183
+ );
184
+ if (binding.isErr()) {
185
+ return binding;
186
+ }
187
+ const validated = await validateCurrentTopo(lease.app, {
188
+ overlays: lease.overlays,
189
+ rootDir: selectedContext.app.rootDir,
190
+ });
191
+ if (validated.isErr()) {
192
+ return validated;
193
+ }
194
+ if (options.validateArtifactBinding !== false) {
195
+ const artifactBinding = await assertValidatedConfiguredAppBinding(
196
+ selectedContext,
197
+ validated.value.committedHash
198
+ );
199
+ if (artifactBinding.isErr()) {
200
+ return artifactBinding;
201
+ }
202
+ }
203
+ return selectedContext.selectedExtent === 'configured-app'
204
+ ? Result.ok({
205
+ ...validated.value,
206
+ project: configuredAppProjectSelection(
207
+ selectedContext,
208
+ lease.app.name
209
+ ),
210
+ selectedExtent: 'configured-app',
211
+ })
212
+ : Result.ok({
213
+ ...validated.value,
214
+ project: standaloneAppProjectSelection(
215
+ selectedContext,
216
+ lease.app.name
217
+ ),
218
+ selectedExtent: 'standalone-app',
219
+ });
220
+ }
221
+ );
222
+ };
223
+
224
+ const workspaceValidationError = (
225
+ message: string,
226
+ context: Record<string, unknown>,
227
+ cause?: Error
228
+ ): ValidationError =>
229
+ new ValidationError(message, {
230
+ ...(cause === undefined ? {} : { cause }),
231
+ context: { ...context, reason: 'workspace-incomplete' },
232
+ });
233
+
234
+ const validateWorkspaceProject = async (
235
+ context: OperatorWorkspaceProjectContext
236
+ ): Promise<TrailResult<ValidateTrailOutput, Error>> => {
237
+ const results: WorkspaceAppValidation[] = [];
238
+ for (const app of context.apps) {
239
+ const validated = await validateAppProject(appContext(context, app), {
240
+ validateArtifactBinding: false,
241
+ });
242
+ if (validated.isErr()) {
243
+ return Result.err(
244
+ workspaceValidationError(
245
+ `Workspace app "${String(app.id)}" failed validation: ${validated.error.message}`,
246
+ {
247
+ appId: app.id,
248
+ configuredAppIds: context.apps.map((item) => item.id),
249
+ projectRoot: context.projectRoot,
250
+ unownedLocks:
251
+ validated.error instanceof ValidationError
252
+ ? validated.error.context?.['unownedLocks']
253
+ : undefined,
254
+ },
255
+ validated.error
256
+ )
257
+ );
258
+ }
259
+ results.push({
260
+ appId: app.id as string,
261
+ committedHash: validated.value.committedHash,
262
+ currentHash: validated.value.currentHash,
263
+ lockPath: app.lockPath,
264
+ stale: false,
265
+ });
266
+ }
267
+
268
+ const view = await deriveWorkspaceView({
269
+ currentAppGraphHashes: Object.fromEntries(
270
+ results.map((result) => [result.appId, result.committedHash])
271
+ ),
272
+ identity: context.identity,
273
+ });
274
+ const incomplete = view.evidence.apps.filter(
275
+ (app) =>
276
+ app.binding !== 'matched' ||
277
+ app.freshness !== 'fresh' ||
278
+ app.status !== 'available'
279
+ );
280
+ const forbiddenAggregateLocks = forbiddenWorkspaceAggregateLocks(view);
281
+ if (
282
+ view.workspaceViewHash === null ||
283
+ view.evidence.configuredCompleteness !== 'complete' ||
284
+ incomplete.length > 0 ||
285
+ forbiddenAggregateLocks.length > 0
286
+ ) {
287
+ return Result.err(
288
+ workspaceValidationError(
289
+ 'Workspace validation cannot claim completeness because app evidence is incomplete, stale, invalid, or contradictory.',
290
+ {
291
+ appEvidence: view.evidence.apps,
292
+ collectionSkips: view.evidence.collectionSkips,
293
+ completeness: view.evidence.configuredCompleteness,
294
+ configuredAppIds: view.evidence.configuredAppIds,
295
+ projectRoot: context.projectRoot,
296
+ unownedLocks: view.evidence.unownedLocks,
297
+ }
298
+ )
299
+ );
300
+ }
301
+ const project = workspaceProjectSelection(context, view);
302
+ if (project.isErr()) {
303
+ return project;
304
+ }
305
+ return Result.ok({
306
+ apps: results,
307
+ project: project.value,
308
+ selectedExtent: 'workspace',
309
+ stale: false,
310
+ workspaceViewHash: project.value.workspaceViewHash,
311
+ });
312
+ };
313
+
314
+ const validateSelectedProject = async (
315
+ input: ValidateTrailInput,
316
+ cwd: string | undefined
317
+ ): Promise<TrailResult<ValidateTrailOutput, Error>> => {
318
+ try {
319
+ const contextResult = await resolveOperatorProjectContext(input, { cwd });
320
+ if (contextResult.isErr()) {
321
+ return contextResult;
322
+ }
323
+ const context = contextResult.value;
324
+ const observable = await assertObservableProjectApps(context);
325
+ if (observable.isErr()) {
326
+ return observable;
327
+ }
328
+ return context.selectedExtent === 'workspace'
329
+ ? await validateWorkspaceProject(context)
330
+ : await validateAppProject(context);
331
+ } catch (error) {
332
+ return Result.err(
333
+ error instanceof Error
334
+ ? error
335
+ : new ValidationError('Unable to validate project context.', {
336
+ context: { detail: String(error) },
337
+ })
338
+ );
339
+ }
340
+ };
341
+
342
+ export const validateTrail = trail('validate', {
343
+ description: 'Validate selected app or workspace locks against current topos',
344
+ implementation: async (input, ctx) => validateSelectedProject(input, ctx.cwd),
345
+ input: validateTrailInputSchema,
346
+ intent: 'read',
347
+ output: validateTrailOutputSchema,
348
+ });