@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.
- package/CHANGELOG.md +1213 -4
- package/README.md +29 -0
- package/package.json +35 -7
- package/src/app.ts +147 -3
- package/src/cli.ts +358 -11
- package/src/completions.ts +240 -0
- package/src/lifecycle-source-io.ts +33 -0
- package/src/load-app-mirror.ts +202 -0
- package/src/local-state-io.ts +173 -0
- package/src/mcp-app.ts +42 -0
- package/src/mcp-options.ts +92 -0
- package/src/mcp.ts +8 -0
- package/src/project-writes.ts +377 -0
- package/src/regrade/audit.ts +571 -0
- package/src/regrade/config.ts +152 -0
- package/src/regrade/history.ts +636 -0
- package/src/regrade/lifecycle.ts +76 -0
- package/src/regrade/live-api-preserve.ts +123 -0
- package/src/regrade/plan-artifact.ts +461 -0
- package/src/regrade/plan-derivation.ts +301 -0
- package/src/regrade/prepared-run.ts +259 -0
- package/src/regrade/receipt-history.ts +446 -0
- package/src/regrade/source-transaction.ts +185 -0
- package/src/release/bindings.ts +58 -0
- package/src/release/check.ts +1146 -0
- package/src/release/cli-bundle.ts +575 -0
- package/src/release/config.ts +73 -0
- package/src/release/contract-facts.ts +425 -0
- package/src/release/homebrew.ts +219 -0
- package/src/release/index.ts +180 -0
- package/src/release/lock-roundtrip-smoke.ts +230 -0
- package/src/release/native-bun-publish.ts +964 -0
- package/src/release/native-bun-registry.ts +830 -0
- package/src/release/notes-cli.ts +171 -0
- package/src/release/notes.ts +390 -0
- package/src/release/pack-coherence.ts +455 -0
- package/src/release/package-route-facts.ts +146 -0
- package/src/release/packed-artifacts-smoke.ts +236 -0
- package/src/release/policy.ts +1700 -0
- package/src/release/semver.ts +104 -0
- package/src/release/smoke.ts +56 -0
- package/src/release/wayfinder-dogfood-smoke.ts +760 -0
- package/src/retired-topo-command.ts +36 -0
- package/src/run-adapter-check.ts +76 -0
- package/src/run-collision.ts +126 -0
- package/src/run-completions-install.ts +179 -0
- package/src/run-example.ts +149 -0
- package/src/run-examples.ts +148 -0
- package/src/run-quiet.ts +75 -0
- package/src/run-regrade-progress.ts +47 -0
- package/src/run-release-check.ts +74 -0
- package/src/run-schema.ts +74 -0
- package/src/run-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch.ts +432 -0
- package/src/run-wayfind-outline.ts +170 -0
- package/src/scaffold-version-sync.ts +183 -0
- package/src/scaffold-versions.generated.ts +12 -0
- package/src/trails/adapter-check.ts +244 -0
- package/src/trails/add-surface.ts +99 -45
- package/src/trails/add-trail.ts +84 -37
- package/src/trails/add-verify.ts +100 -30
- package/src/trails/compile.ts +58 -0
- package/src/trails/completions-complete.ts +165 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/create-adapter.ts +785 -0
- package/src/trails/create-scaffold.ts +402 -106
- package/src/trails/create-versions.ts +62 -0
- package/src/trails/create.ts +186 -72
- package/src/trails/deprecate.ts +59 -0
- package/src/trails/dev-clean.ts +82 -0
- package/src/trails/dev-reset.ts +50 -0
- package/src/trails/dev-stats.ts +72 -0
- package/src/trails/dev-support.ts +360 -0
- package/src/trails/doctor.ts +77 -0
- package/src/trails/draft-promote.ts +949 -0
- package/src/trails/guide.ts +70 -68
- package/src/trails/load-app.ts +1123 -15
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/project.ts +17 -3
- package/src/trails/regrade.ts +4497 -0
- package/src/trails/release-check.ts +113 -0
- package/src/trails/release-smoke.ts +49 -0
- package/src/trails/revise.ts +53 -0
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +475 -0
- package/src/trails/run-examples.ts +129 -0
- package/src/trails/run.ts +434 -0
- package/src/trails/scaffold-json.ts +58 -0
- package/src/trails/survey.ts +877 -214
- package/src/trails/topo-activation.ts +14 -0
- package/src/trails/topo-constants.ts +2 -0
- package/src/trails/topo-history.ts +47 -0
- package/src/trails/topo-output-schemas.ts +259 -0
- package/src/trails/topo-pin.ts +38 -0
- package/src/trails/topo-read-support.ts +368 -0
- package/src/trails/topo-reports.ts +809 -0
- package/src/trails/topo-store-support.ts +323 -0
- package/src/trails/topo-support.ts +228 -0
- package/src/trails/topo-unpin.ts +61 -0
- package/src/trails/topo.ts +92 -0
- package/src/trails/validate.ts +27 -0
- package/src/trails/version-lifecycle-support.ts +936 -0
- package/src/trails/warden-guide.ts +134 -0
- package/src/trails/warden.ts +198 -58
- package/src/trails/wayfind-outline.ts +876 -0
- package/src/trails/wayfind.ts +1053 -0
- package/src/versions.ts +31 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/__tests__/examples.test.ts +0 -6
- package/dist/bin/trails.d.ts +0 -3
- package/dist/bin/trails.d.ts.map +0 -1
- package/dist/bin/trails.js +0 -4
- package/dist/bin/trails.js.map +0 -1
- package/dist/src/app.d.ts +0 -2
- package/dist/src/app.d.ts.map +0 -1
- package/dist/src/app.js +0 -11
- package/dist/src/app.js.map +0 -1
- package/dist/src/clack.d.ts +0 -9
- package/dist/src/clack.d.ts.map +0 -1
- package/dist/src/clack.js +0 -84
- package/dist/src/clack.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -13
- package/dist/src/cli.js.map +0 -1
- package/dist/src/trails/add-surface.d.ts +0 -13
- package/dist/src/trails/add-surface.d.ts.map +0 -1
- package/dist/src/trails/add-surface.js +0 -88
- package/dist/src/trails/add-surface.js.map +0 -1
- package/dist/src/trails/add-trail.d.ts +0 -10
- package/dist/src/trails/add-trail.d.ts.map +0 -1
- package/dist/src/trails/add-trail.js +0 -77
- package/dist/src/trails/add-trail.js.map +0 -1
- package/dist/src/trails/add-verify.d.ts +0 -10
- package/dist/src/trails/add-verify.d.ts.map +0 -1
- package/dist/src/trails/add-verify.js +0 -67
- package/dist/src/trails/add-verify.js.map +0 -1
- package/dist/src/trails/create-scaffold.d.ts +0 -15
- package/dist/src/trails/create-scaffold.d.ts.map +0 -1
- package/dist/src/trails/create-scaffold.js +0 -288
- package/dist/src/trails/create-scaffold.js.map +0 -1
- package/dist/src/trails/create.d.ts +0 -22
- package/dist/src/trails/create.d.ts.map +0 -1
- package/dist/src/trails/create.js +0 -121
- package/dist/src/trails/create.js.map +0 -1
- package/dist/src/trails/guide.d.ts +0 -11
- package/dist/src/trails/guide.d.ts.map +0 -1
- package/dist/src/trails/guide.js +0 -80
- package/dist/src/trails/guide.js.map +0 -1
- package/dist/src/trails/load-app.d.ts +0 -4
- package/dist/src/trails/load-app.d.ts.map +0 -1
- package/dist/src/trails/load-app.js +0 -24
- package/dist/src/trails/load-app.js.map +0 -1
- package/dist/src/trails/project.d.ts +0 -8
- package/dist/src/trails/project.d.ts.map +0 -1
- package/dist/src/trails/project.js +0 -43
- package/dist/src/trails/project.js.map +0 -1
- package/dist/src/trails/survey.d.ts +0 -31
- package/dist/src/trails/survey.d.ts.map +0 -1
- package/dist/src/trails/survey.js +0 -221
- package/dist/src/trails/survey.js.map +0 -1
- package/dist/src/trails/warden.d.ts +0 -19
- package/dist/src/trails/warden.d.ts.map +0 -1
- package/dist/src/trails/warden.js +0 -88
- package/dist/src/trails/warden.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/__tests__/create.test.ts +0 -349
- package/src/__tests__/guide.test.ts +0 -91
- package/src/__tests__/load-app.test.ts +0 -15
- package/src/__tests__/survey.test.ts +0 -159
- package/src/__tests__/warden.test.ts +0 -74
- package/tsconfig.json +0 -9
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAstIdentifierRenameClass,
|
|
3
|
+
deriveFileRenameCandidates,
|
|
4
|
+
deriveVocabularyFormProposals,
|
|
5
|
+
runFileRenameRegrade,
|
|
6
|
+
runRegrade,
|
|
7
|
+
} from '@ontrails/regrade';
|
|
8
|
+
import type {
|
|
9
|
+
RegradeReport,
|
|
10
|
+
VocabularyPreserveInventoryEntry,
|
|
11
|
+
VocabularyRegradePlan,
|
|
12
|
+
} from '@ontrails/regrade';
|
|
13
|
+
|
|
14
|
+
import { isGeneratedRegradeArtifactPath } from './plan-artifact.js';
|
|
15
|
+
import type {
|
|
16
|
+
RegradePlanArtifact,
|
|
17
|
+
RegradePlanDerivation,
|
|
18
|
+
} from './plan-artifact.js';
|
|
19
|
+
|
|
20
|
+
const provenanceForForm = (
|
|
21
|
+
source: RegradePlanDerivation['forms'][number]['source'],
|
|
22
|
+
fields: RegradePlanArtifact['provenance']['fields']
|
|
23
|
+
): 'authored' | 'derived' => {
|
|
24
|
+
if (source === 'seed') {
|
|
25
|
+
return 'authored';
|
|
26
|
+
}
|
|
27
|
+
if (source === 'plan-override') {
|
|
28
|
+
return fields['overrides'] ?? 'derived';
|
|
29
|
+
}
|
|
30
|
+
if (source === 'plan-defer') {
|
|
31
|
+
return fields['deferForms'] ?? 'derived';
|
|
32
|
+
}
|
|
33
|
+
return 'derived';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const namespaceCensus = (
|
|
37
|
+
report: RegradeReport
|
|
38
|
+
): RegradePlanDerivation['namespaces'] => {
|
|
39
|
+
const namespaces = new Map<
|
|
40
|
+
string,
|
|
41
|
+
{ inScope: number; policyClassified: number }
|
|
42
|
+
>();
|
|
43
|
+
for (const occurrence of report.run?.ledger.occurrences ?? []) {
|
|
44
|
+
if (isGeneratedRegradeArtifactPath(occurrence.path)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const namespace = occurrence.path.split('/')[0] ?? occurrence.path;
|
|
48
|
+
const counts = namespaces.get(namespace) ?? {
|
|
49
|
+
inScope: 0,
|
|
50
|
+
policyClassified: 0,
|
|
51
|
+
};
|
|
52
|
+
if (occurrence.scopeTier === 'policy-classified') {
|
|
53
|
+
counts.policyClassified += 1;
|
|
54
|
+
} else {
|
|
55
|
+
counts.inScope += 1;
|
|
56
|
+
}
|
|
57
|
+
namespaces.set(namespace, counts);
|
|
58
|
+
}
|
|
59
|
+
return [...namespaces.entries()]
|
|
60
|
+
.map(([namespace, counts]) => ({
|
|
61
|
+
...counts,
|
|
62
|
+
namespace,
|
|
63
|
+
provenance: 'derived' as const,
|
|
64
|
+
}))
|
|
65
|
+
.toSorted((left, right) => left.namespace.localeCompare(right.namespace));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const observedReviews = (
|
|
69
|
+
report: RegradeReport
|
|
70
|
+
): RegradePlanDerivation['reviews'] => {
|
|
71
|
+
const reviews = new Map<string, RegradePlanDerivation['reviews'][number]>();
|
|
72
|
+
for (const occurrence of report.run?.ledger.occurrences ?? []) {
|
|
73
|
+
if (
|
|
74
|
+
isGeneratedRegradeArtifactPath(occurrence.path) ||
|
|
75
|
+
(occurrence.verdict !== 'deferred' &&
|
|
76
|
+
occurrence.disposition !== 'code-context-out-of-engine' &&
|
|
77
|
+
occurrence.disposition !== 'in-family-unresolved')
|
|
78
|
+
) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const key = `${occurrence.form}\0${occurrence.reason}`;
|
|
82
|
+
const current = reviews.get(key);
|
|
83
|
+
const evidence = {
|
|
84
|
+
column: occurrence.column,
|
|
85
|
+
line: occurrence.line,
|
|
86
|
+
path: occurrence.path,
|
|
87
|
+
};
|
|
88
|
+
reviews.set(key, {
|
|
89
|
+
evidence:
|
|
90
|
+
current === undefined ? [evidence] : [...current.evidence, evidence],
|
|
91
|
+
provenance: 'derived',
|
|
92
|
+
reason: occurrence.reason,
|
|
93
|
+
status: 'pending',
|
|
94
|
+
value: occurrence.form,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return [...reviews.values()]
|
|
98
|
+
.map((review) => ({
|
|
99
|
+
...review,
|
|
100
|
+
evidence: review.evidence.toSorted((left, right) =>
|
|
101
|
+
left.path === right.path
|
|
102
|
+
? (left.line ?? 0) - (right.line ?? 0)
|
|
103
|
+
: left.path.localeCompare(right.path)
|
|
104
|
+
),
|
|
105
|
+
}))
|
|
106
|
+
.toSorted((left, right) => left.value.localeCompare(right.value));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const identifierReviews = (params: {
|
|
110
|
+
readonly plan: VocabularyRegradePlan;
|
|
111
|
+
readonly rootDir: string;
|
|
112
|
+
}): RegradePlanDerivation['reviews'] => {
|
|
113
|
+
const { scope } = params.plan;
|
|
114
|
+
const sourceForms = [
|
|
115
|
+
...new Map(
|
|
116
|
+
deriveVocabularyFormProposals(params.plan).map((form) => [
|
|
117
|
+
form.from,
|
|
118
|
+
form,
|
|
119
|
+
])
|
|
120
|
+
).values(),
|
|
121
|
+
];
|
|
122
|
+
const collection = {
|
|
123
|
+
...(scope?.exclude === undefined ? {} : { exclude: scope.exclude }),
|
|
124
|
+
extensions: scope?.extensions ?? ['.js', '.jsx', '.mjs', '.ts', '.tsx'],
|
|
125
|
+
...(scope?.include === undefined ? {} : { include: scope.include }),
|
|
126
|
+
};
|
|
127
|
+
const entries: RegradeReport['entries'][number][] = [];
|
|
128
|
+
for (const form of sourceForms) {
|
|
129
|
+
const report = runRegrade({
|
|
130
|
+
classes: [
|
|
131
|
+
createAstIdentifierRenameClass({
|
|
132
|
+
from: form.from,
|
|
133
|
+
id: `derived-plan-identifiers:${form.from}->${form.to ?? params.plan.to}`,
|
|
134
|
+
match: 'identifier-segment',
|
|
135
|
+
reviewAllMatches: true,
|
|
136
|
+
to: form.to ?? params.plan.to,
|
|
137
|
+
}),
|
|
138
|
+
],
|
|
139
|
+
collection,
|
|
140
|
+
root: params.rootDir,
|
|
141
|
+
});
|
|
142
|
+
if (report.isErr()) {
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
evidence: [],
|
|
146
|
+
provenance: 'derived',
|
|
147
|
+
reason: `identifier-review-scan-failed: ${report.error.message}`,
|
|
148
|
+
status: 'pending',
|
|
149
|
+
value: form.from,
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
entries.push(...(report.value?.entries ?? []));
|
|
154
|
+
}
|
|
155
|
+
return entries
|
|
156
|
+
.flatMap((entry) =>
|
|
157
|
+
(entry.reviewDetails ?? []).flatMap((detail) =>
|
|
158
|
+
detail.symbol === undefined
|
|
159
|
+
? []
|
|
160
|
+
: [
|
|
161
|
+
{
|
|
162
|
+
evidence: [
|
|
163
|
+
{
|
|
164
|
+
...(detail.span?.column === undefined
|
|
165
|
+
? {}
|
|
166
|
+
: { column: detail.span.column }),
|
|
167
|
+
...(detail.span?.line === undefined
|
|
168
|
+
? {}
|
|
169
|
+
: { line: detail.span.line }),
|
|
170
|
+
path: entry.path,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
provenance: 'derived' as const,
|
|
174
|
+
reason: detail.reason,
|
|
175
|
+
status: 'pending' as const,
|
|
176
|
+
value: detail.symbol,
|
|
177
|
+
},
|
|
178
|
+
]
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
.toSorted((left, right) => left.value.localeCompare(right.value));
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const mergeReviews = (
|
|
185
|
+
...groups: readonly RegradePlanDerivation['reviews'][]
|
|
186
|
+
): RegradePlanDerivation['reviews'] => {
|
|
187
|
+
const merged = new Map<string, RegradePlanDerivation['reviews'][number]>();
|
|
188
|
+
for (const review of groups.flat()) {
|
|
189
|
+
const key = `${review.value}\0${review.reason}`;
|
|
190
|
+
const current = merged.get(key);
|
|
191
|
+
merged.set(key, {
|
|
192
|
+
...review,
|
|
193
|
+
evidence: [...(current?.evidence ?? []), ...review.evidence]
|
|
194
|
+
.filter(
|
|
195
|
+
(entry, index, evidence) =>
|
|
196
|
+
evidence.findIndex(
|
|
197
|
+
(candidate) =>
|
|
198
|
+
candidate.path === entry.path &&
|
|
199
|
+
candidate.line === entry.line &&
|
|
200
|
+
candidate.column === entry.column
|
|
201
|
+
) === index
|
|
202
|
+
)
|
|
203
|
+
.toSorted((left, right) => left.path.localeCompare(right.path)),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return [...merged.values()].toSorted((left, right) =>
|
|
207
|
+
left.value === right.value
|
|
208
|
+
? left.reason.localeCompare(right.reason)
|
|
209
|
+
: left.value.localeCompare(right.value)
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const referenceClosure = (params: {
|
|
214
|
+
readonly candidates: ReturnType<typeof deriveFileRenameCandidates>;
|
|
215
|
+
readonly plan: VocabularyRegradePlan;
|
|
216
|
+
readonly rootDir: string;
|
|
217
|
+
}): RegradePlanDerivation['referenceClosure'] => {
|
|
218
|
+
if (params.candidates.length === 0) {
|
|
219
|
+
return { entries: [], moves: [] };
|
|
220
|
+
}
|
|
221
|
+
const result = runFileRenameRegrade({
|
|
222
|
+
excludeGeneratedArtifacts: true,
|
|
223
|
+
renames: params.candidates,
|
|
224
|
+
root: params.rootDir,
|
|
225
|
+
...(params.plan.scope === undefined ? {} : { scope: params.plan.scope }),
|
|
226
|
+
});
|
|
227
|
+
if (result.isErr()) {
|
|
228
|
+
return { entries: [], issue: result.error.message, moves: [] };
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
entries: result.value.report.entries
|
|
232
|
+
.filter(
|
|
233
|
+
(
|
|
234
|
+
entry
|
|
235
|
+
): entry is typeof entry & {
|
|
236
|
+
readonly outcome: 'needs-review' | 'rewrite';
|
|
237
|
+
} => entry.outcome === 'needs-review' || entry.outcome === 'rewrite'
|
|
238
|
+
)
|
|
239
|
+
.map((entry) => ({
|
|
240
|
+
outcome: entry.outcome,
|
|
241
|
+
path: entry.path,
|
|
242
|
+
provenance: 'derived' as const,
|
|
243
|
+
...(entry.reason === undefined ? {} : { reason: entry.reason }),
|
|
244
|
+
}))
|
|
245
|
+
.toSorted((left, right) => left.path.localeCompare(right.path)),
|
|
246
|
+
moves: result.value.evidence.map(
|
|
247
|
+
({ deferred, from, historical, preserved, rewritten, skipped, to }) => ({
|
|
248
|
+
deferred,
|
|
249
|
+
from,
|
|
250
|
+
historical,
|
|
251
|
+
preserved,
|
|
252
|
+
provenance: 'derived' as const,
|
|
253
|
+
rewritten,
|
|
254
|
+
skipped,
|
|
255
|
+
to,
|
|
256
|
+
})
|
|
257
|
+
),
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export const deriveRegradePlanDerivation = (params: {
|
|
262
|
+
readonly plan: VocabularyRegradePlan;
|
|
263
|
+
readonly preserveInventory: readonly VocabularyPreserveInventoryEntry[];
|
|
264
|
+
readonly provenance: RegradePlanArtifact['provenance'];
|
|
265
|
+
readonly report: RegradeReport;
|
|
266
|
+
readonly rootDir: string;
|
|
267
|
+
}): RegradePlanDerivation => {
|
|
268
|
+
const candidates = deriveFileRenameCandidates({
|
|
269
|
+
plan: params.plan,
|
|
270
|
+
root: params.rootDir,
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
fileRenames: candidates.map((candidate) => ({
|
|
274
|
+
...candidate,
|
|
275
|
+
evidence: [...candidate.evidence],
|
|
276
|
+
provenance: 'derived' as const,
|
|
277
|
+
status: 'pending' as const,
|
|
278
|
+
})),
|
|
279
|
+
forms: deriveVocabularyFormProposals(params.plan).map((form) => ({
|
|
280
|
+
...form,
|
|
281
|
+
provenance: provenanceForForm(form.source, params.provenance.fields),
|
|
282
|
+
})),
|
|
283
|
+
namespaces: namespaceCensus(params.report),
|
|
284
|
+
preserves: params.preserveInventory.map((entry) => ({
|
|
285
|
+
disposition: 'preserve-current-live-api' as const,
|
|
286
|
+
evidence: [...entry.evidence],
|
|
287
|
+
pattern: entry.pattern,
|
|
288
|
+
provenance: 'derived' as const,
|
|
289
|
+
...(entry.reason === undefined ? {} : { reason: entry.reason }),
|
|
290
|
+
})),
|
|
291
|
+
referenceClosure: referenceClosure({
|
|
292
|
+
candidates,
|
|
293
|
+
plan: params.plan,
|
|
294
|
+
rootDir: params.rootDir,
|
|
295
|
+
}),
|
|
296
|
+
reviews: mergeReviews(
|
|
297
|
+
observedReviews(params.report),
|
|
298
|
+
identifierReviews({ plan: params.plan, rootDir: params.rootDir })
|
|
299
|
+
),
|
|
300
|
+
};
|
|
301
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { InternalError, Result, ValidationError } from '@ontrails/core';
|
|
2
|
+
import type { Result as TrailsResult } from '@ontrails/core';
|
|
3
|
+
import {
|
|
4
|
+
regradeReceiptContentHash,
|
|
5
|
+
regradeReceiptPlanContentHash,
|
|
6
|
+
regradeReceiptPlanSchema,
|
|
7
|
+
} from '@ontrails/regrade';
|
|
8
|
+
import type {
|
|
9
|
+
PreparedRegradeRunIdentity,
|
|
10
|
+
RegradeClass,
|
|
11
|
+
} from '@ontrails/regrade';
|
|
12
|
+
import { listGovernedVocabularyTransitions } from '@ontrails/warden';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
15
|
+
import { dirname, extname, isAbsolute, join, relative, sep } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { trailsPackageVersion } from '../versions.js';
|
|
18
|
+
import { canonicalJsonStringify } from './plan-artifact.js';
|
|
19
|
+
import type { RegradePlanArtifact } from './plan-artifact.js';
|
|
20
|
+
|
|
21
|
+
const ignoredLockDirectories = new Set([
|
|
22
|
+
'.agents',
|
|
23
|
+
'.git',
|
|
24
|
+
'.turbo',
|
|
25
|
+
'dist',
|
|
26
|
+
'node_modules',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const normalizePath = (path: string): string => path.split(sep).join('/');
|
|
30
|
+
|
|
31
|
+
const compareCodeUnits = (left: string, right: string): number => {
|
|
32
|
+
if (left < right) {
|
|
33
|
+
return -1;
|
|
34
|
+
}
|
|
35
|
+
if (left > right) {
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const loaderForModuleExtension = (
|
|
42
|
+
extension: string
|
|
43
|
+
): 'js' | 'jsx' | 'ts' | 'tsx' => {
|
|
44
|
+
if (extension === '.tsx') {
|
|
45
|
+
return 'tsx';
|
|
46
|
+
}
|
|
47
|
+
if (extension === '.jsx') {
|
|
48
|
+
return 'jsx';
|
|
49
|
+
}
|
|
50
|
+
return ['.cts', '.mts', '.ts'].includes(extension) ? 'ts' : 'js';
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const collectLockPaths = (rootDir: string): readonly string[] => {
|
|
54
|
+
const paths: string[] = [];
|
|
55
|
+
const visit = (directory: string): void => {
|
|
56
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
if (!ignoredLockDirectories.has(entry.name)) {
|
|
59
|
+
visit(join(directory, entry.name));
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (entry.isFile() && entry.name === 'trails.lock') {
|
|
64
|
+
paths.push(join(directory, entry.name));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
visit(rootDir);
|
|
69
|
+
return paths.toSorted(compareCodeUnits);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const lockStateHash = (
|
|
73
|
+
rootDir: string
|
|
74
|
+
): TrailsResult<string, InternalError> => {
|
|
75
|
+
try {
|
|
76
|
+
const locks = collectLockPaths(rootDir).map((absolutePath) => ({
|
|
77
|
+
contentHash: createHash('sha256')
|
|
78
|
+
.update(readFileSync(absolutePath))
|
|
79
|
+
.digest('hex'),
|
|
80
|
+
path: normalizePath(relative(rootDir, absolutePath)),
|
|
81
|
+
}));
|
|
82
|
+
return Result.ok(regradeReceiptContentHash(locks));
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return Result.err(
|
|
85
|
+
new InternalError('Failed to derive Regrade lock identity.', {
|
|
86
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
87
|
+
context: { rootDir },
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const projectRuleStateHash = (
|
|
94
|
+
rootDir: string
|
|
95
|
+
): TrailsResult<string, InternalError> => {
|
|
96
|
+
try {
|
|
97
|
+
const root = realpathSync(rootDir);
|
|
98
|
+
const entryPaths: string[] = [];
|
|
99
|
+
const rulesFile = join(root, '.trails/rules.ts');
|
|
100
|
+
const rulesDirectory = join(root, '.trails/rules');
|
|
101
|
+
if (existsSync(rulesFile)) {
|
|
102
|
+
entryPaths.push(rulesFile);
|
|
103
|
+
}
|
|
104
|
+
if (existsSync(rulesDirectory)) {
|
|
105
|
+
for (const entry of readdirSync(rulesDirectory, {
|
|
106
|
+
withFileTypes: true,
|
|
107
|
+
})) {
|
|
108
|
+
if (
|
|
109
|
+
entry.isFile() &&
|
|
110
|
+
entry.name.endsWith('.ts') &&
|
|
111
|
+
!entry.name.endsWith('.test.ts') &&
|
|
112
|
+
!entry.name.startsWith('_')
|
|
113
|
+
) {
|
|
114
|
+
entryPaths.push(join(rulesDirectory, entry.name));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const paths = new Set<string>();
|
|
120
|
+
const visitModule = (absolutePath: string): void => {
|
|
121
|
+
if (paths.has(absolutePath)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
paths.add(absolutePath);
|
|
125
|
+
const extension = extname(absolutePath);
|
|
126
|
+
if (
|
|
127
|
+
![
|
|
128
|
+
'.cjs',
|
|
129
|
+
'.cts',
|
|
130
|
+
'.js',
|
|
131
|
+
'.jsx',
|
|
132
|
+
'.mjs',
|
|
133
|
+
'.mts',
|
|
134
|
+
'.ts',
|
|
135
|
+
'.tsx',
|
|
136
|
+
].includes(extension)
|
|
137
|
+
) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const imports = new Bun.Transpiler({
|
|
141
|
+
loader: loaderForModuleExtension(extension),
|
|
142
|
+
}).scanImports(readFileSync(absolutePath, 'utf8'));
|
|
143
|
+
for (const imported of imports) {
|
|
144
|
+
const resolved = Bun.resolveSync(imported.path, dirname(absolutePath));
|
|
145
|
+
const relativePath = relative(root, resolved);
|
|
146
|
+
if (
|
|
147
|
+
isAbsolute(resolved) &&
|
|
148
|
+
relativePath !== '..' &&
|
|
149
|
+
!relativePath.startsWith(`..${sep}`) &&
|
|
150
|
+
!relativePath.split(sep).includes('node_modules')
|
|
151
|
+
) {
|
|
152
|
+
visitModule(resolved);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
for (const entryPath of entryPaths) {
|
|
157
|
+
visitModule(entryPath);
|
|
158
|
+
}
|
|
159
|
+
return Result.ok(
|
|
160
|
+
regradeReceiptContentHash(
|
|
161
|
+
[...paths].toSorted(compareCodeUnits).map((absolutePath) => ({
|
|
162
|
+
contentHash: createHash('sha256')
|
|
163
|
+
.update(readFileSync(absolutePath))
|
|
164
|
+
.digest('hex'),
|
|
165
|
+
path: normalizePath(relative(rootDir, absolutePath)),
|
|
166
|
+
}))
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return Result.err(
|
|
171
|
+
new InternalError('Failed to derive Regrade policy identity.', {
|
|
172
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
173
|
+
context: { rootDir },
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Build the command-local reuse identity from receipt-aligned facts.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* const identity = preparedRegradeRunIdentity({ artifact, rootDir: '.' });
|
|
185
|
+
* if (identity.isOk()) console.log(identity.value.planContentHash);
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
export const preparedRegradeRunIdentity = (params: {
|
|
189
|
+
readonly artifact: RegradePlanArtifact;
|
|
190
|
+
readonly classIds?: readonly string[] | undefined;
|
|
191
|
+
readonly classes?: readonly RegradeClass[] | undefined;
|
|
192
|
+
readonly includeEntries: 'actionable' | 'all';
|
|
193
|
+
readonly rootDir: string;
|
|
194
|
+
}): TrailsResult<PreparedRegradeRunIdentity, Error> => {
|
|
195
|
+
const parsedPlan = regradeReceiptPlanSchema.safeParse(params.artifact.plan);
|
|
196
|
+
if (!parsedPlan.success) {
|
|
197
|
+
return Result.err(
|
|
198
|
+
new InternalError('Failed to derive Regrade plan identity.', {
|
|
199
|
+
context: { issues: parsedPlan.error.issues },
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const lockHash = lockStateHash(params.rootDir);
|
|
204
|
+
if (lockHash.isErr()) {
|
|
205
|
+
return lockHash;
|
|
206
|
+
}
|
|
207
|
+
const usesProjectRules = parsedPlan.data.kind === 'class';
|
|
208
|
+
const policyStateHash = usesProjectRules
|
|
209
|
+
? projectRuleStateHash(params.rootDir)
|
|
210
|
+
: Result.ok(null);
|
|
211
|
+
if (policyStateHash.isErr()) {
|
|
212
|
+
return policyStateHash;
|
|
213
|
+
}
|
|
214
|
+
return Result.ok({
|
|
215
|
+
lockStateHash: lockHash.value,
|
|
216
|
+
planContentHash: regradeReceiptPlanContentHash({
|
|
217
|
+
plan: parsedPlan.data,
|
|
218
|
+
provenance: params.artifact.provenance,
|
|
219
|
+
}),
|
|
220
|
+
policyHash: regradeReceiptContentHash({
|
|
221
|
+
classIds: [...(params.classIds ?? [])].toSorted(),
|
|
222
|
+
classes: (params.classes ?? []).map((regradeClass) => ({
|
|
223
|
+
describe: regradeClass.describe,
|
|
224
|
+
id: regradeClass.id,
|
|
225
|
+
scanTargets: regradeClass.scanTargets ?? null,
|
|
226
|
+
})),
|
|
227
|
+
...(policyStateHash.value === null
|
|
228
|
+
? {}
|
|
229
|
+
: { projectRulesHash: policyStateHash.value }),
|
|
230
|
+
transitions: listGovernedVocabularyTransitions(),
|
|
231
|
+
}),
|
|
232
|
+
scopeHash: regradeReceiptContentHash({
|
|
233
|
+
includeEntries: params.includeEntries,
|
|
234
|
+
scope: params.artifact.plan.scope ?? null,
|
|
235
|
+
}),
|
|
236
|
+
toolVersion: trailsPackageVersion,
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/** Reject an active plan that changed after its prepared evaluation. */
|
|
241
|
+
export const validatePreparedRegradePlanArtifact = (params: {
|
|
242
|
+
readonly current: RegradePlanArtifact;
|
|
243
|
+
readonly currentPath: string;
|
|
244
|
+
readonly expected: RegradePlanArtifact;
|
|
245
|
+
readonly expectedPath: string;
|
|
246
|
+
}): TrailsResult<void, ValidationError> => {
|
|
247
|
+
if (
|
|
248
|
+
params.currentPath !== params.expectedPath ||
|
|
249
|
+
canonicalJsonStringify(params.current) !==
|
|
250
|
+
canonicalJsonStringify(params.expected)
|
|
251
|
+
) {
|
|
252
|
+
return Result.err(
|
|
253
|
+
new ValidationError('Regrade plan changed during apply preflight.', {
|
|
254
|
+
context: { plan: params.expected.path },
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return Result.ok();
|
|
259
|
+
};
|