@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.
@@ -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 abs = resolve(root, rel);
145
- const exists = existsSync(abs);
138
+ const observation = observeSafePath(root, rel);
146
139
  if (expected.kind === "file") {
147
- if (!exists) return false;
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
- if (exists) return false;
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
+ }