-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathTraceWriter.cs
More file actions
239 lines (219 loc) · 8.74 KB
/
TraceWriter.cs
File metadata and controls
239 lines (219 loc) · 8.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
// `mur check --trace <path>` writer. Appends one JSON row per surfaced
// diagnostic to <path> alongside the normal stdout output. The agent never
// reads the trace; it is for offline mining (spec 037 / 038).
//
// Schema is the source of truth (mirrored in spec 038 §0.3):
//
// { ts, code, severity, file, line, col, msg, receiver_type?, member?, mode }
//
// Constraints:
// - Source code text never appears in the trace. (`msg` is the diagnostic
// message; bounded to 1024 chars to keep individual rows under the 2 KB cap
// the unit test enforces.)
// - Absolute paths outside the project root are replaced with "<external>"
// so traces never carry information about a user's machine layout.
// - Absolute paths inside the project root are normalized to project-relative
// form (forward-slash separators) — same rationale: don't carry the
// `C:\Users\<name>\...` prefix into a trace file that ships off-machine.
// - `mode` is always "iteration" until Phase 2 lands the ranker — present as
// a stable schema field so traces written today join cleanly later.
using System.Text.Json;
namespace Microsoft.UI.Reactor.Cli.Check;
internal sealed class TraceWriter : IDisposable
{
readonly StreamWriter writer;
readonly string projectRoot;
readonly string mode;
public const int MaxMessageChars = 1024;
/// <summary>Default mode tag when the writer is opened without an explicit
/// mode. Phase-0 traces and unit tests that don't care about mode use this.</summary>
public const string DefaultMode = "iteration";
TraceWriter(StreamWriter writer, string projectRoot, string mode)
{
this.writer = writer;
this.projectRoot = projectRoot;
this.mode = mode;
}
public static TraceWriter Open(string path, string projectRoot, string mode = DefaultMode)
{
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read);
var sw = new StreamWriter(stream) { AutoFlush = true };
return new TraceWriter(sw, NormalizeRoot(projectRoot), mode);
}
public void Write(CheckCommand.Diag d)
{
var row = ToRow(d, projectRoot, mode);
var json = JsonSerializer.Serialize(row, JsonOpts);
writer.WriteLine(json);
}
/// <summary>
/// Spec 038 §8 "Tracing": record the *full* effective `dotnet build`
/// command line — including any defaults mur injected — at the head of
/// the trace, so replays are bit-faithful even when default-merging
/// changes between mur versions. Schema:
///
/// { ts, kind: "command", argv: ["dotnet", "build", ...], mode }
///
/// Each value in `argv` is at most 256 chars (defensive truncation) to
/// keep the row under the 2 KB cap the writer enforces on diag rows.
/// </summary>
public void WriteCommand(IReadOnlyList<string> effectiveBuildArgs, string mode)
{
var argv = new List<string>(effectiveBuildArgs.Count + 1) { "dotnet" };
foreach (var a in effectiveBuildArgs)
argv.Add(a.Length > 256 ? a[..256] : a);
var row = new CommandRow(
ts: DateTime.UtcNow.ToString("o"),
kind: "command",
argv: argv,
mode: mode);
var json = JsonSerializer.Serialize(row, JsonOpts);
writer.WriteLine(json);
}
/// <summary>
/// Spec 038 §3.1a residual — structured warning when the registry self-
/// disables a rule because one of its declared targets did not resolve
/// against the live compilation. The signal is for maintainers: a Reactor
/// minor release that renames or removes a rule's target should be loud
/// in trace logs the first time the agent runs `mur check` against the
/// new package, not silent. Schema:
///
/// { ts, kind: "rule_self_disabled", rule, unresolved_target, mode }
///
/// Stdout deliberately stays clean — agents don't read trace files, but
/// the rule simply not firing is the only behavioral effect they see, so
/// adding noise to their channel is counterproductive. Maintainer
/// dashboards and post-run mining find this row instead.
/// </summary>
public void WriteRuleSelfDisabled(string ruleName, string unresolvedTarget)
{
var row = new RuleSelfDisabledRow(
ts: DateTime.UtcNow.ToString("o"),
kind: "rule_self_disabled",
rule: ruleName,
unresolved_target: unresolvedTarget,
mode: mode);
var json = JsonSerializer.Serialize(row, JsonOpts);
writer.WriteLine(json);
}
/// <summary>
/// Spec 038 EC3-final watch-item: emit one structured row per Tier-3 rule
/// fire so per-rule firing-rate audits are a 1-line grep against the trace
/// JSONL instead of a multi-step content scan against agent tool outputs.
/// Schema:
///
/// { ts, kind: "rule_fired", rule, code, confidence, evidence, file, line, mode }
///
/// `evidence` is truncated to <see cref="MaxMessageChars"/> to keep the row
/// under the 2 KB cap the writer enforces. Tier-2 suggestions deliberately
/// do not emit this row — Tier-2 firing rates are derivable from telemetry
/// (~/.mur/telemetry/*.jsonl); the trace event exists specifically to make
/// Tier-3 rule firings discoverable without telemetry opt-in.
/// </summary>
public void WriteRuleFired(
string ruleName,
string code,
double confidence,
string evidence,
string file,
int line)
{
var ev = evidence;
if (ev.Length > MaxMessageChars) ev = ev[..MaxMessageChars];
var row = new RuleFiredRow(
ts: DateTime.UtcNow.ToString("o"),
kind: "rule_fired",
rule: ruleName,
code: code,
confidence: confidence,
evidence: ev,
file: SanitizePath(file, projectRoot),
line: line,
mode: mode);
var json = JsonSerializer.Serialize(row, JsonOpts);
writer.WriteLine(json);
}
static readonly JsonSerializerOptions JsonOpts = new()
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
};
internal static TraceRow ToRow(CheckCommand.Diag d, string projectRoot, string mode = DefaultMode)
{
var msg = d.Message;
if (msg.Length > MaxMessageChars) msg = msg[..MaxMessageChars];
return new TraceRow(
ts: DateTime.UtcNow.ToString("o"),
code: d.Code,
severity: SeverityShort(d.Severity),
file: SanitizePath(d.File, projectRoot),
line: d.Line,
col: d.Col,
msg: msg,
receiver_type: null,
member: null,
mode: mode);
}
static string SeverityShort(string sev) => sev switch
{
"error" => "E",
"warning" => "W",
"info" => "I",
_ => sev,
};
static string NormalizeRoot(string root)
{
try { return Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); }
catch { return root; }
}
internal static string SanitizePath(string raw, string projectRoot)
{
if (string.IsNullOrEmpty(raw)) return raw;
if (!Path.IsPathRooted(raw))
return raw.Replace('\\', '/'); // already relative — keep as-is, normalize separators
string full;
try { full = Path.GetFullPath(raw); }
catch { return "<external>"; }
var rootWithSep = projectRoot + Path.DirectorySeparatorChar;
if (full.Equals(projectRoot, StringComparison.OrdinalIgnoreCase))
return ".";
if (full.StartsWith(rootWithSep, StringComparison.OrdinalIgnoreCase))
return full[rootWithSep.Length..].Replace('\\', '/');
return "<external>";
}
public void Dispose() => writer.Dispose();
internal sealed record TraceRow(
string ts,
string code,
string severity,
string file,
int line,
int col,
string msg,
string? receiver_type,
string? member,
string mode);
internal sealed record CommandRow(
string ts,
string kind,
IReadOnlyList<string> argv,
string mode);
internal sealed record RuleSelfDisabledRow(
string ts,
string kind,
string rule,
string unresolved_target,
string mode);
internal sealed record RuleFiredRow(
string ts,
string kind,
string rule,
string code,
double confidence,
string evidence,
string file,
int line,
string mode);
}