@ontrails/trails 1.0.0-beta.3 → 1.0.0-beta.30

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 (157) hide show
  1. package/CHANGELOG.md +886 -0
  2. package/README.md +27 -0
  3. package/package.json +30 -7
  4. package/src/app.ts +125 -2
  5. package/src/cli.ts +344 -11
  6. package/src/completions.ts +240 -0
  7. package/src/lifecycle-source-io.ts +33 -0
  8. package/src/load-app-mirror.ts +202 -0
  9. package/src/local-state-io.ts +173 -0
  10. package/src/mcp-app.ts +40 -0
  11. package/src/mcp-options.ts +76 -0
  12. package/src/mcp.ts +8 -0
  13. package/src/project-writes.ts +377 -0
  14. package/src/release/bindings.ts +39 -0
  15. package/src/release/check.ts +844 -0
  16. package/src/release/config.ts +63 -0
  17. package/src/release/contract-facts.ts +425 -0
  18. package/src/release/index.ts +125 -0
  19. package/src/release/native-bun-publish.ts +651 -0
  20. package/src/release/native-bun-registry.ts +754 -0
  21. package/src/release/pack-coherence.ts +457 -0
  22. package/src/release/packed-artifacts-smoke.ts +236 -0
  23. package/src/release/policy.ts +1684 -0
  24. package/src/release/semver.ts +104 -0
  25. package/src/release/smoke.ts +46 -0
  26. package/src/release/wayfinder-dogfood-smoke.ts +760 -0
  27. package/src/retired-topo-command.ts +36 -0
  28. package/src/run-adapter-check.ts +76 -0
  29. package/src/run-collision.ts +126 -0
  30. package/src/run-completions-install.ts +179 -0
  31. package/src/run-example.ts +149 -0
  32. package/src/run-examples.ts +148 -0
  33. package/src/run-quiet.ts +75 -0
  34. package/src/run-release-check.ts +74 -0
  35. package/src/run-schema.ts +41 -0
  36. package/src/run-trace.ts +273 -0
  37. package/src/run-warden.ts +39 -0
  38. package/src/run-watch.ts +432 -0
  39. package/src/run-wayfind-outline.ts +166 -0
  40. package/src/scaffold-version-sync.ts +183 -0
  41. package/src/scaffold-versions.generated.ts +12 -0
  42. package/src/trails/adapter-check.ts +244 -0
  43. package/src/trails/add-surface.ts +94 -40
  44. package/src/trails/add-trail.ts +79 -41
  45. package/src/trails/add-verify.ts +95 -25
  46. package/src/trails/compile.ts +59 -0
  47. package/src/trails/completions-complete.ts +165 -0
  48. package/src/trails/completions.ts +47 -0
  49. package/src/trails/create-adapter.ts +1084 -0
  50. package/src/trails/create-scaffold.ts +399 -104
  51. package/src/trails/create-versions.ts +62 -0
  52. package/src/trails/create.ts +186 -72
  53. package/src/trails/deprecate.ts +59 -0
  54. package/src/trails/dev-clean.ts +82 -0
  55. package/src/trails/dev-reset.ts +50 -0
  56. package/src/trails/dev-stats.ts +72 -0
  57. package/src/trails/dev-support.ts +360 -0
  58. package/src/trails/doctor.ts +77 -0
  59. package/src/trails/draft-promote.ts +949 -0
  60. package/src/trails/guide.ts +71 -69
  61. package/src/trails/load-app.ts +1193 -16
  62. package/src/trails/operator-context.ts +66 -0
  63. package/src/trails/project.ts +17 -3
  64. package/src/trails/regrade.ts +72 -0
  65. package/src/trails/release-check.ts +105 -0
  66. package/src/trails/release-smoke.ts +48 -0
  67. package/src/trails/revise.ts +53 -0
  68. package/src/trails/root-dir.ts +21 -0
  69. package/src/trails/run-example.ts +475 -0
  70. package/src/trails/run-examples.ts +129 -0
  71. package/src/trails/run.ts +434 -0
  72. package/src/trails/scaffold-json.ts +58 -0
  73. package/src/trails/survey.ts +896 -227
  74. package/src/trails/topo-activation.ts +385 -0
  75. package/src/trails/topo-constants.ts +2 -0
  76. package/src/trails/topo-history.ts +47 -0
  77. package/src/trails/topo-output-schemas.ts +259 -0
  78. package/src/trails/topo-pin.ts +38 -0
  79. package/src/trails/topo-read-support.ts +363 -0
  80. package/src/trails/topo-reports.ts +809 -0
  81. package/src/trails/topo-store-support.ts +307 -0
  82. package/src/trails/topo-support.ts +219 -0
  83. package/src/trails/topo-unpin.ts +61 -0
  84. package/src/trails/topo.ts +92 -0
  85. package/src/trails/validate.ts +27 -0
  86. package/src/trails/version-lifecycle-support.ts +935 -0
  87. package/src/trails/warden-guide.ts +129 -0
  88. package/src/trails/warden.ts +183 -58
  89. package/src/trails/wayfind.ts +1001 -0
  90. package/src/versions.ts +31 -0
  91. package/.turbo/turbo-build.log +0 -1
  92. package/.turbo/turbo-lint.log +0 -3
  93. package/.turbo/turbo-typecheck.log +0 -1
  94. package/__tests__/examples.test.ts +0 -6
  95. package/dist/bin/trails.d.ts +0 -3
  96. package/dist/bin/trails.d.ts.map +0 -1
  97. package/dist/bin/trails.js +0 -4
  98. package/dist/bin/trails.js.map +0 -1
  99. package/dist/src/app.d.ts +0 -2
  100. package/dist/src/app.d.ts.map +0 -1
  101. package/dist/src/app.js +0 -11
  102. package/dist/src/app.js.map +0 -1
  103. package/dist/src/clack.d.ts +0 -9
  104. package/dist/src/clack.d.ts.map +0 -1
  105. package/dist/src/clack.js +0 -84
  106. package/dist/src/clack.js.map +0 -1
  107. package/dist/src/cli.d.ts +0 -2
  108. package/dist/src/cli.d.ts.map +0 -1
  109. package/dist/src/cli.js +0 -13
  110. package/dist/src/cli.js.map +0 -1
  111. package/dist/src/trails/add-surface.d.ts +0 -13
  112. package/dist/src/trails/add-surface.d.ts.map +0 -1
  113. package/dist/src/trails/add-surface.js +0 -88
  114. package/dist/src/trails/add-surface.js.map +0 -1
  115. package/dist/src/trails/add-trail.d.ts +0 -11
  116. package/dist/src/trails/add-trail.d.ts.map +0 -1
  117. package/dist/src/trails/add-trail.js +0 -85
  118. package/dist/src/trails/add-trail.js.map +0 -1
  119. package/dist/src/trails/add-verify.d.ts +0 -10
  120. package/dist/src/trails/add-verify.d.ts.map +0 -1
  121. package/dist/src/trails/add-verify.js +0 -67
  122. package/dist/src/trails/add-verify.js.map +0 -1
  123. package/dist/src/trails/create-scaffold.d.ts +0 -15
  124. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  125. package/dist/src/trails/create-scaffold.js +0 -288
  126. package/dist/src/trails/create-scaffold.js.map +0 -1
  127. package/dist/src/trails/create.d.ts +0 -22
  128. package/dist/src/trails/create.d.ts.map +0 -1
  129. package/dist/src/trails/create.js +0 -121
  130. package/dist/src/trails/create.js.map +0 -1
  131. package/dist/src/trails/guide.d.ts +0 -11
  132. package/dist/src/trails/guide.d.ts.map +0 -1
  133. package/dist/src/trails/guide.js +0 -80
  134. package/dist/src/trails/guide.js.map +0 -1
  135. package/dist/src/trails/load-app.d.ts +0 -4
  136. package/dist/src/trails/load-app.d.ts.map +0 -1
  137. package/dist/src/trails/load-app.js +0 -24
  138. package/dist/src/trails/load-app.js.map +0 -1
  139. package/dist/src/trails/project.d.ts +0 -8
  140. package/dist/src/trails/project.d.ts.map +0 -1
  141. package/dist/src/trails/project.js +0 -43
  142. package/dist/src/trails/project.js.map +0 -1
  143. package/dist/src/trails/survey.d.ts +0 -33
  144. package/dist/src/trails/survey.d.ts.map +0 -1
  145. package/dist/src/trails/survey.js +0 -225
  146. package/dist/src/trails/survey.js.map +0 -1
  147. package/dist/src/trails/warden.d.ts +0 -19
  148. package/dist/src/trails/warden.d.ts.map +0 -1
  149. package/dist/src/trails/warden.js +0 -88
  150. package/dist/src/trails/warden.js.map +0 -1
  151. package/dist/tsconfig.tsbuildinfo +0 -1
  152. package/src/__tests__/create.test.ts +0 -349
  153. package/src/__tests__/guide.test.ts +0 -91
  154. package/src/__tests__/load-app.test.ts +0 -15
  155. package/src/__tests__/survey.test.ts +0 -161
  156. package/src/__tests__/warden.test.ts +0 -74
  157. package/tsconfig.json +0 -9
@@ -1,15 +1,36 @@
1
1
  /**
2
2
  * `create.scaffold` trail -- Creates base project structure.
3
3
  *
4
- * Generates package.json, tsconfig, app.ts, starter trails, and .trails/ directory.
4
+ * Generates package.json, tsconfig, app.ts, starter trails, and scaffold provenance.
5
5
  */
6
6
 
7
- import { mkdirSync } from 'node:fs';
8
- import { dirname, join, resolve } from 'node:path';
7
+ import { resolve } from 'node:path';
9
8
 
10
9
  import { Result, trail } from '@ontrails/core';
10
+ import type { Result as TrailsResult } from '@ontrails/core';
11
11
  import { z } from 'zod';
12
12
 
13
+ import {
14
+ applyProjectOperations,
15
+ planProjectOperations,
16
+ PROJECT_NAME_MESSAGE,
17
+ PROJECT_NAME_PATTERN,
18
+ resolveProjectDir,
19
+ } from '../project-writes.js';
20
+ import type {
21
+ PlannedProjectOperation,
22
+ ProjectWriteOperation,
23
+ } from '../project-writes.js';
24
+ import {
25
+ ontrailsPackageRange,
26
+ scaffoldDependencyVersions,
27
+ trailsPackageVersion,
28
+ } from '../versions.js';
29
+ import {
30
+ stringifyScaffoldJson,
31
+ stringifyScaffoldPackageJson,
32
+ } from './scaffold-json.js';
33
+
13
34
  // ---------------------------------------------------------------------------
14
35
  // Types
15
36
  // ---------------------------------------------------------------------------
@@ -19,73 +40,194 @@ type Starter = 'empty' | 'entity' | 'hello';
19
40
  interface ScaffoldResult {
20
41
  readonly created: string[];
21
42
  readonly dir: string;
43
+ readonly dryRun: boolean;
22
44
  readonly name: string;
45
+ readonly plannedOperations: PlannedProjectOperation[];
23
46
  }
24
47
 
48
+ const frameworkCommandScripts = {
49
+ add: 'trails add',
50
+ compile: 'trails compile',
51
+ completions: 'trails completions',
52
+ deprecate: 'trails deprecate',
53
+ diff: 'trails diff',
54
+ doctor: 'trails doctor',
55
+ guide: 'trails guide',
56
+ revise: 'trails revise',
57
+ run: 'trails run',
58
+ survey: 'trails survey',
59
+ topo: 'trails topo',
60
+ validate: 'trails validate',
61
+ warden: 'trails warden',
62
+ } as const satisfies Record<string, string>;
63
+
25
64
  // ---------------------------------------------------------------------------
26
65
  // Content generators
27
66
  // ---------------------------------------------------------------------------
28
67
 
29
68
  const generatePackageJson = (name: string): string => {
30
69
  const deps: Record<string, string> = {
31
- '@ontrails/core': 'workspace:*',
32
- zod: '^4.0.0',
70
+ '@ontrails/core': ontrailsPackageRange,
71
+ zod: scaffoldDependencyVersions.zod,
33
72
  };
34
73
 
35
74
  const pkg: Record<string, unknown> = {
36
75
  dependencies: Object.fromEntries(
37
76
  Object.entries(deps).toSorted(([a], [b]) => a.localeCompare(b))
38
77
  ),
78
+ devDependencies: Object.fromEntries(
79
+ Object.entries({
80
+ '@ontrails/trails': ontrailsPackageRange,
81
+ '@types/bun': scaffoldDependencyVersions.bunTypes,
82
+ oxfmt: scaffoldDependencyVersions.oxfmt,
83
+ oxlint: scaffoldDependencyVersions.oxlint,
84
+ typescript: scaffoldDependencyVersions.typescript,
85
+ ultracite: scaffoldDependencyVersions.ultracite,
86
+ }).toSorted(([a], [b]) => a.localeCompare(b))
87
+ ),
39
88
  name,
40
- scripts: {
41
- build: 'tsc -b',
42
- lint: 'oxlint ./src',
43
- test: 'bun test',
44
- typecheck: 'tsc --noEmit',
45
- },
89
+ scripts: Object.fromEntries(
90
+ Object.entries({
91
+ build: 'tsc -b',
92
+ 'format:check': 'bunx ultracite check .',
93
+ 'format:fix': 'bunx ultracite fix .',
94
+ lint: 'oxlint ./src',
95
+ test: 'bun test',
96
+ typecheck: 'tsc --noEmit',
97
+ ...frameworkCommandScripts,
98
+ }).toSorted(([a], [b]) => a.localeCompare(b))
99
+ ),
46
100
  type: 'module',
47
101
  version: '0.1.0',
48
102
  };
49
103
 
50
- return JSON.stringify(pkg, null, 2);
104
+ return stringifyScaffoldPackageJson(pkg);
51
105
  };
52
106
 
53
- const TSCONFIG_CONTENT = JSON.stringify(
54
- {
55
- compilerOptions: {
56
- declaration: true,
57
- module: 'ESNext',
58
- moduleResolution: 'bundler',
59
- noUncheckedIndexedAccess: true,
60
- outDir: 'dist',
61
- rootDir: 'src',
62
- skipLibCheck: true,
63
- strict: true,
64
- target: 'ESNext',
65
- verbatimModuleSyntax: true,
66
- },
67
- include: ['src'],
107
+ const generateScaffoldProvenance = (starter: Starter): string =>
108
+ stringifyScaffoldJson({
109
+ generatedAt: new Date().toISOString(),
110
+ scaffoldVersion: trailsPackageVersion,
111
+ schemaVersion: 1,
112
+ template: starter,
113
+ });
114
+
115
+ const TSCONFIG_CONTENT = `{
116
+ "compilerOptions": {
117
+ "declaration": true,
118
+ "module": "ESNext",
119
+ "moduleResolution": "bundler",
120
+ "noUncheckedIndexedAccess": true,
121
+ "outDir": "dist",
122
+ "rootDir": "src",
123
+ "skipLibCheck": true,
124
+ "strict": true,
125
+ "target": "ESNext",
126
+ "verbatimModuleSyntax": true
68
127
  },
69
- null,
70
- 2
71
- );
128
+ "include": ["src"]
129
+ }
130
+ `;
131
+
132
+ const TSCONFIG_TESTS_CONTENT = `{
133
+ "compilerOptions": {
134
+ "noEmit": true,
135
+ "rootDir": ".",
136
+ "types": ["bun"]
137
+ },
138
+ "exclude": [],
139
+ "extends": "./tsconfig.json",
140
+ "include": ["src", "__tests__"]
141
+ }
142
+ `;
143
+
144
+ const AGENTS_CONTENT = `# AGENTS.md
145
+
146
+ This is a Trails project. Trails is an agent-native, contract-first TypeScript framework: author a trail once with typed input, Result output, examples, intent, and meta; surface it through CLI, MCP, HTTP, or future WebSocket without rewriting the contract.
147
+
148
+ ## Commands
149
+
150
+ Use the project scripts first:
151
+
152
+ \`\`\`bash
153
+ bun install
154
+ bun run build
155
+ bun test
156
+ bun run typecheck
157
+ bun run lint
158
+ bun run format:check
159
+ bun run warden
160
+ bun run survey
161
+ bun run guide
162
+ \`\`\`
163
+
164
+ ## Lexicon
165
+
166
+ - \`trail\`, not action or handler
167
+ - \`blaze\`, not handler or impl
168
+ - \`topo\`, not registry or collection
169
+ - \`compose\`, not follow
170
+ - \`surface\`, not transport
171
+ - \`resource\`, not service or dependency
172
+ - \`layer\`, for cross-cutting trail wrapping
173
+
174
+ ## Trail Rules
175
+
176
+ - Blazes return \`Result\`; never throw from trail logic.
177
+ - Use \`Result.ok()\` and \`Result.err()\`; branch with \`isOk()\`, \`isErr()\`, or \`match()\`.
178
+ - Keep trail logic surface-agnostic. Do not import CLI, MCP, HTTP, request, or response types into blazes.
179
+ - Public MCP or HTTP trails declare an \`output\` schema.
180
+ - Trails that compose other trails declare \`composes: [...]\` and invoke them with \`ctx.compose(...)\`.
181
+ - Trails that use infrastructure declare \`resources: [...]\` and access them through the resource helpers.
182
+ - Use \`detours\` for recovery strategies instead of inline retry logic.
183
+ - Prefer examples for happy-path coverage, and add focused tests for edge cases.
184
+ `;
185
+
186
+ const CLAUDE_CONTENT = `# CLAUDE.md
187
+
188
+ ## Compatibility Shim
189
+
190
+ Keep shared project guidance in \`./AGENTS.md\`. Only Claude-specific bootstrap notes belong here.
191
+
192
+ ## Agent Instructions
193
+
194
+ @AGENTS.md
195
+ `;
72
196
 
73
197
  const GITIGNORE_CONTENT = `node_modules/
74
198
  dist/
75
199
  *.tsbuildinfo
76
- .trails/_surface.json
200
+ trails.config.local.*
77
201
  `;
78
202
 
79
- const OXLINTRC_CONTENT = JSON.stringify(
80
- {
81
- extends: ['ultracite'],
203
+ const OXLINT_CONFIG_CONTENT = `import { defineConfig } from 'oxlint';
204
+ import ultracite from 'ultracite/oxlint/core';
205
+
206
+ export default defineConfig({
207
+ extends: [ultracite],
208
+ rules: {
209
+ 'no-warning-comments': [
210
+ 'error',
211
+ {
212
+ location: 'start',
213
+ terms: ['todo:', 'fixme', 'xxx'],
214
+ },
215
+ ],
82
216
  },
83
- null,
84
- 2
85
- );
217
+ });
218
+ `;
86
219
 
87
220
  const OXFMTRC_CONTENT = `{
88
- // ultracite defaults
221
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
222
+ "tabWidth": 2,
223
+ "useTabs": false,
224
+ "semi": true,
225
+ "singleQuote": true,
226
+ "trailingComma": "es5",
227
+ "bracketSpacing": true,
228
+ "arrowParens": "always",
229
+ "proseWrap": "never",
230
+ "printWidth": 80,
89
231
  }
90
232
  `;
91
233
 
@@ -94,6 +236,10 @@ const generateHelloTrail = (): string =>
94
236
  import { z } from 'zod';
95
237
 
96
238
  export const hello = trail('hello', {
239
+ blaze: (input) => {
240
+ const name = input.name ?? 'world';
241
+ return Result.ok({ message: \`Hello, \${name}!\` });
242
+ },
97
243
  description: 'Say hello',
98
244
  examples: [
99
245
  {
@@ -107,30 +253,38 @@ export const hello = trail('hello', {
107
253
  name: 'Named greeting',
108
254
  },
109
255
  ],
110
- implementation: (input) => {
111
- const name = input.name ?? 'world';
112
- return Result.ok({ message: \`Hello, \${name}!\` });
113
- },
114
256
  input: z.object({
115
257
  name: z.string().optional(),
116
258
  }),
259
+ intent: 'read',
117
260
  output: z.object({
118
261
  message: z.string(),
119
262
  }),
120
- readOnly: true,
121
263
  });
122
264
  `;
123
265
 
124
266
  const generateEntityTrails = (): string =>
125
- `import { Result, trail } from '@ontrails/core';
267
+ `import { randomUUID } from 'node:crypto';
268
+
269
+ import { NotFoundError, Result, trail } from '@ontrails/core';
126
270
  import { z } from 'zod';
127
271
 
272
+ import { entityStore } from '../store.js';
273
+
128
274
  const entitySchema = z.object({
129
275
  id: z.string(),
130
276
  name: z.string(),
131
277
  });
132
278
 
133
279
  export const show = trail('entity.show', {
280
+ blaze: (input, ctx) => {
281
+ const store = entityStore.from(ctx);
282
+ const entity = store.get(input.id);
283
+ if (!entity) {
284
+ return Result.err(new NotFoundError(\`Entity "\${input.id}" not found\`));
285
+ }
286
+ return Result.ok(entity);
287
+ },
134
288
  description: 'Show an entity by ID',
135
289
  examples: [
136
290
  {
@@ -139,28 +293,77 @@ export const show = trail('entity.show', {
139
293
  name: 'Show entity',
140
294
  },
141
295
  ],
142
- implementation: (input) => {
143
- return Result.ok({ id: input.id, name: 'Example' });
144
- },
145
296
  input: z.object({ id: z.string() }),
297
+ intent: 'read',
146
298
  output: entitySchema,
147
- readOnly: true,
299
+ resources: [entityStore],
148
300
  });
149
301
 
150
302
  export const add = trail('entity.add', {
303
+ blaze: (input, ctx) => {
304
+ const store = entityStore.from(ctx);
305
+ const entity = { id: randomUUID(), name: input.name };
306
+ store.add(entity);
307
+ return Result.ok(entity);
308
+ },
151
309
  description: 'Add a new entity',
152
310
  examples: [
153
311
  {
154
- expected: { id: '1', name: 'New' },
312
+ expectedMatch: { name: 'New' },
155
313
  input: { name: 'New' },
156
314
  name: 'Add entity',
157
315
  },
158
316
  ],
159
- implementation: (input) => {
160
- return Result.ok({ id: '1', name: input.name });
161
- },
162
317
  input: z.object({ name: z.string() }),
318
+ intent: 'write',
163
319
  output: entitySchema,
320
+ permit: { scopes: ['entity:write'] },
321
+ resources: [entityStore],
322
+ });
323
+
324
+ export const list = trail('entity.list', {
325
+ blaze: (_input, ctx) => {
326
+ const store = entityStore.from(ctx);
327
+ return Result.ok({ entities: store.list() });
328
+ },
329
+ description: 'List entities',
330
+ examples: [
331
+ {
332
+ expected: { entities: [{ id: '1', name: 'Example' }] },
333
+ input: {},
334
+ name: 'List entities',
335
+ },
336
+ ],
337
+ input: z.object({}),
338
+ intent: 'read',
339
+ output: z.object({
340
+ entities: z.array(entitySchema),
341
+ }),
342
+ resources: [entityStore],
343
+ });
344
+
345
+ export const remove = trail('entity.delete', {
346
+ blaze: (input, ctx) => {
347
+ const store = entityStore.from(ctx);
348
+ const deleted = store.delete(input.id);
349
+ return Result.ok({ deleted, id: input.id });
350
+ },
351
+ description: 'Delete an entity by ID',
352
+ examples: [
353
+ {
354
+ expected: { deleted: true, id: '1' },
355
+ input: { id: '1' },
356
+ name: 'Delete entity',
357
+ },
358
+ ],
359
+ input: z.object({ id: z.string() }),
360
+ intent: 'destroy',
361
+ output: z.object({
362
+ deleted: z.boolean(),
363
+ id: z.string(),
364
+ }),
365
+ permit: { scopes: ['entity:write'] },
366
+ resources: [entityStore],
164
367
  });
165
368
  `;
166
369
 
@@ -169,6 +372,9 @@ const generateSearchTrail = (): string =>
169
372
  import { z } from 'zod';
170
373
 
171
374
  export const search = trail('search', {
375
+ blaze: () => {
376
+ return Result.ok({ results: [] });
377
+ },
172
378
  description: 'Search entities by query',
173
379
  examples: [
174
380
  {
@@ -177,41 +383,40 @@ export const search = trail('search', {
177
383
  name: 'Search entities',
178
384
  },
179
385
  ],
180
- implementation: () => {
181
- return Result.ok({ results: [] });
182
- },
183
386
  input: z.object({ query: z.string() }),
387
+ intent: 'read',
184
388
  output: z.object({
185
389
  results: z.array(z.object({ id: z.string(), name: z.string() })),
186
390
  }),
187
- readOnly: true,
188
391
  });
189
392
  `;
190
393
 
191
- const generateOnboardHike = (): string =>
192
- `import { Result, hike } from '@ontrails/core';
394
+ const generateOnboardTrail = (): string =>
395
+ `import { Result, trail } from '@ontrails/core';
193
396
  import { z } from 'zod';
194
397
 
195
- export const onboard = hike('entity.onboard', {
196
- description: 'Onboard a new entity end-to-end',
197
- follows: ['entity.add'],
198
- implementation: async (input, ctx) => {
199
- const result = await ctx.follow('entity.add', { name: input.name });
398
+ export const onboard = trail('entity.onboard', {
399
+ blaze: async (input, ctx) => {
400
+ const result = await ctx.compose('entity.add', { name: input.name });
200
401
  if (result.isErr()) {
201
402
  return result;
202
403
  }
203
404
  return Result.ok({ onboarded: true });
204
405
  },
406
+ composes: ['entity.add'],
407
+ description: 'Onboard a new entity end-to-end',
205
408
  input: z.object({ name: z.string() }),
409
+ intent: 'write',
206
410
  output: z.object({ onboarded: z.boolean() }),
411
+ permit: { scopes: ['entity:write'] },
207
412
  });
208
413
  `;
209
414
 
210
- const generateEntityEvents = (): string =>
211
- `import { event } from '@ontrails/core';
415
+ const generateEntitySignals = (): string =>
416
+ `import { signal } from '@ontrails/core';
212
417
  import { z } from 'zod';
213
418
 
214
- export const entityUpdated = event('entity.updated', {
419
+ export const entityUpdated = signal('entity.updated', {
215
420
  description: 'Fired when an entity is updated',
216
421
  payload: z.object({
217
422
  entityId: z.string(),
@@ -221,21 +426,49 @@ export const entityUpdated = event('entity.updated', {
221
426
  `;
222
427
 
223
428
  const generateStore = (): string =>
224
- `/** In-memory store for entities. */
429
+ `import { Result, resource } from '@ontrails/core';
430
+
431
+ /** In-memory store for entities. */
225
432
 
226
- interface Entity {
433
+ export interface Entity {
227
434
  readonly id: string;
228
435
  readonly name: string;
229
436
  }
230
437
 
231
- const store = new Map<string, Entity>();
438
+ export interface EntityStore {
439
+ add(entity: Entity): void;
440
+ delete(id: string): boolean;
441
+ get(id: string): Entity | undefined;
442
+ list(): Entity[];
443
+ }
444
+
445
+ const defaultEntities: readonly Entity[] = [{ id: '1', name: 'Example' }];
232
446
 
233
- export const getEntity = (id: string): Entity | undefined => store.get(id);
234
- export const addEntity = (entity: Entity): void => {
235
- store.set(entity.id, entity);
447
+ export const createEntityStore = (
448
+ seed: readonly Entity[] = defaultEntities
449
+ ): EntityStore => {
450
+ const store = new Map(seed.map((entity) => [entity.id, entity] as const));
451
+ return {
452
+ add(entity) {
453
+ store.set(entity.id, entity);
454
+ },
455
+ delete(id) {
456
+ return store.delete(id);
457
+ },
458
+ get(id) {
459
+ return store.get(id);
460
+ },
461
+ list() {
462
+ return [...store.values()];
463
+ },
464
+ };
236
465
  };
237
- export const deleteEntity = (id: string): boolean => store.delete(id);
238
- export const listEntities = (): Entity[] => Array.from(store.values());
466
+
467
+ export const entityStore = resource('entity.store', {
468
+ create: () => Result.ok(createEntityStore()),
469
+ description: 'In-memory entity store for the entity starter.',
470
+ mock: createEntityStore,
471
+ });
239
472
  `;
240
473
 
241
474
  const starterImports: Record<
@@ -248,9 +481,10 @@ const starterImports: Record<
248
481
  "import * as entity from './trails/entity.js';",
249
482
  "import * as search from './trails/search.js';",
250
483
  "import * as onboard from './trails/onboard.js';",
251
- "import * as entityEvents from './events/entity-events.js';",
484
+ "import * as entitySignals from './signals/entity-signals.js';",
485
+ "import * as store from './store.js';",
252
486
  ],
253
- modules: ['entity', 'search', 'onboard', 'entityEvents'],
487
+ modules: ['entity', 'search', 'onboard', 'entitySignals', 'store'],
254
488
  },
255
489
  hello: {
256
490
  imports: ["import * as hello from './trails/hello.js';"],
@@ -258,16 +492,31 @@ const starterImports: Record<
258
492
  },
259
493
  };
260
494
 
495
+ const renderTopoExpression = (
496
+ appNameLiteral: string,
497
+ modules: readonly string[]
498
+ ): string => {
499
+ if (modules.length === 0) {
500
+ return `topo(${appNameLiteral})`;
501
+ }
502
+
503
+ if (modules.length === 1) {
504
+ return `topo(${appNameLiteral}, ${modules[0]})`;
505
+ }
506
+
507
+ return `topo(\n ${[appNameLiteral, ...modules].join(',\n ')}\n)`;
508
+ };
509
+
261
510
  const generateAppTs = (name: string, starter: Starter): string => {
262
511
  const { imports, modules } = starterImports[starter];
263
- const topoArgs =
264
- modules.length > 0 ? `'${name}', ${modules.join(', ')}` : `'${name}'`;
512
+ const appNameLiteral = `'${name}'`;
513
+ const topoExpression = renderTopoExpression(appNameLiteral, modules);
265
514
 
266
515
  return [
267
516
  "import { topo } from '@ontrails/core';",
268
517
  ...imports,
269
518
  '',
270
- `export const app = topo(${topoArgs});`,
519
+ `export const app = ${topoExpression};`,
271
520
  '',
272
521
  ].join('\n');
273
522
  };
@@ -281,8 +530,8 @@ const starterFileGenerators: Record<Starter, () => [string, string][]> = {
281
530
  entity: () => [
282
531
  ['src/trails/entity.ts', generateEntityTrails()],
283
532
  ['src/trails/search.ts', generateSearchTrail()],
284
- ['src/trails/onboard.ts', generateOnboardHike()],
285
- ['src/events/entity-events.ts', generateEntityEvents()],
533
+ ['src/trails/onboard.ts', generateOnboardTrail()],
534
+ ['src/signals/entity-signals.ts', generateEntitySignals()],
286
535
  ['src/store.ts', generateStore()],
287
536
  ],
288
537
  hello: () => [['src/trails/hello.ts', generateHelloTrail()]],
@@ -294,59 +543,105 @@ const collectScaffoldFiles = (
294
543
  ): Map<string, string> =>
295
544
  new Map([
296
545
  ['package.json', generatePackageJson(name)],
546
+ ['AGENTS.md', AGENTS_CONTENT],
547
+ ['CLAUDE.md', CLAUDE_CONTENT],
297
548
  ['tsconfig.json', TSCONFIG_CONTENT],
549
+ ['tsconfig.tests.json', TSCONFIG_TESTS_CONTENT],
298
550
  ['.gitignore', GITIGNORE_CONTENT],
299
- ['.oxlintrc.json', OXLINTRC_CONTENT],
551
+ ['oxlint.config.ts', OXLINT_CONFIG_CONTENT],
300
552
  ['.oxfmtrc.jsonc', OXFMTRC_CONTENT],
553
+ ['.trails/scaffold.json', generateScaffoldProvenance(starter)],
301
554
  ['src/app.ts', generateAppTs(name, starter)],
302
555
  ...starterFileGenerators[starter](),
303
556
  ]);
304
557
 
305
- const writeScaffoldFiles = async (
306
- projectDir: string,
558
+ const collectScaffoldOperations = (
307
559
  fileMap: Map<string, string>
308
- ): Promise<string[]> => {
309
- const files: string[] = [];
310
- for (const [relativePath, content] of fileMap) {
311
- const fullPath = join(projectDir, relativePath);
312
- mkdirSync(dirname(fullPath), { recursive: true });
313
- await Bun.write(fullPath, content);
314
- files.push(relativePath);
315
- }
316
- return files;
317
- };
560
+ ): ProjectWriteOperation[] =>
561
+ [...fileMap].map(([path, content]) => ({
562
+ content,
563
+ kind: 'write' as const,
564
+ path,
565
+ }));
318
566
 
319
567
  // ---------------------------------------------------------------------------
320
568
  // Trail definition
321
569
  // ---------------------------------------------------------------------------
322
570
 
323
571
  export const createScaffold = trail('create.scaffold', {
324
- description: 'Scaffold a new Trails project',
325
- implementation: async (input) => {
326
- const projectDir = resolve(input.dir ?? '.', input.name);
572
+ blaze: async (input) => {
573
+ const projectDirResult = resolveProjectDir(input.dir ?? '.', input.name);
574
+ if (projectDirResult.isErr()) {
575
+ return projectDirResult;
576
+ }
577
+
578
+ const projectDir = projectDirResult.value;
327
579
  const starter = (input.starter ?? 'hello') as Starter;
580
+ const dryRun = input.dryRun === true;
328
581
  const fileMap = collectScaffoldFiles(input.name, starter);
329
- const files = await writeScaffoldFiles(projectDir, fileMap);
330
- mkdirSync(join(projectDir, '.trails'), { recursive: true });
582
+ const operations = collectScaffoldOperations(fileMap);
583
+ const plannedOperations: TrailsResult<PlannedProjectOperation[], Error> =
584
+ dryRun
585
+ ? planProjectOperations(projectDir, operations, {
586
+ existing: 'preserve',
587
+ })
588
+ : await applyProjectOperations(projectDir, operations, {
589
+ existing: 'preserve',
590
+ });
591
+ if (plannedOperations.isErr()) {
592
+ return plannedOperations;
593
+ }
594
+
595
+ const created = dryRun
596
+ ? []
597
+ : plannedOperations.value
598
+ .filter((operation) => operation.kind === 'write')
599
+ .map((operation) => operation.path);
331
600
 
332
601
  return Result.ok({
333
- created: files,
334
- dir: projectDir,
602
+ created,
603
+ dir: resolve(projectDir),
604
+ dryRun,
335
605
  name: input.name,
606
+ plannedOperations: plannedOperations.value,
336
607
  } satisfies ScaffoldResult);
337
608
  },
609
+ description: 'Scaffold a new Trails project',
338
610
  input: z.object({
339
611
  dir: z.string().optional().describe('Parent directory'),
340
- name: z.string().describe('Project name'),
612
+ dryRun: z
613
+ .boolean()
614
+ .default(false)
615
+ .describe('Plan scaffold writes without touching the project directory'),
616
+ name: z
617
+ .string()
618
+ .regex(PROJECT_NAME_PATTERN, PROJECT_NAME_MESSAGE)
619
+ .describe('Project name'),
341
620
  starter: z
342
621
  .enum(['hello', 'entity', 'empty'])
343
622
  .default('hello')
344
623
  .describe('Starter trail'),
345
624
  }),
346
- markers: { internal: true },
625
+ intent: 'write',
347
626
  output: z.object({
348
- created: z.array(z.string()),
627
+ created: z
628
+ .array(z.string())
629
+ .describe('Project-relative paths of files written (empty in dry-run)'),
349
630
  dir: z.string(),
631
+ dryRun: z.boolean(),
350
632
  name: z.string(),
633
+ plannedOperations: z.array(
634
+ z.discriminatedUnion('kind', [
635
+ z.object({ kind: z.literal('mkdir'), path: z.string() }),
636
+ z.object({
637
+ from: z.string(),
638
+ kind: z.literal('rename'),
639
+ to: z.string(),
640
+ }),
641
+ z.object({ kind: z.literal('write'), path: z.string() }),
642
+ ])
643
+ ),
351
644
  }),
645
+ permit: { scopes: ['project:write'] },
646
+ visibility: 'internal',
352
647
  });