@ontrails/trails 1.0.0-beta.5 → 1.0.0-beta.50

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 (175) hide show
  1. package/CHANGELOG.md +1213 -4
  2. package/README.md +29 -0
  3. package/package.json +35 -7
  4. package/src/app.ts +147 -3
  5. package/src/cli.ts +358 -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 +42 -0
  11. package/src/mcp-options.ts +92 -0
  12. package/src/mcp.ts +8 -0
  13. package/src/project-writes.ts +377 -0
  14. package/src/regrade/audit.ts +571 -0
  15. package/src/regrade/config.ts +152 -0
  16. package/src/regrade/history.ts +636 -0
  17. package/src/regrade/lifecycle.ts +76 -0
  18. package/src/regrade/live-api-preserve.ts +123 -0
  19. package/src/regrade/plan-artifact.ts +461 -0
  20. package/src/regrade/plan-derivation.ts +301 -0
  21. package/src/regrade/prepared-run.ts +259 -0
  22. package/src/regrade/receipt-history.ts +446 -0
  23. package/src/regrade/source-transaction.ts +185 -0
  24. package/src/release/bindings.ts +58 -0
  25. package/src/release/check.ts +1146 -0
  26. package/src/release/cli-bundle.ts +575 -0
  27. package/src/release/config.ts +73 -0
  28. package/src/release/contract-facts.ts +425 -0
  29. package/src/release/homebrew.ts +219 -0
  30. package/src/release/index.ts +180 -0
  31. package/src/release/lock-roundtrip-smoke.ts +230 -0
  32. package/src/release/native-bun-publish.ts +964 -0
  33. package/src/release/native-bun-registry.ts +830 -0
  34. package/src/release/notes-cli.ts +171 -0
  35. package/src/release/notes.ts +390 -0
  36. package/src/release/pack-coherence.ts +455 -0
  37. package/src/release/package-route-facts.ts +146 -0
  38. package/src/release/packed-artifacts-smoke.ts +236 -0
  39. package/src/release/policy.ts +1700 -0
  40. package/src/release/semver.ts +104 -0
  41. package/src/release/smoke.ts +56 -0
  42. package/src/release/wayfinder-dogfood-smoke.ts +760 -0
  43. package/src/retired-topo-command.ts +36 -0
  44. package/src/run-adapter-check.ts +76 -0
  45. package/src/run-collision.ts +126 -0
  46. package/src/run-completions-install.ts +179 -0
  47. package/src/run-example.ts +149 -0
  48. package/src/run-examples.ts +148 -0
  49. package/src/run-quiet.ts +75 -0
  50. package/src/run-regrade-progress.ts +47 -0
  51. package/src/run-release-check.ts +74 -0
  52. package/src/run-schema.ts +74 -0
  53. package/src/run-trace.ts +273 -0
  54. package/src/run-warden.ts +39 -0
  55. package/src/run-watch.ts +432 -0
  56. package/src/run-wayfind-outline.ts +170 -0
  57. package/src/scaffold-version-sync.ts +183 -0
  58. package/src/scaffold-versions.generated.ts +12 -0
  59. package/src/trails/adapter-check.ts +244 -0
  60. package/src/trails/add-surface.ts +99 -45
  61. package/src/trails/add-trail.ts +84 -37
  62. package/src/trails/add-verify.ts +100 -30
  63. package/src/trails/compile.ts +58 -0
  64. package/src/trails/completions-complete.ts +165 -0
  65. package/src/trails/completions.ts +47 -0
  66. package/src/trails/create-adapter.ts +785 -0
  67. package/src/trails/create-scaffold.ts +402 -106
  68. package/src/trails/create-versions.ts +62 -0
  69. package/src/trails/create.ts +186 -72
  70. package/src/trails/deprecate.ts +59 -0
  71. package/src/trails/dev-clean.ts +82 -0
  72. package/src/trails/dev-reset.ts +50 -0
  73. package/src/trails/dev-stats.ts +72 -0
  74. package/src/trails/dev-support.ts +360 -0
  75. package/src/trails/doctor.ts +77 -0
  76. package/src/trails/draft-promote.ts +949 -0
  77. package/src/trails/guide.ts +70 -68
  78. package/src/trails/load-app.ts +1123 -15
  79. package/src/trails/operator-context.ts +66 -0
  80. package/src/trails/project.ts +17 -3
  81. package/src/trails/regrade.ts +4497 -0
  82. package/src/trails/release-check.ts +113 -0
  83. package/src/trails/release-smoke.ts +49 -0
  84. package/src/trails/revise.ts +53 -0
  85. package/src/trails/root-dir.ts +21 -0
  86. package/src/trails/run-example.ts +475 -0
  87. package/src/trails/run-examples.ts +129 -0
  88. package/src/trails/run.ts +434 -0
  89. package/src/trails/scaffold-json.ts +58 -0
  90. package/src/trails/survey.ts +877 -214
  91. package/src/trails/topo-activation.ts +14 -0
  92. package/src/trails/topo-constants.ts +2 -0
  93. package/src/trails/topo-history.ts +47 -0
  94. package/src/trails/topo-output-schemas.ts +259 -0
  95. package/src/trails/topo-pin.ts +38 -0
  96. package/src/trails/topo-read-support.ts +368 -0
  97. package/src/trails/topo-reports.ts +809 -0
  98. package/src/trails/topo-store-support.ts +323 -0
  99. package/src/trails/topo-support.ts +228 -0
  100. package/src/trails/topo-unpin.ts +61 -0
  101. package/src/trails/topo.ts +92 -0
  102. package/src/trails/validate.ts +27 -0
  103. package/src/trails/version-lifecycle-support.ts +936 -0
  104. package/src/trails/warden-guide.ts +134 -0
  105. package/src/trails/warden.ts +198 -58
  106. package/src/trails/wayfind-outline.ts +876 -0
  107. package/src/trails/wayfind.ts +1053 -0
  108. package/src/versions.ts +31 -0
  109. package/.turbo/turbo-build.log +0 -1
  110. package/.turbo/turbo-lint.log +0 -3
  111. package/.turbo/turbo-typecheck.log +0 -1
  112. package/__tests__/examples.test.ts +0 -6
  113. package/dist/bin/trails.d.ts +0 -3
  114. package/dist/bin/trails.d.ts.map +0 -1
  115. package/dist/bin/trails.js +0 -4
  116. package/dist/bin/trails.js.map +0 -1
  117. package/dist/src/app.d.ts +0 -2
  118. package/dist/src/app.d.ts.map +0 -1
  119. package/dist/src/app.js +0 -11
  120. package/dist/src/app.js.map +0 -1
  121. package/dist/src/clack.d.ts +0 -9
  122. package/dist/src/clack.d.ts.map +0 -1
  123. package/dist/src/clack.js +0 -84
  124. package/dist/src/clack.js.map +0 -1
  125. package/dist/src/cli.d.ts +0 -2
  126. package/dist/src/cli.d.ts.map +0 -1
  127. package/dist/src/cli.js +0 -13
  128. package/dist/src/cli.js.map +0 -1
  129. package/dist/src/trails/add-surface.d.ts +0 -13
  130. package/dist/src/trails/add-surface.d.ts.map +0 -1
  131. package/dist/src/trails/add-surface.js +0 -88
  132. package/dist/src/trails/add-surface.js.map +0 -1
  133. package/dist/src/trails/add-trail.d.ts +0 -10
  134. package/dist/src/trails/add-trail.d.ts.map +0 -1
  135. package/dist/src/trails/add-trail.js +0 -77
  136. package/dist/src/trails/add-trail.js.map +0 -1
  137. package/dist/src/trails/add-verify.d.ts +0 -10
  138. package/dist/src/trails/add-verify.d.ts.map +0 -1
  139. package/dist/src/trails/add-verify.js +0 -67
  140. package/dist/src/trails/add-verify.js.map +0 -1
  141. package/dist/src/trails/create-scaffold.d.ts +0 -15
  142. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  143. package/dist/src/trails/create-scaffold.js +0 -288
  144. package/dist/src/trails/create-scaffold.js.map +0 -1
  145. package/dist/src/trails/create.d.ts +0 -22
  146. package/dist/src/trails/create.d.ts.map +0 -1
  147. package/dist/src/trails/create.js +0 -121
  148. package/dist/src/trails/create.js.map +0 -1
  149. package/dist/src/trails/guide.d.ts +0 -11
  150. package/dist/src/trails/guide.d.ts.map +0 -1
  151. package/dist/src/trails/guide.js +0 -80
  152. package/dist/src/trails/guide.js.map +0 -1
  153. package/dist/src/trails/load-app.d.ts +0 -4
  154. package/dist/src/trails/load-app.d.ts.map +0 -1
  155. package/dist/src/trails/load-app.js +0 -24
  156. package/dist/src/trails/load-app.js.map +0 -1
  157. package/dist/src/trails/project.d.ts +0 -8
  158. package/dist/src/trails/project.d.ts.map +0 -1
  159. package/dist/src/trails/project.js +0 -43
  160. package/dist/src/trails/project.js.map +0 -1
  161. package/dist/src/trails/survey.d.ts +0 -31
  162. package/dist/src/trails/survey.d.ts.map +0 -1
  163. package/dist/src/trails/survey.js +0 -221
  164. package/dist/src/trails/survey.js.map +0 -1
  165. package/dist/src/trails/warden.d.ts +0 -19
  166. package/dist/src/trails/warden.d.ts.map +0 -1
  167. package/dist/src/trails/warden.js +0 -88
  168. package/dist/src/trails/warden.js.map +0 -1
  169. package/dist/tsconfig.tsbuildinfo +0 -1
  170. package/src/__tests__/create.test.ts +0 -349
  171. package/src/__tests__/guide.test.ts +0 -91
  172. package/src/__tests__/load-app.test.ts +0 -15
  173. package/src/__tests__/survey.test.ts +0 -159
  174. package/src/__tests__/warden.test.ts +0 -74
  175. 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,195 @@ 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
127
+ },
128
+ "include": ["src"]
129
+ }
130
+ `;
131
+
132
+ const TSCONFIG_TESTS_CONTENT = `{
133
+ "compilerOptions": {
134
+ "noEmit": true,
135
+ "rootDir": ".",
136
+ "types": ["bun"]
68
137
  },
69
- null,
70
- 2
71
- );
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
+ - \`implementation\`, 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
+ - Implementations 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 implementations.
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,
231
+ "ignorePatterns": ["**/.trails/regrade/history/**"],
89
232
  }
90
233
  `;
91
234
 
@@ -94,6 +237,10 @@ const generateHelloTrail = (): string =>
94
237
  import { z } from 'zod';
95
238
 
96
239
  export const hello = trail('hello', {
240
+ implementation: (input) => {
241
+ const name = input.name ?? 'world';
242
+ return Result.ok({ message: \`Hello, \${name}!\` });
243
+ },
97
244
  description: 'Say hello',
98
245
  examples: [
99
246
  {
@@ -107,30 +254,38 @@ export const hello = trail('hello', {
107
254
  name: 'Named greeting',
108
255
  },
109
256
  ],
110
- run: (input) => {
111
- const name = input.name ?? 'world';
112
- return Result.ok({ message: \`Hello, \${name}!\` });
113
- },
114
257
  input: z.object({
115
258
  name: z.string().optional(),
116
259
  }),
260
+ intent: 'read',
117
261
  output: z.object({
118
262
  message: z.string(),
119
263
  }),
120
- intent: 'read',
121
264
  });
122
265
  `;
123
266
 
124
267
  const generateEntityTrails = (): string =>
125
- `import { Result, trail } from '@ontrails/core';
268
+ `import { randomUUID } from 'node:crypto';
269
+
270
+ import { NotFoundError, Result, trail } from '@ontrails/core';
126
271
  import { z } from 'zod';
127
272
 
273
+ import { entityStore } from '../store.js';
274
+
128
275
  const entitySchema = z.object({
129
276
  id: z.string(),
130
277
  name: z.string(),
131
278
  });
132
279
 
133
280
  export const show = trail('entity.show', {
281
+ implementation: (input, ctx) => {
282
+ const store = entityStore.from(ctx);
283
+ const entity = store.get(input.id);
284
+ if (!entity) {
285
+ return Result.err(new NotFoundError(\`Entity "\${input.id}" not found\`));
286
+ }
287
+ return Result.ok(entity);
288
+ },
134
289
  description: 'Show an entity by ID',
135
290
  examples: [
136
291
  {
@@ -139,28 +294,77 @@ export const show = trail('entity.show', {
139
294
  name: 'Show entity',
140
295
  },
141
296
  ],
142
- run: (input) => {
143
- return Result.ok({ id: input.id, name: 'Example' });
144
- },
145
297
  input: z.object({ id: z.string() }),
146
- output: entitySchema,
147
298
  intent: 'read',
299
+ output: entitySchema,
300
+ resources: [entityStore],
148
301
  });
149
302
 
150
303
  export const add = trail('entity.add', {
304
+ implementation: (input, ctx) => {
305
+ const store = entityStore.from(ctx);
306
+ const entity = { id: randomUUID(), name: input.name };
307
+ store.add(entity);
308
+ return Result.ok(entity);
309
+ },
151
310
  description: 'Add a new entity',
152
311
  examples: [
153
312
  {
154
- expected: { id: '1', name: 'New' },
313
+ expectedMatch: { name: 'New' },
155
314
  input: { name: 'New' },
156
315
  name: 'Add entity',
157
316
  },
158
317
  ],
159
- run: (input) => {
160
- return Result.ok({ id: '1', name: input.name });
161
- },
162
318
  input: z.object({ name: z.string() }),
319
+ intent: 'write',
163
320
  output: entitySchema,
321
+ permit: { scopes: ['entity:write'] },
322
+ resources: [entityStore],
323
+ });
324
+
325
+ export const list = trail('entity.list', {
326
+ implementation: (_input, ctx) => {
327
+ const store = entityStore.from(ctx);
328
+ return Result.ok({ entities: store.list() });
329
+ },
330
+ description: 'List entities',
331
+ examples: [
332
+ {
333
+ expected: { entities: [{ id: '1', name: 'Example' }] },
334
+ input: {},
335
+ name: 'List entities',
336
+ },
337
+ ],
338
+ input: z.object({}),
339
+ intent: 'read',
340
+ output: z.object({
341
+ entities: z.array(entitySchema),
342
+ }),
343
+ resources: [entityStore],
344
+ });
345
+
346
+ export const remove = trail('entity.delete', {
347
+ implementation: (input, ctx) => {
348
+ const store = entityStore.from(ctx);
349
+ const deleted = store.delete(input.id);
350
+ return Result.ok({ deleted, id: input.id });
351
+ },
352
+ description: 'Delete an entity by ID',
353
+ examples: [
354
+ {
355
+ expected: { deleted: true, id: '1' },
356
+ input: { id: '1' },
357
+ name: 'Delete entity',
358
+ },
359
+ ],
360
+ input: z.object({ id: z.string() }),
361
+ intent: 'destroy',
362
+ output: z.object({
363
+ deleted: z.boolean(),
364
+ id: z.string(),
365
+ }),
366
+ permit: { scopes: ['entity:write'] },
367
+ resources: [entityStore],
164
368
  });
165
369
  `;
166
370
 
@@ -169,6 +373,9 @@ const generateSearchTrail = (): string =>
169
373
  import { z } from 'zod';
170
374
 
171
375
  export const search = trail('search', {
376
+ implementation: () => {
377
+ return Result.ok({ results: [] });
378
+ },
172
379
  description: 'Search entities by query',
173
380
  examples: [
174
381
  {
@@ -177,14 +384,11 @@ export const search = trail('search', {
177
384
  name: 'Search entities',
178
385
  },
179
386
  ],
180
- run: () => {
181
- return Result.ok({ results: [] });
182
- },
183
387
  input: z.object({ query: z.string() }),
388
+ intent: 'read',
184
389
  output: z.object({
185
390
  results: z.array(z.object({ id: z.string(), name: z.string() })),
186
391
  }),
187
- intent: 'read',
188
392
  });
189
393
  `;
190
394
 
@@ -193,25 +397,27 @@ const generateOnboardTrail = (): string =>
193
397
  import { z } from 'zod';
194
398
 
195
399
  export const onboard = trail('entity.onboard', {
196
- description: 'Onboard a new entity end-to-end',
197
- follow: ['entity.add'],
198
- run: async (input, ctx) => {
199
- const result = await ctx.follow('entity.add', { name: input.name });
400
+ implementation: async (input, ctx) => {
401
+ const result = await ctx.compose('entity.add', { name: input.name });
200
402
  if (result.isErr()) {
201
403
  return result;
202
404
  }
203
405
  return Result.ok({ onboarded: true });
204
406
  },
407
+ composes: ['entity.add'],
408
+ description: 'Onboard a new entity end-to-end',
205
409
  input: z.object({ name: z.string() }),
410
+ intent: 'write',
206
411
  output: z.object({ onboarded: z.boolean() }),
412
+ permit: { scopes: ['entity:write'] },
207
413
  });
208
414
  `;
209
415
 
210
- const generateEntityEvents = (): string =>
211
- `import { event } from '@ontrails/core';
416
+ const generateEntitySignals = (): string =>
417
+ `import { signal } from '@ontrails/core';
212
418
  import { z } from 'zod';
213
419
 
214
- export const entityUpdated = event('entity.updated', {
420
+ export const entityUpdated = signal('entity.updated', {
215
421
  description: 'Fired when an entity is updated',
216
422
  payload: z.object({
217
423
  entityId: z.string(),
@@ -221,21 +427,49 @@ export const entityUpdated = event('entity.updated', {
221
427
  `;
222
428
 
223
429
  const generateStore = (): string =>
224
- `/** In-memory store for entities. */
430
+ `import { Result, resource } from '@ontrails/core';
431
+
432
+ /** In-memory store for entities. */
225
433
 
226
- interface Entity {
434
+ export interface Entity {
227
435
  readonly id: string;
228
436
  readonly name: string;
229
437
  }
230
438
 
231
- const store = new Map<string, Entity>();
439
+ export interface EntityStore {
440
+ add(entity: Entity): void;
441
+ delete(id: string): boolean;
442
+ get(id: string): Entity | undefined;
443
+ list(): Entity[];
444
+ }
445
+
446
+ const defaultEntities: readonly Entity[] = [{ id: '1', name: 'Example' }];
232
447
 
233
- export const getEntity = (id: string): Entity | undefined => store.get(id);
234
- export const addEntity = (entity: Entity): void => {
235
- store.set(entity.id, entity);
448
+ export const createEntityStore = (
449
+ seed: readonly Entity[] = defaultEntities
450
+ ): EntityStore => {
451
+ const store = new Map(seed.map((entity) => [entity.id, entity] as const));
452
+ return {
453
+ add(entity) {
454
+ store.set(entity.id, entity);
455
+ },
456
+ delete(id) {
457
+ return store.delete(id);
458
+ },
459
+ get(id) {
460
+ return store.get(id);
461
+ },
462
+ list() {
463
+ return [...store.values()];
464
+ },
465
+ };
236
466
  };
237
- export const deleteEntity = (id: string): boolean => store.delete(id);
238
- export const listEntities = (): Entity[] => Array.from(store.values());
467
+
468
+ export const entityStore = resource('entity.store', {
469
+ create: () => Result.ok(createEntityStore()),
470
+ description: 'In-memory entity store for the entity starter.',
471
+ mock: createEntityStore,
472
+ });
239
473
  `;
240
474
 
241
475
  const starterImports: Record<
@@ -248,9 +482,10 @@ const starterImports: Record<
248
482
  "import * as entity from './trails/entity.js';",
249
483
  "import * as search from './trails/search.js';",
250
484
  "import * as onboard from './trails/onboard.js';",
251
- "import * as entityEvents from './events/entity-events.js';",
485
+ "import * as entitySignals from './signals/entity-signals.js';",
486
+ "import * as store from './store.js';",
252
487
  ],
253
- modules: ['entity', 'search', 'onboard', 'entityEvents'],
488
+ modules: ['entity', 'search', 'onboard', 'entitySignals', 'store'],
254
489
  },
255
490
  hello: {
256
491
  imports: ["import * as hello from './trails/hello.js';"],
@@ -258,16 +493,31 @@ const starterImports: Record<
258
493
  },
259
494
  };
260
495
 
496
+ const renderTopoExpression = (
497
+ appNameLiteral: string,
498
+ modules: readonly string[]
499
+ ): string => {
500
+ if (modules.length === 0) {
501
+ return `topo(${appNameLiteral})`;
502
+ }
503
+
504
+ if (modules.length === 1) {
505
+ return `topo(${appNameLiteral}, ${modules[0]})`;
506
+ }
507
+
508
+ return `topo(\n ${[appNameLiteral, ...modules].join(',\n ')}\n)`;
509
+ };
510
+
261
511
  const generateAppTs = (name: string, starter: Starter): string => {
262
512
  const { imports, modules } = starterImports[starter];
263
- const topoArgs =
264
- modules.length > 0 ? `'${name}', ${modules.join(', ')}` : `'${name}'`;
513
+ const appNameLiteral = `'${name}'`;
514
+ const topoExpression = renderTopoExpression(appNameLiteral, modules);
265
515
 
266
516
  return [
267
517
  "import { topo } from '@ontrails/core';",
268
518
  ...imports,
269
519
  '',
270
- `export const app = topo(${topoArgs});`,
520
+ `export const app = ${topoExpression};`,
271
521
  '',
272
522
  ].join('\n');
273
523
  };
@@ -282,7 +532,7 @@ const starterFileGenerators: Record<Starter, () => [string, string][]> = {
282
532
  ['src/trails/entity.ts', generateEntityTrails()],
283
533
  ['src/trails/search.ts', generateSearchTrail()],
284
534
  ['src/trails/onboard.ts', generateOnboardTrail()],
285
- ['src/events/entity-events.ts', generateEntityEvents()],
535
+ ['src/signals/entity-signals.ts', generateEntitySignals()],
286
536
  ['src/store.ts', generateStore()],
287
537
  ],
288
538
  hello: () => [['src/trails/hello.ts', generateHelloTrail()]],
@@ -294,27 +544,26 @@ const collectScaffoldFiles = (
294
544
  ): Map<string, string> =>
295
545
  new Map([
296
546
  ['package.json', generatePackageJson(name)],
547
+ ['AGENTS.md', AGENTS_CONTENT],
548
+ ['CLAUDE.md', CLAUDE_CONTENT],
297
549
  ['tsconfig.json', TSCONFIG_CONTENT],
550
+ ['tsconfig.tests.json', TSCONFIG_TESTS_CONTENT],
298
551
  ['.gitignore', GITIGNORE_CONTENT],
299
- ['.oxlintrc.json', OXLINTRC_CONTENT],
552
+ ['oxlint.config.ts', OXLINT_CONFIG_CONTENT],
300
553
  ['.oxfmtrc.jsonc', OXFMTRC_CONTENT],
554
+ ['.trails/scaffold.json', generateScaffoldProvenance(starter)],
301
555
  ['src/app.ts', generateAppTs(name, starter)],
302
556
  ...starterFileGenerators[starter](),
303
557
  ]);
304
558
 
305
- const writeScaffoldFiles = async (
306
- projectDir: string,
559
+ const collectScaffoldOperations = (
307
560
  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
- };
561
+ ): ProjectWriteOperation[] =>
562
+ [...fileMap].map(([path, content]) => ({
563
+ content,
564
+ kind: 'write' as const,
565
+ path,
566
+ }));
318
567
 
319
568
  // ---------------------------------------------------------------------------
320
569
  // Trail definition
@@ -322,31 +571,78 @@ const writeScaffoldFiles = async (
322
571
 
323
572
  export const createScaffold = trail('create.scaffold', {
324
573
  description: 'Scaffold a new Trails project',
574
+ implementation: async (input) => {
575
+ const projectDirResult = resolveProjectDir(input.dir ?? '.', input.name);
576
+ if (projectDirResult.isErr()) {
577
+ return projectDirResult;
578
+ }
579
+
580
+ const projectDir = projectDirResult.value;
581
+ const starter = (input.starter ?? 'hello') as Starter;
582
+ const dryRun = input.dryRun === true;
583
+ const fileMap = collectScaffoldFiles(input.name, starter);
584
+ const operations = collectScaffoldOperations(fileMap);
585
+ const plannedOperations: TrailsResult<PlannedProjectOperation[], Error> =
586
+ dryRun
587
+ ? planProjectOperations(projectDir, operations, {
588
+ existing: 'preserve',
589
+ })
590
+ : await applyProjectOperations(projectDir, operations, {
591
+ existing: 'preserve',
592
+ });
593
+ if (plannedOperations.isErr()) {
594
+ return plannedOperations;
595
+ }
596
+
597
+ const created = dryRun
598
+ ? []
599
+ : plannedOperations.value
600
+ .filter((operation) => operation.kind === 'write')
601
+ .map((operation) => operation.path);
602
+
603
+ return Result.ok({
604
+ created,
605
+ dir: resolve(projectDir),
606
+ dryRun,
607
+ name: input.name,
608
+ plannedOperations: plannedOperations.value,
609
+ } satisfies ScaffoldResult);
610
+ },
325
611
  input: z.object({
326
612
  dir: z.string().optional().describe('Parent directory'),
327
- name: z.string().describe('Project name'),
613
+ dryRun: z
614
+ .boolean()
615
+ .default(false)
616
+ .describe('Plan scaffold writes without touching the project directory'),
617
+ name: z
618
+ .string()
619
+ .regex(PROJECT_NAME_PATTERN, PROJECT_NAME_MESSAGE)
620
+ .describe('Project name'),
328
621
  starter: z
329
622
  .enum(['hello', 'entity', 'empty'])
330
623
  .default('hello')
331
624
  .describe('Starter trail'),
332
625
  }),
333
- metadata: { internal: true },
626
+ intent: 'write',
334
627
  output: z.object({
335
- created: z.array(z.string()),
628
+ created: z
629
+ .array(z.string())
630
+ .describe('Project-relative paths of files written (empty in dry-run)'),
336
631
  dir: z.string(),
632
+ dryRun: z.boolean(),
337
633
  name: z.string(),
634
+ plannedOperations: z.array(
635
+ z.discriminatedUnion('kind', [
636
+ z.object({ kind: z.literal('mkdir'), path: z.string() }),
637
+ z.object({
638
+ from: z.string(),
639
+ kind: z.literal('rename'),
640
+ to: z.string(),
641
+ }),
642
+ z.object({ kind: z.literal('write'), path: z.string() }),
643
+ ])
644
+ ),
338
645
  }),
339
- run: async (input) => {
340
- const projectDir = resolve(input.dir ?? '.', input.name);
341
- const starter = (input.starter ?? 'hello') as Starter;
342
- const fileMap = collectScaffoldFiles(input.name, starter);
343
- const files = await writeScaffoldFiles(projectDir, fileMap);
344
- mkdirSync(join(projectDir, '.trails'), { recursive: true });
345
-
346
- return Result.ok({
347
- created: files,
348
- dir: projectDir,
349
- name: input.name,
350
- } satisfies ScaffoldResult);
351
- },
646
+ permit: { scopes: ['project:write'] },
647
+ visibility: 'internal',
352
648
  });