Forge
csharpdeeb25a2
1using CascadeIDE.Cockpit.DataBus;
2using CascadeIDE.Models;
3using CascadeIDE.Services;
4
5namespace CascadeIDE.Features.Agent.Environment;
6
7public sealed record VerificationLadderResult(
8 bool Green,
9 string MaxRungReached,
10 IReadOnlyList<AgentTimeSlice> Slices,
11 string? FailureDetail);
12
13/// <summary>Verify rung climb (ADR 0148 W2).</summary>
14public sealed class VerificationLadder
15{
16 private readonly EnvironmentTaskRunner _runner;
17 private readonly AgentRoslynDiagnoseFilesDiagnostics _diagnoseFiles;
18 private readonly AgentEnvironmentSettings _settings;
19 private readonly AgentSandboxManager _sandbox;
20 private readonly EnvironmentTaskDedup _dedup;
21 private readonly IDataBus _dataBus;
22 private readonly IGitCommandRunner? _gitRunner;
23 private readonly Func<string?>? _getWorkspaceRoot;
24
25 public VerificationLadder(
26 IDataBus dataBus,
27 IBuildTestHost host,
28 AgentRoslynDiagnoseFilesDiagnostics diagnoseFiles,
29 AgentEnvironmentSettings settings,
30 AgentSandboxManager sandbox,
31 IGitCommandRunner? gitRunner = null,
32 Func<string?>? getWorkspaceRoot = null)
33 {
34 _dataBus = dataBus;
35 _runner = new EnvironmentTaskRunner(dataBus, host.JobBackend);
36 _diagnoseFiles = diagnoseFiles;
37 _settings = settings;
38 _sandbox = sandbox;
39 _dedup = new EnvironmentTaskDedup(settings.CoalesceWindowMs);
40 _gitRunner = gitRunner;
41 _getWorkspaceRoot = getWorkspaceRoot;
42 }
43
44 public EnvironmentTaskRunner Runner => _runner;
45
46 public async Task<VerificationLadderResult> ClimbAsync(
47 string runId,
48 string solutionPath,
49 AgentVerifyPolicy policy,
50 AgentSandboxLease sandboxLease,
51 CancellationToken cancellationToken)
52 {
53 var slices = new List<AgentTimeSlice>();
54 var envStart = DateTimeOffset.UtcNow;
55 var maxRung = VerifyRung.DiagnoseFiles;
56 var green = true;
57 string? failure = null;
58
59 try
60 {
61 if (policy != AgentVerifyPolicy.Minimal && _settings.Ladder.DiagnoseFilesEnabled)
62 {
63 maxRung = VerifyRung.DiagnoseFiles;
64 var diagnoseStart = DateTimeOffset.UtcNow;
65 var diagnose = await _diagnoseFiles.RunAsync(cancellationToken).ConfigureAwait(false);
66 slices.Add(new AgentTimeSlice(
67 AgentRunPhaseKind.Environment,
68 (DateTimeOffset.UtcNow - diagnoseStart).TotalSeconds,
69 diagnose.Detail));
70 green = diagnose.Green;
71 if (!green)
72 {
73 failure = diagnose.Detail;
74 return Finish(slices, envStart, maxRung, green, failure);
75 }
76 }
77
78 if (green && policy is not AgentVerifyPolicy.Minimal)
79 {
80 maxRung = VerifyRung.CompileProject;
81 slices.Add(new AgentTimeSlice(
82 AgentRunPhaseKind.Environment,
83 0,
84 $"{VerifyRung.CompileProject}: delegated to {VerifyRung.BuildAffected} (MLP)"));
85 }
86
87 if (green)
88 {
89 var dedupKey = $"build|{solutionPath}";
90 var coalesced = _dedup.ShouldCoalesce(dedupKey);
91 if (!coalesced)
92 {
93 maxRung = VerifyRung.BuildAffected;
94 var build = await _runner.RunBuildAsync(
95 runId,
96 solutionPath,
97 waitForCompletion: true,
98 cancellationToken)
99 .ConfigureAwait(false);
100 green = build.Success;
101 if (!green)
102 failure = build.Status;
103 }
104 else
105 {
106 maxRung = VerifyRung.BuildAffected;
107 slices.Add(new AgentTimeSlice(
108 AgentRunPhaseKind.Environment,
109 0,
110 $"{VerifyRung.BuildAffected}: build skipped (dedup/coalesce window; prior build assumed sufficient)"));
111 }
112 }
113
114 if (green && policy is AgentVerifyPolicy.Standard or AgentVerifyPolicy.Strict or AgentVerifyPolicy.CiParity)
115 {
116 sandboxLease = sandboxLease with
117 {
118 Substrate = _sandbox.RecreateSubstrateBeforeTests(sandboxLease),
119 };
120 maxRung = VerifyRung.TestScoped;
121
122 var contract = AgentDevServiceContractValidator.ValidateForTestScoped(
123 _settings.DevServices,
124 sandboxLease.Profile,
125 sandboxLease);
126 slices.Add(new AgentTimeSlice(AgentRunPhaseKind.Environment, 0, contract.Detail));
127 if (!contract.Ok && _settings.DevServices.GateTestScopedOnViolation)
128 {
129 green = false;
130 failure = contract.Detail;
131 return Finish(slices, envStart, maxRung, green, failure);
132 }
133
134 var supplementalEnv = sandboxLease.Substrate is null
135 ? null
136 : AgentSandboxProcessEnvironmentKeys.ForBundle(sandboxLease.Substrate);
137
138 var filter = await AgentTestScopedTouchedTestFilter.BuildFilterExpressionAsync(
139 _settings.Ladder,
140 _gitRunner,
141 _getWorkspaceRoot?.Invoke(),
142 cancellationToken).ConfigureAwait(false);
143
144 var test = await _runner.RunTestsAsync(
145 runId,
146 solutionPath,
147 filterExpression: filter,
148 waitForCompletion: true,
149 cancellationToken,
150 supplementalEnvironmentVariables: supplementalEnv).ConfigureAwait(false);
151 green = test.Success;
152 if (!green)
153 failure = test.Status;
154 }
155
156 if (green && policy is AgentVerifyPolicy.CiParity)
157 {
158 maxRung = VerifyRung.TestFull;
159 if (_settings.Ladder.TestFullRequireExplicit)
160 slices.Add(new AgentTimeSlice(
161 AgentRunPhaseKind.Environment,
162 0,
163 $"{VerifyRung.TestFull}: ci_parity marker (MLP)"));
164 }
165 }
166 catch (OperationCanceledException)
167 {
168 green = false;
169 failure = "cancelled";
170 }
171
172 return Finish(slices, envStart, maxRung, green, failure);
173 }
174
175 private VerificationLadderResult Finish(
176 List<AgentTimeSlice> slices,
177 DateTimeOffset envStart,
178 string maxRung,
179 bool green,
180 string? failure)
181 {
182 var total = (DateTimeOffset.UtcNow - envStart).TotalSeconds;
183 if (!slices.Any(s => s.Detail?.StartsWith(VerifyRung.DiagnoseFiles, StringComparison.Ordinal) == true))
184 slices.Insert(0, new AgentTimeSlice(AgentRunPhaseKind.Environment, total, $"ladder → {maxRung}"));
185 return new(green, maxRung, slices, failure);
186 }
187}
188
View only · write via MCP/CIDE