Forge
csharpdeeb25a2
1using CascadeIDE.Cockpit.DataBus;
2
3namespace CascadeIDE.Features.Agent.Environment;
4
5/// <summary>verify_snapshot_id epoch + stale on write (ADR 0148 §8.1).</summary>
6public sealed class AgentVerifyEpochTracker
7{
8 private readonly IDataBus _dataBus;
9 private readonly object _gate = new();
10 private string? _runId;
11 private string? _snapshotId;
12 private HashSet<string> _watchedPaths = new(StringComparer.OrdinalIgnoreCase);
13 private HashSet<string> _uiStalePaths = new(StringComparer.OrdinalIgnoreCase);
14 private bool _writesInvalidatedVerifyEpoch;
15 private bool _uiStale;
16
17 public AgentVerifyEpochTracker(IDataBus dataBus) => _dataBus = dataBus;
18
19 /// <summary>Писали в workspace во время активного verify — результат лестницы может не соответствовать текущему дереву.</summary>
20 public bool WritesInvalidatedVerifyEpoch
21 {
22 get
23 {
24 lock (_gate)
25 return _writesInvalidatedVerifyEpoch;
26 }
27 }
28
29 /// <summary>UI stale: запись в epoch или отмена/supersede — до следующего verify.</summary>
30 public bool IsUiStale
31 {
32 get
33 {
34 lock (_gate)
35 return _uiStale;
36 }
37 }
38
39 public bool IsPathUiStale(string? filePath)
40 {
41 if (string.IsNullOrWhiteSpace(filePath))
42 return false;
43
44 string full;
45 try
46 {
47 full = Path.GetFullPath(filePath);
48 }
49 catch
50 {
51 return false;
52 }
53
54 lock (_gate)
55 return _uiStale && _uiStalePaths.Contains(full);
56 }
57
58 public void Begin(string runId, string snapshotId, string solutionPath)
59 {
60 lock (_gate)
61 {
62 _runId = runId;
63 _snapshotId = snapshotId;
64 _watchedPaths = new(StringComparer.OrdinalIgnoreCase) { Path.GetFullPath(solutionPath) };
65 _uiStalePaths.Clear();
66 _writesInvalidatedVerifyEpoch = false;
67 _uiStale = false;
68 }
69 }
70
71 public void WatchPath(string? filePath)
72 {
73 if (string.IsNullOrWhiteSpace(filePath))
74 return;
75
76 lock (_gate)
77 {
78 if (_snapshotId is null)
79 return;
80 _watchedPaths.Add(Path.GetFullPath(filePath));
81 }
82 }
83
84 public void NotifyWrite(string? filePath)
85 {
86 if (string.IsNullOrWhiteSpace(filePath))
87 return;
88
89 string? runId;
90 string? snapshotId;
91 lock (_gate)
92 {
93 if (_snapshotId is null)
94 return;
95
96 var full = Path.GetFullPath(filePath);
97 if (!_watchedPaths.Contains(full) && !full.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
98 return;
99
100 runId = _runId;
101 snapshotId = _snapshotId;
102 }
103
104 if (runId is not null && snapshotId is not null)
105 {
106 lock (_gate)
107 {
108 _writesInvalidatedVerifyEpoch = true;
109 MarkUiStaleLocked();
110 }
111
112 _dataBus.Publish(new AgentVerifyEpochStale(runId, snapshotId, "write_in_epoch"));
113 }
114 }
115
116 public void End(string? reason = null)
117 {
118 lock (_gate)
119 {
120 if (_snapshotId is not null && reason is "superseded" or "cancel" && _runId is not null)
121 {
122 MarkUiStaleLocked();
123 _dataBus.Publish(new AgentVerifyEpochStale(_runId, _snapshotId, reason));
124 }
125
126 _runId = null;
127 _snapshotId = null;
128 _watchedPaths.Clear();
129 _writesInvalidatedVerifyEpoch = false;
130 }
131 }
132
133 private void MarkUiStaleLocked()
134 {
135 _uiStale = true;
136 _uiStalePaths = new HashSet<string>(_watchedPaths, StringComparer.OrdinalIgnoreCase);
137 }
138}
139
View only · write via MCP/CIDE