@ontrails/trails 1.0.0-beta.4 → 1.0.0-beta.42

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 (165) hide show
  1. package/CHANGELOG.md +1106 -4
  2. package/README.md +27 -0
  3. package/package.json +31 -7
  4. package/src/app.ts +147 -3
  5. package/src/cli.ts +345 -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 +91 -0
  12. package/src/mcp.ts +8 -0
  13. package/src/project-writes.ts +377 -0
  14. package/src/regrade/config.ts +152 -0
  15. package/src/regrade/history.ts +430 -0
  16. package/src/regrade/live-api-preserve.ts +8 -0
  17. package/src/regrade/plan-artifact.ts +285 -0
  18. package/src/release/bindings.ts +39 -0
  19. package/src/release/check.ts +844 -0
  20. package/src/release/config.ts +63 -0
  21. package/src/release/contract-facts.ts +425 -0
  22. package/src/release/index.ts +142 -0
  23. package/src/release/lock-roundtrip-smoke.ts +230 -0
  24. package/src/release/native-bun-publish.ts +651 -0
  25. package/src/release/native-bun-registry.ts +765 -0
  26. package/src/release/notes-cli.ts +171 -0
  27. package/src/release/notes.ts +390 -0
  28. package/src/release/pack-coherence.ts +455 -0
  29. package/src/release/packed-artifacts-smoke.ts +236 -0
  30. package/src/release/policy.ts +1687 -0
  31. package/src/release/semver.ts +104 -0
  32. package/src/release/smoke.ts +56 -0
  33. package/src/release/wayfinder-dogfood-smoke.ts +760 -0
  34. package/src/retired-topo-command.ts +36 -0
  35. package/src/run-adapter-check.ts +76 -0
  36. package/src/run-collision.ts +126 -0
  37. package/src/run-completions-install.ts +179 -0
  38. package/src/run-example.ts +149 -0
  39. package/src/run-examples.ts +148 -0
  40. package/src/run-quiet.ts +75 -0
  41. package/src/run-release-check.ts +74 -0
  42. package/src/run-schema.ts +74 -0
  43. package/src/run-trace.ts +273 -0
  44. package/src/run-warden.ts +39 -0
  45. package/src/run-watch.ts +432 -0
  46. package/src/run-wayfind-outline.ts +170 -0
  47. package/src/scaffold-version-sync.ts +183 -0
  48. package/src/scaffold-versions.generated.ts +12 -0
  49. package/src/trails/adapter-check.ts +244 -0
  50. package/src/trails/add-surface.ts +99 -45
  51. package/src/trails/add-trail.ts +84 -37
  52. package/src/trails/add-verify.ts +100 -30
  53. package/src/trails/compile.ts +58 -0
  54. package/src/trails/completions-complete.ts +165 -0
  55. package/src/trails/completions.ts +47 -0
  56. package/src/trails/create-adapter.ts +785 -0
  57. package/src/trails/create-scaffold.ts +401 -106
  58. package/src/trails/create-versions.ts +62 -0
  59. package/src/trails/create.ts +186 -72
  60. package/src/trails/deprecate.ts +59 -0
  61. package/src/trails/dev-clean.ts +82 -0
  62. package/src/trails/dev-reset.ts +50 -0
  63. package/src/trails/dev-stats.ts +72 -0
  64. package/src/trails/dev-support.ts +360 -0
  65. package/src/trails/doctor.ts +77 -0
  66. package/src/trails/draft-promote.ts +949 -0
  67. package/src/trails/guide.ts +70 -68
  68. package/src/trails/load-app.ts +1123 -15
  69. package/src/trails/operator-context.ts +66 -0
  70. package/src/trails/project.ts +17 -3
  71. package/src/trails/regrade.ts +3091 -0
  72. package/src/trails/release-check.ts +105 -0
  73. package/src/trails/release-smoke.ts +49 -0
  74. package/src/trails/revise.ts +53 -0
  75. package/src/trails/root-dir.ts +21 -0
  76. package/src/trails/run-example.ts +475 -0
  77. package/src/trails/run-examples.ts +129 -0
  78. package/src/trails/run.ts +434 -0
  79. package/src/trails/scaffold-json.ts +58 -0
  80. package/src/trails/survey.ts +877 -214
  81. package/src/trails/topo-activation.ts +14 -0
  82. package/src/trails/topo-constants.ts +2 -0
  83. package/src/trails/topo-history.ts +47 -0
  84. package/src/trails/topo-output-schemas.ts +259 -0
  85. package/src/trails/topo-pin.ts +38 -0
  86. package/src/trails/topo-read-support.ts +368 -0
  87. package/src/trails/topo-reports.ts +809 -0
  88. package/src/trails/topo-store-support.ts +325 -0
  89. package/src/trails/topo-support.ts +220 -0
  90. package/src/trails/topo-unpin.ts +61 -0
  91. package/src/trails/topo.ts +92 -0
  92. package/src/trails/validate.ts +27 -0
  93. package/src/trails/version-lifecycle-support.ts +936 -0
  94. package/src/trails/warden-guide.ts +134 -0
  95. package/src/trails/warden.ts +198 -58
  96. package/src/trails/wayfind-outline.ts +876 -0
  97. package/src/trails/wayfind.ts +1053 -0
  98. package/src/versions.ts +31 -0
  99. package/.turbo/turbo-build.log +0 -1
  100. package/.turbo/turbo-lint.log +0 -3
  101. package/.turbo/turbo-typecheck.log +0 -1
  102. package/__tests__/examples.test.ts +0 -6
  103. package/dist/bin/trails.d.ts +0 -3
  104. package/dist/bin/trails.d.ts.map +0 -1
  105. package/dist/bin/trails.js +0 -4
  106. package/dist/bin/trails.js.map +0 -1
  107. package/dist/src/app.d.ts +0 -2
  108. package/dist/src/app.d.ts.map +0 -1
  109. package/dist/src/app.js +0 -11
  110. package/dist/src/app.js.map +0 -1
  111. package/dist/src/clack.d.ts +0 -9
  112. package/dist/src/clack.d.ts.map +0 -1
  113. package/dist/src/clack.js +0 -84
  114. package/dist/src/clack.js.map +0 -1
  115. package/dist/src/cli.d.ts +0 -2
  116. package/dist/src/cli.d.ts.map +0 -1
  117. package/dist/src/cli.js +0 -13
  118. package/dist/src/cli.js.map +0 -1
  119. package/dist/src/trails/add-surface.d.ts +0 -13
  120. package/dist/src/trails/add-surface.d.ts.map +0 -1
  121. package/dist/src/trails/add-surface.js +0 -88
  122. package/dist/src/trails/add-surface.js.map +0 -1
  123. package/dist/src/trails/add-trail.d.ts +0 -10
  124. package/dist/src/trails/add-trail.d.ts.map +0 -1
  125. package/dist/src/trails/add-trail.js +0 -77
  126. package/dist/src/trails/add-trail.js.map +0 -1
  127. package/dist/src/trails/add-verify.d.ts +0 -10
  128. package/dist/src/trails/add-verify.d.ts.map +0 -1
  129. package/dist/src/trails/add-verify.js +0 -67
  130. package/dist/src/trails/add-verify.js.map +0 -1
  131. package/dist/src/trails/create-scaffold.d.ts +0 -15
  132. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  133. package/dist/src/trails/create-scaffold.js +0 -288
  134. package/dist/src/trails/create-scaffold.js.map +0 -1
  135. package/dist/src/trails/create.d.ts +0 -22
  136. package/dist/src/trails/create.d.ts.map +0 -1
  137. package/dist/src/trails/create.js +0 -121
  138. package/dist/src/trails/create.js.map +0 -1
  139. package/dist/src/trails/guide.d.ts +0 -11
  140. package/dist/src/trails/guide.d.ts.map +0 -1
  141. package/dist/src/trails/guide.js +0 -80
  142. package/dist/src/trails/guide.js.map +0 -1
  143. package/dist/src/trails/load-app.d.ts +0 -4
  144. package/dist/src/trails/load-app.d.ts.map +0 -1
  145. package/dist/src/trails/load-app.js +0 -24
  146. package/dist/src/trails/load-app.js.map +0 -1
  147. package/dist/src/trails/project.d.ts +0 -8
  148. package/dist/src/trails/project.d.ts.map +0 -1
  149. package/dist/src/trails/project.js +0 -43
  150. package/dist/src/trails/project.js.map +0 -1
  151. package/dist/src/trails/survey.d.ts +0 -31
  152. package/dist/src/trails/survey.d.ts.map +0 -1
  153. package/dist/src/trails/survey.js +0 -221
  154. package/dist/src/trails/survey.js.map +0 -1
  155. package/dist/src/trails/warden.d.ts +0 -19
  156. package/dist/src/trails/warden.d.ts.map +0 -1
  157. package/dist/src/trails/warden.js +0 -88
  158. package/dist/src/trails/warden.js.map +0 -1
  159. package/dist/tsconfig.tsbuildinfo +0 -1
  160. package/src/__tests__/create.test.ts +0 -349
  161. package/src/__tests__/guide.test.ts +0 -91
  162. package/src/__tests__/load-app.test.ts +0 -15
  163. package/src/__tests__/survey.test.ts +0 -159
  164. package/src/__tests__/warden.test.ts +0 -74
  165. package/tsconfig.json +0 -9
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Consolidated, append-only Regrade transition history. One file per
3
+ * transition at `.trails/regrade/history/<transition>.json`; each apply
4
+ * appends a run entry stamped with the plan content hash and the lock hash
5
+ * observed at that run.
6
+ */
7
+
8
+ import { InternalError, Result, ValidationError } from '@ontrails/core';
9
+ import type { Result as TrailsResult } from '@ontrails/core';
10
+ import { regradeReportOutput } from '@ontrails/regrade';
11
+ import type { RegradeReport } from '@ontrails/regrade';
12
+ import { createHash } from 'node:crypto';
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import { dirname, isAbsolute, join } from 'node:path';
15
+ import { z } from 'zod';
16
+
17
+ import {
18
+ regradePlanArtifactSchema,
19
+ regradePlanContentHash,
20
+ regradePlanDirectory,
21
+ regradePlanSlugForBody,
22
+ legacyRegradeSourceHash,
23
+ regradeSourceHash,
24
+ regradeSourceHashMatches,
25
+ regradeSourceHashes,
26
+ rootRelativePath,
27
+ } from './plan-artifact.js';
28
+ import type { RegradePlanArtifact, RegradePlanBody } from './plan-artifact.js';
29
+
30
+ /**
31
+ * Consolidated history schema version. Version 1 was the retired
32
+ * one-file-per-run shape whose filename carried the lock hash.
33
+ */
34
+ export const REGRADE_HISTORY_SCHEMA_VERSION = 2;
35
+
36
+ const rawCompletionReportHash = Symbol('rawCompletionReportHash');
37
+ const rawReportHash = Symbol('rawReportHash');
38
+ const rawCompletionReport = Symbol('rawCompletionReport');
39
+ const rawReport = Symbol('rawReport');
40
+
41
+ const regradeHistoryRunSchema = z
42
+ .object({
43
+ completionReport: regradeReportOutput
44
+ .optional()
45
+ .describe(
46
+ 'Post-apply source evidence used to recognize a completed-state replay'
47
+ ),
48
+ completionReportHash: z
49
+ .string()
50
+ .regex(/^[0-9a-f]{64}$/)
51
+ .optional()
52
+ .describe('Canonical hash of the post-apply completion report'),
53
+ lockHashAtRun: z
54
+ .string()
55
+ .min(1)
56
+ .describe('Regrade source hash observed when this run applied'),
57
+ plan: regradePlanArtifactSchema.describe(
58
+ 'Plan artifact consumed by this run'
59
+ ),
60
+ planContentHash: z
61
+ .string()
62
+ .min(1)
63
+ .describe('Canonical content hash of the resolved plan body'),
64
+ report: regradeReportOutput.describe('Applied report recorded by the run'),
65
+ })
66
+ .strict();
67
+
68
+ const regradeHistoryArtifactSchema = z
69
+ .object({
70
+ id: z.string().min(1).describe('Stable transition identity'),
71
+ kind: z.literal('regrade-history'),
72
+ path: z.string().describe('Root-relative consolidated history path'),
73
+ runs: z.array(regradeHistoryRunSchema).min(1),
74
+ schemaVersion: z.literal(REGRADE_HISTORY_SCHEMA_VERSION),
75
+ })
76
+ .strict();
77
+
78
+ interface RegradeHistoryRun {
79
+ readonly [rawCompletionReportHash]?: string;
80
+ readonly [rawCompletionReport]?: RegradeReport;
81
+ readonly [rawReportHash]?: string;
82
+ readonly [rawReport]?: RegradeReport;
83
+ readonly completionReport: RegradeReport;
84
+ readonly completionReportHash: string;
85
+ readonly lockHashAtRun: string;
86
+ readonly plan: RegradePlanArtifact;
87
+ readonly planContentHash: string;
88
+ readonly report: RegradeReport;
89
+ }
90
+
91
+ export interface RegradeHistoryArtifact {
92
+ readonly id: string;
93
+ readonly kind: 'regrade-history';
94
+ readonly path: string;
95
+ readonly runs: readonly RegradeHistoryRun[];
96
+ readonly schemaVersion: typeof REGRADE_HISTORY_SCHEMA_VERSION;
97
+ }
98
+
99
+ export interface RegradeHistorySummary {
100
+ readonly path: string;
101
+ readonly schemaVersion: number;
102
+ readonly status: 'applied' | 'replay';
103
+ }
104
+
105
+ export const regradeHistoryPathForPlan = (
106
+ rootDir: string,
107
+ plan: RegradePlanBody
108
+ ): string =>
109
+ join(
110
+ regradePlanDirectory(rootDir),
111
+ 'history',
112
+ `${regradePlanSlugForBody(plan)}.json`
113
+ );
114
+
115
+ export const readRegradeHistoryArtifact = (
116
+ path: string
117
+ ): TrailsResult<RegradeHistoryArtifact, InternalError | ValidationError> => {
118
+ if (!existsSync(path)) {
119
+ return Result.err(
120
+ new ValidationError(`Regrade history "${path}" not found.`)
121
+ );
122
+ }
123
+ let parsedJson: unknown;
124
+ try {
125
+ parsedJson = JSON.parse(readFileSync(path, 'utf8'));
126
+ } catch (error) {
127
+ return Result.err(
128
+ new InternalError('Failed to read Regrade history artifact.', {
129
+ ...(error instanceof Error ? { cause: error } : {}),
130
+ context: { path },
131
+ })
132
+ );
133
+ }
134
+ const parsed = regradeHistoryArtifactSchema.safeParse(parsedJson);
135
+ if (!parsed.success) {
136
+ return Result.err(
137
+ new ValidationError('Invalid Regrade history artifact.', {
138
+ context: { issues: parsed.error.issues, path },
139
+ })
140
+ );
141
+ }
142
+ const rawRuns = (
143
+ parsedJson as {
144
+ readonly runs: readonly {
145
+ readonly completionReport?: RegradeReport;
146
+ readonly report: RegradeReport;
147
+ }[];
148
+ }
149
+ ).runs;
150
+ return Result.ok({
151
+ ...parsed.data,
152
+ runs: parsed.data.runs.map((run, index) => {
153
+ // Early schema-v2 histories recorded only the post-apply report. Treat
154
+ // that report and its lock hash as completion evidence when reading
155
+ // those artifacts.
156
+ const completionReport = run.completionReport ?? run.report;
157
+ const normalized = {
158
+ ...run,
159
+ completionReport,
160
+ completionReportHash: run.completionReportHash ?? run.lockHashAtRun,
161
+ } as RegradeHistoryRun;
162
+ const rawRun = rawRuns[index];
163
+ if (rawRun !== undefined) {
164
+ Object.defineProperties(normalized, {
165
+ [rawCompletionReport]: {
166
+ value: rawRun.completionReport ?? rawRun.report,
167
+ },
168
+ [rawCompletionReportHash]: {
169
+ value: legacyRegradeSourceHash(
170
+ rawRun.completionReport ?? rawRun.report
171
+ ),
172
+ },
173
+ [rawReportHash]: {
174
+ value: legacyRegradeSourceHash(rawRun.report),
175
+ },
176
+ [rawReport]: { value: rawRun.report },
177
+ });
178
+ }
179
+ return normalized;
180
+ }),
181
+ } as RegradeHistoryArtifact);
182
+ };
183
+
184
+ /**
185
+ * Deterministic transition identity minted at the first recorded run and
186
+ * preserved for the life of the consolidated history file.
187
+ */
188
+ export const mintTransitionId = (
189
+ slug: string,
190
+ planContentHash: string,
191
+ lockHashAtRun: string
192
+ ): string =>
193
+ createHash('sha256')
194
+ .update(`${slug}\n${planContentHash}\n${lockHashAtRun}`)
195
+ .digest('hex')
196
+ .slice(0, 12);
197
+
198
+ /**
199
+ * Verify every recorded run at its own stamped lock: recompute the plan
200
+ * content hash and lock hash from the recorded plan and report, then compare
201
+ * with the stamped values.
202
+ */
203
+ export const verifyRegradeHistoryRuns = (
204
+ artifact: RegradeHistoryArtifact
205
+ ): TrailsResult<{ readonly runs: number }, ValidationError> => {
206
+ for (const [index, run] of artifact.runs.entries()) {
207
+ if (regradePlanContentHash(run.plan.plan) !== run.planContentHash) {
208
+ return Result.err(
209
+ new ValidationError('Regrade history run stamp mismatch.', {
210
+ context: {
211
+ field: 'planContentHash',
212
+ path: artifact.path,
213
+ run: index,
214
+ },
215
+ })
216
+ );
217
+ }
218
+ if (
219
+ !regradeSourceHashMatches(run.lockHashAtRun, run.report) &&
220
+ run[rawReportHash] !== run.lockHashAtRun
221
+ ) {
222
+ return Result.err(
223
+ new ValidationError('Regrade history run stamp mismatch.', {
224
+ context: { field: 'lockHashAtRun', path: artifact.path, run: index },
225
+ })
226
+ );
227
+ }
228
+ if (
229
+ !regradeSourceHashMatches(
230
+ run.completionReportHash,
231
+ run.completionReport
232
+ ) &&
233
+ run[rawCompletionReportHash] !== run.completionReportHash
234
+ ) {
235
+ return Result.err(
236
+ new ValidationError('Regrade history run stamp mismatch.', {
237
+ context: {
238
+ field: 'completionReportHash',
239
+ path: artifact.path,
240
+ run: index,
241
+ },
242
+ })
243
+ );
244
+ }
245
+ }
246
+ return Result.ok({ runs: artifact.runs.length });
247
+ };
248
+
249
+ /**
250
+ * Append one applied run to the transition's consolidated history file. A
251
+ * run whose plan content hash and source evidence equal either the last run's
252
+ * pre-apply report or completed state is a replay: nothing is written and
253
+ * `status: 'replay'` is surfaced instead of a duplicate record.
254
+ */
255
+ export const appendRegradeHistoryRun = (params: {
256
+ readonly artifact: RegradePlanArtifact;
257
+ readonly completedReport?: RegradeReport;
258
+ readonly report: RegradeReport;
259
+ readonly rootDir: string;
260
+ }): TrailsResult<RegradeHistorySummary, Error> => {
261
+ const absolutePath = regradeHistoryPathForPlan(
262
+ params.rootDir,
263
+ params.artifact.plan
264
+ );
265
+ const relativePath = rootRelativePath(params.rootDir, absolutePath);
266
+ const planContentHash = regradePlanContentHash(params.artifact.plan);
267
+ const lockHashAtRun = regradeSourceHash(params.report);
268
+ const currentSourceHashes = regradeSourceHashes(params.report);
269
+ const completionReport = params.completedReport ?? params.report;
270
+ const entry: RegradeHistoryRun = {
271
+ completionReport,
272
+ completionReportHash: regradeSourceHash(completionReport),
273
+ lockHashAtRun,
274
+ plan: params.artifact,
275
+ planContentHash,
276
+ report: params.report,
277
+ };
278
+
279
+ let prior: RegradeHistoryArtifact | undefined;
280
+ if (existsSync(absolutePath)) {
281
+ const existing = readRegradeHistoryArtifact(absolutePath);
282
+ if (existing.isErr()) {
283
+ return existing;
284
+ }
285
+ prior = existing.value;
286
+ const verified = verifyRegradeHistoryRuns(prior);
287
+ if (verified.isErr()) {
288
+ return verified;
289
+ }
290
+ if (
291
+ params.artifact.transitionId !== undefined &&
292
+ params.artifact.transitionId !== prior.id
293
+ ) {
294
+ return Result.err(
295
+ new ValidationError(
296
+ 'Regrade plan transition id mismatch — refusing to fork the consolidated history.',
297
+ {
298
+ context: {
299
+ history: prior.id,
300
+ path: relativePath,
301
+ plan: params.artifact.transitionId,
302
+ },
303
+ }
304
+ )
305
+ );
306
+ }
307
+ const lastRun = prior.runs.at(-1);
308
+ // A plan that carries the transition id (adjust round-trips, plan
309
+ // re-derivation) may evolve the plan identity on the same spine. A plan
310
+ // WITHOUT the id that disagrees with the recorded plan identity is a
311
+ // name collision, not a continuation — refuse instead of mixing runs
312
+ // from unrelated transitions into one history.
313
+ if (
314
+ params.artifact.transitionId === undefined &&
315
+ lastRun !== undefined &&
316
+ lastRun.plan.plan.id !== params.artifact.plan.id
317
+ ) {
318
+ return Result.err(
319
+ new ValidationError(
320
+ 'Regrade history already records a different plan identity under this transition name. Use `regrade adjust <transition>` to continue it, or pick a different plan name.',
321
+ {
322
+ context: {
323
+ history: lastRun.plan.plan.id,
324
+ path: relativePath,
325
+ plan: params.artifact.plan.id,
326
+ },
327
+ }
328
+ )
329
+ );
330
+ }
331
+ if (
332
+ lastRun !== undefined &&
333
+ lastRun.planContentHash === planContentHash &&
334
+ (currentSourceHashes.includes(lastRun.lockHashAtRun) ||
335
+ currentSourceHashes.includes(lastRun.completionReportHash))
336
+ ) {
337
+ return Result.ok({
338
+ path: relativePath,
339
+ schemaVersion: REGRADE_HISTORY_SCHEMA_VERSION,
340
+ status: 'replay',
341
+ });
342
+ }
343
+ }
344
+
345
+ const artifact: RegradeHistoryArtifact = {
346
+ id:
347
+ prior === undefined
348
+ ? (params.artifact.transitionId ??
349
+ mintTransitionId(
350
+ regradePlanSlugForBody(params.artifact.plan),
351
+ planContentHash,
352
+ lockHashAtRun
353
+ ))
354
+ : prior.id,
355
+ kind: 'regrade-history',
356
+ path: relativePath,
357
+ runs: prior === undefined ? [entry] : [...prior.runs, entry],
358
+ schemaVersion: REGRADE_HISTORY_SCHEMA_VERSION,
359
+ };
360
+ const writableArtifact = {
361
+ id: artifact.id,
362
+ kind: artifact.kind,
363
+ path: artifact.path,
364
+ runs: artifact.runs.map((run) => ({
365
+ completionReport: run[rawCompletionReport] ?? run.completionReport,
366
+ completionReportHash: run.completionReportHash,
367
+ lockHashAtRun: run.lockHashAtRun,
368
+ plan: run.plan,
369
+ planContentHash: run.planContentHash,
370
+ report: run[rawReport] ?? run.report,
371
+ })),
372
+ schemaVersion: artifact.schemaVersion,
373
+ };
374
+ const parsed = regradeHistoryArtifactSchema.safeParse(writableArtifact);
375
+ if (!parsed.success) {
376
+ return Result.err(
377
+ new ValidationError('Invalid Regrade history artifact.', {
378
+ context: { issues: parsed.error.issues, path: relativePath },
379
+ })
380
+ );
381
+ }
382
+ try {
383
+ mkdirSync(dirname(absolutePath), { recursive: true });
384
+ writeFileSync(
385
+ absolutePath,
386
+ `${JSON.stringify(writableArtifact, null, 2)}\n`
387
+ );
388
+ } catch (error) {
389
+ return Result.err(
390
+ new InternalError('Failed to write Regrade history entry.', {
391
+ ...(error instanceof Error ? { cause: error } : {}),
392
+ context: { path: relativePath },
393
+ })
394
+ );
395
+ }
396
+ return Result.ok({
397
+ path: relativePath,
398
+ schemaVersion: REGRADE_HISTORY_SCHEMA_VERSION,
399
+ status: 'applied',
400
+ });
401
+ };
402
+
403
+ const hasPathSeparator = (value: string): boolean =>
404
+ value.includes('/') || value.includes('\\');
405
+
406
+ /**
407
+ * Resolve a transition name (with or without a `.json` suffix) to its
408
+ * consolidated history file. Path references are rejected — graduated
409
+ * history lookups are by transition name only.
410
+ */
411
+ export const resolveRegradeHistoryPath = (
412
+ rootDir: string,
413
+ ref: string
414
+ ): TrailsResult<string, ValidationError> => {
415
+ if (hasPathSeparator(ref) || isAbsolute(ref)) {
416
+ return Result.err(
417
+ new ValidationError(
418
+ `Regrade history reference "${ref}" must be a transition name.`
419
+ )
420
+ );
421
+ }
422
+ const name = ref.endsWith('.json') ? ref.slice(0, -'.json'.length) : ref;
423
+ const path = join(regradePlanDirectory(rootDir), 'history', `${name}.json`);
424
+ if (!existsSync(path)) {
425
+ return Result.err(
426
+ new ValidationError(`No Regrade history for transition "${ref}" found.`)
427
+ );
428
+ }
429
+ return Result.ok(path);
430
+ };
@@ -0,0 +1,8 @@
1
+ import type {
2
+ VocabularyPreserveInventoryEntry,
3
+ VocabularyRegradePlan,
4
+ } from '@ontrails/regrade';
5
+
6
+ export const deriveLiveApiPreserveInventory = async (
7
+ _plan: VocabularyRegradePlan
8
+ ): Promise<readonly VocabularyPreserveInventoryEntry[]> => [];