@ontrails/trails 1.0.0-beta.30 → 1.0.0-beta.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +153 -0
- package/README.md +3 -3
- package/package.json +17 -16
- package/src/app.ts +27 -6
- package/src/cli.ts +3 -2
- package/src/mcp-app.ts +2 -0
- package/src/mcp-options.ts +20 -5
- package/src/regrade/config.ts +152 -0
- package/src/regrade/history.ts +320 -0
- package/src/regrade/live-api-preserve.ts +8 -0
- package/src/regrade/plan-artifact.ts +265 -0
- package/src/release/index.ts +17 -0
- package/src/release/lock-roundtrip-smoke.ts +230 -0
- package/src/release/notes-cli.ts +171 -0
- package/src/release/notes.ts +390 -0
- package/src/release/pack-coherence.ts +1 -3
- package/src/release/smoke.ts +11 -1
- package/src/release/wayfinder-dogfood-smoke.ts +1 -1
- package/src/run-schema.ts +34 -1
- package/src/trails/compile.ts +4 -5
- package/src/trails/create-adapter.ts +19 -79
- package/src/trails/deprecate.ts +1 -1
- package/src/trails/guide.ts +5 -5
- package/src/trails/load-app.ts +47 -116
- package/src/trails/regrade.ts +3017 -27
- package/src/trails/release-smoke.ts +2 -1
- package/src/trails/revise.ts +1 -1
- package/src/trails/survey.ts +37 -50
- package/src/trails/topo-activation.ts +14 -385
- package/src/trails/topo-read-support.ts +28 -24
- package/src/trails/topo-store-support.ts +21 -12
- package/src/trails/topo-support.ts +1 -0
- package/src/trails/topo.ts +2 -2
- package/src/trails/validate.ts +1 -1
- package/src/trails/version-lifecycle-support.ts +11 -11
- package/src/trails/warden-guide.ts +9 -4
- package/src/trails/warden.ts +15 -0
- package/src/trails/wayfind.ts +64 -12
|
@@ -0,0 +1,320 @@
|
|
|
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
|
+
regradeSourceHash,
|
|
23
|
+
rootRelativePath,
|
|
24
|
+
} from './plan-artifact.js';
|
|
25
|
+
import type { RegradePlanArtifact, RegradePlanBody } from './plan-artifact.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Consolidated history schema version. Version 1 was the retired
|
|
29
|
+
* one-file-per-run shape whose filename carried the lock hash.
|
|
30
|
+
*/
|
|
31
|
+
export const REGRADE_HISTORY_SCHEMA_VERSION = 2;
|
|
32
|
+
|
|
33
|
+
const regradeHistoryRunSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
lockHashAtRun: z
|
|
36
|
+
.string()
|
|
37
|
+
.min(1)
|
|
38
|
+
.describe('Regrade source hash observed when this run applied'),
|
|
39
|
+
plan: regradePlanArtifactSchema.describe(
|
|
40
|
+
'Plan artifact consumed by this run'
|
|
41
|
+
),
|
|
42
|
+
planContentHash: z
|
|
43
|
+
.string()
|
|
44
|
+
.min(1)
|
|
45
|
+
.describe('Canonical content hash of the resolved plan body'),
|
|
46
|
+
report: regradeReportOutput.describe('Applied report recorded by the run'),
|
|
47
|
+
})
|
|
48
|
+
.strict();
|
|
49
|
+
|
|
50
|
+
const regradeHistoryArtifactSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
id: z.string().min(1).describe('Stable transition identity'),
|
|
53
|
+
kind: z.literal('regrade-history'),
|
|
54
|
+
path: z.string().describe('Root-relative consolidated history path'),
|
|
55
|
+
runs: z.array(regradeHistoryRunSchema).min(1),
|
|
56
|
+
schemaVersion: z.literal(REGRADE_HISTORY_SCHEMA_VERSION),
|
|
57
|
+
})
|
|
58
|
+
.strict();
|
|
59
|
+
|
|
60
|
+
interface RegradeHistoryRun {
|
|
61
|
+
readonly lockHashAtRun: string;
|
|
62
|
+
readonly plan: RegradePlanArtifact;
|
|
63
|
+
readonly planContentHash: string;
|
|
64
|
+
readonly report: RegradeReport;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RegradeHistoryArtifact {
|
|
68
|
+
readonly id: string;
|
|
69
|
+
readonly kind: 'regrade-history';
|
|
70
|
+
readonly path: string;
|
|
71
|
+
readonly runs: readonly RegradeHistoryRun[];
|
|
72
|
+
readonly schemaVersion: typeof REGRADE_HISTORY_SCHEMA_VERSION;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface RegradeHistorySummary {
|
|
76
|
+
readonly path: string;
|
|
77
|
+
readonly schemaVersion: number;
|
|
78
|
+
readonly status: 'applied' | 'replay';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const regradeHistoryPathForPlan = (
|
|
82
|
+
rootDir: string,
|
|
83
|
+
plan: RegradePlanBody
|
|
84
|
+
): string =>
|
|
85
|
+
join(
|
|
86
|
+
regradePlanDirectory(rootDir),
|
|
87
|
+
'history',
|
|
88
|
+
`${regradePlanSlugForBody(plan)}.json`
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
export const readRegradeHistoryArtifact = (
|
|
92
|
+
path: string
|
|
93
|
+
): TrailsResult<RegradeHistoryArtifact, InternalError | ValidationError> => {
|
|
94
|
+
if (!existsSync(path)) {
|
|
95
|
+
return Result.err(
|
|
96
|
+
new ValidationError(`Regrade history "${path}" not found.`)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
let parsedJson: unknown;
|
|
100
|
+
try {
|
|
101
|
+
parsedJson = JSON.parse(readFileSync(path, 'utf8'));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return Result.err(
|
|
104
|
+
new InternalError('Failed to read Regrade history artifact.', {
|
|
105
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
106
|
+
context: { path },
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const parsed = regradeHistoryArtifactSchema.safeParse(parsedJson);
|
|
111
|
+
if (!parsed.success) {
|
|
112
|
+
return Result.err(
|
|
113
|
+
new ValidationError('Invalid Regrade history artifact.', {
|
|
114
|
+
context: { issues: parsed.error.issues, path },
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return Result.ok(parsed.data as unknown as RegradeHistoryArtifact);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Deterministic transition identity minted at the first recorded run and
|
|
123
|
+
* preserved for the life of the consolidated history file.
|
|
124
|
+
*/
|
|
125
|
+
export const mintTransitionId = (
|
|
126
|
+
slug: string,
|
|
127
|
+
planContentHash: string,
|
|
128
|
+
lockHashAtRun: string
|
|
129
|
+
): string =>
|
|
130
|
+
createHash('sha256')
|
|
131
|
+
.update(`${slug}\n${planContentHash}\n${lockHashAtRun}`)
|
|
132
|
+
.digest('hex')
|
|
133
|
+
.slice(0, 12);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Append one applied run to the transition's consolidated history file. A
|
|
137
|
+
* run whose plan content hash and lock hash both equal the last recorded
|
|
138
|
+
* run's stamps is a replay: nothing is written and `status: 'replay'` is
|
|
139
|
+
* surfaced instead of a duplicate record.
|
|
140
|
+
*/
|
|
141
|
+
export const appendRegradeHistoryRun = (params: {
|
|
142
|
+
readonly artifact: RegradePlanArtifact;
|
|
143
|
+
readonly report: RegradeReport;
|
|
144
|
+
readonly rootDir: string;
|
|
145
|
+
}): TrailsResult<RegradeHistorySummary, Error> => {
|
|
146
|
+
const absolutePath = regradeHistoryPathForPlan(
|
|
147
|
+
params.rootDir,
|
|
148
|
+
params.artifact.plan
|
|
149
|
+
);
|
|
150
|
+
const relativePath = rootRelativePath(params.rootDir, absolutePath);
|
|
151
|
+
const planContentHash = regradePlanContentHash(params.artifact.plan);
|
|
152
|
+
const lockHashAtRun = regradeSourceHash(params.report);
|
|
153
|
+
const entry: RegradeHistoryRun = {
|
|
154
|
+
lockHashAtRun,
|
|
155
|
+
plan: params.artifact,
|
|
156
|
+
planContentHash,
|
|
157
|
+
report: params.report,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
let prior: RegradeHistoryArtifact | undefined;
|
|
161
|
+
if (existsSync(absolutePath)) {
|
|
162
|
+
const existing = readRegradeHistoryArtifact(absolutePath);
|
|
163
|
+
if (existing.isErr()) {
|
|
164
|
+
return existing;
|
|
165
|
+
}
|
|
166
|
+
prior = existing.value;
|
|
167
|
+
if (
|
|
168
|
+
params.artifact.transitionId !== undefined &&
|
|
169
|
+
params.artifact.transitionId !== prior.id
|
|
170
|
+
) {
|
|
171
|
+
return Result.err(
|
|
172
|
+
new ValidationError(
|
|
173
|
+
'Regrade plan transition id mismatch — refusing to fork the consolidated history.',
|
|
174
|
+
{
|
|
175
|
+
context: {
|
|
176
|
+
history: prior.id,
|
|
177
|
+
path: relativePath,
|
|
178
|
+
plan: params.artifact.transitionId,
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const lastRun = prior.runs.at(-1);
|
|
185
|
+
// A plan that carries the transition id (adjust round-trips, plan
|
|
186
|
+
// re-derivation) may evolve the plan identity on the same spine. A plan
|
|
187
|
+
// WITHOUT the id that disagrees with the recorded plan identity is a
|
|
188
|
+
// name collision, not a continuation — refuse instead of mixing runs
|
|
189
|
+
// from unrelated transitions into one history.
|
|
190
|
+
if (
|
|
191
|
+
params.artifact.transitionId === undefined &&
|
|
192
|
+
lastRun !== undefined &&
|
|
193
|
+
lastRun.plan.plan.id !== params.artifact.plan.id
|
|
194
|
+
) {
|
|
195
|
+
return Result.err(
|
|
196
|
+
new ValidationError(
|
|
197
|
+
'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.',
|
|
198
|
+
{
|
|
199
|
+
context: {
|
|
200
|
+
history: lastRun.plan.plan.id,
|
|
201
|
+
path: relativePath,
|
|
202
|
+
plan: params.artifact.plan.id,
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (
|
|
209
|
+
lastRun !== undefined &&
|
|
210
|
+
lastRun.planContentHash === planContentHash &&
|
|
211
|
+
lastRun.lockHashAtRun === lockHashAtRun
|
|
212
|
+
) {
|
|
213
|
+
return Result.ok({
|
|
214
|
+
path: relativePath,
|
|
215
|
+
schemaVersion: REGRADE_HISTORY_SCHEMA_VERSION,
|
|
216
|
+
status: 'replay',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const artifact: RegradeHistoryArtifact = {
|
|
222
|
+
id:
|
|
223
|
+
prior === undefined
|
|
224
|
+
? (params.artifact.transitionId ??
|
|
225
|
+
mintTransitionId(
|
|
226
|
+
regradePlanSlugForBody(params.artifact.plan),
|
|
227
|
+
planContentHash,
|
|
228
|
+
lockHashAtRun
|
|
229
|
+
))
|
|
230
|
+
: prior.id,
|
|
231
|
+
kind: 'regrade-history',
|
|
232
|
+
path: relativePath,
|
|
233
|
+
runs: prior === undefined ? [entry] : [...prior.runs, entry],
|
|
234
|
+
schemaVersion: REGRADE_HISTORY_SCHEMA_VERSION,
|
|
235
|
+
};
|
|
236
|
+
const parsed = regradeHistoryArtifactSchema.safeParse(artifact);
|
|
237
|
+
if (!parsed.success) {
|
|
238
|
+
return Result.err(
|
|
239
|
+
new ValidationError('Invalid Regrade history artifact.', {
|
|
240
|
+
context: { issues: parsed.error.issues, path: relativePath },
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
246
|
+
writeFileSync(absolutePath, `${JSON.stringify(parsed.data, null, 2)}\n`);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return Result.err(
|
|
249
|
+
new InternalError('Failed to write Regrade history entry.', {
|
|
250
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
251
|
+
context: { path: relativePath },
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return Result.ok({
|
|
256
|
+
path: relativePath,
|
|
257
|
+
schemaVersion: REGRADE_HISTORY_SCHEMA_VERSION,
|
|
258
|
+
status: 'applied',
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Verify every recorded run at its own stamped lock: recompute the plan
|
|
264
|
+
* content hash and lock hash from the recorded plan and report, then compare
|
|
265
|
+
* with the stamped values.
|
|
266
|
+
*/
|
|
267
|
+
export const verifyRegradeHistoryRuns = (
|
|
268
|
+
artifact: RegradeHistoryArtifact
|
|
269
|
+
): TrailsResult<{ readonly runs: number }, ValidationError> => {
|
|
270
|
+
for (const [index, run] of artifact.runs.entries()) {
|
|
271
|
+
if (regradePlanContentHash(run.plan.plan) !== run.planContentHash) {
|
|
272
|
+
return Result.err(
|
|
273
|
+
new ValidationError('Regrade history run stamp mismatch.', {
|
|
274
|
+
context: {
|
|
275
|
+
field: 'planContentHash',
|
|
276
|
+
path: artifact.path,
|
|
277
|
+
run: index,
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (regradeSourceHash(run.report) !== run.lockHashAtRun) {
|
|
283
|
+
return Result.err(
|
|
284
|
+
new ValidationError('Regrade history run stamp mismatch.', {
|
|
285
|
+
context: { field: 'lockHashAtRun', path: artifact.path, run: index },
|
|
286
|
+
})
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return Result.ok({ runs: artifact.runs.length });
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const hasPathSeparator = (value: string): boolean =>
|
|
294
|
+
value.includes('/') || value.includes('\\');
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Resolve a transition name (with or without a `.json` suffix) to its
|
|
298
|
+
* consolidated history file. Path references are rejected — graduated
|
|
299
|
+
* history lookups are by transition name only.
|
|
300
|
+
*/
|
|
301
|
+
export const resolveRegradeHistoryPath = (
|
|
302
|
+
rootDir: string,
|
|
303
|
+
ref: string
|
|
304
|
+
): TrailsResult<string, ValidationError> => {
|
|
305
|
+
if (hasPathSeparator(ref) || isAbsolute(ref)) {
|
|
306
|
+
return Result.err(
|
|
307
|
+
new ValidationError(
|
|
308
|
+
`Regrade history reference "${ref}" must be a transition name.`
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
const name = ref.endsWith('.json') ? ref.slice(0, -'.json'.length) : ref;
|
|
313
|
+
const path = join(regradePlanDirectory(rootDir), 'history', `${name}.json`);
|
|
314
|
+
if (!existsSync(path)) {
|
|
315
|
+
return Result.err(
|
|
316
|
+
new ValidationError(`No Regrade history for transition "${ref}" found.`)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return Result.ok(path);
|
|
320
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saved Regrade plan artifact shape plus the slug, path, and hash helpers
|
|
3
|
+
* shared by the `regrade` trails and the consolidated Regrade history module.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isPlainObject } from '@ontrails/core';
|
|
7
|
+
import { vocabularyRegradePlanSchema } from '@ontrails/regrade';
|
|
8
|
+
import type {
|
|
9
|
+
RegradeReport,
|
|
10
|
+
RegradeReportEntry,
|
|
11
|
+
VocabularyRegradePlan,
|
|
12
|
+
} from '@ontrails/regrade';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { join, normalize, relative } from 'node:path';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
|
|
17
|
+
export const REGRADE_PLAN_SCHEMA_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
const regradePlanProvenanceValueSchema = z.enum(['authored', 'derived']);
|
|
20
|
+
|
|
21
|
+
const classRegradePlanScopeSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
exclude: z
|
|
24
|
+
.array(z.string())
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Root-relative path globs excluded from the class run'),
|
|
27
|
+
extensions: z
|
|
28
|
+
.array(z.string())
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Source file extensions scanned by the class run'),
|
|
31
|
+
include: z
|
|
32
|
+
.array(z.string())
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Root-relative path globs collected during the class run'),
|
|
35
|
+
})
|
|
36
|
+
.strict();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A saved class-mode Regrade plan: which classes run, over what scope, and
|
|
40
|
+
* why. The parallel payload to {@link vocabularyRegradePlanSchema} — the
|
|
41
|
+
* `kind` discriminant keeps existing vocabulary plan artifacts
|
|
42
|
+
* byte-compatible.
|
|
43
|
+
*/
|
|
44
|
+
const classRegradePlanSchema = z.object({
|
|
45
|
+
classIds: z
|
|
46
|
+
.array(z.string().min(1))
|
|
47
|
+
.min(1)
|
|
48
|
+
.describe('Regrade class ids this plan runs'),
|
|
49
|
+
id: z.string().min(1).describe('Stable Regrade plan identifier'),
|
|
50
|
+
intent: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Human-authored migration intent for the class run'),
|
|
54
|
+
kind: z.literal('class').describe('Regrade plan kind'),
|
|
55
|
+
name: z
|
|
56
|
+
.string()
|
|
57
|
+
.min(1)
|
|
58
|
+
.optional()
|
|
59
|
+
.describe(
|
|
60
|
+
'Authored transition name; keys the saved plan and consolidated history filenames'
|
|
61
|
+
),
|
|
62
|
+
scope: classRegradePlanScopeSchema
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('Collection scope for the class run'),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export type ClassRegradePlan = z.output<typeof classRegradePlanSchema>;
|
|
68
|
+
|
|
69
|
+
const regradePlanBodySchema = z.discriminatedUnion('kind', [
|
|
70
|
+
vocabularyRegradePlanSchema,
|
|
71
|
+
classRegradePlanSchema,
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
export type RegradePlanBody = VocabularyRegradePlan | ClassRegradePlan;
|
|
75
|
+
|
|
76
|
+
const regradeExpansionCandidateSchema = z.union([
|
|
77
|
+
z.object({
|
|
78
|
+
evidence: z
|
|
79
|
+
.array(
|
|
80
|
+
z.object({
|
|
81
|
+
column: z.number().optional(),
|
|
82
|
+
detail: z.string().optional(),
|
|
83
|
+
line: z.number().optional(),
|
|
84
|
+
path: z.string(),
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
.default([]),
|
|
88
|
+
kind: z.enum(['file-rename', 'form', 'namespace', 'preserve']),
|
|
89
|
+
reason: z.string().optional(),
|
|
90
|
+
status: z.enum(['pending', 'rejected']).default('pending'),
|
|
91
|
+
suggestedClassification: z.string(),
|
|
92
|
+
value: z.string(),
|
|
93
|
+
}),
|
|
94
|
+
z
|
|
95
|
+
.object({
|
|
96
|
+
detail: z.string().optional(),
|
|
97
|
+
path: z.string(),
|
|
98
|
+
status: z.enum(['pending', 'rejected']).default('pending'),
|
|
99
|
+
})
|
|
100
|
+
.transform((candidate) => ({
|
|
101
|
+
evidence: [
|
|
102
|
+
{
|
|
103
|
+
...(candidate.detail === undefined
|
|
104
|
+
? {}
|
|
105
|
+
: { detail: candidate.detail }),
|
|
106
|
+
path: candidate.path,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
kind: 'file-rename' as const,
|
|
110
|
+
...(candidate.detail === undefined ? {} : { reason: candidate.detail }),
|
|
111
|
+
status: candidate.status,
|
|
112
|
+
suggestedClassification: 'legacy-path-candidate',
|
|
113
|
+
value: candidate.path,
|
|
114
|
+
})),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
export const regradePlanArtifactSchema = z
|
|
118
|
+
.object({
|
|
119
|
+
expansion: z
|
|
120
|
+
.object({
|
|
121
|
+
candidates: z.array(regradeExpansionCandidateSchema).default([]),
|
|
122
|
+
})
|
|
123
|
+
.optional(),
|
|
124
|
+
kind: z.literal('regrade-plan'),
|
|
125
|
+
path: z.string(),
|
|
126
|
+
plan: regradePlanBodySchema,
|
|
127
|
+
provenance: z.object({
|
|
128
|
+
fields: z.record(z.string(), regradePlanProvenanceValueSchema),
|
|
129
|
+
}),
|
|
130
|
+
schemaVersion: z.literal(REGRADE_PLAN_SCHEMA_VERSION),
|
|
131
|
+
sourceHash: z.string(),
|
|
132
|
+
transitionId: z
|
|
133
|
+
.string()
|
|
134
|
+
.min(1)
|
|
135
|
+
.optional()
|
|
136
|
+
.describe(
|
|
137
|
+
'Stable transition identity this plan re-runs; preserves the consolidated history spine'
|
|
138
|
+
),
|
|
139
|
+
})
|
|
140
|
+
.strict();
|
|
141
|
+
|
|
142
|
+
export interface RegradePlanExpansion {
|
|
143
|
+
readonly candidates: readonly {
|
|
144
|
+
readonly evidence: readonly {
|
|
145
|
+
readonly column?: number | undefined;
|
|
146
|
+
readonly detail?: string | undefined;
|
|
147
|
+
readonly line?: number | undefined;
|
|
148
|
+
readonly path: string;
|
|
149
|
+
}[];
|
|
150
|
+
readonly kind: 'file-rename' | 'form' | 'namespace' | 'preserve';
|
|
151
|
+
readonly reason?: string | undefined;
|
|
152
|
+
readonly status: 'pending' | 'rejected';
|
|
153
|
+
readonly suggestedClassification: string;
|
|
154
|
+
readonly value: string;
|
|
155
|
+
}[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface RegradePlanArtifact {
|
|
159
|
+
readonly expansion?: RegradePlanExpansion | undefined;
|
|
160
|
+
readonly kind: 'regrade-plan';
|
|
161
|
+
readonly path: string;
|
|
162
|
+
readonly plan: RegradePlanBody;
|
|
163
|
+
readonly provenance: {
|
|
164
|
+
readonly fields: Readonly<Record<string, 'authored' | 'derived'>>;
|
|
165
|
+
};
|
|
166
|
+
readonly schemaVersion: typeof REGRADE_PLAN_SCHEMA_VERSION;
|
|
167
|
+
readonly sourceHash: string;
|
|
168
|
+
readonly transitionId?: string | undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A plan artifact narrowed to a vocabulary plan body. */
|
|
172
|
+
export type VocabularyRegradePlanArtifact = RegradePlanArtifact & {
|
|
173
|
+
readonly plan: VocabularyRegradePlan;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const regradeSlugText = (text: string): string =>
|
|
177
|
+
text
|
|
178
|
+
.toLowerCase()
|
|
179
|
+
.replaceAll(/[^a-z0-9]+/g, '-')
|
|
180
|
+
.replaceAll(/^-|-$/g, '');
|
|
181
|
+
|
|
182
|
+
const regradePlanSlug = (plan: Pick<VocabularyRegradePlan, 'from' | 'to'>) =>
|
|
183
|
+
regradeSlugText(`${plan.from}-to-${plan.to}`);
|
|
184
|
+
|
|
185
|
+
export const regradePlanSlugForBody = (plan: RegradePlanBody): string =>
|
|
186
|
+
plan.kind === 'class'
|
|
187
|
+
? regradeSlugText(plan.name ?? plan.classIds.join('-'))
|
|
188
|
+
: regradePlanSlug(plan);
|
|
189
|
+
|
|
190
|
+
const normalizeRelativePath = (path: string): string =>
|
|
191
|
+
normalize(path).replaceAll('\\', '/');
|
|
192
|
+
|
|
193
|
+
export const rootRelativePath = (
|
|
194
|
+
rootDir: string,
|
|
195
|
+
absolutePath: string
|
|
196
|
+
): string => normalizeRelativePath(relative(rootDir, absolutePath));
|
|
197
|
+
|
|
198
|
+
export const regradePlanDirectory = (rootDir: string): string =>
|
|
199
|
+
join(rootDir, '.trails', 'regrade');
|
|
200
|
+
|
|
201
|
+
export const regradePlanPathForPlan = (
|
|
202
|
+
rootDir: string,
|
|
203
|
+
plan: RegradePlanBody
|
|
204
|
+
): string =>
|
|
205
|
+
join(regradePlanDirectory(rootDir), `${regradePlanSlugForBody(plan)}.json`);
|
|
206
|
+
|
|
207
|
+
const sourceHashEntryFacts = (
|
|
208
|
+
entries: readonly RegradeReportEntry[]
|
|
209
|
+
): readonly Pick<
|
|
210
|
+
RegradeReportEntry,
|
|
211
|
+
'classId' | 'notes' | 'outcome' | 'path' | 'reason' | 'reviewDetails'
|
|
212
|
+
>[] =>
|
|
213
|
+
entries
|
|
214
|
+
.filter(
|
|
215
|
+
(entry) => entry.outcome === 'rewrite' || entry.outcome === 'needs-review'
|
|
216
|
+
)
|
|
217
|
+
.map(({ classId, notes, outcome, path, reason, reviewDetails }) => ({
|
|
218
|
+
...(classId === undefined ? {} : { classId }),
|
|
219
|
+
...(notes === undefined ? {} : { notes }),
|
|
220
|
+
outcome,
|
|
221
|
+
path,
|
|
222
|
+
...(reason === undefined ? {} : { reason }),
|
|
223
|
+
...(reviewDetails === undefined ? {} : { reviewDetails }),
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
export const regradeSourceHash = (report: RegradeReport): string =>
|
|
227
|
+
createHash('sha256')
|
|
228
|
+
.update(
|
|
229
|
+
JSON.stringify({
|
|
230
|
+
entries: sourceHashEntryFacts(report.entries),
|
|
231
|
+
ledger: report.run?.ledger,
|
|
232
|
+
selectedClassIds: report.selectedClassIds,
|
|
233
|
+
})
|
|
234
|
+
)
|
|
235
|
+
.digest('hex');
|
|
236
|
+
|
|
237
|
+
const canonicalizeJsonValue = (value: unknown): unknown => {
|
|
238
|
+
if (Array.isArray(value)) {
|
|
239
|
+
return value.map((entry) => canonicalizeJsonValue(entry));
|
|
240
|
+
}
|
|
241
|
+
if (isPlainObject(value)) {
|
|
242
|
+
return Object.fromEntries(
|
|
243
|
+
Object.keys(value)
|
|
244
|
+
.toSorted()
|
|
245
|
+
.map((key) => [key, canonicalizeJsonValue(value[key])])
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return value;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* JSON.stringify with recursively sorted object keys so structurally equal
|
|
253
|
+
* values serialize identically regardless of key insertion order. Arrays keep
|
|
254
|
+
* their authored order.
|
|
255
|
+
*/
|
|
256
|
+
export const canonicalJsonStringify = (value: unknown): string =>
|
|
257
|
+
JSON.stringify(canonicalizeJsonValue(value));
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Canonical content hash of a resolved Regrade plan body — the authored
|
|
261
|
+
* migration intent. Stable across key insertion order; changes on any edit to
|
|
262
|
+
* the plan contents.
|
|
263
|
+
*/
|
|
264
|
+
export const regradePlanContentHash = (plan: RegradePlanBody): string =>
|
|
265
|
+
createHash('sha256').update(canonicalJsonStringify(plan)).digest('hex');
|
package/src/release/index.ts
CHANGED
|
@@ -89,6 +89,18 @@ export {
|
|
|
89
89
|
type RegistryView,
|
|
90
90
|
type RegistryWorkspace,
|
|
91
91
|
} from './native-bun-registry.js';
|
|
92
|
+
export {
|
|
93
|
+
collectReleaseNotesInput,
|
|
94
|
+
dedupeReleaseChanges,
|
|
95
|
+
extractChangelogEntry,
|
|
96
|
+
renderReleaseNotes,
|
|
97
|
+
type ReleaseNotesCollectOptions,
|
|
98
|
+
type ReleaseNotesChange,
|
|
99
|
+
type ReleaseNotesInput,
|
|
100
|
+
type ReleaseNotesPackageVersion,
|
|
101
|
+
type ReleaseNotesParsedChange,
|
|
102
|
+
} from './notes.js';
|
|
103
|
+
export { runReleaseNotesCli } from './notes-cli.js';
|
|
92
104
|
export {
|
|
93
105
|
channelIntentForDistTag,
|
|
94
106
|
evaluateReleasePolicy,
|
|
@@ -115,6 +127,11 @@ export {
|
|
|
115
127
|
type ReleaseSmokeCheckResult,
|
|
116
128
|
type ReleaseSmokeResult,
|
|
117
129
|
} from './smoke.js';
|
|
130
|
+
export {
|
|
131
|
+
runLockRoundtripSmoke,
|
|
132
|
+
type LockRoundtripSmokeOptions,
|
|
133
|
+
type LockRoundtripSmokeResult,
|
|
134
|
+
} from './lock-roundtrip-smoke.js';
|
|
118
135
|
export {
|
|
119
136
|
runPackedArtifactsSmoke,
|
|
120
137
|
type PackedArtifactsSmokeResult,
|