@treeseed/sdk 0.12.43 → 0.12.45

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 (37) hide show
  1. package/dist/guarantees/index.d.ts +83 -0
  2. package/dist/guarantees/index.js +688 -86
  3. package/dist/hosting/graph.js +40 -6
  4. package/dist/operations/services/git-workflow.d.ts +16 -1
  5. package/dist/operations/services/git-workflow.js +65 -5
  6. package/dist/operations/services/github-api.js +0 -19
  7. package/dist/operations/services/hosted-service-checks.js +17 -1
  8. package/dist/operations/services/live-hosted-service-checks.d.ts +4 -0
  9. package/dist/operations/services/live-hosted-service-checks.js +7 -4
  10. package/dist/operations/services/local-cleanup.js +3 -7
  11. package/dist/operations/services/package-adapters.d.ts +14 -0
  12. package/dist/operations/services/package-adapters.js +36 -3
  13. package/dist/operations/services/package-artifacts.d.ts +37 -0
  14. package/dist/operations/services/package-artifacts.js +99 -0
  15. package/dist/operations/services/railway-deploy.js +78 -18
  16. package/dist/operations/services/railway-source-policy.d.ts +19 -0
  17. package/dist/operations/services/railway-source-policy.js +66 -0
  18. package/dist/operations/services/repository-save-orchestrator.js +12 -5
  19. package/dist/platform/desired-state.js +2 -3
  20. package/dist/reconcile/builtin-adapters.js +10 -2
  21. package/dist/reconcile/providers/railway-iac.d.ts +2 -0
  22. package/dist/reconcile/providers/railway-iac.js +31 -3
  23. package/dist/scenes/builtin-plugins.js +36 -5
  24. package/dist/scenes/device-matrix.js +2 -0
  25. package/dist/scenes/environment.js +1 -1
  26. package/dist/scenes/runner.js +28 -16
  27. package/dist/scenes/schema.js +31 -2
  28. package/dist/scenes/types.d.ts +25 -2
  29. package/dist/scenes/visual-audit-fixtures.js +9 -3
  30. package/dist/workflow/operations.d.ts +27 -92
  31. package/dist/workflow/operations.js +236 -144
  32. package/dist/workflow/runs.d.ts +1 -0
  33. package/dist/workflow/runs.js +57 -0
  34. package/dist/workflow-support.d.ts +1 -0
  35. package/dist/workflow-support.js +8 -0
  36. package/dist/workflow.d.ts +2 -0
  37. package/package.json +4 -1
@@ -15,6 +15,19 @@ function mailpitApiUrl(mailpitUrl, pathname) {
15
15
  function stringValue(value) {
16
16
  return typeof value === "string" ? value : "";
17
17
  }
18
+ function shortRuntimeHash(value) {
19
+ let hash = 2166136261;
20
+ for (let index = 0; index < value.length; index += 1) {
21
+ hash ^= value.charCodeAt(index);
22
+ hash = Math.imul(hash, 16777619);
23
+ }
24
+ return (hash >>> 0).toString(36).padStart(7, "0").slice(0, 10);
25
+ }
26
+ function sceneRuntimeValue(value, context) {
27
+ const runSlug = context.runId.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "").slice(0, 48);
28
+ const runShort = shortRuntimeHash(context.runId);
29
+ return value.replace(/\{\{\s*runId\s*\}\}/gu, context.runId).replace(/\{\{\s*runSlug\s*\}\}/gu, runSlug).replace(/\{\{\s*runShort\s*\}\}/gu, runShort);
30
+ }
18
31
  function mailpitMessageId(value) {
19
32
  if (!value || typeof value !== "object") return "";
20
33
  const record = value;
@@ -57,6 +70,16 @@ function extractConfirmationUrl(body) {
57
70
  const relative = body.match(/(?:\/auth\/confirm-email\?[^"' <>\n]+|\/team-invites\/[^"' <>\n]+\/accept)/u)?.[0];
58
71
  return relative ?? null;
59
72
  }
73
+ function resolveMailpitConfirmationUrl(confirmationUrl, context) {
74
+ try {
75
+ const parsed = new URL(confirmationUrl);
76
+ if (parsed.pathname === "/auth/confirm-email" || /^\/team-invites\/[^/]+\/accept$/u.test(parsed.pathname)) {
77
+ return context.resolveUrl(`${parsed.pathname}${parsed.search}${parsed.hash}`);
78
+ }
79
+ } catch {
80
+ }
81
+ return confirmationUrl.startsWith("/") ? context.resolveUrl(confirmationUrl) : confirmationUrl;
82
+ }
60
83
  async function sleep(ms) {
61
84
  await new Promise((resolve) => setTimeout(resolve, ms));
62
85
  }
@@ -68,7 +91,11 @@ async function navigateScenePage(page, url) {
68
91
  let lastError = null;
69
92
  for (let attempt = 1; attempt <= 3; attempt += 1) {
70
93
  try {
71
- await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
94
+ const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
95
+ const status = response?.status();
96
+ if (typeof status === "number" && status >= 400) {
97
+ throw sceneErrorDiagnostic("scene.navigation_http_error", `Navigation to ${response?.url() ?? url} returned HTTP ${status}.`, "workflow.action.goto");
98
+ }
72
99
  return;
73
100
  } catch (error) {
74
101
  lastError = error;
@@ -76,7 +103,7 @@ async function navigateScenePage(page, url) {
76
103
  await sleep(500 * attempt);
77
104
  }
78
105
  }
79
- throw lastError instanceof Error ? lastError : new Error(String(lastError ?? `Navigation failed for ${url}`));
106
+ throw lastError && typeof lastError === "object" && "code" in lastError ? lastError : lastError instanceof Error ? lastError : new Error(String(lastError ?? `Navigation failed for ${url}`));
80
107
  }
81
108
  async function assertionReport(kind, action, selector) {
82
109
  const startedAt = /* @__PURE__ */ new Date();
@@ -132,7 +159,7 @@ function createBuiltInTreeseedScenePlugins() {
132
159
  if (!("fill" in action)) return { ok: false, diagnostics: [sceneErrorDiagnostic("scene.invalid_action", "Expected fill action.", "workflow.action.fill")] };
133
160
  const locator = context.resolveSelector(action.fill);
134
161
  await locator.waitFor({ state: "visible", timeout: 1e4 });
135
- await locator.fill(action.fill.value);
162
+ await locator.fill(sceneRuntimeValue(action.fill.value, context));
136
163
  return { ok: true, diagnostics: [] };
137
164
  }
138
165
  },
@@ -208,7 +235,11 @@ function createBuiltInTreeseedScenePlugins() {
208
235
  summary: "Confirm the latest local Mailpit email by navigating the browser to its confirmation link.",
209
236
  async run({ action, step, context }) {
210
237
  if (!("mailpitConfirmLatest" in action)) return { ok: false, diagnostics: [sceneErrorDiagnostic("scene.invalid_action", "Expected mailpitConfirmLatest action.", `workflow.${step.id}.action.mailpitConfirmLatest`)] };
211
- const { mailpitUrl, email, subjectIncludes, displayInboxSeconds, displayMessageSeconds } = action.mailpitConfirmLatest;
238
+ const raw = action.mailpitConfirmLatest;
239
+ const mailpitUrl = sceneRuntimeValue(raw.mailpitUrl, context);
240
+ const email = sceneRuntimeValue(raw.email, context);
241
+ const subjectIncludes = raw.subjectIncludes ? sceneRuntimeValue(raw.subjectIncludes, context) : void 0;
242
+ const { displayInboxSeconds, displayMessageSeconds } = raw;
212
243
  try {
213
244
  const listResponse = await fetch(mailpitApiUrl(mailpitUrl, "/api/v1/messages"));
214
245
  if (!listResponse.ok) {
@@ -233,7 +264,7 @@ function createBuiltInTreeseedScenePlugins() {
233
264
  if (!confirmationUrl) {
234
265
  return { ok: false, diagnostics: [sceneErrorDiagnostic("scene.mailpit_confirm_link_not_found", `No confirmation link was found in Mailpit message ${id}.`, `workflow.${step.id}.action.mailpitConfirmLatest`)] };
235
266
  }
236
- const resolvedUrl = confirmationUrl.startsWith("/") ? context.resolveUrl(confirmationUrl) : confirmationUrl;
267
+ const resolvedUrl = resolveMailpitConfirmationUrl(confirmationUrl, context);
237
268
  const mailpitBase = mailpitUrl.endsWith("/") ? mailpitUrl : `${mailpitUrl}/`;
238
269
  if (displayInboxSeconds && displayInboxSeconds > 0) {
239
270
  const search = new URL("search", mailpitBase);
@@ -63,6 +63,8 @@ async function runTreeseedSceneDeviceMatrix(input) {
63
63
  scene: input.scene,
64
64
  environment: input.environment,
65
65
  device,
66
+ browser: input.browser,
67
+ authRole: input.authRole,
66
68
  record: input.record,
67
69
  artifactMode: input.artifactMode,
68
70
  mode: input.mode,
@@ -39,7 +39,7 @@ async function prepareTreeseedSceneEnvironment(input) {
39
39
  baseUrl = healthUrl(existing);
40
40
  } else {
41
41
  try {
42
- const result = await startTreeseedManagedDev({ cwd: input.projectRoot, surfaces: "web,api,operations-runner", webRuntime: "local", env: input.env });
42
+ const result = await startTreeseedManagedDev({ cwd: input.projectRoot, surfaces: "web,api", webRuntime: "local", env: input.env });
43
43
  instances = result.instances;
44
44
  const web = result.instances.find((entry) => entry.surface === "web" || entry.id === "web");
45
45
  started = Boolean(web && running(web));
@@ -1,4 +1,4 @@
1
- import { copyFileSync, writeFileSync } from "node:fs";
1
+ import { writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { sceneActionKind, sceneExpectationKinds } from "./schema.js";
4
4
  import { planTreeseedScene, validateTreeseedScene } from "./planner.js";
@@ -126,6 +126,7 @@ function planForInput(input, validation) {
126
126
  }
127
127
  const scene = validation.scene;
128
128
  const environment = input.environment ?? scene.target.environment;
129
+ const browser = input.browser ?? scene.target.browser;
129
130
  const workflowSteps = scene.workflow.map((step) => ({
130
131
  id: step.id,
131
132
  title: step.title,
@@ -145,7 +146,7 @@ function planForInput(input, validation) {
145
146
  title: scene.title,
146
147
  environment,
147
148
  baseUrl: scene.target.baseUrl,
148
- browser: scene.target.browser,
149
+ browser,
149
150
  viewport: scene.target.viewport,
150
151
  workflowSteps,
151
152
  enabledActions: actionIds,
@@ -174,6 +175,20 @@ function planForInput(input, validation) {
174
175
  function canContinueAfterFailure(scene, step) {
175
176
  return scene.runtime.mode === "demo" || scene.runtime.mode === "training" || step.demoOnly === true || step.continueOnFailure === true || scene.runtime.failure.continueOnFailure === true;
176
177
  }
178
+ function sceneWithRunOverrides(scene, input) {
179
+ if (!input.authRole) return scene;
180
+ return {
181
+ ...scene,
182
+ setup: {
183
+ ...scene.setup,
184
+ auth: {
185
+ ...scene.setup.auth ?? {},
186
+ required: input.authRole !== "anonymous",
187
+ role: scene.setup.auth?.role ?? input.authRole
188
+ }
189
+ }
190
+ };
191
+ }
177
192
  async function runTreeseedScene(input) {
178
193
  const startedAt = now();
179
194
  const validation = validationForInput(input);
@@ -188,7 +203,8 @@ async function runTreeseedScene(input) {
188
203
  diagnostics: validation.diagnostics
189
204
  });
190
205
  }
191
- const scene = validation.scene;
206
+ const scene = sceneWithRunOverrides(validation.scene, input);
207
+ const browser = input.browser ?? scene.target.browser;
192
208
  const deviceResolution = resolveTreeseedSceneDeviceProfile({ scene, device: input.device });
193
209
  if (!deviceResolution.profile && deviceResolution.diagnostics.some((entry) => entry.severity === "error")) {
194
210
  return reportFromBlock({
@@ -197,7 +213,7 @@ async function runTreeseedScene(input) {
197
213
  runId: input.runId ?? null,
198
214
  startedAt,
199
215
  environment: input.environment ?? scene.target.environment,
200
- browser: scene.target.browser,
216
+ browser,
201
217
  diagnostics: deviceResolution.diagnostics
202
218
  });
203
219
  }
@@ -214,7 +230,7 @@ async function runTreeseedScene(input) {
214
230
  runId: input.runId ?? null,
215
231
  startedAt,
216
232
  environment: plan.environment,
217
- browser: scene.target.browser,
233
+ browser,
218
234
  device,
219
235
  diagnostics: plan.diagnostics
220
236
  });
@@ -263,7 +279,7 @@ async function runTreeseedScene(input) {
263
279
  runId: paths.runId,
264
280
  startedAt,
265
281
  environment: plan.environment,
266
- browser: scene.target.browser,
282
+ browser,
267
283
  device,
268
284
  diagnostics: [...plan.diagnostics, ...setupDiagnostics],
269
285
  artifacts,
@@ -289,7 +305,7 @@ async function runTreeseedScene(input) {
289
305
  runId: paths.runId,
290
306
  startedAt,
291
307
  environment: plan.environment,
292
- browser: scene.target.browser,
308
+ browser,
293
309
  device,
294
310
  diagnostics: [...plan.diagnostics, ...setupDiagnostics, ...baseUrl.diagnostics],
295
311
  artifacts,
@@ -338,7 +354,7 @@ async function runTreeseedScene(input) {
338
354
  let sessionClosed = false;
339
355
  try {
340
356
  session = await adapter.launch({
341
- browser: scene.target.browser,
357
+ browser,
342
358
  viewport: capture.viewport,
343
359
  videoSize: capture.videoSize,
344
360
  recordVideoDir: videoDir,
@@ -380,7 +396,7 @@ async function runTreeseedScene(input) {
380
396
  timeline.push("network", entry, currentStepId);
381
397
  });
382
398
  if (tracePath) await session.startTracing?.();
383
- if (scene.setup.auth?.role && scene.setup.auth.role !== "anonymous") {
399
+ if (scene.setup.auth?.role && scene.setup.auth.role !== "anonymous" && scene.setup.auth.seedOnly !== true) {
384
400
  const signInDiagnostics = await signInTreeseedSceneVisualAuditRole({
385
401
  page: session.page,
386
402
  baseUrl: baseUrl.baseUrl,
@@ -446,6 +462,7 @@ async function runTreeseedScene(input) {
446
462
  projectRoot: input.projectRoot,
447
463
  scene,
448
464
  environment: plan.environment,
465
+ runId: paths.runId,
449
466
  session,
450
467
  baseUrl: baseUrl.baseUrl,
451
468
  timeline,
@@ -507,15 +524,10 @@ async function runTreeseedScene(input) {
507
524
  await session.page.screenshot({ path: viewportScreenshotPath, fullPage: false });
508
525
  artifacts.viewportScreenshotPaths?.push(viewportScreenshotPath);
509
526
  timeline.push("screenshot.viewport", { path: viewportScreenshotPath }, step.id);
510
- if (screenshotPath && recordingVideo) {
511
- copyFileSync(viewportScreenshotPath, screenshotPath);
512
- artifacts.screenshotPaths.push(screenshotPath);
513
- timeline.push("screenshot", { path: screenshotPath, captureKind: "viewport-copy" }, step.id);
514
- }
515
527
  } catch {
516
528
  }
517
529
  }
518
- if (screenshotPath && !recordingVideo) {
530
+ if (screenshotPath) {
519
531
  try {
520
532
  await session.page.screenshot({ path: screenshotPath, fullPage: true });
521
533
  artifacts.screenshotPaths.push(screenshotPath);
@@ -637,7 +649,7 @@ async function runTreeseedScene(input) {
637
649
  durationMs: duration(startedAt, finishedAt),
638
650
  environment: plan.environment,
639
651
  baseUrl: baseUrl.baseUrl,
640
- browser: scene.target.browser,
652
+ browser,
641
653
  device,
642
654
  capture,
643
655
  workflowStatus,
@@ -6,7 +6,7 @@ import {
6
6
  TREESEED_SCENE_SCHEMA_VERSION
7
7
  } from "./types.js";
8
8
  const FILESYSTEM_SAFE_SCENE_ID = /^[a-z0-9][a-z0-9._-]*$/u;
9
- const TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set(["schemaVersion", "id", "title", "description", "audience", "mode", "target", "devices", "setup", "artifacts", "workflow", "chapters", "overlays", "diagrams", "render", "runtime", "training", "visualAudit", "xScenario"]);
9
+ const TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set(["schemaVersion", "id", "title", "description", "audience", "journey", "mode", "target", "devices", "setup", "artifacts", "workflow", "chapters", "overlays", "diagrams", "render", "runtime", "training", "visualAudit", "xScenario"]);
10
10
  const FILESYSTEM_SAFE_CHECKPOINT_ID = /^[a-z0-9][a-z0-9._-]*$/u;
11
11
  const DIAGRAM_PLACEMENTS = ["overlay", "interstitial", "standalone"];
12
12
  const CAPTION_FORMATS = ["vtt", "srt"];
@@ -108,6 +108,34 @@ function stringArrayField(record, field, path, diagnostics) {
108
108
  });
109
109
  return strings;
110
110
  }
111
+ function stateRefArray(record, field, path, diagnostics) {
112
+ const value = arrayField(record, field, path, diagnostics);
113
+ if (!value) return void 0;
114
+ const refs = [];
115
+ value.forEach((entry, index) => {
116
+ const entryPath = `${path}.${field}[${index}]`;
117
+ if (!isRecord(entry)) {
118
+ diagnostics.push(sceneErrorDiagnostic("scene.invalid_state_ref", `Expected ${field} entry to be an object.`, entryPath));
119
+ return;
120
+ }
121
+ refs.push({ key: requireString(entry, "key", entryPath, diagnostics), kind: requireString(entry, "kind", entryPath, diagnostics) });
122
+ });
123
+ return refs;
124
+ }
125
+ function parseJourney(record, diagnostics) {
126
+ const journey = objectField(record, "journey", "manifest", diagnostics);
127
+ if (!journey) return void 0;
128
+ const kind = optionalString(journey, "kind");
129
+ if (kind && !["service", "page", "visual-audit"].includes(kind)) diagnostics.push(sceneErrorDiagnostic("scene.invalid_journey_kind", `Unsupported journey kind: ${kind}.`, "journey.kind"));
130
+ return {
131
+ kind: kind === "page" || kind === "visual-audit" ? kind : "service",
132
+ proves: stringArrayField(journey, "proves", "journey", diagnostics),
133
+ minimumSteps: positiveNumberField(journey, "minimumSteps", void 0, "journey", diagnostics),
134
+ requiresInteractiveAction: booleanField(journey, "requiresInteractiveAction", false, "journey", diagnostics),
135
+ producesState: stateRefArray(journey, "producesState", "journey", diagnostics),
136
+ consumesState: stateRefArray(journey, "consumesState", "journey", diagnostics)
137
+ };
138
+ }
111
139
  function enumArrayField(record, field, allowed, defaultValue, path, diagnostics) {
112
140
  const value = record[field];
113
141
  if (value === void 0) return defaultValue;
@@ -379,7 +407,7 @@ function parseSetup(value, targetEnvironment, diagnostics) {
379
407
  const dev = objectField(record, "dev", "setup", diagnostics);
380
408
  if (dev) setup.dev = { required: booleanField(dev, "required", false, "setup.dev", diagnostics), command: optionalString(dev, "command"), reuseExisting: booleanField(dev, "reuseExisting", true, "setup.dev", diagnostics) };
381
409
  const auth = objectField(record, "auth", "setup", diagnostics);
382
- if (auth) setup.auth = { profile: optionalString(auth, "profile"), required: booleanField(auth, "required", false, "setup.auth", diagnostics), ...optionalString(auth, "role") ? { role: optionalString(auth, "role") } : {} };
410
+ if (auth) setup.auth = { profile: optionalString(auth, "profile"), required: booleanField(auth, "required", false, "setup.auth", diagnostics), seedOnly: booleanField(auth, "seedOnly", false, "setup.auth", diagnostics), ...optionalString(auth, "role") ? { role: optionalString(auth, "role") } : {} };
383
411
  const seed = objectField(record, "seed", "setup", diagnostics);
384
412
  if (seed) {
385
413
  const environments = (arrayField(seed, "environments", "setup.seed", diagnostics) ?? [targetEnvironment]).map((entry, index) => parseEnvironment(entry, `setup.seed.environments[${index}]`, diagnostics, targetEnvironment));
@@ -970,6 +998,7 @@ function parseTreeseedSceneManifest(value, diagnostics) {
970
998
  title,
971
999
  description: optionalString(value, "description"),
972
1000
  audience: stringArrayField(value, "audience", "manifest", diagnostics),
1001
+ journey: parseJourney(value, diagnostics),
973
1002
  mode,
974
1003
  target,
975
1004
  devices,
@@ -99,6 +99,7 @@ export type TreeseedSceneSetup = {
99
99
  profile?: string;
100
100
  required: boolean;
101
101
  role?: TreeseedSceneVisualAuditRole;
102
+ seedOnly?: boolean;
102
103
  };
103
104
  seed?: {
104
105
  name?: string;
@@ -393,6 +394,20 @@ export type TreeseedSceneManifest = {
393
394
  title: string;
394
395
  description?: string;
395
396
  audience: string[];
397
+ journey?: {
398
+ kind: 'service' | 'page' | 'visual-audit';
399
+ proves?: string[];
400
+ minimumSteps?: number;
401
+ requiresInteractiveAction?: boolean;
402
+ producesState?: Array<{
403
+ key: string;
404
+ kind: string;
405
+ }>;
406
+ consumesState?: Array<{
407
+ key: string;
408
+ kind: string;
409
+ }>;
410
+ };
396
411
  mode: TreeseedSceneMode;
397
412
  target: TreeseedSceneTarget;
398
413
  devices: TreeseedSceneDeviceConfig;
@@ -469,6 +484,8 @@ export type TreeseedSceneRunOptions = {
469
484
  scene: string | TreeseedSceneManifest;
470
485
  environment?: TreeseedSceneEnvironment;
471
486
  device?: TreeseedSceneDeviceProfileId;
487
+ browser?: TreeseedSceneBrowser;
488
+ authRole?: TreeseedSceneVisualAuditRole;
472
489
  record?: boolean;
473
490
  artifactMode?: 'full' | 'screenshots';
474
491
  runId?: string;
@@ -491,6 +508,8 @@ export type TreeseedSceneDeviceMatrixOptions = {
491
508
  scene: string;
492
509
  environment?: TreeseedSceneEnvironment;
493
510
  devices?: TreeseedSceneDeviceProfileId[];
511
+ browser?: TreeseedSceneBrowser;
512
+ authRole?: TreeseedSceneVisualAuditRole;
494
513
  record?: boolean;
495
514
  artifactMode?: 'full' | 'screenshots';
496
515
  mode?: TreeseedSceneExecutionMode;
@@ -967,6 +986,7 @@ export type TreeseedSceneOperationWaitOptions = {
967
986
  projectRoot: string;
968
987
  scene: TreeseedSceneManifest;
969
988
  environment: TreeseedSceneEnvironment;
989
+ runId: string;
970
990
  baseUrl: string;
971
991
  spec: TreeseedSceneOperationWaitSpec;
972
992
  linkedOperationIds?: string[];
@@ -1812,9 +1832,12 @@ export type TreeseedSceneLocator = {
1812
1832
  };
1813
1833
  export type TreeseedScenePage = {
1814
1834
  goto(url: string, options?: {
1815
- waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
1835
+ waitUntil?: 'load' | 'domcontentLoaded' | 'domcontentloaded' | 'networkidle';
1816
1836
  timeout?: number;
1817
- }): Promise<void>;
1837
+ }): Promise<{
1838
+ status(): number;
1839
+ url(): string;
1840
+ } | null | undefined>;
1818
1841
  url(): string;
1819
1842
  locator(selector: string): TreeseedSceneLocator;
1820
1843
  getByTestId(testId: string): TreeseedSceneLocator;
@@ -52,10 +52,11 @@ function configuredValue(input, name) {
52
52
  }
53
53
  }
54
54
  function serviceHeaders(input) {
55
+ const localDevServiceSecret = input?.environment === "local" ? "treeseed-web-service-dev-secret" : null;
55
56
  return {
56
57
  "content-type": "application/json",
57
58
  "x-treeseed-service-id": configuredValue(input, "TREESEED_API_WEB_SERVICE_ID") ?? configuredValue(input, "TREESEED_WEB_SERVICE_ID") ?? "web",
58
- "x-treeseed-service-secret": configuredValue(input, "TREESEED_API_WEB_SERVICE_SECRET") ?? configuredValue(input, "TREESEED_WEB_SERVICE_SECRET") ?? "treeseed-web-service-dev-secret"
59
+ "x-treeseed-service-secret": process.env.TREESEED_API_WEB_SERVICE_SECRET?.trim() ?? process.env.TREESEED_WEB_SERVICE_SECRET?.trim() ?? localDevServiceSecret ?? configuredValue(input, "TREESEED_API_WEB_SERVICE_SECRET") ?? configuredValue(input, "TREESEED_WEB_SERVICE_SECRET") ?? "treeseed-web-service-dev-secret"
59
60
  };
60
61
  }
61
62
  async function sleep(ms) {
@@ -123,12 +124,13 @@ async function seedVisualAuditFixtures(input) {
123
124
  async function ensureTreeseedSceneVisualAuditRoleFixtures(input) {
124
125
  const diagnostics = [];
125
126
  const roles = [...new Set(input.roles)].filter((role) => role !== "anonymous");
126
- diagnostics.push(...await seedVisualAuditFixtures({
127
+ const seedDiagnostics = await seedVisualAuditFixtures({
127
128
  baseUrl: input.baseUrl,
128
129
  roles,
129
130
  projectRoot: input.projectRoot,
130
131
  environment: input.environment
131
- }));
132
+ });
133
+ let roleSetupFailed = false;
132
134
  for (const role of roles) {
133
135
  const user = treeseedSceneVisualAuditUserForRole(role);
134
136
  if (!user) continue;
@@ -138,6 +140,7 @@ async function ensureTreeseedSceneVisualAuditRoleFixtures(input) {
138
140
  continue;
139
141
  } catch (error) {
140
142
  if (!isAuthFailure(error)) {
143
+ roleSetupFailed = true;
141
144
  diagnostics.push(sceneWarningDiagnostic(
142
145
  "scene.visual_audit_fixture_unavailable",
143
146
  `Visual audit fixture setup for ${role} failed against ${input.baseUrl}: ${error instanceof Error ? error.message : String(error ?? "local fixture API is unavailable")}. Authenticated screenshots require the local API and database to be healthy.`,
@@ -160,12 +163,14 @@ async function ensureTreeseedSceneVisualAuditRoleFixtures(input) {
160
163
  if (payload.confirmationToken) {
161
164
  await client.confirmWebEmail({ token: payload.confirmationToken });
162
165
  } else if (payload.confirmationRequired) {
166
+ roleSetupFailed = true;
163
167
  diagnostics.push(sceneWarningDiagnostic("scene.visual_audit_fixture_unavailable", `Visual audit fixture user ${user.email} requires email confirmation, but the local API did not return a confirmation token.`, "roles"));
164
168
  }
165
169
  } catch (error) {
166
170
  try {
167
171
  await client.webSignIn({ login: user.email, password: user.password });
168
172
  } catch {
173
+ roleSetupFailed = true;
169
174
  diagnostics.push(sceneWarningDiagnostic(
170
175
  "scene.visual_audit_fixture_unavailable",
171
176
  `Visual audit fixture setup for ${role} failed against ${input.baseUrl}: ${error instanceof Error ? error.message : String(error ?? "local fixture API is unavailable")}. Authenticated screenshots require the local API and database to be healthy.`,
@@ -174,6 +179,7 @@ async function ensureTreeseedSceneVisualAuditRoleFixtures(input) {
174
179
  }
175
180
  }
176
181
  }
182
+ if (roleSetupFailed) diagnostics.unshift(...seedDiagnostics);
177
183
  return diagnostics;
178
184
  }
179
185
  async function signInTreeseedSceneVisualAuditRole(input) {
@@ -529,10 +529,10 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
529
529
  workspaceLinks: import("../operations/services/workspace-dependency-mode.js").WorkspaceDependencyModeReport;
530
530
  sceneArtifacts: "full" | "screenshots";
531
531
  localCleanup: import("../workflow-support.js").TreeseedLocalCleanupReport | null;
532
- ciMode: "hosted" | "off";
532
+ ciMode: "off";
533
533
  lane: string;
534
534
  verifyMode: "action-first" | "local-only" | import("../workflow.js").TreeseedWorkflowVerifyMode;
535
- releaseCandidateMode: TreeseedReleaseCandidateMode;
535
+ releaseCandidateMode: "skip";
536
536
  applicationSelection: WorkflowApplicationSelection;
537
537
  } & {
538
538
  finalState?: WorkflowStatePayload;
@@ -589,10 +589,10 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
589
589
  lockfileValidation: import("../operations/services/repository-save-orchestrator.js").RepositoryLockfileValidationResult | null;
590
590
  }[];
591
591
  };
592
- ciMode: "hosted" | "off";
592
+ ciMode: "off";
593
593
  lane: string;
594
594
  verifyMode: "action-first" | "local-only" | import("../workflow.js").TreeseedWorkflowVerifyMode;
595
- releaseCandidateMode: TreeseedReleaseCandidateMode;
595
+ releaseCandidateMode: "skip";
596
596
  applicationSelection: WorkflowApplicationSelection;
597
597
  workflowGates: Record<string, unknown>[] | {
598
598
  name: string;
@@ -619,6 +619,10 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
619
619
  status: string;
620
620
  reused: number;
621
621
  records: number;
622
+ } | {
623
+ mode: never;
624
+ status: "skipped";
625
+ reason: string;
622
626
  } | null;
623
627
  releaseProof: import("../operations.js").TreeseedProofRunResult | {
624
628
  skipped: boolean;
@@ -696,10 +700,10 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
696
700
  workspaceLinks: import("../operations/services/workspace-dependency-mode.js").WorkspaceDependencyModeReport;
697
701
  sceneArtifacts: "full" | "screenshots";
698
702
  localCleanup: import("../workflow-support.js").TreeseedLocalCleanupReport | null;
699
- ciMode: "hosted" | "off";
703
+ ciMode: "off";
700
704
  lane: string;
701
705
  verifyMode: "action-first" | "local-only" | import("../workflow.js").TreeseedWorkflowVerifyMode;
702
- releaseCandidateMode: TreeseedReleaseCandidateMode;
706
+ releaseCandidateMode: "skip";
703
707
  applicationSelection: WorkflowApplicationSelection;
704
708
  } & {
705
709
  finalState?: WorkflowStatePayload;
@@ -756,10 +760,10 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
756
760
  lockfileValidation: import("../operations/services/repository-save-orchestrator.js").RepositoryLockfileValidationResult | null;
757
761
  }[];
758
762
  };
759
- ciMode: "hosted" | "off";
763
+ ciMode: "off";
760
764
  lane: string;
761
765
  verifyMode: "action-first" | "local-only" | import("../workflow.js").TreeseedWorkflowVerifyMode;
762
- releaseCandidateMode: TreeseedReleaseCandidateMode;
766
+ releaseCandidateMode: "skip";
763
767
  applicationSelection: WorkflowApplicationSelection;
764
768
  workflowGates: Record<string, unknown>[] | {
765
769
  name: string;
@@ -786,6 +790,10 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
786
790
  status: string;
787
791
  reused: number;
788
792
  records: number;
793
+ } | {
794
+ mode: never;
795
+ status: "skipped";
796
+ reason: string;
789
797
  } | null;
790
798
  releaseProof: import("../operations.js").TreeseedProofRunResult | {
791
799
  skipped: boolean;
@@ -1156,88 +1164,6 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
1156
1164
  cached: boolean;
1157
1165
  }[];
1158
1166
  };
1159
- productionHosting: {
1160
- status: "skipped";
1161
- reason: string;
1162
- environment: "staging" | "prod";
1163
- selectedApps: string[];
1164
- selectedResources: {
1165
- id: string;
1166
- host: string;
1167
- serviceType: string;
1168
- placement: import("../hosting/contracts.js").TreeseedServicePlacement;
1169
- serviceName: string | null;
1170
- }[];
1171
- reconcile?: undefined;
1172
- postApplyStatus?: undefined;
1173
- liveVerification?: undefined;
1174
- } | {
1175
- status: "reconciled";
1176
- environment: "staging" | "prod";
1177
- selectedApps: string[];
1178
- selectedResources: {
1179
- id: string;
1180
- host: string;
1181
- serviceType: string;
1182
- placement: import("../hosting/contracts.js").TreeseedServicePlacement;
1183
- serviceName: string | null;
1184
- }[];
1185
- reconcile: {
1186
- target: TreeseedReconcileTarget;
1187
- units: TreeseedDesiredUnit[];
1188
- plans: import("../reconcile/contracts.js").TreeseedReconcilePlan[];
1189
- results: TreeseedReconcileResult[];
1190
- state: import("../reconcile/contracts.js").TreeseedReconcileStateRecord;
1191
- timings: import("../timing.js").TreeseedTimingEntry[];
1192
- };
1193
- postApplyStatus: {
1194
- target: TreeseedReconcileTarget;
1195
- ready: boolean;
1196
- blockers: string[];
1197
- warnings: string[];
1198
- units: {
1199
- unitId: string;
1200
- unitType: import("../reconcile/contracts.js").TreeseedReconcileUnitType;
1201
- provider: string;
1202
- status: import("../reconcile/contracts.js").TreeseedReconcileStatusKind;
1203
- exists: boolean;
1204
- locators: Record<string, string | null>;
1205
- warnings: string[];
1206
- verification: import("../reconcile/contracts.js").TreeseedUnitVerificationResult | null;
1207
- }[];
1208
- };
1209
- liveVerification: import("../workflow-support.js").TreeseedLiveHostedServiceCheckReport;
1210
- reason?: undefined;
1211
- };
1212
- productionApiGuarantees: {
1213
- ok: true;
1214
- environment: string;
1215
- runId: string;
1216
- outputRoot: string;
1217
- counts: {
1218
- planned: number;
1219
- passed: number;
1220
- failed: number;
1221
- skipped: number;
1222
- blocked: number;
1223
- releaseBlockingFailures: number;
1224
- };
1225
- };
1226
- productionWebVerification: Record<string, unknown> | null;
1227
- productionFinalGuarantees: {
1228
- ok: true;
1229
- environment: string;
1230
- runId: string;
1231
- outputRoot: string;
1232
- counts: {
1233
- planned: number;
1234
- passed: number;
1235
- failed: number;
1236
- skipped: number;
1237
- blocked: number;
1238
- releaseBlockingFailures: number;
1239
- };
1240
- };
1241
1167
  backMerge: {
1242
1168
  packages: {
1243
1169
  status: string;
@@ -1404,7 +1330,7 @@ export declare function workflowRecover(helpers: WorkflowOperationHelpers, input
1404
1330
  } | null;
1405
1331
  classification: import("./runs.js").TreeseedWorkflowRunClassification;
1406
1332
  }[];
1407
- prunedRuns: {
1333
+ prunedRuns: ({
1408
1334
  runId: string;
1409
1335
  command: TreeseedWorkflowRunCommand;
1410
1336
  status: import("./runs.js").TreeseedWorkflowRunStatus;
@@ -1418,7 +1344,16 @@ export declare function workflowRecover(helpers: WorkflowOperationHelpers, input
1418
1344
  at: string;
1419
1345
  } | null;
1420
1346
  classification: import("./runs.js").TreeseedWorkflowRunClassification;
1421
- }[];
1347
+ } | {
1348
+ runId: string;
1349
+ command: TreeseedWorkflowRunCommand;
1350
+ status: import("./runs.js").TreeseedWorkflowRunStatus;
1351
+ classification: {
1352
+ state: "stale";
1353
+ reasons: string[];
1354
+ classifiedAt: string;
1355
+ };
1356
+ })[];
1422
1357
  markedObsoleteRun: {
1423
1358
  runId: string;
1424
1359
  command: TreeseedWorkflowRunCommand;