@xfey/tutti 0.1.42 → 0.1.44

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.
Files changed (55) hide show
  1. package/dist/artifacts/candidate-validator.d.ts +39 -0
  2. package/dist/artifacts/candidate-validator.js +512 -0
  3. package/dist/artifacts/index.d.ts +3 -1
  4. package/dist/artifacts/index.js +3 -1
  5. package/dist/artifacts/manifest-contract.d.ts +30 -0
  6. package/dist/artifacts/manifest-contract.js +214 -0
  7. package/dist/artifacts/manifest.d.ts +3 -12
  8. package/dist/artifacts/manifest.js +29 -201
  9. package/dist/artifacts/preview-runtime.d.ts +29 -10
  10. package/dist/artifacts/preview-runtime.js +400 -155
  11. package/dist/artifacts/process-boundary.d.ts +33 -0
  12. package/dist/artifacts/process-boundary.js +134 -0
  13. package/dist/control-plane/types.d.ts +7 -2
  14. package/dist/prompt-templates/index.d.ts +1 -1
  15. package/dist/prompt-templates/index.js +1 -0
  16. package/dist/provider-usage/index.js +1 -0
  17. package/dist/run-pipeline/artifact-applicability.d.ts +49 -0
  18. package/dist/run-pipeline/artifact-applicability.js +281 -0
  19. package/dist/run-pipeline/openai.d.ts +6 -0
  20. package/dist/run-pipeline/openai.js +72 -2
  21. package/dist/run-pipeline/promotion-reconcile.d.ts +9 -1
  22. package/dist/run-pipeline/promotion-reconcile.js +65 -11
  23. package/dist/server-shell/cli/launch.d.ts +2 -0
  24. package/dist/server-shell/cli/launch.js +7 -1
  25. package/dist/server-shell/http/host-tunnel.js +1 -1
  26. package/dist/server-shell/http/routes/project-api/artifacts-routes.js +63 -14
  27. package/dist/server-shell/http/routes/project-api/types.d.ts +2 -0
  28. package/dist/server-shell/local-console/project-service.d.ts +5 -0
  29. package/dist/server-shell/local-console/project-service.js +25 -0
  30. package/dist/server-shell/local-console/server.js +10 -0
  31. package/dist/workspace-ops/index.d.ts +1 -1
  32. package/dist/workspace-ops/index.js +1 -1
  33. package/dist/workspace-ops/run-workspaces.d.ts +1 -0
  34. package/dist/workspace-ops/run-workspaces.js +27 -0
  35. package/node_modules/@tutti/shared/dist/schemas/api/artifacts.d.ts +28 -8
  36. package/node_modules/@tutti/shared/dist/schemas/api/artifacts.js +34 -9
  37. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  38. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  39. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +5 -1
  40. package/node_modules/@tutti/shared/dist/utils/redaction/index.d.ts +1 -1
  41. package/node_modules/@tutti/shared/dist/utils/redaction/index.js +14 -0
  42. package/package.json +1 -1
  43. package/prompts/README.md +1 -0
  44. package/prompts/artifacts/README.md +7 -0
  45. package/prompts/artifacts/applicability.md +41 -0
  46. package/prompts/prompt-flow-map.md +29 -26
  47. package/prompts/runs/README.md +3 -3
  48. package/prompts/runs/task-continuation.md +36 -4
  49. package/prompts/runs/task-retry.md +42 -6
  50. package/prompts/runs/task-run.md +36 -4
  51. package/web/assets/index-B7I1QwT_.js +69 -0
  52. package/web/assets/index-F-ouhuJ5.css +1 -0
  53. package/web/index.html +2 -2
  54. package/web/assets/index-Sqw_t67u.css +0 -1
  55. package/web/assets/index-cYUjFgtc.js +0 -29
@@ -0,0 +1,39 @@
1
+ import type { ChildProcessWithoutNullStreams } from "node:child_process";
2
+ import { type ValidatedArtifactContract } from "./manifest-contract.js";
3
+ import { type ArtifactPreviewProcessPlan } from "./process-boundary.js";
4
+ export type ArtifactValidationReasonCode = "artifact_applicability_failed" | "artifact_manifest_missing" | "artifact_manifest_invalid" | "artifact_static_entry_invalid" | "artifact_preview_script_missing" | "artifact_preview_start_failed" | "artifact_preview_not_ready" | "artifact_preview_entry_invalid" | "artifact_preview_modified_candidate";
5
+ export type ArtifactValidationDiagnosticSource = "applicability_judge" | "manifest_parser" | "package_json" | "preview_process" | "ready_probe" | "candidate_diff";
6
+ export type ArtifactValidationDiagnostic = {
7
+ source: ArtifactValidationDiagnosticSource;
8
+ text: string;
9
+ truncated: boolean;
10
+ };
11
+ export type CandidateArtifactValidationFailure = {
12
+ reason_code: ArtifactValidationReasonCode;
13
+ summary: string;
14
+ guidance: string;
15
+ diagnostic: ArtifactValidationDiagnostic;
16
+ };
17
+ export type CandidateArtifactValidationResult = {
18
+ status: "not_declared";
19
+ } | {
20
+ status: "valid";
21
+ declaration: ValidatedArtifactContract;
22
+ } | {
23
+ status: "invalid";
24
+ failure: CandidateArtifactValidationFailure;
25
+ };
26
+ export type ValidateCandidateArtifactOptions = {
27
+ repoRoot: string;
28
+ baseEnv?: NodeJS.ProcessEnv;
29
+ spawnProcess?: (plan: ArtifactPreviewProcessPlan) => ChildProcessWithoutNullStreams;
30
+ allocatePort?: () => Promise<number>;
31
+ fetchImpl?: typeof fetch;
32
+ terminateProcess?: (child: ChildProcessWithoutNullStreams) => Promise<void>;
33
+ fingerprintCandidate?: (repoRoot: string) => string;
34
+ readyTimeoutMs?: number;
35
+ probeIntervalMs?: number;
36
+ probeRequestTimeoutMs?: number;
37
+ };
38
+ export declare function validateCandidateArtifact(options: ValidateCandidateArtifactOptions): Promise<CandidateArtifactValidationResult>;
39
+ //# sourceMappingURL=candidate-validator.d.ts.map
@@ -0,0 +1,512 @@
1
+ import { lstatSync, readFileSync, realpathSync } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve } from "node:path";
3
+ import { fingerprintWorkspaceCandidate } from "../workspace-ops/index.js";
4
+ import { ARTIFACT_MANIFEST_PATH, MAX_ARTIFACT_MANIFEST_BYTES, parseArtifactManifestContract, } from "./manifest-contract.js";
5
+ import { allocateArtifactPreviewPort, ArtifactDiagnosticBuffer, cleanupArtifactProcessRuntime, createArtifactPreviewEnv, createArtifactProcessRuntime, spawnArtifactPreviewProcess, terminateArtifactProcessTree, } from "./process-boundary.js";
6
+ const MAX_PACKAGE_JSON_BYTES = 1024 * 1024;
7
+ const MAX_ENTRY_RESPONSE_BYTES = 1024 * 1024;
8
+ const DEFAULT_READY_TIMEOUT_MS = 20_000;
9
+ const DEFAULT_PROBE_INTERVAL_MS = 250;
10
+ const DEFAULT_PROBE_REQUEST_TIMEOUT_MS = 2_000;
11
+ const MAX_ENTRY_REDIRECTS = 3;
12
+ function directDiagnostic(source, text, sensitive) {
13
+ const buffer = new ArtifactDiagnosticBuffer();
14
+ buffer.append("stderr", text);
15
+ const result = buffer.read([sensitive.repoRoot, ...(sensitive.extra ?? [])]);
16
+ return {
17
+ source,
18
+ text: result.text.replace(/^\[stderr\]\n/u, ""),
19
+ truncated: result.truncated,
20
+ };
21
+ }
22
+ function failure(options) {
23
+ return {
24
+ status: "invalid",
25
+ failure: {
26
+ reason_code: options.reasonCode,
27
+ summary: options.summary,
28
+ guidance: options.guidance,
29
+ diagnostic: directDiagnostic(options.source, options.detail, options.sensitive),
30
+ },
31
+ };
32
+ }
33
+ function isContainedPath(root, target) {
34
+ const relativePath = relative(root, target);
35
+ return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
36
+ }
37
+ function requireContainedRealPath(options) {
38
+ try {
39
+ const unresolved = resolve(options.repoRealPath, options.repoRelativePath);
40
+ const realPath = realpathSync.native(unresolved);
41
+ if (!isContainedPath(options.repoRealPath, realPath)) {
42
+ return { ok: false, detail: `${options.label} resolves outside the candidate repository` };
43
+ }
44
+ const stat = lstatSync(realPath);
45
+ const expected = options.expectedKind === "file" ? stat.isFile() : stat.isDirectory();
46
+ if (!expected) {
47
+ return { ok: false, detail: `${options.label} is not a ${options.expectedKind}` };
48
+ }
49
+ return { ok: true, path: realPath };
50
+ }
51
+ catch (error) {
52
+ const detail = error instanceof Error ? error.message : `${options.label} is unavailable`;
53
+ return { ok: false, detail };
54
+ }
55
+ }
56
+ function readCandidateManifest(repoRealPath) {
57
+ const manifestPath = join(repoRealPath, ARTIFACT_MANIFEST_PATH);
58
+ let stat;
59
+ try {
60
+ stat = lstatSync(manifestPath);
61
+ }
62
+ catch (error) {
63
+ if (error.code === "ENOENT") {
64
+ return { status: "not_declared" };
65
+ }
66
+ return {
67
+ status: "invalid",
68
+ detail: error instanceof Error ? error.message : "Artifact manifest cannot be read",
69
+ };
70
+ }
71
+ if (!stat.isFile()) {
72
+ return { status: "invalid", detail: "Artifact manifest path is not a regular file" };
73
+ }
74
+ if (stat.size > MAX_ARTIFACT_MANIFEST_BYTES) {
75
+ return { status: "invalid", detail: "Artifact manifest is too large" };
76
+ }
77
+ try {
78
+ const parsed = parseArtifactManifestContract(readFileSync(manifestPath));
79
+ return parsed.ok
80
+ ? { status: "declared", contract: parsed.declaration }
81
+ : { status: "invalid", detail: parsed.error.message };
82
+ }
83
+ catch (error) {
84
+ return {
85
+ status: "invalid",
86
+ detail: error instanceof Error ? error.message : "Artifact manifest cannot be read",
87
+ };
88
+ }
89
+ }
90
+ function validateStaticCandidate(repoRealPath, contract) {
91
+ const runtime = contract.runtime;
92
+ if (runtime.kind !== "static") {
93
+ throw new Error("Expected a static Artifact contract");
94
+ }
95
+ const root = requireContainedRealPath({
96
+ repoRealPath,
97
+ repoRelativePath: runtime.root_path,
98
+ expectedKind: "directory",
99
+ label: "Static artifact root",
100
+ });
101
+ if (!root.ok) {
102
+ return failure({
103
+ reasonCode: "artifact_static_entry_invalid",
104
+ summary: "The static Artifact root is not usable.",
105
+ guidance: "Point root at an existing candidate directory inside the repository.",
106
+ source: "manifest_parser",
107
+ detail: root.detail,
108
+ sensitive: { repoRoot: repoRealPath },
109
+ });
110
+ }
111
+ const entry = requireContainedRealPath({
112
+ repoRealPath: root.path,
113
+ repoRelativePath: runtime.entry_path,
114
+ expectedKind: "file",
115
+ label: "Static artifact entry",
116
+ });
117
+ if (!entry.ok) {
118
+ return failure({
119
+ reasonCode: "artifact_static_entry_invalid",
120
+ summary: "The static Artifact entry is not usable.",
121
+ guidance: "Point entry at an existing HTML file inside the declared static root.",
122
+ source: "manifest_parser",
123
+ detail: entry.detail,
124
+ sensitive: { repoRoot: repoRealPath, extra: [root.path] },
125
+ });
126
+ }
127
+ return { status: "valid", declaration: contract };
128
+ }
129
+ function readPreviewScript(options) {
130
+ const packageJson = requireContainedRealPath({
131
+ repoRealPath: options.repoRealPath,
132
+ repoRelativePath: relative(options.repoRealPath, join(options.cwdPath, "package.json")),
133
+ expectedKind: "file",
134
+ label: "Server artifact package.json",
135
+ });
136
+ if (!packageJson.ok) {
137
+ return packageJson;
138
+ }
139
+ try {
140
+ const stat = lstatSync(packageJson.path);
141
+ if (stat.size > MAX_PACKAGE_JSON_BYTES) {
142
+ return { ok: false, detail: "Server artifact package.json is too large" };
143
+ }
144
+ const parsed = JSON.parse(readFileSync(packageJson.path, "utf8"));
145
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
146
+ return { ok: false, detail: "Server artifact package.json must contain a JSON object" };
147
+ }
148
+ const scripts = parsed.scripts;
149
+ if (scripts === null || typeof scripts !== "object" || Array.isArray(scripts)) {
150
+ return { ok: false, detail: "package.json scripts.artifact:preview is required" };
151
+ }
152
+ const preview = scripts["artifact:preview"];
153
+ if (typeof preview !== "string" || preview.trim().length === 0) {
154
+ return { ok: false, detail: "package.json scripts.artifact:preview is required" };
155
+ }
156
+ return { ok: true };
157
+ }
158
+ catch (error) {
159
+ return {
160
+ ok: false,
161
+ detail: error instanceof Error ? error.message : "Server artifact package.json is invalid",
162
+ };
163
+ }
164
+ }
165
+ async function readBoundedResponseBody(response) {
166
+ const contentLength = Number(response.headers.get("content-length"));
167
+ if (Number.isFinite(contentLength) && contentLength > MAX_ENTRY_RESPONSE_BYTES) {
168
+ throw new Error("Artifact entry response is too large");
169
+ }
170
+ if (response.body === null) {
171
+ return Buffer.alloc(0);
172
+ }
173
+ const reader = response.body.getReader();
174
+ const chunks = [];
175
+ let total = 0;
176
+ try {
177
+ while (true) {
178
+ const result = await reader.read();
179
+ if (result.done) {
180
+ break;
181
+ }
182
+ const chunk = Buffer.from(result.value);
183
+ total += chunk.byteLength;
184
+ if (total > MAX_ENTRY_RESPONSE_BYTES) {
185
+ await reader.cancel();
186
+ throw new Error("Artifact entry response is too large");
187
+ }
188
+ chunks.push(chunk);
189
+ }
190
+ }
191
+ finally {
192
+ reader.releaseLock();
193
+ }
194
+ return Buffer.concat(chunks, total);
195
+ }
196
+ function looksLikeHtml(content) {
197
+ const prefix = content.subarray(0, 1024).toString("utf8").trimStart().toLowerCase();
198
+ return prefix.startsWith("<!doctype html") || prefix.startsWith("<html");
199
+ }
200
+ async function probeServerEntry(options) {
201
+ let current = options.entryUrl;
202
+ for (let redirects = 0; redirects <= MAX_ENTRY_REDIRECTS; redirects += 1) {
203
+ const controller = new AbortController();
204
+ const timeout = setTimeout(() => controller.abort(), options.requestTimeoutMs);
205
+ let response;
206
+ try {
207
+ response = await options.fetchImpl(current, {
208
+ method: "GET",
209
+ redirect: "manual",
210
+ signal: controller.signal,
211
+ });
212
+ }
213
+ catch (error) {
214
+ const text = error instanceof Error ? error.message : "Artifact entry request failed";
215
+ return { kind: "retry", text };
216
+ }
217
+ finally {
218
+ clearTimeout(timeout);
219
+ }
220
+ if (response.status >= 300 && response.status < 400) {
221
+ const location = response.headers.get("location");
222
+ if (location === null) {
223
+ return { kind: "invalid", text: `Artifact entry returned HTTP ${response.status}` };
224
+ }
225
+ if (redirects === MAX_ENTRY_REDIRECTS) {
226
+ return { kind: "invalid", text: "Artifact entry redirected too many times" };
227
+ }
228
+ const next = new URL(location, current);
229
+ if (next.origin !== options.entryUrl.origin) {
230
+ return { kind: "invalid", text: "Artifact entry redirected outside its preview instance" };
231
+ }
232
+ current = next;
233
+ continue;
234
+ }
235
+ if (response.status < 200 || response.status >= 300) {
236
+ return { kind: "invalid", text: `Artifact entry returned HTTP ${response.status}` };
237
+ }
238
+ try {
239
+ const content = await readBoundedResponseBody(response);
240
+ const mediaType = response.headers.get("content-type")?.toLowerCase() ?? "";
241
+ if (!mediaType.includes("text/html") && !looksLikeHtml(content)) {
242
+ return {
243
+ kind: "invalid",
244
+ text: `Artifact entry returned ${mediaType || "non-HTML content"}`,
245
+ };
246
+ }
247
+ return { kind: "valid" };
248
+ }
249
+ catch (error) {
250
+ return {
251
+ kind: "invalid",
252
+ text: error instanceof Error ? error.message : "Artifact entry response is invalid",
253
+ };
254
+ }
255
+ }
256
+ return { kind: "invalid", text: "Artifact entry redirected too many times" };
257
+ }
258
+ function delay(milliseconds) {
259
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));
260
+ }
261
+ async function validateServerCandidate(repoRealPath, contract, options) {
262
+ const runtime = contract.runtime;
263
+ if (runtime.kind !== "server") {
264
+ throw new Error("Expected a server Artifact contract");
265
+ }
266
+ const cwd = requireContainedRealPath({
267
+ repoRealPath,
268
+ repoRelativePath: runtime.cwd_path,
269
+ expectedKind: "directory",
270
+ label: "Server artifact cwd",
271
+ });
272
+ if (!cwd.ok) {
273
+ return failure({
274
+ reasonCode: "artifact_preview_script_missing",
275
+ summary: "The server Artifact package directory is not usable.",
276
+ guidance: "Point cwd at an npm package directory inside the candidate repository.",
277
+ source: "package_json",
278
+ detail: cwd.detail,
279
+ sensitive: { repoRoot: repoRealPath },
280
+ });
281
+ }
282
+ const script = readPreviewScript({ repoRealPath, cwdPath: cwd.path });
283
+ if (!script.ok) {
284
+ return failure({
285
+ reasonCode: "artifact_preview_script_missing",
286
+ summary: "The server Artifact preview script is missing or invalid.",
287
+ guidance: "Add a non-empty scripts.artifact:preview command to cwd/package.json.",
288
+ source: "package_json",
289
+ detail: script.detail,
290
+ sensitive: { repoRoot: repoRealPath, extra: [cwd.path] },
291
+ });
292
+ }
293
+ const fingerprint = options.fingerprintCandidate ?? fingerprintWorkspaceCandidate;
294
+ let beforeFingerprint;
295
+ try {
296
+ beforeFingerprint = fingerprint(repoRealPath);
297
+ }
298
+ catch (error) {
299
+ return failure({
300
+ reasonCode: "artifact_preview_modified_candidate",
301
+ summary: "Tutti could not capture the candidate state before preview validation.",
302
+ guidance: "Keep the candidate repository in a valid Git worktree and retry.",
303
+ source: "candidate_diff",
304
+ detail: error instanceof Error ? error.message : "Candidate state capture failed",
305
+ sensitive: { repoRoot: repoRealPath },
306
+ });
307
+ }
308
+ const processRuntime = createArtifactProcessRuntime();
309
+ const diagnostics = new ArtifactDiagnosticBuffer();
310
+ let child;
311
+ let port;
312
+ let result;
313
+ try {
314
+ try {
315
+ port = await (options.allocatePort ?? allocateArtifactPreviewPort)();
316
+ const plan = {
317
+ command: "npm",
318
+ args: ["run", "artifact:preview"],
319
+ cwd: cwd.path,
320
+ env: createArtifactPreviewEnv({
321
+ sourceEnv: options.baseEnv ?? process.env,
322
+ port,
323
+ runtime: processRuntime,
324
+ }),
325
+ port,
326
+ };
327
+ child = (options.spawnProcess ?? spawnArtifactPreviewProcess)(plan);
328
+ }
329
+ catch (error) {
330
+ result = failure({
331
+ reasonCode: "artifact_preview_start_failed",
332
+ summary: "The server Artifact preview could not start.",
333
+ guidance: "Fix the artifact:preview script so npm can start it with the injected HOST and PORT.",
334
+ source: "preview_process",
335
+ detail: error instanceof Error ? error.message : "Preview process could not start",
336
+ sensitive: { repoRoot: repoRealPath, extra: [cwd.path, processRuntime.root] },
337
+ });
338
+ }
339
+ if (child !== undefined && port !== undefined) {
340
+ child.stdout.on("data", (chunk) => diagnostics.append("stdout", chunk));
341
+ child.stderr.on("data", (chunk) => diagnostics.append("stderr", chunk));
342
+ let processFailure;
343
+ child.once("error", (error) => {
344
+ processFailure = error.message;
345
+ });
346
+ child.once("close", (code, signal) => {
347
+ processFailure =
348
+ code === null
349
+ ? `Preview process exited with signal ${signal ?? "unknown"}`
350
+ : `Preview process exited with code ${code}`;
351
+ });
352
+ const deadline = Date.now() + (options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS);
353
+ let lastRetryDetail = "Artifact entry did not accept a connection";
354
+ while (result === undefined && Date.now() < deadline) {
355
+ if (processFailure !== undefined) {
356
+ const processOutput = diagnostics.read([
357
+ repoRealPath,
358
+ cwd.path,
359
+ processRuntime.root,
360
+ String(port),
361
+ ]);
362
+ const fallbackDiagnostic = directDiagnostic("preview_process", processFailure, {
363
+ repoRoot: repoRealPath,
364
+ extra: [cwd.path, processRuntime.root, String(port)],
365
+ });
366
+ result = {
367
+ status: "invalid",
368
+ failure: {
369
+ reason_code: "artifact_preview_start_failed",
370
+ summary: "The server Artifact preview exited before its entry became ready.",
371
+ guidance: "Fix the artifact:preview script and make it keep listening on the injected HOST and PORT.",
372
+ diagnostic: {
373
+ source: "preview_process",
374
+ text: processOutput.text.trim().length > 0
375
+ ? processOutput.text
376
+ : fallbackDiagnostic.text,
377
+ truncated: processOutput.truncated || fallbackDiagnostic.truncated,
378
+ },
379
+ },
380
+ };
381
+ break;
382
+ }
383
+ const probe = await probeServerEntry({
384
+ fetchImpl: options.fetchImpl ?? fetch,
385
+ entryUrl: new URL(`http://127.0.0.1:${port}${runtime.entry_path}`),
386
+ requestTimeoutMs: options.probeRequestTimeoutMs ?? DEFAULT_PROBE_REQUEST_TIMEOUT_MS,
387
+ });
388
+ if (probe.kind === "valid") {
389
+ result = { status: "valid", declaration: contract };
390
+ break;
391
+ }
392
+ if (probe.kind === "invalid") {
393
+ result = failure({
394
+ reasonCode: "artifact_preview_entry_invalid",
395
+ summary: "The server Artifact entry is not a renderable HTML response.",
396
+ guidance: "Make the declared entry return successful HTML from the preview server.",
397
+ source: "ready_probe",
398
+ detail: probe.text,
399
+ sensitive: {
400
+ repoRoot: repoRealPath,
401
+ extra: [cwd.path, processRuntime.root, String(port)],
402
+ },
403
+ });
404
+ break;
405
+ }
406
+ lastRetryDetail = probe.text;
407
+ await delay(options.probeIntervalMs ?? DEFAULT_PROBE_INTERVAL_MS);
408
+ }
409
+ if (result === undefined) {
410
+ const processOutput = diagnostics.read([
411
+ repoRealPath,
412
+ cwd.path,
413
+ processRuntime.root,
414
+ String(port),
415
+ ]);
416
+ const fallbackDiagnostic = directDiagnostic("ready_probe", lastRetryDetail, {
417
+ repoRoot: repoRealPath,
418
+ extra: [cwd.path, processRuntime.root, String(port)],
419
+ });
420
+ result = {
421
+ status: "invalid",
422
+ failure: {
423
+ reason_code: "artifact_preview_not_ready",
424
+ summary: "The server Artifact preview did not become ready in time.",
425
+ guidance: "Make artifact:preview listen promptly on the injected HOST and PORT and serve the declared entry.",
426
+ diagnostic: {
427
+ source: processOutput.text.trim().length > 0 ? "preview_process" : "ready_probe",
428
+ text: processOutput.text.trim().length > 0 ? processOutput.text : fallbackDiagnostic.text,
429
+ truncated: processOutput.truncated || fallbackDiagnostic.truncated,
430
+ },
431
+ },
432
+ };
433
+ }
434
+ }
435
+ }
436
+ finally {
437
+ if (child !== undefined) {
438
+ await (options.terminateProcess ?? terminateArtifactProcessTree)(child);
439
+ }
440
+ cleanupArtifactProcessRuntime(processRuntime);
441
+ }
442
+ let afterFingerprint;
443
+ try {
444
+ afterFingerprint = fingerprint(repoRealPath);
445
+ }
446
+ catch (error) {
447
+ return failure({
448
+ reasonCode: "artifact_preview_modified_candidate",
449
+ summary: "Tutti could not verify the candidate state after preview validation.",
450
+ guidance: "Ensure artifact:preview exits cleanly without changing repository files.",
451
+ source: "candidate_diff",
452
+ detail: error instanceof Error ? error.message : "Candidate state capture failed",
453
+ sensitive: { repoRoot: repoRealPath },
454
+ });
455
+ }
456
+ if (afterFingerprint !== beforeFingerprint) {
457
+ return failure({
458
+ reasonCode: "artifact_preview_modified_candidate",
459
+ summary: "The server Artifact preview modified the candidate repository.",
460
+ guidance: "Make artifact:preview read-only, or generate required tracked files before validation.",
461
+ source: "candidate_diff",
462
+ detail: "Candidate Git state changed while artifact:preview was running",
463
+ sensitive: { repoRoot: repoRealPath },
464
+ });
465
+ }
466
+ return (result ??
467
+ failure({
468
+ reasonCode: "artifact_preview_start_failed",
469
+ summary: "The server Artifact preview validation did not complete.",
470
+ guidance: "Fix the artifact:preview script and retry.",
471
+ source: "preview_process",
472
+ detail: "Preview validation ended without a result",
473
+ sensitive: { repoRoot: repoRealPath },
474
+ }));
475
+ }
476
+ export async function validateCandidateArtifact(options) {
477
+ let repoRealPath;
478
+ try {
479
+ repoRealPath = realpathSync.native(options.repoRoot);
480
+ if (!lstatSync(repoRealPath).isDirectory()) {
481
+ throw new Error("Candidate repository root is not a directory");
482
+ }
483
+ }
484
+ catch (error) {
485
+ return failure({
486
+ reasonCode: "artifact_manifest_invalid",
487
+ summary: "The candidate repository could not be inspected for an Artifact.",
488
+ guidance: "Retry with a valid candidate workspace.",
489
+ source: "manifest_parser",
490
+ detail: error instanceof Error ? error.message : "Candidate repository is unavailable",
491
+ sensitive: { repoRoot: options.repoRoot },
492
+ });
493
+ }
494
+ const candidate = readCandidateManifest(repoRealPath);
495
+ if (candidate.status === "not_declared") {
496
+ return { status: "not_declared" };
497
+ }
498
+ if (candidate.status === "invalid") {
499
+ return failure({
500
+ reasonCode: "artifact_manifest_invalid",
501
+ summary: "The candidate Artifact manifest is invalid.",
502
+ guidance: "Rewrite tutti.artifact.json using the complete strict manifest contract.",
503
+ source: "manifest_parser",
504
+ detail: candidate.detail,
505
+ sensitive: { repoRoot: repoRealPath },
506
+ });
507
+ }
508
+ return candidate.contract.runtime.kind === "static"
509
+ ? validateStaticCandidate(repoRealPath, candidate.contract)
510
+ : await validateServerCandidate(repoRealPath, candidate.contract, options);
511
+ }
512
+ //# sourceMappingURL=candidate-validator.js.map
@@ -1,4 +1,6 @@
1
1
  export { ARTIFACT_MANIFEST_PATH, ARTIFACT_PREVIEW_BASE_PATH, buildArtifactPreviewUrl, readArtifactDeclaration, readArtifactProjection, type ArtifactDeclaration, type DeclaredArtifactRuntime, type ReadArtifactDeclarationOptions, } from "./manifest.js";
2
- export { ARTIFACT_PREVIEW_DEFAULT_BASE_PATH, ArtifactPreviewManager, type ArtifactPreviewCommandResult, type ArtifactPreviewHeartbeatResult, type ArtifactPreviewManagerOptions, type ArtifactPreviewProcessPlan, type ArtifactPreviewStartResult, type ArtifactPreviewStopResult, type ArtifactServerPreviewResponse, } from "./preview-runtime.js";
2
+ export { validateCandidateArtifact, type ArtifactValidationDiagnostic, type ArtifactValidationDiagnosticSource, type ArtifactValidationReasonCode, type CandidateArtifactValidationFailure, type CandidateArtifactValidationResult, type ValidateCandidateArtifactOptions, } from "./candidate-validator.js";
3
+ export { parseArtifactManifestContract, type ArtifactContractError, type ArtifactContractResult, type ValidatedArtifactContract, type ValidatedArtifactRuntime, } from "./manifest-contract.js";
4
+ export { ARTIFACT_PREVIEW_REQUEST_BODY_LIMIT_BYTES, ARTIFACT_PREVIEW_RESPONSE_BODY_LIMIT_BYTES, ARTIFACT_PREVIEW_DEFAULT_BASE_PATH, ArtifactPreviewManager, isArtifactPreviewHttpMethod, type ArtifactPreviewCommandResult, type ArtifactPreviewHeartbeatResult, type ArtifactPreviewHttpMethod, type ArtifactPreviewManagerOptions, type ArtifactPreviewProcessPlan, type ArtifactPreviewStartResult, type ArtifactPreviewStopResult, type ArtifactServerPreviewResponse, } from "./preview-runtime.js";
3
5
  export { ARTIFACT_PREVIEW_CSP, readArtifactStaticPreview, rewriteArtifactPreviewResourceUrls, type ArtifactStaticPreviewFile, type ReadArtifactStaticPreviewOptions, } from "./static-preview.js";
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,6 @@
1
1
  export { ARTIFACT_MANIFEST_PATH, ARTIFACT_PREVIEW_BASE_PATH, buildArtifactPreviewUrl, readArtifactDeclaration, readArtifactProjection, } from "./manifest.js";
2
- export { ARTIFACT_PREVIEW_DEFAULT_BASE_PATH, ArtifactPreviewManager, } from "./preview-runtime.js";
2
+ export { validateCandidateArtifact, } from "./candidate-validator.js";
3
+ export { parseArtifactManifestContract, } from "./manifest-contract.js";
4
+ export { ARTIFACT_PREVIEW_REQUEST_BODY_LIMIT_BYTES, ARTIFACT_PREVIEW_RESPONSE_BODY_LIMIT_BYTES, ARTIFACT_PREVIEW_DEFAULT_BASE_PATH, ArtifactPreviewManager, isArtifactPreviewHttpMethod, } from "./preview-runtime.js";
3
5
  export { ARTIFACT_PREVIEW_CSP, readArtifactStaticPreview, rewriteArtifactPreviewResourceUrls, } from "./static-preview.js";
4
6
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,30 @@
1
+ import type { ArtifactManifest } from "@tutti/shared/schemas/api";
2
+ export declare const ARTIFACT_MANIFEST_PATH: "tutti.artifact.json";
3
+ export declare const MAX_ARTIFACT_MANIFEST_BYTES: number;
4
+ export type ArtifactContractError = {
5
+ code: "too_large" | "unsupported_encoding" | "invalid_json" | "invalid_schema" | "invalid_path" | "invalid_entry";
6
+ message: string;
7
+ };
8
+ export type ValidatedArtifactRuntime = {
9
+ kind: "static";
10
+ root_path: string;
11
+ entry_path: string;
12
+ entry_repo_path: string;
13
+ } | {
14
+ kind: "server";
15
+ cwd_path: string;
16
+ entry_path: string;
17
+ };
18
+ export type ValidatedArtifactContract = {
19
+ manifest: ArtifactManifest;
20
+ runtime: ValidatedArtifactRuntime;
21
+ };
22
+ export type ArtifactContractResult = {
23
+ ok: true;
24
+ declaration: ValidatedArtifactContract;
25
+ } | {
26
+ ok: false;
27
+ error: ArtifactContractError;
28
+ };
29
+ export declare function parseArtifactManifestContract(buffer: Buffer): ArtifactContractResult;
30
+ //# sourceMappingURL=manifest-contract.d.ts.map