ast-mcp-server 0.13.0 → 0.14.0
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 +25 -1
- package/README.md +56 -25
- package/cordis.patch.yml +8 -1
- package/dist/batch/schema.js +1 -1
- package/dist/server.js +15 -1
- package/dist/services/authenticated-publication.js +181 -0
- package/dist/services/call-spines.js +10 -5
- package/dist/services/compiler-worker-host.js +36 -16
- package/dist/services/compiler-worker-protocol.js +70 -2
- package/dist/services/context-builder.js +2 -1
- package/dist/services/diagnostics.js +293 -9
- package/dist/services/h03-timeout-fixture.js +335 -0
- package/dist/services/impact.js +46 -14
- package/dist/services/managed-file.js +3 -50
- package/dist/services/operation-plan-file.js +17 -18
- package/dist/services/operations.js +231 -87
- package/dist/services/project.js +29 -16
- package/dist/services/public-errors.js +7 -5
- package/dist/services/relationships.js +403 -4
- package/dist/services/runtime-policy.js +68 -2
- package/dist/services/test-candidates.js +35 -2
- package/dist/tools/explore.js +5 -3
- package/dist/tools/find_test_candidates.js +24 -3
- package/dist/tools/get_impact.js +12 -3
- package/dist/tools/relationship-schema.js +12 -1
- package/dist/tools/result.js +48 -0
- package/dist/worker-runtime.js +6 -1
- package/docs/support.md +4 -2
- package/package.json +16 -8
- package/skills/structural-code-editing/SKILL.md +8 -0
- package/skills/structural-code-editing/releases.json +1 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { appendFileSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { PUBLIC_ERROR_CODES, sanitizePublicText } from "./public-errors.js";
|
|
5
|
+
import { parseH03FixtureDescriptor, parseH05FixtureDescriptor, } from "./runtime-policy.js";
|
|
6
|
+
const FIXTURE_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
7
|
+
export class H03TimeoutFixtureError extends Error {
|
|
8
|
+
name = "H03TimeoutFixtureError";
|
|
9
|
+
code = "STALE_GENERATION";
|
|
10
|
+
}
|
|
11
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12
|
+
/** Closed test-only lifecycle controller used by H-05 through ast_get_project_status. */
|
|
13
|
+
export class H05LifecycleFixtureController {
|
|
14
|
+
#descriptor;
|
|
15
|
+
#generationAbort = new AbortController();
|
|
16
|
+
#gates = new Map();
|
|
17
|
+
#events = [];
|
|
18
|
+
#active = 0;
|
|
19
|
+
#abortListeners = 0;
|
|
20
|
+
#staleSettlements = 0;
|
|
21
|
+
// prettier-ignore
|
|
22
|
+
constructor(descriptor) { if (!statSync(descriptor.controlDirectory, { throwIfNoEntry: false })?.isDirectory())
|
|
23
|
+
throw new Error("H-05 fixture control path must be an existing directory."); this.#descriptor = Object.freeze({ ...descriptor }); }
|
|
24
|
+
// prettier-ignore
|
|
25
|
+
capture(command) { if (Object.keys(command).sort().join(",") !== "callId,correlationId,fixtureId" || !FIXTURE_ID.test(command.callId) || !FIXTURE_ID.test(command.fixtureId) || !UUID.test(command.correlationId))
|
|
26
|
+
throw new Error("Invalid H-05 fixture command."); return Object.freeze({ ...command, generation: this.#descriptor.generation, ownerToken: this.#descriptor.ownerToken }); }
|
|
27
|
+
// prettier-ignore
|
|
28
|
+
hold(identity) { this.#assertCurrent(identity); const key = this.#key(identity); if (this.#gates.has(key))
|
|
29
|
+
throw new Error("Invalid or duplicate H-05 fixture id."); let release; const promise = new Promise((resolve) => (release = resolve)); this.#gates.set(key, { promise, release }); }
|
|
30
|
+
// prettier-ignore
|
|
31
|
+
advance(descriptor) { if (descriptor.generation <= this.#descriptor.generation)
|
|
32
|
+
throw new Error("H-05 fixture generation must increase."); this.#descriptor = Object.freeze({ ...descriptor }); this.#generationAbort.abort(); this.#generationAbort = new AbortController(); for (const gate of this.#gates.values())
|
|
33
|
+
gate.release(); this.#gates.clear(); }
|
|
34
|
+
// prettier-ignore
|
|
35
|
+
async run(identity, context, operation) {
|
|
36
|
+
this.#assertIdentity(identity);
|
|
37
|
+
const eventIdentity = { callId: identity.callId, correlationId: identity.correlationId, fixtureId: identity.fixtureId, generation: identity.generation, ownerToken: identity.ownerToken };
|
|
38
|
+
const generationSignal = this.#generationAbort.signal, signal = AbortSignal.any([context.signal, generationSignal]);
|
|
39
|
+
const checkpoint = () => { if (!this.accepts(identity) || generationSignal.aborted)
|
|
40
|
+
throw new H03TimeoutFixtureError(); context.checkpoint(); };
|
|
41
|
+
const hooked = Object.freeze({ get sequence() { return context.sequence; }, get phase() { return context.phase; }, signal, checkpoint, markExecuting: () => context.markExecuting(), enterCompletionCritical: () => { checkpoint(); context.enterCompletionCritical(); } });
|
|
42
|
+
this.#active += 1;
|
|
43
|
+
this.#emit({ ...eventIdentity, phase: "started" });
|
|
44
|
+
try {
|
|
45
|
+
const gate = this.#gates.get(this.#key(identity));
|
|
46
|
+
if (gate)
|
|
47
|
+
await this.#awaitGate(identity, gate, hooked);
|
|
48
|
+
checkpoint();
|
|
49
|
+
const result = await operation(hooked);
|
|
50
|
+
checkpoint();
|
|
51
|
+
this.#emit({ ...eventIdentity, phase: "terminal", outcome: "succeeded" });
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const code = error?.code, stale = !this.accepts(identity) || generationSignal.aborted, deadline = code === "OPERATION_DEADLINE_EXCEEDED", cancelled = code === "REQUEST_CANCELLED" || context.signal.aborted;
|
|
56
|
+
if (stale)
|
|
57
|
+
this.#staleSettlements += 1;
|
|
58
|
+
this.#emit({ ...eventIdentity, phase: "terminal", outcome: stale ? "STALE_GENERATION" : deadline ? code : cancelled ? "REQUEST_CANCELLED" : "failed" });
|
|
59
|
+
if (stale)
|
|
60
|
+
this.#staleSettlements -= 1;
|
|
61
|
+
throw stale ? new H03TimeoutFixtureError() : error;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
this.#active -= 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// prettier-ignore
|
|
68
|
+
drainEvents() { const events = Object.freeze(this.#events); this.#events = []; return events; }
|
|
69
|
+
// prettier-ignore
|
|
70
|
+
snapshot() { return Object.freeze({ active: this.#active, held: this.#gates.size, abortListeners: this.#abortListeners, timers: 0, staleSettlements: this.#staleSettlements }); }
|
|
71
|
+
// prettier-ignore
|
|
72
|
+
async #awaitGate(identity, gate, context) { let rejectAbort; const aborted = new Promise((_, reject) => (rejectAbort = reject)); const abort = () => { this.#gates.delete(this.#key(identity)); try {
|
|
73
|
+
context.checkpoint();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
rejectAbort(error);
|
|
77
|
+
} }; context.signal.addEventListener("abort", abort, { once: true }); this.#abortListeners += 1; try {
|
|
78
|
+
if (context.signal.aborted)
|
|
79
|
+
abort();
|
|
80
|
+
await Promise.race([gate.promise, aborted]);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
context.signal.removeEventListener("abort", abort);
|
|
84
|
+
this.#abortListeners -= 1;
|
|
85
|
+
} }
|
|
86
|
+
// prettier-ignore
|
|
87
|
+
#emit(event) { const normalized = Object.freeze(Object.fromEntries(Object.entries(event).filter(([, value]) => value !== undefined))); this.#events.push(normalized); appendFileSync(path.join(this.#descriptor.controlDirectory, "events.jsonl"), `${JSON.stringify(normalized)}\n`, { encoding: "utf8", flag: "a" }); }
|
|
88
|
+
// prettier-ignore
|
|
89
|
+
accepts(identity) { return identity.generation === this.#descriptor.generation && identity.ownerToken === this.#descriptor.ownerToken; }
|
|
90
|
+
// prettier-ignore
|
|
91
|
+
#assertCurrent(identity) { this.#assertIdentity(identity); if (!this.accepts(identity))
|
|
92
|
+
throw new H03TimeoutFixtureError(); }
|
|
93
|
+
// prettier-ignore
|
|
94
|
+
#assertIdentity(identity) { if (!Object.isFrozen(identity) || !FIXTURE_ID.test(identity.callId) || !FIXTURE_ID.test(identity.fixtureId) || !UUID.test(identity.correlationId))
|
|
95
|
+
throw new Error("Invalid H-05 lifecycle identity."); }
|
|
96
|
+
// prettier-ignore
|
|
97
|
+
#key(identity) { return `${identity.generation}:${identity.fixtureId}`; }
|
|
98
|
+
}
|
|
99
|
+
export class H03TimeoutFixtureController {
|
|
100
|
+
#descriptor;
|
|
101
|
+
#gates = new Map();
|
|
102
|
+
#events = [];
|
|
103
|
+
#generation;
|
|
104
|
+
#generationAbort = new AbortController();
|
|
105
|
+
#active = 0;
|
|
106
|
+
#abortListeners = 0;
|
|
107
|
+
#staleSettlements = 0;
|
|
108
|
+
constructor(descriptor) {
|
|
109
|
+
const directory = statSync(descriptor.controlDirectory, { throwIfNoEntry: false });
|
|
110
|
+
if (!directory?.isDirectory())
|
|
111
|
+
throw new Error("H-03 fixture control path must be an existing directory.");
|
|
112
|
+
this.#descriptor = descriptor;
|
|
113
|
+
this.#generation = descriptor.generation;
|
|
114
|
+
}
|
|
115
|
+
hold(fixtureId) {
|
|
116
|
+
this.#assertId(fixtureId);
|
|
117
|
+
const key = this.#key(fixtureId, this.#generation);
|
|
118
|
+
if (this.#gates.has(key))
|
|
119
|
+
throw new Error("Invalid or duplicate H-03 fixture id.");
|
|
120
|
+
let release;
|
|
121
|
+
const promise = new Promise((resolve) => (release = resolve));
|
|
122
|
+
this.#gates.set(key, { promise, release });
|
|
123
|
+
}
|
|
124
|
+
release(fixtureId, generation) {
|
|
125
|
+
if (!FIXTURE_ID.test(fixtureId) || generation !== this.#generation)
|
|
126
|
+
return false;
|
|
127
|
+
const key = this.#key(fixtureId, generation);
|
|
128
|
+
const gate = this.#gates.get(key);
|
|
129
|
+
if (!gate)
|
|
130
|
+
return false;
|
|
131
|
+
this.#gates.delete(key);
|
|
132
|
+
gate.release();
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
advanceGeneration(generation) {
|
|
136
|
+
if (!Number.isSafeInteger(generation) || generation <= this.#generation)
|
|
137
|
+
throw new Error("H-03 fixture generation must increase.");
|
|
138
|
+
this.#generation = generation;
|
|
139
|
+
this.#generationAbort.abort();
|
|
140
|
+
this.#generationAbort = new AbortController();
|
|
141
|
+
for (const gate of this.#gates.values())
|
|
142
|
+
gate.release();
|
|
143
|
+
this.#gates.clear();
|
|
144
|
+
}
|
|
145
|
+
async run(fixtureId, generation, context, operation, callId = fixtureId) {
|
|
146
|
+
this.#assertId(fixtureId);
|
|
147
|
+
this.#assertId(callId);
|
|
148
|
+
if (generation !== this.#generation)
|
|
149
|
+
return this.#stale(fixtureId, generation, callId);
|
|
150
|
+
const generationSignal = this.#generationAbort.signal;
|
|
151
|
+
const signal = AbortSignal.any([context.signal, generationSignal]);
|
|
152
|
+
const hooked = this.#context(context, signal, generationSignal, fixtureId, generation, callId);
|
|
153
|
+
this.#active += 1;
|
|
154
|
+
this.#emit({ callId, fixtureId, generation, nonce: this.#descriptor.nonce, phase: "started" });
|
|
155
|
+
try {
|
|
156
|
+
const gate = this.#gates.get(this.#key(fixtureId, generation));
|
|
157
|
+
if (gate)
|
|
158
|
+
await this.#awaitGate(this.#key(fixtureId, generation), gate, hooked);
|
|
159
|
+
hooked.checkpoint();
|
|
160
|
+
const result = await operation(hooked);
|
|
161
|
+
hooked.checkpoint();
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
this.#active -= 1;
|
|
166
|
+
this.#emit({
|
|
167
|
+
callId,
|
|
168
|
+
fixtureId,
|
|
169
|
+
generation,
|
|
170
|
+
nonce: this.#descriptor.nonce,
|
|
171
|
+
phase: "terminal",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
drainEvents() {
|
|
176
|
+
const events = Object.freeze(this.#events);
|
|
177
|
+
this.#events = [];
|
|
178
|
+
return events;
|
|
179
|
+
}
|
|
180
|
+
snapshot() {
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
active: this.#active,
|
|
183
|
+
held: this.#gates.size,
|
|
184
|
+
abortListeners: this.#abortListeners,
|
|
185
|
+
staleSettlements: this.#staleSettlements,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
#context(context, signal, generationSignal, fixtureId, generation, callId) {
|
|
189
|
+
const checkpoint = () => {
|
|
190
|
+
if (generationSignal.aborted || generation !== this.#generation)
|
|
191
|
+
this.#stale(fixtureId, generation, callId);
|
|
192
|
+
context.checkpoint();
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
get sequence() {
|
|
196
|
+
return context.sequence;
|
|
197
|
+
},
|
|
198
|
+
get phase() {
|
|
199
|
+
return context.phase;
|
|
200
|
+
},
|
|
201
|
+
signal,
|
|
202
|
+
checkpoint,
|
|
203
|
+
markExecuting: () => context.markExecuting(),
|
|
204
|
+
enterCompletionCritical: () => {
|
|
205
|
+
checkpoint();
|
|
206
|
+
context.enterCompletionCritical();
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async #awaitGate(key, gate, context) {
|
|
211
|
+
let rejectAbort;
|
|
212
|
+
const aborted = new Promise((_, reject) => (rejectAbort = reject));
|
|
213
|
+
const abort = () => {
|
|
214
|
+
if (this.#gates.get(key) === gate)
|
|
215
|
+
this.#gates.delete(key);
|
|
216
|
+
try {
|
|
217
|
+
context.checkpoint();
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
rejectAbort(error);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
context.signal.addEventListener("abort", abort, { once: true });
|
|
224
|
+
this.#abortListeners += 1;
|
|
225
|
+
try {
|
|
226
|
+
if (context.signal.aborted)
|
|
227
|
+
abort();
|
|
228
|
+
await Promise.race([gate.promise, aborted]);
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
context.signal.removeEventListener("abort", abort);
|
|
232
|
+
this.#abortListeners -= 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
#stale(fixtureId, generation, callId) {
|
|
236
|
+
this.#staleSettlements += 1;
|
|
237
|
+
this.#emit({ callId, fixtureId, generation, nonce: this.#descriptor.nonce, phase: "stale" });
|
|
238
|
+
throw new H03TimeoutFixtureError();
|
|
239
|
+
}
|
|
240
|
+
#emit(event) {
|
|
241
|
+
this.#events.push(Object.freeze(event));
|
|
242
|
+
appendFileSync(path.join(this.#descriptor.controlDirectory, "events.jsonl"), `${JSON.stringify(event)}\n`, { encoding: "utf8", flag: "a" });
|
|
243
|
+
}
|
|
244
|
+
#assertId(fixtureId) {
|
|
245
|
+
if (!FIXTURE_ID.test(fixtureId))
|
|
246
|
+
throw new Error("Invalid H-03 fixture id.");
|
|
247
|
+
}
|
|
248
|
+
#key(fixtureId, generation) {
|
|
249
|
+
return `${generation}:${fixtureId}`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
let configuredH05Controller;
|
|
253
|
+
let configuredH05Descriptor;
|
|
254
|
+
// Capture the closed command once before scheduler enqueue; mutable files cannot relabel it.
|
|
255
|
+
// prettier-ignore
|
|
256
|
+
export function captureConfiguredH05Command() { const raw = process.env.AST_H05_FIXTURE; if (raw === undefined)
|
|
257
|
+
return undefined; const descriptor = parseH05FixtureDescriptor(raw); if (!descriptor)
|
|
258
|
+
throw new Error("Invalid H-05 fixture descriptor."); if (configuredH05Descriptor) {
|
|
259
|
+
const sameGeneration = descriptor.generation === configuredH05Descriptor.generation, sameIdentity = descriptor.controlDirectory === configuredH05Descriptor.controlDirectory && descriptor.nonce === configuredH05Descriptor.nonce && descriptor.ownerToken === configuredH05Descriptor.ownerToken;
|
|
260
|
+
if (sameGeneration && !sameIdentity)
|
|
261
|
+
throw new Error("H-05 fixture descriptor drift.");
|
|
262
|
+
if (descriptor.generation < configuredH05Descriptor.generation)
|
|
263
|
+
throw new Error("H-05 fixture generation regressed.");
|
|
264
|
+
if (descriptor.generation > configuredH05Descriptor.generation) {
|
|
265
|
+
configuredH05Controller?.advance({ ...configuredH05Descriptor, generation: descriptor.generation });
|
|
266
|
+
configuredH05Controller = undefined;
|
|
267
|
+
configuredH05Descriptor = descriptor;
|
|
268
|
+
const replacement = new H05LifecycleFixtureController(descriptor);
|
|
269
|
+
configuredH05Controller = replacement;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
configuredH05Controller = new H05LifecycleFixtureController(descriptor);
|
|
274
|
+
configuredH05Descriptor = descriptor;
|
|
275
|
+
} if (!configuredH05Controller)
|
|
276
|
+
throw new Error("H-05 fixture generation is retired."); const value = JSON.parse(readFileSync(path.join(descriptor.controlDirectory, "command.json"), "utf8")); if (Object.keys(value).sort().join(",") !== "callId,correlationId,fixtureId,mode,nonce,ownerToken" || value.nonce !== descriptor.nonce || value.ownerToken !== descriptor.ownerToken || !["hold", "pass", "readback"].includes(value.mode))
|
|
277
|
+
throw new Error("Invalid H-05 fixture command."); const command = Object.freeze({ ...value }); const captured = configuredH05Controller.capture({ callId: command.callId, correlationId: command.correlationId, fixtureId: command.fixtureId }); return Object.freeze({ controller: configuredH05Controller, descriptor, command, captured }); }
|
|
278
|
+
// prettier-ignore
|
|
279
|
+
export async function runConfiguredH05Fixture(context, operation, configured) { const { controller, descriptor, command, captured } = configured; if (!controller.accepts(captured))
|
|
280
|
+
throw new H03TimeoutFixtureError(); if (command.mode === "readback") {
|
|
281
|
+
const snapshot = controller.snapshot(), eventsDrained = controller.drainEvents().length;
|
|
282
|
+
appendFileSync(path.join(descriptor.controlDirectory, "events.jsonl"), `${JSON.stringify({ ...snapshot, callId: captured.callId, correlationId: captured.correlationId, eventsDrained, fixtureId: captured.fixtureId, generation: captured.generation, phase: "readback" })}\n`);
|
|
283
|
+
const result = await operation(context);
|
|
284
|
+
if (!controller.accepts(captured))
|
|
285
|
+
throw new H03TimeoutFixtureError();
|
|
286
|
+
context.checkpoint();
|
|
287
|
+
return result;
|
|
288
|
+
} if (command.mode === "hold")
|
|
289
|
+
controller.hold(captured); return controller.run(captured, context, operation); }
|
|
290
|
+
let configuredController;
|
|
291
|
+
let configuredDescriptor;
|
|
292
|
+
const errorCommands = new WeakMap();
|
|
293
|
+
// Compact closed test-only wiring preserves PR3's hard review budget.
|
|
294
|
+
// prettier-ignore
|
|
295
|
+
function configuredFixture() { const descriptor = parseH03FixtureDescriptor(process.env.AST_H03_FIXTURE); if (!descriptor)
|
|
296
|
+
return undefined; if (!configuredController || configuredDescriptor?.generation !== descriptor.generation) {
|
|
297
|
+
configuredDescriptor = descriptor;
|
|
298
|
+
configuredController = new H03TimeoutFixtureController(descriptor);
|
|
299
|
+
} return { controller: configuredController, descriptor }; }
|
|
300
|
+
// prettier-ignore
|
|
301
|
+
function readCommand(descriptor) { const value = JSON.parse(readFileSync(path.join(descriptor.controlDirectory, "command.json"), "utf8")); if (Object.keys(value).sort().join(",") !== "callId,fixtureId,mode,nonce" || typeof value.callId !== "string" || !FIXTURE_ID.test(value.callId) || typeof value.fixtureId !== "string" || !FIXTURE_ID.test(value.fixtureId) || value.nonce !== descriptor.nonce || !["hold", "pass", "readback"].includes(value.mode))
|
|
302
|
+
throw new Error("Invalid H-03 fixture command."); return value; }
|
|
303
|
+
// Capture once before scheduler enqueue; later command.json writes cannot relabel this request.
|
|
304
|
+
// prettier-ignore
|
|
305
|
+
export function captureConfiguredH03Command() { const fixture = configuredFixture(); return fixture && Object.freeze({ ...fixture, command: readCommand(fixture.descriptor) }); }
|
|
306
|
+
// prettier-ignore
|
|
307
|
+
export function bindConfiguredH03Error(error, captured) { if (captured && typeof error === "object" && error !== null)
|
|
308
|
+
errorCommands.set(error, captured); }
|
|
309
|
+
export function configuredH05CorrelationId(error) {
|
|
310
|
+
const configured = typeof error === "object" && error !== null ? errorCommands.get(error) : undefined;
|
|
311
|
+
return configured && "captured" in configured ? configured.captured.correlationId : undefined;
|
|
312
|
+
}
|
|
313
|
+
// prettier-ignore
|
|
314
|
+
export async function runConfiguredH03Fixture(context, operation, captured) { const { controller, descriptor, command } = captured; if (command.mode === "readback") {
|
|
315
|
+
const snapshot = controller.snapshot(), eventsDrained = controller.drainEvents().length;
|
|
316
|
+
appendFileSync(path.join(descriptor.controlDirectory, "events.jsonl"), `${JSON.stringify({ ...snapshot, callId: command.callId, eventsDrained, fixtureId: command.fixtureId, generation: descriptor.generation, nonce: descriptor.nonce, phase: "readback" })}\n`);
|
|
317
|
+
return operation(context);
|
|
318
|
+
} if (command.mode === "hold")
|
|
319
|
+
controller.hold(command.fixtureId); return controller.run(command.fixtureId, descriptor.generation, context, operation, command.callId); }
|
|
320
|
+
// prettier-ignore
|
|
321
|
+
export function emitConfiguredH03HostEvent(phase, generation) { const descriptor = parseH03FixtureDescriptor(process.env.AST_H03_FIXTURE); if (!descriptor)
|
|
322
|
+
return; appendFileSync(path.join(descriptor.controlDirectory, "events.jsonl"), `${JSON.stringify({ fixtureId: "host", generation, nonce: descriptor.nonce, phase })}\n`, { encoding: "utf8", flag: "a" }); }
|
|
323
|
+
// prettier-ignore
|
|
324
|
+
export function emitConfiguredH03ErrorEvidence(text, error) { const configured = typeof error === "object" && error !== null ? errorCommands.get(error) : undefined; if (!configured)
|
|
325
|
+
return; const { descriptor, command } = configured; if (!("captured" in configured)) {
|
|
326
|
+
appendFileSync(path.join(descriptor.controlDirectory, "events.jsonl"), `${JSON.stringify({ callId: command.callId, fixtureId: command.fixtureId, generation: descriptor.generation, nonce: descriptor.nonce, phase: "error", result: { isError: true, error: { info: { name: error instanceof Error ? error.name : "Error" }, message: text } } })}\n`);
|
|
327
|
+
return;
|
|
328
|
+
} const { controller, captured } = configured; if (!controller.accepts(captured))
|
|
329
|
+
return; let publicError; try {
|
|
330
|
+
publicError = JSON.parse(text);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
publicError = undefined;
|
|
334
|
+
} const source = publicError?.error, code = typeof source?.code === "string" && PUBLIC_ERROR_CODES.some((value) => value === source.code) ? source.code : "INTERNAL_ERROR", correlation_id = typeof source?.correlation_id === "string" && UUID.test(source.correlation_id) ? source.correlation_id : captured.correlationId, name = error instanceof Error && /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(error.name) ? error.name : "Error", message = typeof source?.message === "string" ? sanitizePublicText(source.message) : "An internal error occurred."; const identity = { callId: captured.callId, correlationId: captured.correlationId, fixtureId: captured.fixtureId, generation: captured.generation, ownerToken: captured.ownerToken, phase: "error", resources: controller.snapshot(), result: { isError: true, error: { code, message, correlation_id, info: { name } } } }; let serialized = JSON.stringify(identity); if (Buffer.byteLength(serialized) > 4096)
|
|
335
|
+
serialized = JSON.stringify({ ...identity, result: { ...identity.result, error: { ...identity.result.error, message: "Evidence message omitted: bounded record exceeded." } } }); appendFileSync(path.join(descriptor.controlDirectory, "events.jsonl"), `${serialized}\n`); }
|
package/dist/services/impact.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createTruncationMetadata, createSourceLocation, } from "./read-contract
|
|
|
3
3
|
import { getSourceFileOrThrow } from "./project.js";
|
|
4
4
|
import { findLocatedDeclaration } from "./symbols.js";
|
|
5
5
|
import { NO_REQUEST_CONTEXT } from "./request-context.js";
|
|
6
|
-
import { createCompilerRelationshipResolver, RELATIONSHIP_EDGE_KINDS, } from "./relationships.js";
|
|
6
|
+
import { createCompilerRelationshipResolver, isRelationshipCoverageComplete, mergeRelationshipCoverage, RELATIONSHIP_EDGE_KINDS, } from "./relationships.js";
|
|
7
7
|
export const IMPACT_DIRECTIONS = Object.freeze(["incoming", "outgoing", "both"]);
|
|
8
8
|
export const DEFAULT_IMPACT_MAX_DEPTH = 3;
|
|
9
9
|
export const DEFAULT_IMPACT_MAX_NODES = 100;
|
|
@@ -24,7 +24,7 @@ export function isExactImpactEdge(edge) {
|
|
|
24
24
|
Array.isArray(edge.freshness.causes) &&
|
|
25
25
|
edge.freshness.causes.length === 0);
|
|
26
26
|
}
|
|
27
|
-
export function
|
|
27
|
+
export function assertCompleteExactImpactEvidence(impact) {
|
|
28
28
|
if (typeof impact !== "object" || impact === null || !Array.isArray(impact.edges)) {
|
|
29
29
|
throw new Error("Impact evidence is invalid.");
|
|
30
30
|
}
|
|
@@ -35,6 +35,9 @@ export function assertExactImpactEvidence(impact) {
|
|
|
35
35
|
throw new Error("Test candidates require fresh exact compiler impact evidence.");
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
export function assertExactImpactEvidence(impact) {
|
|
39
|
+
assertCompleteExactImpactEvidence(impact);
|
|
40
|
+
}
|
|
38
41
|
const IMPACT_PROBE_WORK_ITEMS = 1_024;
|
|
39
42
|
const IMPACT_RELATIONSHIP_WORK_ITEMS = 100_000;
|
|
40
43
|
function assertIntegerInRange(value, minimum, maximum, name) {
|
|
@@ -152,7 +155,7 @@ function collectNeighbors(current, edges, options, requestContext) {
|
|
|
152
155
|
}
|
|
153
156
|
return [...neighbors.values()].sort(neighborOrder);
|
|
154
157
|
}
|
|
155
|
-
function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestContext = NO_REQUEST_CONTEXT) {
|
|
158
|
+
function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestContext = NO_REQUEST_CONTEXT, semanticAuthority = false) {
|
|
156
159
|
requestContext.checkpoint();
|
|
157
160
|
if (typeof root !== "object" || root === null) {
|
|
158
161
|
throw new Error("Impact traversal input is invalid.");
|
|
@@ -165,6 +168,27 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
165
168
|
const queue = [{ key: endpointKey(root), endpoint: root, depth: 0 }];
|
|
166
169
|
const truncationReasons = new Set();
|
|
167
170
|
let maxDepthReached = 0;
|
|
171
|
+
let coverage = [];
|
|
172
|
+
let relationshipWorkItems = 0;
|
|
173
|
+
let relationshipWorkLimitReached = false;
|
|
174
|
+
const resolveNeighbors = (current, providerOptions, context, maxEdges, stopAfterFirst, allowedNeighborKeys, excludedRelationshipIds, maxWorkItems) => {
|
|
175
|
+
const remainingWorkItems = IMPACT_RELATIONSHIP_WORK_ITEMS - relationshipWorkItems;
|
|
176
|
+
if (remainingWorkItems <= 0) {
|
|
177
|
+
relationshipWorkLimitReached = true;
|
|
178
|
+
return {
|
|
179
|
+
neighbors: [],
|
|
180
|
+
incomplete: true,
|
|
181
|
+
excludedNeighbors: false,
|
|
182
|
+
workLimitReached: true,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const result = neighborsFor(current, providerOptions, context, maxEdges, stopAfterFirst, allowedNeighborKeys, excludedRelationshipIds, Math.min(maxWorkItems ?? remainingWorkItems, remainingWorkItems));
|
|
186
|
+
coverage = mergeRelationshipCoverage(coverage, result.coverage ?? []);
|
|
187
|
+
relationshipWorkItems += result.work?.work_items ?? 0;
|
|
188
|
+
relationshipWorkLimitReached ||=
|
|
189
|
+
result.workLimitReached || result.work?.work_limit_reached === true;
|
|
190
|
+
return result;
|
|
191
|
+
};
|
|
168
192
|
const classifyObservedNeighbor = (current, neighbor, edgeBlocked) => {
|
|
169
193
|
const nextKey = endpointKey(neighbor.endpoint);
|
|
170
194
|
const isKnownNode = nodes.has(nextKey);
|
|
@@ -182,7 +206,7 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
182
206
|
const current = queue.shift();
|
|
183
207
|
const remainingEdges = normalized.max_edges - selectedEdges.size;
|
|
184
208
|
if (remainingEdges <= 0) {
|
|
185
|
-
const probe =
|
|
209
|
+
const probe = resolveNeighbors(current.endpoint, normalized, requestContext, 1, true, undefined, new Set(selectedEdges.keys()), IMPACT_PROBE_WORK_ITEMS);
|
|
186
210
|
for (const neighbor of probe.neighbors) {
|
|
187
211
|
classifyObservedNeighbor(current, neighbor, true);
|
|
188
212
|
}
|
|
@@ -196,11 +220,9 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
196
220
|
? "record_limit"
|
|
197
221
|
: undefined;
|
|
198
222
|
const allowedNeighborKeys = restrictionReason ? new Set(nodes.keys()) : undefined;
|
|
199
|
-
const batch =
|
|
223
|
+
const batch = resolveNeighbors(current.endpoint, normalized, requestContext, remainingEdges, false, allowedNeighborKeys, new Set(selectedEdges.keys()));
|
|
200
224
|
if (batch.excludedNeighbors && restrictionReason)
|
|
201
225
|
truncationReasons.add(restrictionReason);
|
|
202
|
-
if (batch.workLimitReached)
|
|
203
|
-
truncationReasons.add("record_limit");
|
|
204
226
|
let skippedNewNode = false;
|
|
205
227
|
for (const neighbor of batch.neighbors) {
|
|
206
228
|
requestContext.checkpoint();
|
|
@@ -236,7 +258,7 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
236
258
|
if (restrictionReason) {
|
|
237
259
|
if (batch.incomplete && !batch.workLimitReached)
|
|
238
260
|
truncationReasons.add("edge_limit");
|
|
239
|
-
const probe =
|
|
261
|
+
const probe = resolveNeighbors(current.endpoint, normalized, requestContext, 1, true, undefined, new Set(selectedEdges.keys()), IMPACT_PROBE_WORK_ITEMS);
|
|
240
262
|
for (const neighbor of probe.neighbors) {
|
|
241
263
|
classifyObservedNeighbor(current, neighbor, selectedEdges.size >= normalized.max_edges);
|
|
242
264
|
}
|
|
@@ -244,11 +266,9 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
244
266
|
}
|
|
245
267
|
if ((skippedNewNode || nodes.size >= normalized.max_nodes) &&
|
|
246
268
|
selectedEdges.size < normalized.max_edges) {
|
|
247
|
-
const knownBatch =
|
|
269
|
+
const knownBatch = resolveNeighbors(current.endpoint, normalized, requestContext, normalized.max_edges - selectedEdges.size, false, new Set(nodes.keys()), new Set(selectedEdges.keys()));
|
|
248
270
|
if (knownBatch.excludedNeighbors)
|
|
249
271
|
truncationReasons.add("record_limit");
|
|
250
|
-
if (knownBatch.workLimitReached)
|
|
251
|
-
truncationReasons.add("record_limit");
|
|
252
272
|
for (const neighbor of knownBatch.neighbors) {
|
|
253
273
|
requestContext.checkpoint();
|
|
254
274
|
if (selectedEdges.size >= normalized.max_edges) {
|
|
@@ -263,7 +283,7 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
263
283
|
}
|
|
264
284
|
else if (batch.incomplete && !batch.workLimitReached) {
|
|
265
285
|
truncationReasons.add("edge_limit");
|
|
266
|
-
const overflowProbe =
|
|
286
|
+
const overflowProbe = resolveNeighbors(current.endpoint, normalized, requestContext, 1, true, undefined, new Set(selectedEdges.keys()), IMPACT_PROBE_WORK_ITEMS);
|
|
267
287
|
for (const neighbor of overflowProbe.neighbors) {
|
|
268
288
|
classifyObservedNeighbor(current, neighbor, true);
|
|
269
289
|
}
|
|
@@ -271,6 +291,9 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
271
291
|
}
|
|
272
292
|
const orderedReasons = ["depth_limit", "record_limit", "edge_limit"].filter((reason) => truncationReasons.has(reason));
|
|
273
293
|
const truncated = orderedReasons.length > 0;
|
|
294
|
+
const boundedIncomplete = truncated || relationshipWorkLimitReached;
|
|
295
|
+
const semanticIncomplete = boundedIncomplete || !isRelationshipCoverageComplete(coverage);
|
|
296
|
+
const incomplete = semanticAuthority ? semanticIncomplete : boundedIncomplete;
|
|
274
297
|
requestContext.checkpoint();
|
|
275
298
|
return {
|
|
276
299
|
root,
|
|
@@ -287,7 +310,14 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
287
310
|
max_depth: normalized.max_depth,
|
|
288
311
|
max_nodes: normalized.max_nodes,
|
|
289
312
|
max_edges: normalized.max_edges,
|
|
290
|
-
|
|
313
|
+
coverage,
|
|
314
|
+
work: {
|
|
315
|
+
work_items: relationshipWorkItems,
|
|
316
|
+
max_work_items: IMPACT_RELATIONSHIP_WORK_ITEMS,
|
|
317
|
+
work_limit_reached: relationshipWorkLimitReached,
|
|
318
|
+
},
|
|
319
|
+
proven_empty: selectedEdges.size === 0 && !semanticIncomplete,
|
|
320
|
+
incomplete,
|
|
291
321
|
truncation: createTruncationMetadata(truncated, truncated ? orderedReasons[0] : null),
|
|
292
322
|
truncation_reasons: orderedReasons,
|
|
293
323
|
};
|
|
@@ -326,9 +356,11 @@ export function traverseCompilerImpact(project, projectRoot, root, freshness, op
|
|
|
326
356
|
});
|
|
327
357
|
return {
|
|
328
358
|
neighbors: collectNeighbors(current, resolution.edges, normalized, requestContext),
|
|
359
|
+
coverage: resolution.coverage,
|
|
360
|
+
work: resolution.work,
|
|
329
361
|
incomplete: resolution.incomplete,
|
|
330
362
|
excludedNeighbors: resolution.excluded_neighbors,
|
|
331
363
|
workLimitReached: resolution.work_limit_reached,
|
|
332
364
|
};
|
|
333
|
-
}, options, requestContext);
|
|
365
|
+
}, options, requestContext, options.authority === "semantic");
|
|
334
366
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { constants as fsConstants } from "node:fs";
|
|
4
3
|
import { lstat, mkdir, open, realpath, rm } from "node:fs/promises";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import { runPublicationPrimitive } from "./authenticated-publication.js";
|
|
6
6
|
export class ManagedFileApplyError extends Error {
|
|
7
7
|
commitState;
|
|
8
8
|
rollbackState;
|
|
@@ -23,8 +23,6 @@ export class ManagedFileRollbackError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
const GNU_LN_PATH = "/usr/bin/ln";
|
|
25
25
|
const GNU_MV_PATH = "/usr/bin/mv";
|
|
26
|
-
const COREUTILS_TIMEOUT_MS = 5_000;
|
|
27
|
-
const COREUTILS_STDERR_LIMIT = 4_096;
|
|
28
26
|
function errno(error) {
|
|
29
27
|
return error.code;
|
|
30
28
|
}
|
|
@@ -34,51 +32,6 @@ function objectIdentity(stats) {
|
|
|
34
32
|
function identitiesEqual(left, right) {
|
|
35
33
|
return (left !== undefined && right !== undefined && left.dev === right.dev && left.ino === right.ino);
|
|
36
34
|
}
|
|
37
|
-
async function runCoreutils(executable, args, inheritedDescriptors) {
|
|
38
|
-
await new Promise((resolve, reject) => {
|
|
39
|
-
const child = spawn(executable, args, {
|
|
40
|
-
env: { LANG: "C", LC_ALL: "C", PATH: "/usr/bin:/bin" },
|
|
41
|
-
shell: false,
|
|
42
|
-
stdio: ["ignore", "ignore", "pipe", ...inheritedDescriptors],
|
|
43
|
-
windowsHide: true,
|
|
44
|
-
});
|
|
45
|
-
let stderrBytes = 0;
|
|
46
|
-
let settled = false;
|
|
47
|
-
let requestedFailure;
|
|
48
|
-
const requestFailure = (message, cause) => {
|
|
49
|
-
if (settled || requestedFailure !== undefined)
|
|
50
|
-
return;
|
|
51
|
-
requestedFailure = new Error(message, cause === undefined ? undefined : { cause });
|
|
52
|
-
clearTimeout(timeout);
|
|
53
|
-
child.kill("SIGKILL");
|
|
54
|
-
};
|
|
55
|
-
const timeout = setTimeout(() => requestFailure(`Managed Linux primitive timed out: ${path.basename(executable)}`), COREUTILS_TIMEOUT_MS);
|
|
56
|
-
child.stderr.on("data", (chunk) => {
|
|
57
|
-
stderrBytes += chunk.byteLength;
|
|
58
|
-
if (stderrBytes > COREUTILS_STDERR_LIMIT) {
|
|
59
|
-
requestFailure(`Managed Linux primitive exceeded its stderr limit: ${path.basename(executable)}`);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
child.on("error", (error) => {
|
|
63
|
-
requestFailure(`Managed Linux primitive is unavailable: ${path.basename(executable)}`, error);
|
|
64
|
-
});
|
|
65
|
-
child.on("close", (code, signal) => {
|
|
66
|
-
if (settled)
|
|
67
|
-
return;
|
|
68
|
-
settled = true;
|
|
69
|
-
clearTimeout(timeout);
|
|
70
|
-
if (requestedFailure !== undefined) {
|
|
71
|
-
reject(requestedFailure);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (code === 0 && signal === null) {
|
|
75
|
-
resolve();
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
reject(new Error(`Managed Linux primitive failed: ${path.basename(executable)} (${signal ?? code ?? "unknown"})`));
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
35
|
export function hashBytes(content) {
|
|
83
36
|
return createHash("sha256").update(content).digest("hex");
|
|
84
37
|
}
|
|
@@ -425,10 +378,10 @@ async function removePathIfOwned(directory, basename, expectedIdentity) {
|
|
|
425
378
|
return (await capturePathIdentity(directory, basename)) === undefined ? "removed" : "changed";
|
|
426
379
|
}
|
|
427
380
|
async function linkHeldFile(source, directory, basename) {
|
|
428
|
-
await
|
|
381
|
+
await runPublicationPrimitive(GNU_LN_PATH, ["-L", "-T", "--", "/proc/self/fd/3", `/proc/self/fd/4/${basename}`], [source.fd, directory.fd]);
|
|
429
382
|
}
|
|
430
383
|
async function exchangeDirectoryEntries(directory, leftBasename, rightBasename) {
|
|
431
|
-
await
|
|
384
|
+
await runPublicationPrimitive(GNU_MV_PATH, [
|
|
432
385
|
"--exchange",
|
|
433
386
|
"--no-copy",
|
|
434
387
|
"-T",
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { applyOperation, configureOperationApply, exportOperationRecord, importOperationRecord, } from "./operations.js";
|
|
7
7
|
import { ensurePrivateDirectory, resolveRuntimeStateDirectory, } from "./runtime-state.js";
|
|
8
|
-
export const PLAN_SCHEMA_VERSION =
|
|
8
|
+
export const PLAN_SCHEMA_VERSION = 2;
|
|
9
9
|
export const MAX_PLAN_BYTES = 50 * 1024 * 1024;
|
|
10
10
|
const HashSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
11
11
|
const IsoDateSchema = z.string().datetime({ offset: true });
|
|
@@ -62,12 +62,13 @@ const PersistedOperationSchema = z
|
|
|
62
62
|
files: z.array(PersistedFileSchema).min(1),
|
|
63
63
|
})
|
|
64
64
|
.strict();
|
|
65
|
-
const PlanEnvelopeSchema = z
|
|
66
|
-
.object({
|
|
67
|
-
schema_version: z.literal(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
const PlanEnvelopeSchema = z.discriminatedUnion("schema_version", [
|
|
66
|
+
z.object({ schema_version: z.literal(1), operation: PersistedOperationSchema }).strict(),
|
|
67
|
+
z.object({ schema_version: z.literal(2), operation: PersistedOperationSchema }).strict(),
|
|
68
|
+
]);
|
|
69
|
+
function planEnvelope(version, operation) {
|
|
70
|
+
return version === 1 ? { schema_version: 1, operation } : { schema_version: 2, operation };
|
|
71
|
+
}
|
|
71
72
|
function stateDirectory(options) {
|
|
72
73
|
return resolveRuntimeStateDirectory(options);
|
|
73
74
|
}
|
|
@@ -147,10 +148,7 @@ async function readPlanEnvelope(planFileInput) {
|
|
|
147
148
|
export async function persistOperationPlan(operationId, options = {}) {
|
|
148
149
|
const plansDirectory = path.join(stateDirectory(options), "plans");
|
|
149
150
|
const planFile = path.join(plansDirectory, `${operationId}.astplan`);
|
|
150
|
-
await atomicWritePlan(planFile,
|
|
151
|
-
schema_version: PLAN_SCHEMA_VERSION,
|
|
152
|
-
operation: exportOperationRecord(operationId),
|
|
153
|
-
});
|
|
151
|
+
await atomicWritePlan(planFile, planEnvelope(PLAN_SCHEMA_VERSION, exportOperationRecord(operationId)));
|
|
154
152
|
return planFile;
|
|
155
153
|
}
|
|
156
154
|
export async function applyPersistedOperation(planFileInput, planHash, options = {}) {
|
|
@@ -158,15 +156,16 @@ export async function applyPersistedOperation(planFileInput, planHash, options =
|
|
|
158
156
|
throw new Error("plan_hash must be a 64-character lowercase SHA-256 digest.");
|
|
159
157
|
}
|
|
160
158
|
const current = await readPlanEnvelope(planFileInput);
|
|
161
|
-
const
|
|
162
|
-
|
|
159
|
+
const { schema_version: planVersion, operation } = current.envelope;
|
|
160
|
+
if (planVersion === 1 && operation.status === "prepared") {
|
|
161
|
+
throw new Error("Legacy v1 prepared plans are not authorized for apply.");
|
|
162
|
+
}
|
|
163
|
+
const operationId = operation.operation_id;
|
|
164
|
+
importOperationRecord(operation, planHash, planVersion);
|
|
163
165
|
configureOperationApply(operationId, {
|
|
164
166
|
stateDirectory: options.stateDirectory,
|
|
165
167
|
receiptWriter: async () => {
|
|
166
|
-
await atomicWritePlan(current.planFile,
|
|
167
|
-
schema_version: PLAN_SCHEMA_VERSION,
|
|
168
|
-
operation: exportOperationRecord(operationId),
|
|
169
|
-
});
|
|
168
|
+
await atomicWritePlan(current.planFile, planEnvelope(planVersion, exportOperationRecord(operationId)));
|
|
170
169
|
},
|
|
171
170
|
});
|
|
172
171
|
return applyOperation(operationId, planHash);
|
|
@@ -175,7 +174,7 @@ export async function inspectPersistedPlan(planFileInput) {
|
|
|
175
174
|
const { envelope } = await readPlanEnvelope(planFileInput);
|
|
176
175
|
const operation = envelope.operation;
|
|
177
176
|
return {
|
|
178
|
-
schema_version:
|
|
177
|
+
schema_version: envelope.schema_version,
|
|
179
178
|
operation_id: operation.operation_id,
|
|
180
179
|
plan_hash: operation.plan_hash,
|
|
181
180
|
status: operation.status,
|