Forge
csharp4405de34
1#nullable enable
2
3using CascadeIDE.Models.Intercom;
4
5namespace CascadeIDE.Services.Intercom;
6
7/// <summary>Inferred index message ordinal ↔ code anchors (ADR 0137 фаза 1).</summary>
8public static class IntercomMessageCodeCorrespondenceProjector
9{
10 public const string MatchKindInferred = "inferred";
11 public const string MatchKindExplicit = "explicit";
12
13 public sealed record LaneMessage(
14 int Ordinal,
15 int MessageIndex,
16 Guid MessageId,
17 IReadOnlyList<AttachmentAnchor> Attachments);
18
19 public sealed record InferredEntry(
20 int Ordinal,
21 int MessageIndex,
22 Guid MessageId,
23 string File,
24 string? MemberKey,
25 string? AttachmentShape,
26 string? AnchorId,
27 int? LineStart,
28 int? LineEnd,
29 string MatchKind);
30
31 public sealed record MatchHit(
32 int Ordinal,
33 int MessageIndex,
34 Guid MessageId,
35 string MatchKind);
36
37 public static IReadOnlyList<InferredEntry> BuildInferred(IReadOnlyList<LaneMessage> lane)
38 {
39 var list = new List<InferredEntry>();
40 foreach (var msg in lane)
41 {
42 foreach (var anchor in msg.Attachments)
43 {
44 if (string.IsNullOrWhiteSpace(anchor.File))
45 continue;
46
47 var file = anchor.File.Replace('\\', '/');
48 int? start = anchor.LineStart;
49 int? end = anchor.LineEnd ?? anchor.LineStart;
50 list.Add(new InferredEntry(
51 msg.Ordinal,
52 msg.MessageIndex,
53 msg.MessageId,
54 file,
55 anchor.MemberKey,
56 anchor.AttachmentShape,
57 anchor.Id,
58 start,
59 end,
60 MatchKindInferred));
61 }
62 }
63
64 return list;
65 }
66
67 public static IReadOnlyList<InferredEntry> BuildCombined(
68 IReadOnlyList<LaneMessage> lane,
69 IReadOnlyList<IntercomMessageRangeRelatedProjector.ExplicitRelate> explicitRelates)
70 {
71 var list = BuildInferred(lane).ToList();
72 if (explicitRelates.Count == 0)
73 return list;
74
75 var byOrdinal = lane.ToDictionary(m => m.Ordinal);
76 foreach (var relate in explicitRelates)
77 {
78 foreach (var segment in relate.OrdinalSegments)
79 {
80 for (var ordinal = segment.StartOrdinal; ordinal <= segment.EndOrdinal; ordinal++)
81 {
82 if (!byOrdinal.TryGetValue(ordinal, out var msg))
83 continue;
84
85 appendEntriesFromAnchor(
86 list,
87 msg.Ordinal,
88 msg.MessageIndex,
89 msg.MessageId,
90 relate.CodeRef,
91 MatchKindExplicit);
92 }
93 }
94 }
95
96 return list;
97 }
98
99 public static IReadOnlyList<MatchHit> Find(
100 IReadOnlyList<InferredEntry> entries,
101 IntercomCodeRefQuery query,
102 string? workspaceRoot)
103 {
104 var queryFile = normalizeFileKey(query.File, workspaceRoot);
105 if (queryFile.Length == 0)
106 return [];
107
108 var hits = new List<MatchHit>();
109 var seen = new HashSet<(int Ordinal, int MessageIndex)>();
110
111 foreach (var entry in entries)
112 {
113 var entryFile = normalizeFileKey(entry.File, workspaceRoot);
114 if (!string.Equals(entryFile, queryFile, StringComparison.OrdinalIgnoreCase))
115 continue;
116
117 if (!matches(query, entry))
118 continue;
119
120 var key = (entry.Ordinal, entry.MessageIndex);
121 if (!seen.Add(key))
122 continue;
123
124 hits.Add(new MatchHit(entry.Ordinal, entry.MessageIndex, entry.MessageId, entry.MatchKind));
125 }
126
127 return hits.OrderBy(h => h.Ordinal).ToList();
128 }
129
130 private static bool matches(IntercomCodeRefQuery query, InferredEntry entry)
131 {
132 if (!string.IsNullOrWhiteSpace(query.MemberKey) && !string.IsNullOrWhiteSpace(entry.MemberKey))
133 return string.Equals(query.MemberKey, entry.MemberKey, StringComparison.Ordinal);
134
135 if (!query.HasLineRange)
136 return string.IsNullOrWhiteSpace(query.MemberKey);
137
138 return matchesLines(query, entry);
139 }
140
141 private static bool matchesLines(IntercomCodeRefQuery query, InferredEntry entry)
142 {
143 if (!query.HasLineRange)
144 return true;
145
146 if (entry.LineStart is null)
147 return false;
148
149 var entryEnd = entry.LineEnd ?? entry.LineStart.Value;
150 return rangesOverlap(
151 query.LineStart!.Value,
152 query.LineEnd!.Value,
153 entry.LineStart.Value,
154 entryEnd);
155 }
156
157 private static bool rangesOverlap(int aStart, int aEnd, int bStart, int bEnd) =>
158 aStart <= bEnd && bStart <= aEnd;
159
160 private static void appendEntriesFromAnchor(
161 List<InferredEntry> list,
162 int ordinal,
163 int messageIndex,
164 Guid messageId,
165 AttachmentAnchor anchor,
166 string matchKind)
167 {
168 if (string.IsNullOrWhiteSpace(anchor.File))
169 return;
170
171 var file = anchor.File.Replace('\\', '/');
172 int? start = anchor.LineStart;
173 int? end = anchor.LineEnd ?? anchor.LineStart;
174 list.Add(new InferredEntry(
175 ordinal,
176 messageIndex,
177 messageId,
178 file,
179 anchor.MemberKey,
180 anchor.AttachmentShape,
181 anchor.Id,
182 start,
183 end,
184 matchKind));
185 }
186
187 private static string normalizeFileKey(string file, string? workspaceRoot)
188 {
189 if (string.IsNullOrWhiteSpace(file))
190 return "";
191
192 var trimmed = file.Trim().Replace('\\', '/');
193 if (Path.IsPathRooted(trimmed))
194 {
195 var rel = AttachmentAnchorPaths.ToWorkspaceRelative(trimmed, workspaceRoot);
196 return (rel ?? trimmed).Replace('\\', '/');
197 }
198
199 return trimmed;
200 }
201}
202
View only · write via MCP/CIDE