@abelxiaoxing/cadence 1.4.1 → 1.5.1

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.
@@ -0,0 +1,142 @@
1
+ import { DesignPlanValidationError } from "./design-diagnostics.ts";
2
+ import type {
3
+ DraftVerification,
4
+ PlanDraft,
5
+ PlanTaskInput,
6
+ } from "./plan-draft.ts";
7
+
8
+ /** Small author input. Evidence, scope and the complete suite remain explicit. */
9
+ export interface SingleTaskDraft {
10
+ changeId: string;
11
+ changeContract?: PlanDraft["changeContract"];
12
+ singleTask: {
13
+ taskId: string;
14
+ objective: string;
15
+ context: PlanTaskInput["context"];
16
+ roots: string[];
17
+ read: string[];
18
+ greenWrite: string[];
19
+ redWrite?: string[];
20
+ expectedFailure?: string;
21
+ verificationMode?: "behavior" | "mechanical" | "refactor";
22
+ verification: DraftVerification;
23
+ fullSuite: DraftVerification;
24
+ impactClosure: PlanTaskInput["impactClosure"];
25
+ agents: PlanTaskInput["agents"];
26
+ recovery?: { artifactAttempts: number; repairAttempts: number };
27
+ };
28
+ }
29
+
30
+ function invalid(code: string, field = "singleTask"): never {
31
+ throw new DesignPlanValidationError(code, [{ code, field }]);
32
+ }
33
+
34
+ function record(value: unknown): value is Record<string, unknown> {
35
+ return value !== null && typeof value === "object" && !Array.isArray(value);
36
+ }
37
+
38
+ /** Mechanical expansion only; the ordinary compiler validates all resulting fields. */
39
+ export function expandSingleTaskDraft(value: unknown): unknown {
40
+ if (!record(value) || !Object.hasOwn(value, "singleTask")) return value;
41
+ if (
42
+ Object.keys(value).some(
43
+ (key) => !["changeId", "changeContract", "singleTask"].includes(key),
44
+ ) ||
45
+ !record(value.singleTask)
46
+ )
47
+ invalid("single-task-draft-invalid");
48
+ const input = value.singleTask;
49
+ const required = [
50
+ "taskId",
51
+ "objective",
52
+ "context",
53
+ "roots",
54
+ "read",
55
+ "greenWrite",
56
+ "verification",
57
+ "fullSuite",
58
+ "impactClosure",
59
+ "agents",
60
+ ];
61
+ const optional = [
62
+ "redWrite",
63
+ "expectedFailure",
64
+ "verificationMode",
65
+ "recovery",
66
+ ];
67
+ if (
68
+ required.some((key) => !Object.hasOwn(input, key)) ||
69
+ Object.keys(input).some((key) => ![...required, ...optional].includes(key))
70
+ )
71
+ invalid("single-task-draft-invalid");
72
+ const task = structuredClone(
73
+ input,
74
+ ) as unknown as SingleTaskDraft["singleTask"];
75
+ const mode = task.verificationMode ?? "behavior";
76
+ if (mode === "behavior" && (!task.redWrite?.length || !task.expectedFailure))
77
+ invalid("single-task-red-required");
78
+ if (
79
+ mode !== "behavior" &&
80
+ (task.redWrite !== undefined || task.expectedFailure !== undefined)
81
+ )
82
+ invalid("single-task-red-unexpected");
83
+ if (
84
+ Object.hasOwn(input, "recovery") &&
85
+ (!record(task.recovery) ||
86
+ !Object.hasOwn(task.recovery, "artifactAttempts") ||
87
+ !Object.hasOwn(task.recovery, "repairAttempts") ||
88
+ Object.keys(task.recovery).some(
89
+ (key) => !["artifactAttempts", "repairAttempts"].includes(key),
90
+ ))
91
+ )
92
+ invalid("single-task-recovery-invalid");
93
+ return {
94
+ changeId: value.changeId,
95
+ ...(value.changeContract === undefined
96
+ ? {}
97
+ : { changeContract: value.changeContract }),
98
+ tasks: [
99
+ {
100
+ taskId: task.taskId,
101
+ objective: task.objective,
102
+ context: task.context,
103
+ roots: task.roots,
104
+ read: task.read,
105
+ dependsOn: [],
106
+ verificationMode: mode,
107
+ phases: {
108
+ ...(mode === "behavior"
109
+ ? {
110
+ red: {
111
+ write: task.redWrite,
112
+ delete: [],
113
+ verification: {
114
+ ...task.verification,
115
+ expectedFailure: task.expectedFailure,
116
+ },
117
+ },
118
+ }
119
+ : {}),
120
+ green: {
121
+ write: task.greenWrite,
122
+ delete: [],
123
+ verification: task.verification,
124
+ },
125
+ },
126
+ affectedVerification: task.fullSuite,
127
+ repairVerification: task.fullSuite,
128
+ scheduling: { conflicts: [], resources: [] },
129
+ agents: task.agents,
130
+ approvedDependencies: [],
131
+ impactClosure: task.impactClosure,
132
+ },
133
+ ],
134
+ outputs: [],
135
+ verification: {
136
+ baseline: { fullSuite: task.fullSuite },
137
+ change: { fullSuite: task.fullSuite, postApply: task.fullSuite },
138
+ artifactCorrection: { maxAttempts: task.recovery?.artifactAttempts ?? 2 },
139
+ repair: { maxAttempts: task.recovery?.repairAttempts ?? 1 },
140
+ },
141
+ };
142
+ }
@@ -28,5 +28,25 @@ export function isTransportTimeoutCode(
28
28
  export function transportFailureError(code: string): Error {
29
29
  return isTransportTimeoutCode(code)
30
30
  ? new TransportTimeout(code)
31
- : new Error("transport-failure");
31
+ : new Error(
32
+ code === "child-provider-rate-limited" ? code : "transport-failure",
33
+ );
34
+ }
35
+
36
+ /** Snapshot at the start of each request; Workers cannot override these values. */
37
+ export function requestBounds(env: NodeJS.ProcessEnv = process.env) {
38
+ const read = (name: string, fallback: number) => {
39
+ const value = Number(env[name] ?? fallback);
40
+ if (!Number.isSafeInteger(value) || value < 1 || value > 1_200_000)
41
+ throw new Error("request-budget-invalid");
42
+ return value;
43
+ };
44
+ return {
45
+ ...REQUEST_BOUNDS,
46
+ firstProgressMs: read(
47
+ "ABEL_FIRST_PROGRESS_MS",
48
+ REQUEST_BOUNDS.firstProgressMs,
49
+ ),
50
+ streamIdleMs: read("ABEL_STREAM_IDLE_MS", REQUEST_BOUNDS.streamIdleMs),
51
+ };
32
52
  }
@@ -16,6 +16,7 @@ import {
16
16
  verificationInputPaths,
17
17
  verificationSteps,
18
18
  } from "./contracts.ts";
19
+ import { executionProfile } from "./execution-profile.ts";
19
20
  import {
20
21
  type Bound,
21
22
  isCurrent,
@@ -828,9 +829,10 @@ function validateAtomicCapability(
828
829
  continue;
829
830
  if (
830
831
  lstatSync(path.join(root, relative)).size > 64 * 1024 ||
831
- unsupportedExecutionConfiguration(
832
- readFileSync(path.join(root, relative), "utf8"),
833
- )
832
+ (executionProfile().mode !== "local-trusted" &&
833
+ unsupportedExecutionConfiguration(
834
+ readFileSync(path.join(root, relative), "utf8"),
835
+ ))
834
836
  ) {
835
837
  return adapterFailure(
836
838
  step.id,
@@ -0,0 +1,76 @@
1
+ import type {
2
+ VerificationFailureSummary,
3
+ VerificationObservation,
4
+ } from "./contracts.ts";
5
+
6
+ /** Diagnostic text is evidence for repair, never a failure identity or authority. */
7
+
8
+ export function diagnosticText(value: string, maximum = 4096): string {
9
+ const clean = value
10
+ .replace(
11
+ new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "gu"),
12
+ "",
13
+ )
14
+ .replace(/(Bearer\s+)\S+/giu, "$1[redacted]")
15
+ .replace(
16
+ /((?:api[_-]?key|token|password|secret)\s*[:=]\s*)[^\s,;]+/giu,
17
+ "$1[redacted]",
18
+ )
19
+ .replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/giu, "$1[redacted]@");
20
+ return clean.length <= maximum
21
+ ? clean
22
+ : `${clean.slice(0, Math.floor(maximum / 2))}\n…[truncated]…\n${clean.slice(-Math.floor(maximum / 2))}`;
23
+ }
24
+
25
+ /** Bounded operation-local feedback; no raw transcripts or global cross-run cache. */
26
+ export class VerificationFeedback {
27
+ readonly #entries = new Map<string, VerificationFailureSummary>();
28
+ observe(result: VerificationObservation): void {
29
+ if (result.kind === "cancelled") return;
30
+ const id =
31
+ result.kind === "unavailable"
32
+ ? result.verificationId
33
+ : result.evidence.id;
34
+ this.#entries.delete(id);
35
+ if (result.kind === "accepted") return;
36
+ const summary = result.diagnostic ?? {
37
+ verificationId: id,
38
+ code: result.code,
39
+ failures: [],
40
+ stdout: "",
41
+ stderr: "",
42
+ truncated: false,
43
+ nextStep:
44
+ "Repair the verification environment or adapter before changing product code.",
45
+ };
46
+ this.#entries.set(id, structuredClone(summary));
47
+ const oldest = this.#entries.keys().next().value;
48
+ if (this.#entries.size > 32 && oldest !== undefined)
49
+ this.#entries.delete(oldest);
50
+ }
51
+ current(): VerificationFailureSummary[] {
52
+ const result: VerificationFailureSummary[] = [];
53
+ let bytes = 0;
54
+ for (const entry of [...this.#entries.values()].reverse()) {
55
+ const summary = {
56
+ ...entry,
57
+ failures: entry.failures
58
+ .slice(0, 4)
59
+ .map((text) => diagnosticText(text, 256)),
60
+ stdout: diagnosticText(entry.stdout, 512),
61
+ stderr: diagnosticText(entry.stderr, 1024),
62
+ truncated:
63
+ entry.truncated ||
64
+ entry.failures.length > 4 ||
65
+ entry.failures.some((text) => text.length > 256) ||
66
+ entry.stdout.length > 512 ||
67
+ entry.stderr.length > 1024,
68
+ };
69
+ const size = Buffer.byteLength(JSON.stringify(summary), "utf8");
70
+ if (bytes + size > 6000) break;
71
+ result.push(summary);
72
+ bytes += size;
73
+ }
74
+ return structuredClone(result.reverse());
75
+ }
76
+ }
@@ -0,0 +1,280 @@
1
+ import {
2
+ constants,
3
+ cpSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readdirSync,
7
+ realpathSync,
8
+ rmSync,
9
+ symlinkSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import path from "node:path";
13
+ import type { ExecutionProfile } from "./execution-profile.ts";
14
+ import type { IsolationMount } from "./isolation-backend.ts";
15
+ import type { VerificationRunnerBinding } from "./verification-capability.ts";
16
+
17
+ const CACHES = [".vite", ".vite-temp"];
18
+ function within(root: string, candidate: string): boolean {
19
+ const relative = path.relative(root, candidate);
20
+ return (
21
+ relative !== ".." &&
22
+ !relative.startsWith(`..${path.sep}`) &&
23
+ !path.isAbsolute(relative)
24
+ );
25
+ }
26
+
27
+ /** A disposable dependency view: immutable packages and independently writable caches. */
28
+ export function prepareVerificationEnvironmentIo(
29
+ root: string,
30
+ dependencyOwner: string,
31
+ runnerBindings: readonly VerificationRunnerBinding[],
32
+ profile: ExecutionProfile,
33
+ environment: NodeJS.ProcessEnv,
34
+ checkCancelled: () => void,
35
+ privateRoot: string,
36
+ ) {
37
+ const cleanup = () => rmSync(privateRoot, { recursive: true, force: true });
38
+ try {
39
+ const local = profile.mode === "local-trusted";
40
+ if (local && realpathSync(root) === realpathSync(dependencyOwner))
41
+ throw new Error("trusted-candidate-required");
42
+ const mounts: IsolationMount[] = [
43
+ { source: privateRoot, target: "/cadence", writable: true },
44
+ ];
45
+ for (const directory of [
46
+ "home",
47
+ "cache",
48
+ "tmp",
49
+ "reports",
50
+ "dependencies",
51
+ ]) {
52
+ mkdirSync(path.join(privateRoot, directory), { mode: 0o700 });
53
+ }
54
+ writeFileSync(path.join(privateRoot, "home/user.npmrc"), "");
55
+ writeFileSync(path.join(privateRoot, "home/global.npmrc"), "");
56
+ const dependencies = path.join(dependencyOwner, "node_modules");
57
+ const stat = lstatSync(dependencies, { throwIfNoEntry: false });
58
+ if (stat) {
59
+ const target = path.join(root, "node_modules");
60
+ const targetStat = lstatSync(target, { throwIfNoEntry: false });
61
+ if (
62
+ !stat.isDirectory() ||
63
+ stat.isSymbolicLink() ||
64
+ (targetStat &&
65
+ (!targetStat.isDirectory() || targetStat.isSymbolicLink()))
66
+ ) {
67
+ throw new Error("dependency-path-unsafe");
68
+ }
69
+ if (!targetStat) mkdirSync(target, { mode: 0o700 });
70
+ const view = path.join(privateRoot, "dependencies");
71
+ mounts.push({ source: view, target: "/workspace/node_modules" });
72
+ const entries = readdirSync(dependencies);
73
+ if (entries.length > 20_000) throw new Error("dependency-view-too-large");
74
+ const canonical = realpathSync(dependencies);
75
+ let viewEntries = 0;
76
+ const projectEntry = (name: string) => {
77
+ checkCancelled();
78
+ if (++viewEntries > 20_000)
79
+ throw new Error("dependency-view-too-large");
80
+ if (CACHES.includes(name)) return;
81
+ const source = path.join(dependencies, name);
82
+ const placeholder = path.join(view, name);
83
+ const entry = lstatSync(source);
84
+ if (entry.isSymbolicLink()) {
85
+ const resolved = realpathSync(source);
86
+ if (within(canonical, resolved)) {
87
+ symlinkSync(
88
+ path.relative(path.dirname(source), resolved),
89
+ placeholder,
90
+ );
91
+ } else if (within(realpathSync(dependencyOwner), resolved)) {
92
+ const relative = path.relative(
93
+ realpathSync(dependencyOwner),
94
+ resolved,
95
+ );
96
+ const candidate = path.join(root, relative);
97
+ if (!lstatSync(candidate, { throwIfNoEntry: false }))
98
+ throw new Error("workspace-dependency-missing");
99
+ symlinkSync(
100
+ local
101
+ ? candidate
102
+ : `/workspace/${relative.split(path.sep).join("/")}`,
103
+ placeholder,
104
+ );
105
+ } else throw new Error("dependency-path-unsafe");
106
+ } else if (
107
+ entry.isDirectory() &&
108
+ name.startsWith("@") &&
109
+ !name.includes(path.sep)
110
+ ) {
111
+ mkdirSync(placeholder);
112
+ for (const child of readdirSync(source))
113
+ projectEntry(path.join(name, child));
114
+ } else {
115
+ if (entry.isDirectory()) mkdirSync(placeholder);
116
+ else if (entry.isFile()) writeFileSync(placeholder, "");
117
+ else throw new Error("dependency-path-unsafe");
118
+ mounts.push({
119
+ source,
120
+ target: `/workspace/node_modules/${name.split(path.sep).join("/")}`,
121
+ });
122
+ }
123
+ };
124
+ for (const name of entries) projectEntry(name);
125
+ for (const cache of CACHES) {
126
+ mkdirSync(path.join(view, cache));
127
+ const source = path.join(privateRoot, "cache", cache);
128
+ mkdirSync(source);
129
+ mounts.push({
130
+ source,
131
+ target: `/workspace/node_modules/${cache}`,
132
+ writable: true,
133
+ });
134
+ }
135
+ }
136
+ if (local && stat) {
137
+ // Private copies keep package caches/writes away from consumer dependencies.
138
+ const copyEntry = (name: string) => {
139
+ checkCancelled();
140
+ const source = path.join(dependencies, name);
141
+ const destination = path.join(root, "node_modules", name);
142
+ if (CACHES.includes(name)) {
143
+ mkdirSync(destination, { recursive: true });
144
+ return;
145
+ }
146
+ removeEntry(destination, checkCancelled);
147
+ const stat = lstatSync(source);
148
+ if (stat.isSymbolicLink()) {
149
+ const resolved = realpathSync(source);
150
+ const translated = within(canonicalRoot(dependencyOwner), resolved)
151
+ ? path.join(
152
+ root,
153
+ path.relative(canonicalRoot(dependencyOwner), resolved),
154
+ )
155
+ : resolved;
156
+ symlinkSync(translated, destination);
157
+ } else if (
158
+ stat.isDirectory() &&
159
+ name.startsWith("@") &&
160
+ !name.includes(path.sep)
161
+ ) {
162
+ mkdirSync(destination);
163
+ for (const child of readdirSync(source))
164
+ copyEntry(path.join(name, child));
165
+ } else
166
+ cpSync(source, destination, {
167
+ recursive: true,
168
+ filter: () => {
169
+ checkCancelled();
170
+ return true;
171
+ },
172
+ mode: constants.COPYFILE_FICLONE,
173
+ verbatimSymlinks: true,
174
+ });
175
+ };
176
+ for (const name of readdirSync(path.join(privateRoot, "dependencies")))
177
+ copyEntry(name);
178
+ }
179
+
180
+ const sources = new Map<string, string>();
181
+ const bindings = runnerBindings.map((binding) => {
182
+ if (local || !binding.mountSource) return { ...binding };
183
+ const source = path.resolve(binding.mountSource);
184
+ const stat = lstatSync(source);
185
+ const executable = realpathSync(binding.executablePath);
186
+ if (
187
+ !stat.isDirectory() ||
188
+ stat.isSymbolicLink() ||
189
+ !within(source, executable)
190
+ ) {
191
+ throw new Error("runner-mount-unavailable");
192
+ }
193
+ let target = sources.get(source);
194
+ if (!target) {
195
+ target = `/cadence-runners/${sources.size}`;
196
+ sources.set(source, target);
197
+ mounts.push({ source, target });
198
+ }
199
+ const translate = (file: string) =>
200
+ path.posix.join(target, ...path.relative(source, file).split(path.sep));
201
+ return {
202
+ ...binding,
203
+ executablePath: translate(executable),
204
+ ...(binding.fixedArgs
205
+ ? {
206
+ fixedArgs: binding.fixedArgs.map((arg) =>
207
+ path.isAbsolute(arg) && within(source, arg)
208
+ ? translate(arg)
209
+ : arg,
210
+ ),
211
+ }
212
+ : {}),
213
+ };
214
+ });
215
+ checkCancelled();
216
+ return {
217
+ privateRoot,
218
+ mounts,
219
+ bindings,
220
+ reportRoot: local ? privateRoot : "/cadence",
221
+ environment: {
222
+ ...(local
223
+ ? Object.fromEntries(
224
+ profile.inheritEnvironment.flatMap((name) =>
225
+ environment[name] === undefined
226
+ ? []
227
+ : [[name, environment[name] ?? ""]],
228
+ ),
229
+ )
230
+ : {}),
231
+ CI: "1",
232
+ HOME: local ? path.join(privateRoot, "home") : "/cadence/home",
233
+ XDG_CACHE_HOME: local
234
+ ? path.join(privateRoot, "cache")
235
+ : "/cadence/cache",
236
+ TMPDIR: local ? path.join(privateRoot, "tmp") : "/cadence/tmp",
237
+ npm_config_cache: local
238
+ ? path.join(privateRoot, "cache/npm")
239
+ : "/cadence/cache/npm",
240
+ npm_config_userconfig: local
241
+ ? path.join(privateRoot, "home/user.npmrc")
242
+ : "/cadence/home/user.npmrc",
243
+ npm_config_globalconfig: local
244
+ ? path.join(privateRoot, "home/global.npmrc")
245
+ : "/cadence/home/global.npmrc",
246
+ PATH: [
247
+ ...new Set(
248
+ // Honor explicitly resolved private runtimes before system fallbacks.
249
+ [
250
+ ...bindings.filter((binding) => binding.mountSource),
251
+ ...bindings.filter((binding) => !binding.mountSource),
252
+ ].map((binding) => path.posix.dirname(binding.executablePath)),
253
+ ),
254
+ ...(local ? (environment.PATH ?? "").split(path.delimiter) : []),
255
+ "/usr/local/bin",
256
+ "/usr/bin",
257
+ "/bin",
258
+ ].join(":"),
259
+ },
260
+ };
261
+ } catch (error) {
262
+ cleanup();
263
+ throw error;
264
+ }
265
+ }
266
+
267
+ function canonicalRoot(root: string): string {
268
+ return realpathSync(root);
269
+ }
270
+
271
+ /** Per-entry cancellation, including replacement of an earlier dependency view. */
272
+ function removeEntry(file: string, checkCancelled: () => void): void {
273
+ checkCancelled();
274
+ const stat = lstatSync(file, { throwIfNoEntry: false });
275
+ if (!stat) return;
276
+ if (stat.isDirectory())
277
+ for (const name of readdirSync(file))
278
+ removeEntry(path.join(file, name), checkCancelled);
279
+ rmSync(file, { recursive: stat.isDirectory(), force: true });
280
+ }