@treeseed/sdk 0.12.44 → 0.12.46

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 (40) 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 +8 -5
  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 +88 -19
  19. package/dist/operations/services/workspace-dependency-mode.js +4 -0
  20. package/dist/platform/desired-state.js +3 -3
  21. package/dist/reconcile/builtin-adapters.js +10 -2
  22. package/dist/reconcile/providers/railway-iac.d.ts +2 -0
  23. package/dist/reconcile/providers/railway-iac.js +31 -3
  24. package/dist/reconcile/providers/release-private.d.ts +10 -0
  25. package/dist/reconcile/providers/release-private.js +45 -1
  26. package/dist/scenes/builtin-plugins.js +36 -5
  27. package/dist/scenes/device-matrix.js +2 -0
  28. package/dist/scenes/environment.js +1 -1
  29. package/dist/scenes/runner.js +28 -16
  30. package/dist/scenes/schema.js +31 -2
  31. package/dist/scenes/types.d.ts +25 -2
  32. package/dist/scenes/visual-audit-fixtures.js +9 -3
  33. package/dist/workflow/operations.d.ts +27 -92
  34. package/dist/workflow/operations.js +252 -144
  35. package/dist/workflow/runs.d.ts +1 -0
  36. package/dist/workflow/runs.js +57 -0
  37. package/dist/workflow-support.d.ts +1 -0
  38. package/dist/workflow-support.js +8 -0
  39. package/dist/workflow.d.ts +2 -0
  40. package/package.json +4 -1
@@ -1,10 +1,10 @@
1
- import { execFile } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, relative, resolve, sep } from "node:path";
4
- import { promisify } from "node:util";
5
4
  import { parse as parseYaml } from "yaml";
6
5
  const TREESEED_GUARANTEE_SCHEMA_VERSION = "treeseed.guarantee/v1";
7
6
  const TREESEED_GUARANTEE_VERIFIERS_SCHEMA_VERSION = "treeseed.guarantee-verifiers/v1";
7
+ const TREESEED_GUARANTEE_JOURNEY_AUDIT_SCHEMA_VERSION = "treeseed.guarantee-journey-audit/v1";
8
8
  const TAXONOMY_PATTERN = /^[a-z][a-z0-9-]*$/u;
9
9
  const GUARANTEE_ID_PATTERN = /^guarantee\.[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*){2,}\.\d{3}$/u;
10
10
  const KNOWN_GATES = /* @__PURE__ */ new Set(["smoke", "core", "release", "security", "migration", "demo", "backlog", "future"]);
@@ -13,7 +13,6 @@ const KNOWN_SURFACES = /* @__PURE__ */ new Set(["admin-ui", "agent-runtime", "ap
13
13
  const KNOWN_DEVICES = /* @__PURE__ */ new Set(["desktop_chromium", "desktop_firefox", "desktop_webkit", "tablet_chromium", "mobile_chromium", "mobile_webkit"]);
14
14
  const KNOWN_VERIFIER_KINDS = /* @__PURE__ */ new Set(["apiAcceptanceCase", "vitestCase", "nodeScript", "packageScript", "scene", "manualEvidence", "todo"]);
15
15
  const EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".treeseed", "coverage"]);
16
- const execFileAsync = promisify(execFile);
17
16
  function diagnostic(severity, code, message, path, sourcePath) {
18
17
  return { severity, code, message, ...path ? { path } : {}, ...sourcePath ? { sourcePath } : {} };
19
18
  }
@@ -344,39 +343,200 @@ function selectedByFilter(manifest, filter = {}) {
344
343
  if (filter.type && manifest.type !== filter.type) return false;
345
344
  if (filter.subtype && manifest.subtype !== filter.subtype) return false;
346
345
  if (filter.ownerPackage && manifest.ownerPackage !== filter.ownerPackage) return false;
346
+ if (filter.ownerPackages && filter.ownerPackages.length > 0 && !filter.ownerPackages.includes(manifest.ownerPackage)) return false;
347
+ if (filter.sceneBacked === true && !manifest.scene?.manifest) return false;
347
348
  if (filter.status && manifest.status !== filter.status) return false;
348
349
  if (filter.ids && filter.ids.length > 0 && !filter.ids.includes(manifest.id)) return false;
349
350
  if (filter.journeyIndexes && filter.journeyIndexes.length > 0 && (!manifest.journeyIndex || !filter.journeyIndexes.includes(manifest.journeyIndex))) return false;
350
351
  return true;
351
352
  }
353
+ function sortGuaranteeEntries(a, b) {
354
+ return (a.manifest.journeyIndex ?? 99999) - (b.manifest.journeyIndex ?? 99999) || a.manifest.id.localeCompare(b.manifest.id);
355
+ }
352
356
  function validateFilter(filter, diagnostics) {
353
357
  for (const field of ["type", "subtype"]) {
354
358
  const value = filter?.[field];
355
359
  if (value && !TAXONOMY_PATTERN.test(value)) diagnostics.push(diagnostic("error", `guarantee_filter.invalid_${field}`, `Filter ${field} must be lowercase kebab-case. Try "${normalizeTreeseedGuaranteeTaxonomy(value)}".`, field));
356
360
  }
357
361
  }
358
- function filterTreeseedGuarantees(input) {
359
- const valid = input.guarantees.filter((entry) => Boolean(entry.manifest));
362
+ function readSceneYaml(scenePath) {
363
+ try {
364
+ const value = parseYaml(readFileSync(scenePath, "utf8"));
365
+ return isRecord(value) ? value : null;
366
+ } catch {
367
+ return null;
368
+ }
369
+ }
370
+ function sceneManifestPathForGuarantee(entry) {
371
+ const manifest = entry.manifest.scene?.manifest;
372
+ if (!manifest) return void 0;
373
+ return resolve(dirname(entry.sourcePath), manifest);
374
+ }
375
+ function sceneRouteFromYaml(value) {
376
+ const workflow = Array.isArray(value?.workflow) ? value.workflow : [];
377
+ for (const step of workflow) {
378
+ const action = isRecord(step) && isRecord(step.action) ? step.action : null;
379
+ const goto = isRecord(action?.goto) ? action.goto : null;
380
+ const url = stringValue(goto?.url) || stringValue(goto?.path) || stringValue(action?.goto);
381
+ if (url) return url;
382
+ }
383
+ return void 0;
384
+ }
385
+ function sceneStateKeys(value, key) {
386
+ const journey = isRecord(value?.journey) ? value.journey : null;
387
+ const entries = Array.isArray(journey?.[key]) ? journey[key] : [];
388
+ return entries.map((entry) => isRecord(entry) ? stringValue(entry.key) : stringValue(entry)).filter(Boolean);
389
+ }
390
+ function implicitAuthDependencyFor(entry) {
391
+ if (entry.manifest.type === "user" && entry.manifest.subtype === "auth") return void 0;
392
+ const entryRoute = entry.manifest.scene?.entryRoute;
393
+ const scenePath = sceneManifestPathForGuarantee(entry);
394
+ const sceneRoute = scenePath && existsSync(scenePath) ? sceneRouteFromYaml(readSceneYaml(scenePath)) : void 0;
395
+ const route = entryRoute || sceneRoute;
396
+ return route?.startsWith("/app/") || route === "/app" ? "guarantee.user.auth.user-login.004" : void 0;
397
+ }
398
+ function dependencyIdsForGuarantee(input) {
399
+ const deps = /* @__PURE__ */ new Map();
400
+ const add = (id, reason) => {
401
+ if (!id || id === input.entry.manifest.id || !input.byId.has(id)) return;
402
+ const reasons = deps.get(id) ?? /* @__PURE__ */ new Set();
403
+ reasons.add(reason);
404
+ deps.set(id, reasons);
405
+ };
406
+ for (const id of input.entry.manifest.dependencies.guarantees ?? []) add(id, "explicit-guarantee");
407
+ for (const journeyIndex of input.entry.manifest.dependencies.journeys ?? []) add(input.byJourneyIndex.get(journeyIndex)?.manifest.id, "journey-index");
408
+ for (const dep of input.entry.manifest.dependsOnGuarantees ?? []) {
409
+ const [ownerPackage, ref] = dep.includes(":") ? dep.split(/:(.+)/u).filter(Boolean) : ["", dep];
410
+ const match = input.valid.find((candidate) => (!ownerPackage || candidate.manifest.ownerPackage === ownerPackage) && candidate.manifest.status === "active" && (candidate.manifest.id === ref || allVerifierRefs(candidate.manifest).includes(ref)));
411
+ add(match?.manifest.id, "depends-on-verifier");
412
+ }
413
+ add(implicitAuthDependencyFor(input.entry), "implicit-auth");
414
+ return deps;
415
+ }
416
+ function buildTreeseedGuaranteeDependencyGraph(input) {
417
+ const valid = input.guarantees.filter((entry) => Boolean(entry.manifest)).sort(sortGuaranteeEntries);
360
418
  const byId = new Map(valid.map((entry) => [entry.manifest.id, entry]));
361
- const selected = /* @__PURE__ */ new Map();
362
- for (const entry of valid) {
363
- if (selectedByFilter(entry.manifest, input.filter)) selected.set(entry.manifest.id, entry);
419
+ const byJourneyIndex = new Map(valid.flatMap((entry) => entry.manifest.journeyIndex ? [[entry.manifest.journeyIndex, entry]] : []));
420
+ const selectedIds = new Set(valid.filter((entry) => selectedByFilter(entry.manifest, input.filter)).map((entry) => entry.manifest.id));
421
+ const includeIds = new Set(selectedIds);
422
+ const reasonById = /* @__PURE__ */ new Map();
423
+ const diagnostics = [];
424
+ const visitInclude = (id, chain) => {
425
+ const entry = byId.get(id);
426
+ if (!entry) return;
427
+ if (chain.includes(id)) {
428
+ diagnostics.push(diagnostic("error", "guarantee.dependency_cycle", `Guarantee dependency cycle: ${[...chain, id].join(" -> ")}.`, "dependencies", entry.sourcePath));
429
+ return;
430
+ }
431
+ const deps = dependencyIdsForGuarantee({ entry, byId, byJourneyIndex, valid });
432
+ for (const [depId, reasons] of deps) {
433
+ for (const reason of reasons) {
434
+ const existing = reasonById.get(depId) ?? /* @__PURE__ */ new Set();
435
+ existing.add(reason);
436
+ reasonById.set(depId, existing);
437
+ }
438
+ if (input.includeDependencies !== false && !includeIds.has(depId)) includeIds.add(depId);
439
+ if (input.includeDependencies !== false) visitInclude(depId, [...chain, id]);
440
+ }
441
+ };
442
+ for (const id of [...selectedIds]) visitInclude(id, []);
443
+ const included = valid.filter((entry) => includeIds.has(entry.manifest.id));
444
+ const includedIds = new Set(included.map((entry) => entry.manifest.id));
445
+ const depMap = /* @__PURE__ */ new Map();
446
+ const reasonMap = /* @__PURE__ */ new Map();
447
+ const stateProduces = /* @__PURE__ */ new Map();
448
+ const stateConsumes = /* @__PURE__ */ new Map();
449
+ for (const entry of included) {
450
+ const scenePath = sceneManifestPathForGuarantee(entry);
451
+ const scene = scenePath && existsSync(scenePath) ? readSceneYaml(scenePath) : null;
452
+ stateProduces.set(entry.manifest.id, sceneStateKeys(scene, "producesState"));
453
+ stateConsumes.set(entry.manifest.id, sceneStateKeys(scene, "consumesState"));
454
+ const deps = dependencyIdsForGuarantee({ entry, byId, byJourneyIndex, valid });
455
+ const filtered = [...deps.keys()].filter((id) => includedIds.has(id)).sort((a, b) => sortGuaranteeEntries(byId.get(a), byId.get(b)));
456
+ depMap.set(entry.manifest.id, filtered);
457
+ const reasons = /* @__PURE__ */ new Set();
458
+ for (const depId of filtered) for (const reason of deps.get(depId) ?? []) reasons.add(reason);
459
+ for (const reason of reasonById.get(entry.manifest.id) ?? []) reasons.add(reason);
460
+ reasonMap.set(entry.manifest.id, reasons);
364
461
  }
365
- if (input.includeDependencies !== false) {
366
- const visit = (id) => {
367
- const entry = byId.get(id);
368
- if (!entry) return;
369
- for (const dep of entry.manifest.dependencies.guarantees ?? []) {
370
- const depEntry = byId.get(dep);
371
- if (depEntry && !selected.has(dep)) {
372
- selected.set(dep, depEntry);
373
- visit(dep);
374
- }
462
+ const producersByStateKey = /* @__PURE__ */ new Map();
463
+ for (const [id, keys] of stateProduces) {
464
+ for (const key of keys) {
465
+ producersByStateKey.set(key, [...producersByStateKey.get(key) ?? [], id]);
466
+ }
467
+ }
468
+ for (const [key, producers] of producersByStateKey) {
469
+ const unique = [...new Set(producers)];
470
+ if (unique.length > 1) {
471
+ diagnostics.push(diagnostic(
472
+ "error",
473
+ "guarantee.state_duplicate_producer",
474
+ `State key ${key} is produced by multiple included guarantees: ${unique.join(", ")}.`,
475
+ "journey.producesState",
476
+ byId.get(unique[0])?.sourcePath
477
+ ));
478
+ }
479
+ producersByStateKey.set(key, unique);
480
+ }
481
+ for (const [id, keys] of stateConsumes) {
482
+ for (const key of keys) {
483
+ const producers = producersByStateKey.get(key) ?? [];
484
+ const producer = producers.length === 1 ? producers[0] : void 0;
485
+ if (producer && producer !== id && includedIds.has(producer)) {
486
+ const deps = depMap.get(id) ?? [];
487
+ if (!deps.includes(producer)) deps.push(producer);
488
+ depMap.set(id, deps.sort((a, b) => sortGuaranteeEntries(byId.get(a), byId.get(b))));
489
+ const reasons = reasonMap.get(id) ?? /* @__PURE__ */ new Set();
490
+ reasons.add("state");
491
+ reasonMap.set(id, reasons);
375
492
  }
376
- };
377
- for (const id of [...selected.keys()]) visit(id);
493
+ }
378
494
  }
379
- return [...selected.values()].sort((a, b) => (a.manifest.journeyIndex ?? 99999) - (b.manifest.journeyIndex ?? 99999) || a.manifest.id.localeCompare(b.manifest.id));
495
+ const visiting = /* @__PURE__ */ new Set();
496
+ const visited = /* @__PURE__ */ new Set();
497
+ const ordered = [];
498
+ const visitOrder = (id, chain) => {
499
+ if (visited.has(id)) return;
500
+ if (visiting.has(id)) {
501
+ diagnostics.push(diagnostic("error", "guarantee.dependency_cycle", `Guarantee dependency cycle: ${[...chain, id].join(" -> ")}.`, "dependencies", byId.get(id)?.sourcePath));
502
+ return;
503
+ }
504
+ visiting.add(id);
505
+ for (const dep of depMap.get(id) ?? []) visitOrder(dep, [...chain, id]);
506
+ visiting.delete(id);
507
+ visited.add(id);
508
+ ordered.push(id);
509
+ };
510
+ for (const entry of included) visitOrder(entry.manifest.id, []);
511
+ const inverse = /* @__PURE__ */ new Map();
512
+ for (const [id, deps] of depMap) for (const dep of deps) inverse.set(dep, [...inverse.get(dep) ?? [], id]);
513
+ const depthCache = /* @__PURE__ */ new Map();
514
+ const depth = (id, chain = []) => {
515
+ if (depthCache.has(id)) return depthCache.get(id);
516
+ if (chain.includes(id)) {
517
+ diagnostics.push(diagnostic("error", "guarantee.dependency_cycle", `Guarantee dependency cycle: ${[...chain, id].join(" -> ")}.`, "dependencies", byId.get(id)?.sourcePath));
518
+ return 0;
519
+ }
520
+ const value = Math.max(0, ...(depMap.get(id) ?? []).map((dep) => depth(dep, [...chain, id]) + 1));
521
+ depthCache.set(id, value);
522
+ return value;
523
+ };
524
+ const meta = /* @__PURE__ */ new Map();
525
+ for (const [index, id] of ordered.entries()) {
526
+ meta.set(id, {
527
+ dependsOn: depMap.get(id) ?? [],
528
+ dependencyOf: (inverse.get(id) ?? []).sort((a, b) => sortGuaranteeEntries(byId.get(a), byId.get(b))),
529
+ dependencyReason: [...reasonMap.get(id) ?? /* @__PURE__ */ new Set()],
530
+ dependencyDepth: depth(id),
531
+ executionOrder: index,
532
+ producesState: stateProduces.get(id) ?? [],
533
+ consumesState: stateConsumes.get(id) ?? []
534
+ });
535
+ }
536
+ return { entries: ordered.map((id) => byId.get(id)).filter(Boolean), selectedIds, meta, diagnostics };
537
+ }
538
+ function filterTreeseedGuarantees(input) {
539
+ return buildTreeseedGuaranteeDependencyGraph(input).entries;
380
540
  }
381
541
  function validateTreeseedGuaranteeRegistry(input) {
382
542
  const diagnostics = [
@@ -470,35 +630,47 @@ function refs(contract) {
470
630
  function planTreeseedGuarantees(input) {
471
631
  const registry = discoverTreeseedGuarantees({ workspaceRoot: input.workspaceRoot, filter: input.filter });
472
632
  const selectedWithoutDeps = filterTreeseedGuarantees({ guarantees: registry.guarantees, filter: input.filter, includeDependencies: false });
473
- const selectedIds = new Set(selectedWithoutDeps.map((entry) => entry.manifest.id));
474
- const entries = filterTreeseedGuarantees({ guarantees: registry.guarantees, filter: input.filter, includeDependencies: input.includeDependencies !== false }).map((entry) => ({
475
- id: entry.manifest.id,
476
- ...entry.manifest.journeyIndex ? { journeyIndex: entry.manifest.journeyIndex } : {},
477
- type: entry.manifest.type,
478
- subtype: entry.manifest.subtype,
479
- journey: entry.manifest.journey,
480
- ownerPackage: entry.manifest.ownerPackage,
481
- ...entry.manifest.surface ? { surface: entry.manifest.surface } : {},
482
- status: entry.manifest.status,
483
- gates: entry.manifest.gates,
484
- sourcePath: entry.relativePath,
485
- selected: selectedIds.has(entry.manifest.id),
486
- dependency: !selectedIds.has(entry.manifest.id),
487
- ...entry.manifest.scene?.manifest ? { sceneManifest: entry.manifest.scene.manifest } : {},
488
- apiVerifierRefs: refs(entry.manifest.api),
489
- contentVerifierRefs: refs(entry.manifest.content),
490
- auditVerifierRefs: refs(entry.manifest.audit),
491
- evidenceRequired: entry.manifest.evidence.required
492
- }));
493
- const errors = registry.diagnostics.filter((entry) => entry.severity === "error").length;
494
- const warnings = registry.diagnostics.filter((entry) => entry.severity === "warning").length;
633
+ const graph = buildTreeseedGuaranteeDependencyGraph({ guarantees: registry.guarantees, filter: input.filter, includeDependencies: input.includeDependencies !== false });
634
+ const selectedIds = graph.selectedIds;
635
+ const entries = graph.entries.map((entry) => {
636
+ const meta = graph.meta.get(entry.manifest.id);
637
+ return {
638
+ id: entry.manifest.id,
639
+ ...entry.manifest.journeyIndex ? { journeyIndex: entry.manifest.journeyIndex } : {},
640
+ type: entry.manifest.type,
641
+ subtype: entry.manifest.subtype,
642
+ journey: entry.manifest.journey,
643
+ ownerPackage: entry.manifest.ownerPackage,
644
+ ...entry.manifest.surface ? { surface: entry.manifest.surface } : {},
645
+ status: entry.manifest.status,
646
+ gates: entry.manifest.gates,
647
+ sourcePath: entry.relativePath,
648
+ selected: selectedIds.has(entry.manifest.id),
649
+ dependency: !selectedIds.has(entry.manifest.id),
650
+ ...entry.manifest.scene?.manifest ? { sceneManifest: entry.manifest.scene.manifest } : {},
651
+ apiVerifierRefs: refs(entry.manifest.api),
652
+ contentVerifierRefs: refs(entry.manifest.content),
653
+ auditVerifierRefs: refs(entry.manifest.audit),
654
+ evidenceRequired: entry.manifest.evidence.required,
655
+ dependencyDepth: meta?.dependencyDepth ?? 0,
656
+ dependencyOf: meta?.dependencyOf ?? [],
657
+ dependsOn: meta?.dependsOn ?? [],
658
+ dependencyReason: meta?.dependencyReason ?? [],
659
+ executionOrder: meta?.executionOrder ?? 0,
660
+ ...meta?.producesState.length ? { producesState: meta.producesState } : {},
661
+ ...meta?.consumesState.length ? { consumesState: meta.consumesState } : {}
662
+ };
663
+ });
664
+ const diagnostics = [...registry.diagnostics, ...graph.diagnostics];
665
+ const errors = diagnostics.filter((entry) => entry.severity === "error").length;
666
+ const warnings = diagnostics.filter((entry) => entry.severity === "warning").length;
495
667
  return {
496
- ok: registry.ok,
668
+ ok: errors === 0,
497
669
  workspaceRoot: resolve(input.workspaceRoot),
498
670
  filter: input.filter ?? {},
499
671
  environment: input.environment ?? "local",
500
672
  entries,
501
- diagnostics: registry.diagnostics,
673
+ diagnostics,
502
674
  counts: {
503
675
  total: registry.counts.total,
504
676
  selected: selectedWithoutDeps.length,
@@ -508,6 +680,181 @@ function planTreeseedGuarantees(input) {
508
680
  }
509
681
  };
510
682
  }
683
+ function collectFiles(root, predicate, out = []) {
684
+ if (!existsSync(root)) return out;
685
+ for (const name of readdirSync(root)) {
686
+ if (EXCLUDED_DIRS.has(name)) continue;
687
+ const path = resolve(root, name);
688
+ let stat;
689
+ try {
690
+ stat = statSync(path);
691
+ } catch {
692
+ continue;
693
+ }
694
+ if (stat.isDirectory()) collectFiles(path, predicate, out);
695
+ else if (stat.isFile() && predicate(path)) out.push(path);
696
+ }
697
+ return out;
698
+ }
699
+ function astroRoutePatternFromPath(root, filePath) {
700
+ const relativePath = relative(root, filePath).replace(/\\/gu, "/");
701
+ const withoutExtension = relativePath.replace(/\.(astro|tsx|ts|jsx|js)$/u, "");
702
+ const segments = withoutExtension.split("/").filter(Boolean);
703
+ if (segments.at(-1) === "index") segments.pop();
704
+ return `/${segments.map((segment) => {
705
+ if (/^\[\.\.\..+\]$/u.test(segment)) return "**";
706
+ if (/^\[.+\]$/u.test(segment)) return "*";
707
+ return segment;
708
+ }).join("/")}`.replace(/\/+$/u, "") || "/";
709
+ }
710
+ function collectAstroRoutePatterns(workspaceRoot) {
711
+ const roots = [
712
+ resolve(workspaceRoot, "src/pages"),
713
+ resolve(workspaceRoot, "packages/admin/src/pages"),
714
+ resolve(workspaceRoot, "packages/core/src/pages")
715
+ ];
716
+ const patterns = /* @__PURE__ */ new Set();
717
+ for (const root of roots) {
718
+ for (const path of collectFiles(root, (entry) => /\.(astro|tsx|ts|jsx|js)$/u.test(entry))) {
719
+ patterns.add(astroRoutePatternFromPath(root, path));
720
+ }
721
+ }
722
+ return patterns;
723
+ }
724
+ function normalizeRoutePath(route) {
725
+ if (!route) return void 0;
726
+ if (/^https?:\/\//u.test(route)) {
727
+ try {
728
+ return new URL(route).pathname || "/";
729
+ } catch {
730
+ return route;
731
+ }
732
+ }
733
+ return route.split(/[?#]/u)[0] || "/";
734
+ }
735
+ function routePatternMatches(pattern, route) {
736
+ const patternSegments = pattern.split("/").filter(Boolean);
737
+ const routeSegments = route.split("/").filter(Boolean);
738
+ if (patternSegments.includes("**")) {
739
+ const index = patternSegments.indexOf("**");
740
+ return patternSegments.slice(0, index).every((segment, segmentIndex) => segment === "*" || segment === routeSegments[segmentIndex]);
741
+ }
742
+ if (patternSegments.length !== routeSegments.length) return false;
743
+ return patternSegments.every((segment, index) => segment === "*" || segment === routeSegments[index]);
744
+ }
745
+ function actionKindFromSceneStep(step) {
746
+ const action = isRecord(step) && isRecord(step.action) ? step.action : null;
747
+ return action ? Object.keys(action)[0] ?? "unknown" : "unknown";
748
+ }
749
+ function sceneHasAcceptanceAssertions(step) {
750
+ return isRecord(step) && isRecord(step.expect) && Object.keys(step.expect).length > 0;
751
+ }
752
+ function sceneUsesOnlyStableSelectors(value) {
753
+ const text = JSON.stringify(value ?? {});
754
+ if (!text.includes('"selector"')) return true;
755
+ return /data-scene|data-testid|testId|getByRole|getByLabel|getByText|"internal":true/iu.test(text);
756
+ }
757
+ function auditTreeseedGuaranteeJourneys(input) {
758
+ const workspaceRoot = resolve(input.workspaceRoot);
759
+ const registry = discoverTreeseedGuarantees({ workspaceRoot, filter: input.filter });
760
+ const routes = collectAstroRoutePatterns(workspaceRoot);
761
+ const valid = registry.guarantees.filter((entry) => Boolean(entry.manifest));
762
+ const graph = buildTreeseedGuaranteeDependencyGraph({ guarantees: registry.guarantees, filter: input.filter, includeDependencies: true });
763
+ const items = [];
764
+ for (const entry of valid.filter((candidate) => selectedByFilter(candidate.manifest, input.filter))) {
765
+ const scenePath = sceneManifestPathForGuarantee(entry);
766
+ if (!scenePath) {
767
+ items.push({
768
+ guaranteeId: entry.manifest.id,
769
+ status: entry.manifest.status,
770
+ ownerPackage: entry.manifest.ownerPackage,
771
+ type: entry.manifest.type,
772
+ subtype: entry.manifest.subtype,
773
+ journey: entry.manifest.journey,
774
+ sourcePath: entry.relativePath,
775
+ routeExists: true,
776
+ sceneWorkflowStepCount: 0,
777
+ interactiveStepCount: 0,
778
+ classification: "non-ui-guarantee",
779
+ requiredAction: "none",
780
+ diagnostics: []
781
+ });
782
+ continue;
783
+ }
784
+ const scene = existsSync(scenePath) ? readSceneYaml(scenePath) : null;
785
+ const workflow = Array.isArray(scene?.workflow) ? scene.workflow : [];
786
+ const journey = isRecord(scene?.journey) ? scene.journey : null;
787
+ const serviceJourney = journey?.kind === "service";
788
+ const minimumSteps = typeof journey?.minimumSteps === "number" ? journey.minimumSteps : 2;
789
+ const actionKinds = workflow.map(actionKindFromSceneStep);
790
+ const interactiveStepCount = actionKinds.filter((kind) => kind !== "goto" && kind !== "pause").length;
791
+ const currentRoute = normalizeRoutePath(entry.manifest.scene?.entryRoute || sceneRouteFromYaml(scene));
792
+ const routeExists = Boolean(currentRoute && !currentRoute.includes(":") && [...routes].some((pattern) => routePatternMatches(pattern, currentRoute))) || !currentRoute;
793
+ const missingSelectors2 = entry.manifest.status === "active" && !sceneUsesOnlyStableSelectors(scene);
794
+ const weak = workflow.length < minimumSteps || interactiveStepCount === 0 || entry.manifest.status === "active" && (!serviceJourney || workflow.some((step) => !sceneHasAcceptanceAssertions(step)));
795
+ const diagnostics2 = [];
796
+ if (!existsSync(scenePath)) diagnostics2.push(diagnostic("error", "guarantee.scene_missing_manifest", `Scene manifest does not exist: ${relative(workspaceRoot, scenePath)}.`, "scene.manifest", entry.sourcePath));
797
+ if (!routeExists) diagnostics2.push(diagnostic(entry.manifest.status === "active" ? "error" : "warning", "guarantee.scene_missing_route", `Scene entry route ${currentRoute ?? "(unknown)"} does not map to a known Astro route.`, "scene.entryRoute", entry.sourcePath));
798
+ if (entry.manifest.status === "active" && !serviceJourney) diagnostics2.push(diagnostic("error", "guarantee.scene_missing_service_journey", "Active scene-backed guarantees must declare journey.kind: service in the scene manifest.", "scene.journey.kind", entry.sourcePath));
799
+ if (weak) diagnostics2.push(...validateGuaranteeSceneJourneyContract({ scenePath, sourcePath: entry.sourcePath }).map((item) => entry.manifest.status === "active" ? item : { ...item, severity: "warning" }));
800
+ if (missingSelectors2) diagnostics2.push(diagnostic("error", "guarantee.scene_missing_stable_selectors", "Active service journey scenes must use stable data-scene, data-testid, or role/text selectors instead of brittle CSS selectors.", "scene.workflow", entry.sourcePath));
801
+ const classification = !routeExists ? "missing-product-route" : missingSelectors2 ? "missing-stable-selectors" : entry.manifest.status !== "active" && weak ? "planned-product-contract" : weak ? "weak-page-only-scene" : "valid-service-journey";
802
+ const requiredAction = classification === "missing-product-route" ? entry.manifest.status === "active" ? "downgrade-status" : "fix-route" : classification === "missing-stable-selectors" ? "add-selectors" : classification === "weak-page-only-scene" ? "author-scene" : "none";
803
+ items.push({
804
+ guaranteeId: entry.manifest.id,
805
+ status: entry.manifest.status,
806
+ ownerPackage: entry.manifest.ownerPackage,
807
+ type: entry.manifest.type,
808
+ subtype: entry.manifest.subtype,
809
+ journey: entry.manifest.journey,
810
+ sourcePath: entry.relativePath,
811
+ scenePath: relative(workspaceRoot, scenePath),
812
+ ...currentRoute ? { currentRoute, resolvedRoute: currentRoute } : {},
813
+ routeExists,
814
+ sceneWorkflowStepCount: workflow.length,
815
+ interactiveStepCount,
816
+ classification,
817
+ requiredAction,
818
+ diagnostics: diagnostics2
819
+ });
820
+ }
821
+ const sceneBackedItems = items.filter((entry) => entry.scenePath);
822
+ const activeSceneBacked = sceneBackedItems.filter((entry) => entry.status === "active");
823
+ const weakSceneContracts = sceneBackedItems.filter((entry) => entry.classification === "weak-page-only-scene" || entry.classification === "planned-product-contract").length;
824
+ const missingRoutes = sceneBackedItems.filter((entry) => entry.classification === "missing-product-route").length;
825
+ const missingSelectors = sceneBackedItems.filter((entry) => entry.classification === "missing-stable-selectors").length;
826
+ const activeSceneBackedWeak = activeSceneBacked.filter((entry) => entry.classification === "weak-page-only-scene").length;
827
+ const activeMissingRoutes = activeSceneBacked.filter((entry) => entry.classification === "missing-product-route").length;
828
+ const activeMissingSelectors = activeSceneBacked.filter((entry) => entry.classification === "missing-stable-selectors").length;
829
+ const diagnostics = [...registry.diagnostics, ...graph.diagnostics, ...items.flatMap((entry) => entry.diagnostics)];
830
+ const audit = {
831
+ schemaVersion: TREESEED_GUARANTEE_JOURNEY_AUDIT_SCHEMA_VERSION,
832
+ workspaceRoot,
833
+ generatedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
834
+ totals: {
835
+ guarantees: valid.length,
836
+ sceneBacked: sceneBackedItems.length,
837
+ activeSceneBacked: activeSceneBacked.length,
838
+ weakSceneContracts,
839
+ missingRoutes,
840
+ missingSelectors,
841
+ dependencyErrors: graph.diagnostics.filter((entry) => entry.severity === "error").length,
842
+ activeSceneBackedWeak,
843
+ activeMissingRoutes,
844
+ activeMissingSelectors
845
+ },
846
+ items,
847
+ diagnostics,
848
+ ok: activeSceneBackedWeak === 0 && activeMissingRoutes === 0 && activeMissingSelectors === 0 && diagnostics.every((entry) => entry.severity !== "error")
849
+ };
850
+ if (input.writeReport) {
851
+ const outputPath = assertPathInsideWorkspace(workspaceRoot, resolve(workspaceRoot, input.writeReport));
852
+ mkdirSync(dirname(outputPath), { recursive: true });
853
+ writeFileSync(outputPath, `${JSON.stringify(audit, null, 2)}
854
+ `);
855
+ }
856
+ return audit;
857
+ }
511
858
  function csvEscape(value) {
512
859
  const text = Array.isArray(value) ? value.join("; ") : String(value ?? "");
513
860
  return /[",\n\r]/u.test(text) ? `"${text.replace(/"/gu, '""')}"` : text;
@@ -642,13 +989,23 @@ async function writeCommandEvidence(input) {
642
989
  const evidencePath = resolve(input.outputRoot, "evidence", `${safeRef}.json`);
643
990
  mkdirSync(dirname(evidencePath), { recursive: true });
644
991
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
992
+ const cwd = input.cwd ? resolve(input.workspaceRoot, input.cwd) : resolve(input.workspaceRoot);
993
+ const renderedCommand = [input.command, ...input.args].join(" ");
994
+ input.onProgress?.(`[guarantees][verifier] ${input.ref}: running ${renderedCommand}`);
645
995
  try {
646
- const result = await execFileAsync(input.command, input.args, {
647
- cwd: input.cwd ? resolve(input.workspaceRoot, input.cwd) : resolve(input.workspaceRoot),
996
+ const result = await runVerifierCommand({
997
+ command: input.command,
998
+ args: input.args,
999
+ cwd,
648
1000
  env: input.env ? { ...process.env, ...input.env } : process.env,
649
- timeout: Math.max(1, input.timeoutSeconds ?? 300) * 1e3,
650
- maxBuffer: 1024 * 1024 * 20
1001
+ timeoutMs: Math.max(1, input.timeoutSeconds ?? 300) * 1e3,
1002
+ onProgress: input.onProgress,
1003
+ ref: input.ref
651
1004
  });
1005
+ const successError = input.validateSuccess?.(result);
1006
+ if (successError) {
1007
+ throw Object.assign(new Error(successError), { stdout: result.stdout, stderr: result.stderr, code: 1 });
1008
+ }
652
1009
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
653
1010
  writeFileSync(evidencePath, `${JSON.stringify({
654
1011
  ref: input.ref,
@@ -663,6 +1020,7 @@ async function writeCommandEvidence(input) {
663
1020
  stderr: result.stderr
664
1021
  }, null, 2)}
665
1022
  `, "utf8");
1023
+ input.onProgress?.(`[guarantees][verifier] ${input.ref}: passed`);
666
1024
  return {
667
1025
  status: "passed",
668
1026
  summary: `${input.ref} passed.`,
@@ -685,6 +1043,7 @@ async function writeCommandEvidence(input) {
685
1043
  error: commandError.message
686
1044
  }, null, 2)}
687
1045
  `, "utf8");
1046
+ input.onProgress?.(`[guarantees][verifier] ${input.ref}: failed - ${commandError.message}`, "stderr");
688
1047
  return {
689
1048
  status: "failed",
690
1049
  summary: `${input.ref} failed.`,
@@ -693,6 +1052,75 @@ async function writeCommandEvidence(input) {
693
1052
  };
694
1053
  }
695
1054
  }
1055
+ function stripAnsi(value) {
1056
+ return value.replace(/\u001B\[[0-9;]*m/gu, "");
1057
+ }
1058
+ function vitestExecutedAssertionCount(output) {
1059
+ const text = stripAnsi(output).replace(/\r\n/gu, "\n");
1060
+ const testsLine = text.split("\n").find((line) => /^\s*Tests\s+/u.test(line));
1061
+ if (!testsLine) return 0;
1062
+ const passed = [...testsLine.matchAll(/(\d+)\s+passed/gu)].reduce((total, match) => total + Number(match[1] ?? 0), 0);
1063
+ const failed = [...testsLine.matchAll(/(\d+)\s+failed/gu)].reduce((total, match) => total + Number(match[1] ?? 0), 0);
1064
+ return passed + failed;
1065
+ }
1066
+ function validateTreeseedVitestVerifierOutput(result) {
1067
+ const output = `${result.stdout}
1068
+ ${result.stderr}`;
1069
+ if (vitestExecutedAssertionCount(output) > 0) return null;
1070
+ return "Vitest verifier completed without executing any assertions. Check the verifier testName/testFile; skipped-only or no-match runs are not valid guarantee evidence.";
1071
+ }
1072
+ function runVerifierCommand(input) {
1073
+ return new Promise((resolvePromise, reject) => {
1074
+ const child = spawn(input.command, input.args, {
1075
+ cwd: input.cwd,
1076
+ env: input.env,
1077
+ stdio: ["ignore", "pipe", "pipe"]
1078
+ });
1079
+ let stdout = "";
1080
+ let stderr = "";
1081
+ let settled = false;
1082
+ const timer = setTimeout(() => {
1083
+ if (settled) return;
1084
+ input.onProgress?.(`[guarantees][verifier] ${input.ref}: timed out after ${Math.round(input.timeoutMs / 1e3)}s`, "stderr");
1085
+ child.kill("SIGTERM");
1086
+ setTimeout(() => {
1087
+ if (!settled) child.kill("SIGKILL");
1088
+ }, 2e3).unref();
1089
+ }, input.timeoutMs);
1090
+ timer.unref();
1091
+ child.stdout.on("data", (chunk) => {
1092
+ const text = chunk.toString();
1093
+ stdout += text;
1094
+ for (const line of text.split(/\r?\n/u)) {
1095
+ if (line.trim()) input.onProgress?.(`[guarantees][verifier][stdout] ${input.ref}: ${line}`);
1096
+ }
1097
+ });
1098
+ child.stderr.on("data", (chunk) => {
1099
+ const text = chunk.toString();
1100
+ stderr += text;
1101
+ for (const line of text.split(/\r?\n/u)) {
1102
+ if (line.trim()) input.onProgress?.(`[guarantees][verifier][stderr] ${input.ref}: ${line}`, "stderr");
1103
+ }
1104
+ });
1105
+ child.on("error", (error) => {
1106
+ if (settled) return;
1107
+ settled = true;
1108
+ clearTimeout(timer);
1109
+ reject(Object.assign(error, { stdout, stderr }));
1110
+ });
1111
+ child.on("close", (code, signal) => {
1112
+ if (settled) return;
1113
+ settled = true;
1114
+ clearTimeout(timer);
1115
+ if (code === 0) {
1116
+ resolvePromise({ stdout, stderr });
1117
+ return;
1118
+ }
1119
+ const message = signal ? `${input.command} ${input.args.join(" ")} exited with signal ${signal}` : `${input.command} ${input.args.join(" ")} exited with code ${code ?? 1}`;
1120
+ reject(Object.assign(new Error(message), { stdout, stderr, code: code ?? signal ?? 1 }));
1121
+ });
1122
+ });
1123
+ }
696
1124
  function evidenceEnvSummary(env) {
697
1125
  if (!env) return void 0;
698
1126
  return Object.fromEntries(Object.entries(env).map(([key, value]) => [
@@ -700,6 +1128,52 @@ function evidenceEnvSummary(env) {
700
1128
  /SECRET|TOKEN|KEY|PASSWORD/iu.test(key) && value ? "<redacted>" : value
701
1129
  ]));
702
1130
  }
1131
+ function sceneActionKindFromManifestAction(action) {
1132
+ if (!action || typeof action !== "object") return "unknown";
1133
+ const keys = Object.keys(action);
1134
+ return keys[0] ?? "unknown";
1135
+ }
1136
+ function validateGuaranteeSceneJourneyContract(input) {
1137
+ const diagnostics = [];
1138
+ let value = null;
1139
+ try {
1140
+ value = parseYaml(readFileSync(input.scenePath, "utf8"));
1141
+ } catch (error) {
1142
+ diagnostics.push(diagnostic("error", "guarantee.scene_unreadable", error instanceof Error ? error.message : String(error ?? "Scene manifest could not be read."), "scene.manifest", input.sourcePath));
1143
+ return diagnostics;
1144
+ }
1145
+ if (!value || typeof value !== "object") {
1146
+ diagnostics.push(diagnostic("error", "guarantee.scene_invalid_manifest", "Scene manifest must be an object.", "scene.manifest", input.sourcePath));
1147
+ return diagnostics;
1148
+ }
1149
+ const workflow = Array.isArray(value.workflow) ? value.workflow : [];
1150
+ if (workflow.length === 0) {
1151
+ diagnostics.push(diagnostic("error", "guarantee.scene_empty_journey", "Active scene guarantee has no workflow steps.", "scene.workflow", input.sourcePath));
1152
+ return diagnostics;
1153
+ }
1154
+ const journey = isRecord(value.journey) ? value.journey : null;
1155
+ const minimumSteps = typeof journey?.minimumSteps === "number" ? journey.minimumSteps : 2;
1156
+ if (journey?.kind !== "service") {
1157
+ diagnostics.push(diagnostic("error", "guarantee.scene_missing_service_journey", "Scene-backed active guarantees must declare journey.kind: service so evidence is tied to a service journey contract.", "scene.journey.kind", input.sourcePath));
1158
+ }
1159
+ const actionKinds = workflow.map((step) => sceneActionKindFromManifestAction(step?.action));
1160
+ const interactiveActions = actionKinds.filter((kind) => kind !== "goto" && kind !== "pause");
1161
+ if (workflow.length < minimumSteps || interactiveActions.length === 0) {
1162
+ diagnostics.push(diagnostic(
1163
+ "error",
1164
+ "guarantee.scene_weak_journey_contract",
1165
+ `Scene workflow has ${workflow.length} step${workflow.length === 1 ? "" : "s"} (${actionKinds.join(", ")}). A service journey guarantee must exercise user/service actions after opening the entry route, so evidence captures the actual journey instead of only the first page.`,
1166
+ "scene.workflow",
1167
+ input.sourcePath
1168
+ ));
1169
+ }
1170
+ for (const [index, step] of workflow.entries()) {
1171
+ if (!sceneHasAcceptanceAssertions(step)) {
1172
+ diagnostics.push(diagnostic("error", "guarantee.scene_step_missing_assertions", `Workflow step ${index + 1} is missing acceptance assertions.`, `scene.workflow[${index}].expect`, input.sourcePath));
1173
+ }
1174
+ }
1175
+ return diagnostics;
1176
+ }
703
1177
  function apiAcceptanceEnvironment(environment) {
704
1178
  const baseUrl = apiAcceptanceBaseUrl(environment);
705
1179
  const serviceId = process.env.TREESEED_ACCEPTANCE_SERVICE_ID ?? process.env.TREESEED_API_WEB_SERVICE_ID ?? process.env.TREESEED_WEB_SERVICE_ID ?? (environment === "local" ? "web" : void 0);
@@ -759,7 +1233,8 @@ async function defaultTreeseedGuaranteeVerifierExecutor(input) {
759
1233
  command: "npm",
760
1234
  args: ["-w", "packages/api", "run", "test:acceptance", "--", "--environment", input.environment, "--base-url", apiAcceptanceBaseUrl(input.environment), "--case", definition.caseId, "--json"],
761
1235
  timeoutSeconds: definition.timeoutSeconds,
762
- env: apiAcceptanceEnvironment(input.environment)
1236
+ env: apiAcceptanceEnvironment(input.environment),
1237
+ onProgress: input.onProgress
763
1238
  });
764
1239
  }
765
1240
  if (definition.kind === "vitestCase") {
@@ -772,7 +1247,9 @@ async function defaultTreeseedGuaranteeVerifierExecutor(input) {
772
1247
  ref: input.ref,
773
1248
  command: "npm",
774
1249
  args: ["-w", workspace, "exec", "--", "vitest", "run", "--config", "./vitest.config.js", definition.testFile, ...definition.testName ? ["-t", definition.testName] : []],
775
- timeoutSeconds: definition.timeoutSeconds
1250
+ timeoutSeconds: definition.timeoutSeconds,
1251
+ onProgress: input.onProgress,
1252
+ validateSuccess: validateTreeseedVitestVerifierOutput
776
1253
  });
777
1254
  }
778
1255
  if (definition.kind === "packageScript") {
@@ -785,7 +1262,8 @@ async function defaultTreeseedGuaranteeVerifierExecutor(input) {
785
1262
  ref: input.ref,
786
1263
  command: "npm",
787
1264
  args: ["-w", workspace, "run", definition.command, "--", ...definition.args ?? []],
788
- timeoutSeconds: definition.timeoutSeconds
1265
+ timeoutSeconds: definition.timeoutSeconds,
1266
+ onProgress: input.onProgress
789
1267
  });
790
1268
  }
791
1269
  if (definition.kind === "nodeScript") {
@@ -799,7 +1277,8 @@ async function defaultTreeseedGuaranteeVerifierExecutor(input) {
799
1277
  command: "node",
800
1278
  args: ["--import", "tsx", definition.command, ...definition.args ?? []],
801
1279
  cwd: definition.cwd,
802
- timeoutSeconds: definition.timeoutSeconds
1280
+ timeoutSeconds: definition.timeoutSeconds,
1281
+ onProgress: input.onProgress
803
1282
  });
804
1283
  }
805
1284
  return {
@@ -808,41 +1287,88 @@ async function defaultTreeseedGuaranteeVerifierExecutor(input) {
808
1287
  diagnostics: [diagnostic("error", "guarantee.unsupported_verifier_kind", `Unsupported verifier kind "${definition.kind}".`, input.ref, input.guarantee.sourcePath)]
809
1288
  };
810
1289
  }
1290
+ function sceneAuthRoleForGuarantee(manifest) {
1291
+ if (manifest.actors.allowed.includes("anonymous_user") || manifest.actors.allowed.includes("anonymous")) return void 0;
1292
+ const actors = manifest.actors.allowed.map((actor) => actor.toLowerCase());
1293
+ if (actors.some((actor) => /owner|operator|seller|platform|host/iu.test(actor))) return "owner";
1294
+ if (actors.some((actor) => /admin|manager|lead/iu.test(actor))) return "admin";
1295
+ if (actors.some((actor) => /member|contributor|authenticated|participant|viewer/iu.test(actor))) return "member";
1296
+ return "owner";
1297
+ }
1298
+ function browserForGuaranteeDevice(device) {
1299
+ if (device?.includes("firefox")) return "firefox";
1300
+ if (device?.includes("webkit")) return "webkit";
1301
+ return "chromium";
1302
+ }
1303
+ function sceneDeviceRunsForGuarantee(devices) {
1304
+ const requested = devices.length > 0 ? devices : ["desktop_chromium"];
1305
+ return requested.map((device) => ({
1306
+ id: device.toLowerCase().replace(/[^a-z0-9._-]+/gu, "-"),
1307
+ device,
1308
+ browser: browserForGuaranteeDevice(device)
1309
+ }));
1310
+ }
1311
+ function sceneReportEvidencePaths(workspaceRoot, report) {
1312
+ const primaryScreenshots = [
1313
+ ...(report.steps ?? []).map((step) => step.screenshotPath).filter(Boolean),
1314
+ ...report.artifacts?.screenshotPaths ?? []
1315
+ ].filter((path) => Boolean(path && !path.includes("/screenshots/viewport/")));
1316
+ return sortedUnique([
1317
+ ...primaryScreenshots,
1318
+ report.playwrightTracePath ?? void 0,
1319
+ report.artifacts?.runRoot
1320
+ ].filter(Boolean).map((entry) => relativeEvidencePath(workspaceRoot, entry)));
1321
+ }
811
1322
  async function defaultTreeseedGuaranteeSceneExecutor(input) {
812
1323
  try {
1324
+ const contractDiagnostics = validateGuaranteeSceneJourneyContract({ scenePath: input.scenePath, sourcePath: input.guarantee.sourcePath });
813
1325
  const scenes = await import("../scenes/index.js");
814
- const devices = input.guarantee.manifest.devices.required;
815
- if (devices.length > 1 && typeof scenes.runTreeseedSceneDeviceMatrix === "function") {
816
- const report2 = await scenes.runTreeseedSceneDeviceMatrix({
817
- projectRoot: input.workspaceRoot,
818
- scene: input.scenePath,
819
- environment: input.environment,
820
- record: input.record,
821
- artifactMode: input.artifactMode,
822
- mode: "acceptance",
823
- devices
824
- });
1326
+ const authRole = sceneAuthRoleForGuarantee(input.guarantee.manifest);
1327
+ const runs = sceneDeviceRunsForGuarantee(input.device ? [input.device] : input.guarantee.manifest.devices.required);
1328
+ if (runs.length > 1) {
1329
+ const runReports = [];
1330
+ for (const run2 of runs) {
1331
+ const report2 = await scenes.runTreeseedScene({
1332
+ projectRoot: input.workspaceRoot,
1333
+ scene: input.scenePath,
1334
+ environment: input.environment,
1335
+ device: run2.device,
1336
+ browser: run2.browser,
1337
+ authRole,
1338
+ record: input.record,
1339
+ artifactMode: input.artifactMode,
1340
+ mode: "acceptance",
1341
+ runId: `${input.runId}-${run2.id}`
1342
+ });
1343
+ runReports.push(report2);
1344
+ }
1345
+ const ok2 = contractDiagnostics.length === 0 && runReports.every((entry) => entry.ok);
825
1346
  return {
826
- status: report2.ok ? "passed" : "failed",
827
- summary: report2.ok ? "Scene device matrix passed." : "Scene device matrix failed.",
828
- evidence: [report2.matrixPath, ...(report2.runReports ?? []).map((entry) => entry.artifacts?.runRoot)].filter(Boolean).map((entry) => relativeEvidencePath(input.workspaceRoot, entry)),
829
- diagnostics: report2.diagnostics ?? []
1347
+ status: ok2 ? "passed" : "failed",
1348
+ summary: ok2 ? "Scene device matrix passed." : contractDiagnostics.length > 0 ? "Scene is not a complete service journey." : "Scene device matrix failed.",
1349
+ evidence: runReports.flatMap((entry) => sceneReportEvidencePaths(input.workspaceRoot, entry)),
1350
+ diagnostics: [...contractDiagnostics, ...runReports.flatMap((entry) => entry.diagnostics ?? [])]
830
1351
  };
831
1352
  }
1353
+ const run = runs[0];
832
1354
  const report = await scenes.runTreeseedScene({
833
1355
  projectRoot: input.workspaceRoot,
834
1356
  scene: input.scenePath,
835
1357
  environment: input.environment,
836
- device: input.device ?? devices[0],
1358
+ device: run.device,
1359
+ browser: run.browser,
1360
+ authRole,
837
1361
  record: input.record,
838
1362
  artifactMode: input.artifactMode,
839
- mode: "acceptance"
1363
+ mode: "acceptance",
1364
+ runId: `${input.runId}-${run.id}`
840
1365
  });
1366
+ const ok = contractDiagnostics.length === 0 && report.ok;
841
1367
  return {
842
- status: report.ok ? "passed" : "failed",
843
- summary: report.ok ? "Scene passed." : "Scene failed.",
844
- evidence: [report.artifacts?.runRoot, report.playwrightTracePath].filter(Boolean).map((entry) => relativeEvidencePath(input.workspaceRoot, entry)),
845
- diagnostics: report.diagnostics ?? []
1368
+ status: ok ? "passed" : "failed",
1369
+ summary: ok ? "Scene passed." : contractDiagnostics.length > 0 ? "Scene is not a complete service journey." : "Scene failed.",
1370
+ evidence: sceneReportEvidencePaths(input.workspaceRoot, report),
1371
+ diagnostics: [...contractDiagnostics, ...report.diagnostics ?? []]
846
1372
  };
847
1373
  } catch (error) {
848
1374
  return {
@@ -916,6 +1442,7 @@ async function runGuaranteeSteps(input) {
916
1442
  const evidence = [];
917
1443
  const addStep = async (step, run) => {
918
1444
  const stepStartedAt = (/* @__PURE__ */ new Date()).toISOString();
1445
+ input.onProgress?.(`[guarantees][step] ${input.guarantee.manifest.id}: starting ${step.kind}${step.ref ? ` ${step.ref}` : ""}`);
919
1446
  const result = await run();
920
1447
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
921
1448
  const nextStep = {
@@ -930,6 +1457,7 @@ async function runGuaranteeSteps(input) {
930
1457
  steps.push(nextStep);
931
1458
  evidence.push(...nextStep.evidence ?? []);
932
1459
  diagnostics.push(...nextStep.diagnostics ?? []);
1460
+ input.onProgress?.(`[guarantees][step] ${input.guarantee.manifest.id}: ${nextStep.status} ${step.kind}${step.ref ? ` ${step.ref}` : ""}`);
933
1461
  };
934
1462
  const scene = input.guarantee.manifest.scene;
935
1463
  if (scene?.required && scene.manifest) {
@@ -973,7 +1501,8 @@ async function runGuaranteeSteps(input) {
973
1501
  guarantee: input.guarantee,
974
1502
  ref,
975
1503
  definition: resolution.definition,
976
- kind: group.kind
1504
+ kind: group.kind,
1505
+ onProgress: input.onProgress
977
1506
  });
978
1507
  input.verifierCache.set(cacheKey, result);
979
1508
  return result;
@@ -1012,16 +1541,64 @@ async function runTreeseedGuarantees(input) {
1012
1541
  const diagnostics = [...registry.diagnostics, ...plan.diagnostics];
1013
1542
  const allResolutions = verifierDefinitionsByRef(registry.verifierRegistries);
1014
1543
  const verifierCache = /* @__PURE__ */ new Map();
1015
- const selectedWithoutDeps = filterTreeseedGuarantees({ guarantees: registry.guarantees, filter, includeDependencies: false });
1016
- const selectedIds = new Set(selectedWithoutDeps.map((entry) => entry.manifest.id));
1017
- const runEntries = filterTreeseedGuarantees({ guarantees: registry.guarantees, filter, includeDependencies: input.includeDependencies !== false });
1544
+ const graph = buildTreeseedGuaranteeDependencyGraph({ guarantees: registry.guarantees, filter, includeDependencies: input.includeDependencies !== false });
1545
+ diagnostics.push(...graph.diagnostics);
1546
+ const selectedIds = graph.selectedIds;
1547
+ const runEntries = graph.entries;
1548
+ const planEntryById = new Map(plan.entries.map((entry) => [entry.id, entry]));
1549
+ const resultById = /* @__PURE__ */ new Map();
1018
1550
  const results = [];
1551
+ input.onProgress?.(`[guarantees][run] planned ${runEntries.length} guarantee entries for ${environment}`);
1019
1552
  if (registry.ok && plan.ok) {
1020
- for (const entry of runEntries) {
1553
+ for (const [index, entry] of runEntries.entries()) {
1554
+ input.onProgress?.(`[guarantees][run] ${index + 1}/${runEntries.length} ${entry.manifest.id} (${entry.manifest.ownerPackage}) ${entry.manifest.journey}`);
1555
+ const planEntry = planEntryById.get(entry.manifest.id);
1556
+ const blockingDependencies = (planEntry?.dependsOn ?? []).map((id) => resultById.get(id)).filter((result2) => Boolean(result2 && result2.status !== "passed"));
1557
+ if (blockingDependencies.length > 0) {
1558
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1559
+ const blockedBy = blockingDependencies.map((result2) => result2.id);
1560
+ const dependencyDiagnostic = diagnostic(
1561
+ "error",
1562
+ "guarantee.dependency_failed",
1563
+ `Dependency ${blockedBy.join(", ")} failed before ${entry.manifest.id}.`,
1564
+ "dependencies.guarantees",
1565
+ entry.sourcePath
1566
+ );
1567
+ const blocked = {
1568
+ id: entry.manifest.id,
1569
+ ...entry.manifest.journeyIndex ? { journeyIndex: entry.manifest.journeyIndex } : {},
1570
+ type: entry.manifest.type,
1571
+ subtype: entry.manifest.subtype,
1572
+ journey: entry.manifest.journey,
1573
+ ownerPackage: entry.manifest.ownerPackage,
1574
+ status: "blocked",
1575
+ selected: selectedIds.has(entry.manifest.id),
1576
+ dependency: !selectedIds.has(entry.manifest.id),
1577
+ sourcePath: entry.relativePath,
1578
+ startedAt: now,
1579
+ completedAt: now,
1580
+ steps: [{
1581
+ id: "dependency",
1582
+ kind: "verifier",
1583
+ status: "blocked",
1584
+ summary: `Blocked by failed prerequisite: ${blockedBy.join(", ")}.`,
1585
+ diagnostics: [dependencyDiagnostic],
1586
+ startedAt: now,
1587
+ completedAt: now
1588
+ }],
1589
+ evidence: [],
1590
+ diagnostics: [dependencyDiagnostic]
1591
+ };
1592
+ results.push(blocked);
1593
+ resultById.set(blocked.id, blocked);
1594
+ input.onProgress?.(`[guarantees][run] ${entry.manifest.id}: blocked by ${blockedBy.join(", ")}`, "stderr");
1595
+ continue;
1596
+ }
1021
1597
  if (entry.manifest.status !== "active") {
1022
1598
  if (input.includePlanned) {
1023
1599
  const now = (/* @__PURE__ */ new Date()).toISOString();
1024
- results.push({
1600
+ input.onProgress?.(`[guarantees][run] ${entry.manifest.id}: skipped because status is ${entry.manifest.status}`);
1601
+ const skipped = {
1025
1602
  id: entry.manifest.id,
1026
1603
  ...entry.manifest.journeyIndex ? { journeyIndex: entry.manifest.journeyIndex } : {},
1027
1604
  type: entry.manifest.type,
@@ -1037,7 +1614,9 @@ async function runTreeseedGuarantees(input) {
1037
1614
  steps: [{ id: "status", kind: "verifier", status: "skipped", summary: `Guarantee is ${entry.manifest.status}.`, startedAt: now, completedAt: now }],
1038
1615
  evidence: [],
1039
1616
  diagnostics: []
1040
- });
1617
+ };
1618
+ results.push(skipped);
1619
+ resultById.set(skipped.id, skipped);
1041
1620
  }
1042
1621
  continue;
1043
1622
  }
@@ -1050,7 +1629,8 @@ async function runTreeseedGuarantees(input) {
1050
1629
  diagnostics.push(...resolution.diagnostics);
1051
1630
  if (!resolution.ok) {
1052
1631
  const now = (/* @__PURE__ */ new Date()).toISOString();
1053
- results.push({
1632
+ input.onProgress?.(`[guarantees][run] ${entry.manifest.id}: blocked by unresolved verifier refs`, "stderr");
1633
+ const blocked = {
1054
1634
  id: entry.manifest.id,
1055
1635
  ...entry.manifest.journeyIndex ? { journeyIndex: entry.manifest.journeyIndex } : {},
1056
1636
  type: entry.manifest.type,
@@ -1066,10 +1646,12 @@ async function runTreeseedGuarantees(input) {
1066
1646
  steps: [],
1067
1647
  evidence: [],
1068
1648
  diagnostics: resolution.diagnostics
1069
- });
1649
+ };
1650
+ results.push(blocked);
1651
+ resultById.set(blocked.id, blocked);
1070
1652
  continue;
1071
1653
  }
1072
- results.push(await runGuaranteeSteps({
1654
+ const result = await runGuaranteeSteps({
1073
1655
  workspaceRoot,
1074
1656
  environment,
1075
1657
  runId,
@@ -1083,8 +1665,12 @@ async function runTreeseedGuarantees(input) {
1083
1665
  verifierCache,
1084
1666
  record: input.record,
1085
1667
  sceneArtifacts: input.sceneArtifacts,
1086
- device: input.device
1087
- }));
1668
+ device: input.device,
1669
+ onProgress: input.onProgress
1670
+ });
1671
+ results.push(result);
1672
+ resultById.set(result.id, result);
1673
+ input.onProgress?.(`[guarantees][run] ${entry.manifest.id}: ${result.status}`);
1088
1674
  }
1089
1675
  }
1090
1676
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -1109,11 +1695,20 @@ async function runTreeseedGuarantees(input) {
1109
1695
  startedAt,
1110
1696
  completedAt,
1111
1697
  outputRoot,
1698
+ statePath: relativeEvidencePath(workspaceRoot, resolve(outputRoot, "state.json")),
1112
1699
  plan,
1113
1700
  results,
1114
1701
  diagnostics,
1115
1702
  counts
1116
1703
  };
1704
+ mkdirSync(outputRoot, { recursive: true });
1705
+ const state = {
1706
+ schemaVersion: "treeseed.guarantee-run-state/v1",
1707
+ runId,
1708
+ values: {}
1709
+ };
1710
+ writeFileSync(resolve(outputRoot, "state.json"), `${JSON.stringify(state, null, 2)}
1711
+ `);
1117
1712
  const writeResult = writeTreeseedGuaranteeRunReport({ report, registry });
1118
1713
  if (!writeResult.ok) {
1119
1714
  report.ok = false;
@@ -1153,9 +1748,12 @@ function fileExists(path) {
1153
1748
  return existsSync(path) && statSync(path).isFile();
1154
1749
  }
1155
1750
  export {
1751
+ TREESEED_GUARANTEE_JOURNEY_AUDIT_SCHEMA_VERSION,
1156
1752
  TREESEED_GUARANTEE_SCHEMA_VERSION,
1157
1753
  TREESEED_GUARANTEE_VERIFIERS_SCHEMA_VERSION,
1158
1754
  assertPathInsideWorkspace,
1755
+ auditTreeseedGuaranteeJourneys,
1756
+ browserForGuaranteeDevice,
1159
1757
  createTreeseedGuaranteeStatusReport,
1160
1758
  discoverTreeseedGuarantees,
1161
1759
  exportTreeseedGuaranteesCsv,
@@ -1169,9 +1767,13 @@ export {
1169
1767
  planTreeseedGuarantees,
1170
1768
  resolveTreeseedGuaranteeVerifierRefs,
1171
1769
  runTreeseedGuarantees,
1770
+ sceneAuthRoleForGuarantee,
1771
+ sceneDeviceRunsForGuarantee,
1172
1772
  slugifyTreeseedGuaranteeJourney,
1773
+ validateGuaranteeSceneJourneyContract,
1173
1774
  validateTreeseedGuarantee,
1174
1775
  validateTreeseedGuaranteeRegistry,
1776
+ validateTreeseedVitestVerifierOutput,
1175
1777
  writeTreeseedGuaranteeRunReport,
1176
1778
  writeTreeseedGuaranteesExport
1177
1779
  };