@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.
- package/dist/guarantees/index.d.ts +83 -0
- package/dist/guarantees/index.js +688 -86
- package/dist/hosting/graph.js +40 -6
- package/dist/operations/services/git-workflow.d.ts +16 -1
- package/dist/operations/services/git-workflow.js +65 -5
- package/dist/operations/services/github-api.js +0 -19
- package/dist/operations/services/hosted-service-checks.js +17 -1
- package/dist/operations/services/live-hosted-service-checks.d.ts +4 -0
- package/dist/operations/services/live-hosted-service-checks.js +8 -5
- package/dist/operations/services/local-cleanup.js +3 -7
- package/dist/operations/services/package-adapters.d.ts +14 -0
- package/dist/operations/services/package-adapters.js +36 -3
- package/dist/operations/services/package-artifacts.d.ts +37 -0
- package/dist/operations/services/package-artifacts.js +99 -0
- package/dist/operations/services/railway-deploy.js +78 -18
- package/dist/operations/services/railway-source-policy.d.ts +19 -0
- package/dist/operations/services/railway-source-policy.js +66 -0
- package/dist/operations/services/repository-save-orchestrator.js +88 -19
- package/dist/operations/services/workspace-dependency-mode.js +4 -0
- package/dist/platform/desired-state.js +3 -3
- package/dist/reconcile/builtin-adapters.js +10 -2
- package/dist/reconcile/providers/railway-iac.d.ts +2 -0
- package/dist/reconcile/providers/railway-iac.js +31 -3
- package/dist/reconcile/providers/release-private.d.ts +10 -0
- package/dist/reconcile/providers/release-private.js +45 -1
- package/dist/scenes/builtin-plugins.js +36 -5
- package/dist/scenes/device-matrix.js +2 -0
- package/dist/scenes/environment.js +1 -1
- package/dist/scenes/runner.js +28 -16
- package/dist/scenes/schema.js +31 -2
- package/dist/scenes/types.d.ts +25 -2
- package/dist/scenes/visual-audit-fixtures.js +9 -3
- package/dist/workflow/operations.d.ts +27 -92
- package/dist/workflow/operations.js +252 -144
- package/dist/workflow/runs.d.ts +1 -0
- package/dist/workflow/runs.js +57 -0
- package/dist/workflow-support.d.ts +1 -0
- package/dist/workflow-support.js +8 -0
- package/dist/workflow.d.ts +2 -0
- package/package.json +4 -1
package/dist/guarantees/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
|
359
|
-
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
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
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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:
|
|
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
|
|
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
|
|
647
|
-
|
|
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
|
-
|
|
650
|
-
|
|
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
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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:
|
|
827
|
-
summary:
|
|
828
|
-
evidence:
|
|
829
|
-
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:
|
|
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:
|
|
843
|
-
summary:
|
|
844
|
-
evidence:
|
|
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
|
|
1016
|
-
|
|
1017
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|