| 1 | #nullable enable |
| 2 | |
| 3 | using System.Text.Json; |
| 4 | using System.Text.Json.Serialization; |
| 5 | using CascadeIDE.Features.Workspace; |
| 6 | using CascadeIDE.Features.Workspace.DataAcquisition; |
| 7 | using CascadeIDE.Services; |
| 8 | |
| 9 | namespace CascadeIDE.Features.WorkspaceNavigation.Application; |
| 10 | |
| 11 | /// <summary>JSON для MCP <c>get_correspondence_context</c> (ADR 0156 §3).</summary> |
| 12 | public static class WorkspaceCorrespondenceContextBuilder |
| 13 | { |
| 14 | private static readonly JsonSerializerOptions JsonOptions = new() |
| 15 | { |
| 16 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, |
| 17 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, |
| 18 | WriteIndented = false, |
| 19 | }; |
| 20 | |
| 21 | public static string BuildJson(string? workspaceRoot, string? absoluteFilePath, bool hasCodeGraph = false) |
| 22 | { |
| 23 | if (string.IsNullOrWhiteSpace(workspaceRoot) || string.IsNullOrWhiteSpace(absoluteFilePath)) |
| 24 | return JsonSerializer.Serialize( |
| 25 | new CorrespondenceContextPayload(null, [], "", "", null, [], []), |
| 26 | JsonOptions); |
| 27 | |
| 28 | var root = workspaceRoot.Trim(); |
| 29 | var file = absoluteFilePath.Trim(); |
| 30 | var workspaceToml = RepositoryWorkspaceTomlLoader.TryLoad(root); |
| 31 | var correspondence = WorkspaceCorrespondenceResolver.Resolve(root, file); |
| 32 | var explicitAnchors = WorkspaceCorrespondenceCodeAnchorsLoader.LoadFromWorkspaceToml(workspaceToml, root); |
| 33 | var reverse = DocReverseAnchorResolver.Resolve( |
| 34 | root, |
| 35 | file, |
| 36 | correspondence.AdrDocPaths, |
| 37 | explicitAnchors); |
| 38 | |
| 39 | var rel = WorkspaceAdrMapResolver.TryComputeRepoRelativePath(root, file); |
| 40 | var signals = CorrespondenceLayersProjection.FromCorrespondence( |
| 41 | hasHciOrientation: false, |
| 42 | hasFeature: !string.IsNullOrWhiteSpace(correspondence.FeatureLine), |
| 43 | hasAdrDocs: correspondence.AdrDocPaths.Length > 0, |
| 44 | hasCodeGraph: hasCodeGraph); |
| 45 | |
| 46 | var payload = new CorrespondenceContextPayload( |
| 47 | File: rel, |
| 48 | ActiveLayers: ToLayerIds(signals), |
| 49 | LayersBadge: CorrespondenceLayersProjection.BuildLayersBadge(signals), |
| 50 | LayersTooltip: CorrespondenceLayersProjection.BuildLayersTooltip(signals), |
| 51 | Feature: string.IsNullOrWhiteSpace(correspondence.FeatureLine) |
| 52 | ? null |
| 53 | : new FeaturePayload(correspondence.FeatureLine, correspondence.FeatureDocPaths), |
| 54 | ForwardDocs: correspondence.AdrDocPaths |
| 55 | .Select(p => new ForwardDocPayload(p, WorkspaceAdrMapResolver.GuessAdrPreviewTitle(p))) |
| 56 | .ToArray(), |
| 57 | ReverseAnchors: reverse |
| 58 | .Select(m => new ReverseAnchorPayload( |
| 59 | m.DocPath, |
| 60 | m.DocTitle, |
| 61 | m.Provenance, |
| 62 | m.Kind, |
| 63 | new CodeAnchorPayload( |
| 64 | m.CodeAnchor.File, |
| 65 | m.CodeAnchor.LineStart, |
| 66 | m.CodeAnchor.LineEnd, |
| 67 | m.CodeAnchor.MemberKey, |
| 68 | m.CodeAnchor.SyntaxScope), |
| 69 | m.Excerpt, |
| 70 | m.DocLineHint)) |
| 71 | .ToArray()); |
| 72 | |
| 73 | return JsonSerializer.Serialize(payload, JsonOptions); |
| 74 | } |
| 75 | |
| 76 | private static string[] ToLayerIds(CorrespondenceLayerSignals signals) |
| 77 | { |
| 78 | var ids = new List<string>(6); |
| 79 | if (signals.SemanticCanonL0) ids.Add("L0"); |
| 80 | if (signals.FeatureRegistryL1p) ids.Add("L1p"); |
| 81 | if (signals.AdrPathMapL1) ids.Add("L1"); |
| 82 | if (signals.CodeIntentGraphL2) ids.Add("L2"); |
| 83 | if (signals.ChangeIntentL3) ids.Add("L3"); |
| 84 | if (signals.DiscourseCodeL4) ids.Add("L4"); |
| 85 | return ids.ToArray(); |
| 86 | } |
| 87 | |
| 88 | private sealed record CorrespondenceContextPayload( |
| 89 | string? File, |
| 90 | string[] ActiveLayers, |
| 91 | string LayersBadge, |
| 92 | string LayersTooltip, |
| 93 | FeaturePayload? Feature, |
| 94 | ForwardDocPayload[] ForwardDocs, |
| 95 | ReverseAnchorPayload[] ReverseAnchors); |
| 96 | |
| 97 | private sealed record FeaturePayload(string Line, string[] Docs); |
| 98 | |
| 99 | private sealed record ForwardDocPayload(string Path, string Title); |
| 100 | |
| 101 | private sealed record ReverseAnchorPayload( |
| 102 | string DocPath, |
| 103 | string DocTitle, |
| 104 | string Provenance, |
| 105 | string Kind, |
| 106 | CodeAnchorPayload CodeAnchor, |
| 107 | string Excerpt, |
| 108 | int? DocLineHint); |
| 109 | |
| 110 | private sealed record CodeAnchorPayload( |
| 111 | string File, |
| 112 | int? LineStart, |
| 113 | int? LineEnd, |
| 114 | string? MemberKey, |
| 115 | string? SyntaxScope); |
| 116 | } |
| 117 | |