@abelxiaoxing/cadence 1.0.12 → 1.0.13
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/package.json +1 -1
- package/prompts/abel-design.md +19 -10
- package/prompts/abel-implement.md +19 -6
- package/skills/abel-workflow/SKILL.md +32 -10
- package/src/candidate-preflight.ts +9 -6
- package/src/child-session.ts +75 -25
- package/src/contracts.ts +438 -230
- package/src/file-snapshot.ts +11 -20
- package/src/implement-graph.ts +571 -0
- package/src/patch.ts +103 -21
- package/src/runtime.ts +445 -119
- package/src/safe-path.ts +66 -0
- package/src/submit-tool.ts +26 -13
- package/src/verification-capability.ts +52 -67
- package/src/worker.ts +49 -0
package/src/file-snapshot.ts
CHANGED
|
@@ -3,16 +3,11 @@
|
|
|
3
3
|
// markers for proposed new paths. Currency is decided per bound path; no
|
|
4
4
|
// global workspace revision participates.
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
-
import {
|
|
7
|
-
existsSync,
|
|
8
|
-
lstatSync,
|
|
9
|
-
readdirSync,
|
|
10
|
-
readFileSync,
|
|
11
|
-
readlinkSync,
|
|
12
|
-
statSync,
|
|
13
|
-
} from "node:fs";
|
|
6
|
+
import { lstatSync, readdirSync, readFileSync, readlinkSync } from "node:fs";
|
|
14
7
|
import { join, resolve } from "node:path";
|
|
15
8
|
|
|
9
|
+
import { observeSafePath } from "./safe-path.ts";
|
|
10
|
+
|
|
16
11
|
export interface FileBound {
|
|
17
12
|
kind: "file";
|
|
18
13
|
sha256: string;
|
|
@@ -37,9 +32,8 @@ function sha256(bytes: Buffer): string {
|
|
|
37
32
|
|
|
38
33
|
/** Snapshot one existing regular file under root. */
|
|
39
34
|
export function snapshotFile(root: string, relPath: string): FileBound | null {
|
|
35
|
+
if (observeSafePath(root, relPath).kind !== "file") return null;
|
|
40
36
|
const abs = resolve(root, relPath);
|
|
41
|
-
const st = statSync(abs, { throwIfNoEntry: false });
|
|
42
|
-
if (!st?.isFile()) return null;
|
|
43
37
|
const bytes = readFileSync(abs);
|
|
44
38
|
return { sha256: sha256(bytes), bytes: bytes.length, kind: "file" };
|
|
45
39
|
}
|
|
@@ -99,9 +93,8 @@ export function snapshotDirManifest(
|
|
|
99
93
|
root: string,
|
|
100
94
|
relDir: string,
|
|
101
95
|
): DirBound | null {
|
|
96
|
+
if (observeSafePath(root, relDir).kind !== "directory") return null;
|
|
102
97
|
const abs = resolve(root, relDir);
|
|
103
|
-
const st = statSync(abs, { throwIfNoEntry: false });
|
|
104
|
-
if (!st?.isDirectory()) return null;
|
|
105
98
|
return { manifest: recursiveDirectoryManifest(abs), kind: "dir" };
|
|
106
99
|
}
|
|
107
100
|
|
|
@@ -117,8 +110,9 @@ export function snapshotFiles(
|
|
|
117
110
|
if (entry) bound[p] = entry;
|
|
118
111
|
else bound[p] = { kind: "absent", absent: true };
|
|
119
112
|
}
|
|
120
|
-
for (const p of opts.absent ?? [])
|
|
113
|
+
for (const p of opts.absent ?? []) {
|
|
121
114
|
bound[p] = { kind: "absent", absent: true };
|
|
115
|
+
}
|
|
122
116
|
return bound;
|
|
123
117
|
}
|
|
124
118
|
|
|
@@ -141,12 +135,9 @@ export function isCurrent(root: string, bound: Bound): boolean {
|
|
|
141
135
|
if (!now || now.manifest !== expected.manifest) return false;
|
|
142
136
|
continue;
|
|
143
137
|
}
|
|
144
|
-
const
|
|
145
|
-
const exists = existsSync(abs);
|
|
138
|
+
const observation = observeSafePath(root, rel);
|
|
146
139
|
if (expected.kind === "file") {
|
|
147
|
-
if (
|
|
148
|
-
const st = statSync(abs);
|
|
149
|
-
if (!st.isFile()) return false;
|
|
140
|
+
if (observation.kind !== "file") return false;
|
|
150
141
|
const now = snapshotFile(root, rel);
|
|
151
142
|
if (
|
|
152
143
|
!now ||
|
|
@@ -154,8 +145,8 @@ export function isCurrent(root: string, bound: Bound): boolean {
|
|
|
154
145
|
now.bytes !== expected.bytes
|
|
155
146
|
)
|
|
156
147
|
return false;
|
|
157
|
-
} else if (expected.absent) {
|
|
158
|
-
|
|
148
|
+
} else if (expected.absent && observation.kind !== "absent") {
|
|
149
|
+
return false;
|
|
159
150
|
}
|
|
160
151
|
}
|
|
161
152
|
return true;
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
type ImplementationPhase,
|
|
4
|
+
type ImplementGraphBoundary,
|
|
5
|
+
type ImplementGraphOutput,
|
|
6
|
+
type ImplementGraphReadinessCode,
|
|
7
|
+
type ImplementGraphReadinessDiagnostic,
|
|
8
|
+
type ImplementTaskBoundary,
|
|
9
|
+
type VerificationInputBinding,
|
|
10
|
+
validateImplementGraphBoundary,
|
|
11
|
+
verificationInputPaths,
|
|
12
|
+
} from "./contracts.ts";
|
|
13
|
+
import { isSafeRegularFile } from "./safe-path.ts";
|
|
14
|
+
import { validateVerificationAdapterCapability } from "./verification-capability.ts";
|
|
15
|
+
|
|
16
|
+
function canonicalJson(value: unknown): string {
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
19
|
+
}
|
|
20
|
+
if (value && typeof value === "object") {
|
|
21
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
22
|
+
.filter(([, entry]) => entry !== undefined)
|
|
23
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
24
|
+
return `{${entries
|
|
25
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
|
|
26
|
+
.join(",")}}`;
|
|
27
|
+
}
|
|
28
|
+
return JSON.stringify(value) ?? "null";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function hashImplementGraphBoundary(value: unknown): string {
|
|
32
|
+
return createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ImplementExecutionFacts {
|
|
36
|
+
completedTasks: readonly string[];
|
|
37
|
+
blockedTasks?: readonly string[];
|
|
38
|
+
appliedPhases?: ReadonlyArray<{
|
|
39
|
+
taskId: string;
|
|
40
|
+
phase: ImplementationPhase;
|
|
41
|
+
}>;
|
|
42
|
+
candidate?: { taskId: string; phase: ImplementationPhase };
|
|
43
|
+
dependencyOwner?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ImplementPhaseReadiness {
|
|
47
|
+
taskId: string;
|
|
48
|
+
phase: ImplementationPhase;
|
|
49
|
+
ready: boolean;
|
|
50
|
+
completed?: true;
|
|
51
|
+
diagnostics: ImplementGraphReadinessDiagnostic[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ImplementGraphReadiness {
|
|
55
|
+
closure: {
|
|
56
|
+
executable: boolean;
|
|
57
|
+
diagnostics: ImplementGraphReadinessDiagnostic[];
|
|
58
|
+
};
|
|
59
|
+
phases: ImplementPhaseReadiness[];
|
|
60
|
+
outputs: ImplementOutputReadiness[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ImplementOutputReadiness {
|
|
64
|
+
outputId: string;
|
|
65
|
+
producerTaskId: string;
|
|
66
|
+
producerPhase: ImplementationPhase;
|
|
67
|
+
status: "pending" | "available" | "unavailable";
|
|
68
|
+
diagnostic?: ImplementGraphReadinessDiagnostic;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const IMPLEMENT_PHASE_INDEX: Record<ImplementationPhase, number> = {
|
|
72
|
+
red: 0,
|
|
73
|
+
green: 1,
|
|
74
|
+
refactor: 2,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function graphDiagnostic(
|
|
78
|
+
code: ImplementGraphReadinessCode,
|
|
79
|
+
details: Omit<
|
|
80
|
+
Extract<ImplementGraphReadinessDiagnostic, { kind: "graph-readiness" }>,
|
|
81
|
+
"kind" | "code"
|
|
82
|
+
> = {},
|
|
83
|
+
): ImplementGraphReadinessDiagnostic {
|
|
84
|
+
return { kind: "graph-readiness", code, ...details };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function taskPhases(
|
|
88
|
+
task: ImplementTaskBoundary,
|
|
89
|
+
): Array<[ImplementationPhase, ImplementTaskBoundary["phases"]["red"]]> {
|
|
90
|
+
return [
|
|
91
|
+
["red", task.phases.red],
|
|
92
|
+
["green", task.phases.green],
|
|
93
|
+
...(task.phases.refactor
|
|
94
|
+
? ([["refactor", task.phases.refactor]] as Array<
|
|
95
|
+
[ImplementationPhase, ImplementTaskBoundary["phases"]["red"]]
|
|
96
|
+
>)
|
|
97
|
+
: []),
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function withinTaskRoots(
|
|
102
|
+
task: ImplementTaskBoundary,
|
|
103
|
+
candidate: string,
|
|
104
|
+
): boolean {
|
|
105
|
+
return task.roots.some(
|
|
106
|
+
(root) =>
|
|
107
|
+
root === "." || candidate === root || candidate.startsWith(`${root}/`),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function dependencyAncestors(
|
|
112
|
+
taskId: string,
|
|
113
|
+
tasks: ReadonlyMap<string, ImplementTaskBoundary>,
|
|
114
|
+
): Set<string> {
|
|
115
|
+
const ancestors = new Set<string>();
|
|
116
|
+
const pending = [...(tasks.get(taskId)?.dependsOn ?? [])];
|
|
117
|
+
while (pending.length > 0) {
|
|
118
|
+
const dependency = pending.pop() as string;
|
|
119
|
+
if (ancestors.has(dependency)) continue;
|
|
120
|
+
ancestors.add(dependency);
|
|
121
|
+
pending.push(...(tasks.get(dependency)?.dependsOn ?? []));
|
|
122
|
+
}
|
|
123
|
+
return ancestors;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function bindingPath(
|
|
127
|
+
binding: VerificationInputBinding,
|
|
128
|
+
outputs: ReadonlyMap<string, ImplementGraphOutput>,
|
|
129
|
+
): string | undefined {
|
|
130
|
+
return binding.kind === "workspace"
|
|
131
|
+
? binding.path
|
|
132
|
+
: outputs.get(binding.outputId)?.path;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function sameStringSet(
|
|
136
|
+
left: readonly string[],
|
|
137
|
+
right: readonly string[],
|
|
138
|
+
): boolean {
|
|
139
|
+
return (
|
|
140
|
+
left.length === right.length && left.every((entry) => right.includes(entry))
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function appendDistinct(
|
|
145
|
+
diagnostics: ImplementGraphReadinessDiagnostic[],
|
|
146
|
+
diagnostic: ImplementGraphReadinessDiagnostic,
|
|
147
|
+
): void {
|
|
148
|
+
if (
|
|
149
|
+
!diagnostics.some(
|
|
150
|
+
(candidate) => JSON.stringify(candidate) === JSON.stringify(diagnostic),
|
|
151
|
+
)
|
|
152
|
+
) {
|
|
153
|
+
diagnostics.push(diagnostic);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function assessImplementGraphReadiness(
|
|
158
|
+
root: string,
|
|
159
|
+
value: unknown,
|
|
160
|
+
facts: ImplementExecutionFacts,
|
|
161
|
+
): ImplementGraphReadiness {
|
|
162
|
+
const validation = validateImplementGraphBoundary(value);
|
|
163
|
+
if (!validation.ok) {
|
|
164
|
+
const diagnostic = graphDiagnostic(validation.diagnostic.code, {
|
|
165
|
+
...(validation.diagnostic.code === "invalid-output-path" &&
|
|
166
|
+
validation.diagnostic.outputId
|
|
167
|
+
? { outputId: validation.diagnostic.outputId }
|
|
168
|
+
: {}),
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
closure: { executable: false, diagnostics: [diagnostic] },
|
|
172
|
+
phases: [],
|
|
173
|
+
outputs: [],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const graph: ImplementGraphBoundary = validation.value;
|
|
178
|
+
const diagnostics: ImplementGraphReadinessDiagnostic[] = [];
|
|
179
|
+
const tasks = new Map<string, ImplementTaskBoundary>();
|
|
180
|
+
for (const task of graph.tasks) {
|
|
181
|
+
if (tasks.has(task.taskId)) {
|
|
182
|
+
diagnostics.push(
|
|
183
|
+
graphDiagnostic("duplicate-task-id", { taskId: task.taskId }),
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
tasks.set(task.taskId, task);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const task of graph.tasks) {
|
|
191
|
+
for (const dependencyTaskId of task.dependsOn) {
|
|
192
|
+
if (!tasks.has(dependencyTaskId) || dependencyTaskId === task.taskId) {
|
|
193
|
+
diagnostics.push(
|
|
194
|
+
graphDiagnostic("unknown-dependency", {
|
|
195
|
+
taskId: task.taskId,
|
|
196
|
+
dependencyTaskId,
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (dependencyAncestors(task.taskId, tasks).has(task.taskId)) {
|
|
202
|
+
diagnostics.push(
|
|
203
|
+
graphDiagnostic("dependency-cycle", { taskId: task.taskId }),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const outputs = new Map<string, ImplementGraphOutput>();
|
|
209
|
+
const outputPaths = new Map<string, ImplementGraphOutput>();
|
|
210
|
+
for (const output of graph.outputs) {
|
|
211
|
+
if (outputs.has(output.id)) {
|
|
212
|
+
diagnostics.push(
|
|
213
|
+
graphDiagnostic("duplicate-output-id", { outputId: output.id }),
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
outputs.set(output.id, output);
|
|
217
|
+
}
|
|
218
|
+
const prior = outputPaths.get(output.path);
|
|
219
|
+
if (prior) {
|
|
220
|
+
diagnostics.push(
|
|
221
|
+
graphDiagnostic("multiple-output-producers", {
|
|
222
|
+
outputId: output.id,
|
|
223
|
+
path: output.path,
|
|
224
|
+
producerTaskId: output.producer.taskId,
|
|
225
|
+
producerPhase: output.producer.phase,
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
} else {
|
|
229
|
+
outputPaths.set(output.path, output);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const producer = tasks.get(output.producer.taskId);
|
|
233
|
+
const producerPhase = producer?.phases[output.producer.phase];
|
|
234
|
+
if (
|
|
235
|
+
!producer ||
|
|
236
|
+
!producerPhase ||
|
|
237
|
+
!withinTaskRoots(producer, output.path)
|
|
238
|
+
) {
|
|
239
|
+
diagnostics.push(
|
|
240
|
+
graphDiagnostic("output-producer-invalid", {
|
|
241
|
+
outputId: output.id,
|
|
242
|
+
producerTaskId: output.producer.taskId,
|
|
243
|
+
producerPhase: output.producer.phase,
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
} else if (!producerPhase.write.includes(output.path)) {
|
|
247
|
+
diagnostics.push(
|
|
248
|
+
graphDiagnostic("output-write-not-approved", {
|
|
249
|
+
outputId: output.id,
|
|
250
|
+
producerTaskId: output.producer.taskId,
|
|
251
|
+
producerPhase: output.producer.phase,
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const phaseStaticDiagnostics = new Map<
|
|
258
|
+
string,
|
|
259
|
+
ImplementGraphReadinessDiagnostic[]
|
|
260
|
+
>();
|
|
261
|
+
for (const task of graph.tasks) {
|
|
262
|
+
const ancestors = dependencyAncestors(task.taskId, tasks);
|
|
263
|
+
for (const [phase, boundary] of taskPhases(task)) {
|
|
264
|
+
const key = `${task.taskId}\0${phase}`;
|
|
265
|
+
const current: ImplementGraphReadinessDiagnostic[] = [];
|
|
266
|
+
phaseStaticDiagnostics.set(key, current);
|
|
267
|
+
const verificationId = boundary.verification.id;
|
|
268
|
+
const requiredPaths = verificationInputPaths(boundary.verification);
|
|
269
|
+
const boundPaths = boundary.verificationInputs
|
|
270
|
+
.map((binding) => bindingPath(binding, outputs))
|
|
271
|
+
.filter((entry): entry is string => entry !== undefined);
|
|
272
|
+
|
|
273
|
+
for (const binding of boundary.verificationInputs) {
|
|
274
|
+
if (binding.kind === "workspace") {
|
|
275
|
+
const graphOutput = outputPaths.get(binding.path);
|
|
276
|
+
if (graphOutput) {
|
|
277
|
+
appendDistinct(
|
|
278
|
+
current,
|
|
279
|
+
graphDiagnostic("workspace-input-has-producer", {
|
|
280
|
+
taskId: task.taskId,
|
|
281
|
+
phase,
|
|
282
|
+
verificationId,
|
|
283
|
+
outputId: graphOutput.id,
|
|
284
|
+
path: binding.path,
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (!isSafeRegularFile(root, binding.path)) {
|
|
289
|
+
appendDistinct(
|
|
290
|
+
current,
|
|
291
|
+
graphDiagnostic("workspace-input-unavailable", {
|
|
292
|
+
taskId: task.taskId,
|
|
293
|
+
phase,
|
|
294
|
+
verificationId,
|
|
295
|
+
path: binding.path,
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const output = outputs.get(binding.outputId);
|
|
303
|
+
if (!output) {
|
|
304
|
+
appendDistinct(
|
|
305
|
+
current,
|
|
306
|
+
graphDiagnostic("output-not-declared", {
|
|
307
|
+
taskId: task.taskId,
|
|
308
|
+
phase,
|
|
309
|
+
verificationId,
|
|
310
|
+
outputId: binding.outputId,
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (output.producer.taskId === task.taskId) {
|
|
316
|
+
if (
|
|
317
|
+
IMPLEMENT_PHASE_INDEX[output.producer.phase] >
|
|
318
|
+
IMPLEMENT_PHASE_INDEX[phase]
|
|
319
|
+
) {
|
|
320
|
+
appendDistinct(
|
|
321
|
+
current,
|
|
322
|
+
graphDiagnostic("producer-phase-after-consumer", {
|
|
323
|
+
taskId: task.taskId,
|
|
324
|
+
phase,
|
|
325
|
+
verificationId,
|
|
326
|
+
outputId: output.id,
|
|
327
|
+
producerTaskId: output.producer.taskId,
|
|
328
|
+
producerPhase: output.producer.phase,
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
} else if (!ancestors.has(output.producer.taskId)) {
|
|
333
|
+
appendDistinct(
|
|
334
|
+
current,
|
|
335
|
+
graphDiagnostic("producer-not-dependency", {
|
|
336
|
+
taskId: task.taskId,
|
|
337
|
+
phase,
|
|
338
|
+
verificationId,
|
|
339
|
+
outputId: output.id,
|
|
340
|
+
producerTaskId: output.producer.taskId,
|
|
341
|
+
producerPhase: output.producer.phase,
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!sameStringSet(requiredPaths, boundPaths)) {
|
|
348
|
+
appendDistinct(
|
|
349
|
+
current,
|
|
350
|
+
graphDiagnostic("verification-input-binding-mismatch", {
|
|
351
|
+
taskId: task.taskId,
|
|
352
|
+
phase,
|
|
353
|
+
verificationId,
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const capability = validateVerificationAdapterCapability(
|
|
359
|
+
root,
|
|
360
|
+
boundary.verification,
|
|
361
|
+
{ dependencyOwner: facts.dependencyOwner ?? root },
|
|
362
|
+
);
|
|
363
|
+
if (!capability.ok) {
|
|
364
|
+
const { message: _message, ...diagnostic } = capability.diagnostic;
|
|
365
|
+
appendDistinct(current, {
|
|
366
|
+
...diagnostic,
|
|
367
|
+
taskId: task.taskId,
|
|
368
|
+
phase,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
diagnostics.push(...current);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const closure = {
|
|
376
|
+
executable: diagnostics.length === 0,
|
|
377
|
+
diagnostics,
|
|
378
|
+
};
|
|
379
|
+
const completedTasks = new Set(facts.completedTasks);
|
|
380
|
+
const blockedTasks = new Set(facts.blockedTasks ?? []);
|
|
381
|
+
const appliedPhases = new Set(
|
|
382
|
+
(facts.appliedPhases ?? []).map(
|
|
383
|
+
(entry) => `${entry.taskId}\0${entry.phase}`,
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
const outputReadiness: ImplementOutputReadiness[] = graph.outputs.map(
|
|
387
|
+
(output) => {
|
|
388
|
+
const observed =
|
|
389
|
+
completedTasks.has(output.producer.taskId) ||
|
|
390
|
+
appliedPhases.has(
|
|
391
|
+
`${output.producer.taskId}\0${output.producer.phase}`,
|
|
392
|
+
) ||
|
|
393
|
+
(facts.candidate?.taskId === output.producer.taskId &&
|
|
394
|
+
facts.candidate.phase === output.producer.phase);
|
|
395
|
+
if (!observed) {
|
|
396
|
+
return {
|
|
397
|
+
outputId: output.id,
|
|
398
|
+
producerTaskId: output.producer.taskId,
|
|
399
|
+
producerPhase: output.producer.phase,
|
|
400
|
+
status: "pending" as const,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
if (isSafeRegularFile(root, output.path)) {
|
|
404
|
+
return {
|
|
405
|
+
outputId: output.id,
|
|
406
|
+
producerTaskId: output.producer.taskId,
|
|
407
|
+
producerPhase: output.producer.phase,
|
|
408
|
+
status: "available" as const,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
outputId: output.id,
|
|
413
|
+
producerTaskId: output.producer.taskId,
|
|
414
|
+
producerPhase: output.producer.phase,
|
|
415
|
+
status: "unavailable" as const,
|
|
416
|
+
diagnostic: graphDiagnostic("producer-output-unavailable", {
|
|
417
|
+
outputId: output.id,
|
|
418
|
+
producerTaskId: output.producer.taskId,
|
|
419
|
+
producerPhase: output.producer.phase,
|
|
420
|
+
}),
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
const outputStatus = new Map(
|
|
425
|
+
outputReadiness.map((entry) => [entry.outputId, entry]),
|
|
426
|
+
);
|
|
427
|
+
const phases: ImplementPhaseReadiness[] = [];
|
|
428
|
+
|
|
429
|
+
for (const task of graph.tasks) {
|
|
430
|
+
for (const [phase, boundary] of taskPhases(task)) {
|
|
431
|
+
const staticDiagnostics = phaseStaticDiagnostics.get(
|
|
432
|
+
`${task.taskId}\0${phase}`,
|
|
433
|
+
) as ImplementGraphReadinessDiagnostic[];
|
|
434
|
+
if (!closure.executable) {
|
|
435
|
+
phases.push({
|
|
436
|
+
taskId: task.taskId,
|
|
437
|
+
phase,
|
|
438
|
+
ready: false,
|
|
439
|
+
diagnostics:
|
|
440
|
+
staticDiagnostics.length > 0 ? staticDiagnostics : diagnostics,
|
|
441
|
+
});
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (completedTasks.has(task.taskId)) {
|
|
445
|
+
phases.push({
|
|
446
|
+
taskId: task.taskId,
|
|
447
|
+
phase,
|
|
448
|
+
ready: false,
|
|
449
|
+
completed: true,
|
|
450
|
+
diagnostics: [],
|
|
451
|
+
});
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const current: ImplementGraphReadinessDiagnostic[] = [];
|
|
456
|
+
if (blockedTasks.has(task.taskId)) {
|
|
457
|
+
current.push(
|
|
458
|
+
graphDiagnostic("dependency-blocked", {
|
|
459
|
+
taskId: task.taskId,
|
|
460
|
+
phase,
|
|
461
|
+
dependencyTaskId: task.taskId,
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
for (const dependencyTaskId of task.dependsOn) {
|
|
466
|
+
if (!completedTasks.has(dependencyTaskId)) {
|
|
467
|
+
appendDistinct(
|
|
468
|
+
current,
|
|
469
|
+
graphDiagnostic("dependency-blocked", {
|
|
470
|
+
taskId: task.taskId,
|
|
471
|
+
phase,
|
|
472
|
+
dependencyTaskId,
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const earlierPhases = taskPhases(task)
|
|
479
|
+
.map(([candidate]) => candidate)
|
|
480
|
+
.filter(
|
|
481
|
+
(candidate) =>
|
|
482
|
+
IMPLEMENT_PHASE_INDEX[candidate] < IMPLEMENT_PHASE_INDEX[phase],
|
|
483
|
+
);
|
|
484
|
+
if (
|
|
485
|
+
earlierPhases.some(
|
|
486
|
+
(candidate) => !appliedPhases.has(`${task.taskId}\0${candidate}`),
|
|
487
|
+
)
|
|
488
|
+
) {
|
|
489
|
+
appendDistinct(
|
|
490
|
+
current,
|
|
491
|
+
graphDiagnostic("phase-blocked", { taskId: task.taskId, phase }),
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (current.length === 0) {
|
|
496
|
+
for (const binding of boundary.verificationInputs) {
|
|
497
|
+
if (binding.kind !== "output") continue;
|
|
498
|
+
const output = outputs.get(binding.outputId);
|
|
499
|
+
if (!output) continue;
|
|
500
|
+
const sameTask = output.producer.taskId === task.taskId;
|
|
501
|
+
const samePhase = sameTask && output.producer.phase === phase;
|
|
502
|
+
const readiness = outputStatus.get(output.id);
|
|
503
|
+
const published =
|
|
504
|
+
!sameTask && completedTasks.has(output.producer.taskId);
|
|
505
|
+
if (readiness?.status === "unavailable") {
|
|
506
|
+
appendDistinct(
|
|
507
|
+
current,
|
|
508
|
+
graphDiagnostic("producer-output-unavailable", {
|
|
509
|
+
taskId: task.taskId,
|
|
510
|
+
phase,
|
|
511
|
+
outputId: output.id,
|
|
512
|
+
producerTaskId: output.producer.taskId,
|
|
513
|
+
producerPhase: output.producer.phase,
|
|
514
|
+
}),
|
|
515
|
+
);
|
|
516
|
+
} else if (
|
|
517
|
+
(!sameTask && !published) ||
|
|
518
|
+
(sameTask && !samePhase && readiness?.status !== "available")
|
|
519
|
+
) {
|
|
520
|
+
appendDistinct(
|
|
521
|
+
current,
|
|
522
|
+
sameTask
|
|
523
|
+
? graphDiagnostic("phase-blocked", {
|
|
524
|
+
taskId: task.taskId,
|
|
525
|
+
phase,
|
|
526
|
+
})
|
|
527
|
+
: graphDiagnostic("dependency-blocked", {
|
|
528
|
+
taskId: task.taskId,
|
|
529
|
+
phase,
|
|
530
|
+
dependencyTaskId: output.producer.taskId,
|
|
531
|
+
}),
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (
|
|
537
|
+
facts.candidate?.taskId === task.taskId &&
|
|
538
|
+
facts.candidate.phase === phase
|
|
539
|
+
) {
|
|
540
|
+
for (const output of graph.outputs.filter(
|
|
541
|
+
(entry) =>
|
|
542
|
+
entry.producer.taskId === task.taskId &&
|
|
543
|
+
entry.producer.phase === phase,
|
|
544
|
+
)) {
|
|
545
|
+
if (outputStatus.get(output.id)?.status !== "available") {
|
|
546
|
+
appendDistinct(
|
|
547
|
+
current,
|
|
548
|
+
graphDiagnostic("producer-output-unavailable", {
|
|
549
|
+
taskId: task.taskId,
|
|
550
|
+
phase,
|
|
551
|
+
outputId: output.id,
|
|
552
|
+
producerTaskId: output.producer.taskId,
|
|
553
|
+
producerPhase: output.producer.phase,
|
|
554
|
+
}),
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
phases.push({
|
|
562
|
+
taskId: task.taskId,
|
|
563
|
+
phase,
|
|
564
|
+
ready: current.length === 0,
|
|
565
|
+
diagnostics: current,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return { closure, phases, outputs: outputReadiness };
|
|
571
|
+
}
|