Forge
csharpdeeb25a2
1#nullable enable
2
3using CascadeIDE.Features.Workspace;
4using CascadeIDE.Features.Workspace.DataAcquisition;
5using CascadeIDE.Services;
6
7namespace CascadeIDE.Features.WorkspaceNavigation.Application;
8
9/// <summary>ADR/feature/doc correspondence для якоря карты навигации (ADR 0061).</summary>
10public static class WorkspaceCorrespondenceResolver
11{
12 public sealed record Result(
13 string FeatureLine,
14 string[] FeatureDocPaths,
15 string DocsCoverageLine,
16 string AdrLine,
17 string? AdrFirstDocPath,
18 string[] AdrDocPaths);
19
20 public static Result Resolve(string? workspaceRoot, string? navigationPath)
21 {
22 if (string.IsNullOrWhiteSpace(workspaceRoot) || string.IsNullOrWhiteSpace(navigationPath))
23 return Empty;
24
25 try
26 {
27 var root = workspaceRoot.Trim();
28 var parsed = RepositoryWorkspaceTomlLoader.TryLoad(root);
29 var feature = WorkspaceFeatureResolver.ResolveFeatureFromWorkspaceToml(parsed, root, navigationPath.Trim());
30 var featureLine = WorkspaceFeatureResolver.BuildFeatureLine(feature);
31 var featureDocs = feature?.Docs is { Count: > 0 }
32 ? feature.Docs.Select(x => (x ?? "").Trim()).Where(x => x.Length > 0).ToArray()
33 : [];
34
35 var docPaths = new List<string>();
36 if (feature?.Docs is { Count: > 0 })
37 docPaths.AddRange(feature.Docs.Select(x => (x ?? "").Trim()).Where(x => x.Length > 0));
38
39 var mapped = WorkspaceAdrMapResolver.ResolveAdrDocPathsFromWorkspaceToml(parsed, root, navigationPath.Trim());
40 foreach (var m in mapped)
41 {
42 if (docPaths.Contains(m, StringComparer.OrdinalIgnoreCase))
43 continue;
44 docPaths.Add(m);
45 }
46
47 var autoInclude = WorkspaceAdrMapResolver.NormalizeAutoInclude(parsed?.Workspace?.Adr?.AutoInclude);
48 var maxRelated = parsed?.Workspace?.Adr?.MaxRelated is int mr && mr > 0 ? mr : 8;
49 if (autoInclude == WorkspaceAdrMapResolver.AutoIncludeLinked && docPaths.Count > 0)
50 {
51 var primary = docPaths[0];
52 var absPrimary = WorkspaceAdrMapResolver.TryResolveAbsoluteDocPath(root, primary);
53 if (!string.IsNullOrWhiteSpace(absPrimary)
54 && WorkspaceTextFileReader.TryReadAllText(absPrimary, out var md))
55 {
56 var linked = WorkspaceAdrMapResolver.ExtractLinkedAdrDocPathsFromMarkdown(
57 md,
58 primary,
59 parsed?.Workspace?.Adr);
60 if (linked.Count > 0)
61 {
62 var merged = new List<string>(docPaths);
63 foreach (var l in linked)
64 {
65 if (merged.Count >= docPaths.Count + maxRelated)
66 break;
67 if (merged.Contains(l, StringComparer.OrdinalIgnoreCase))
68 continue;
69 merged.Add(l);
70 }
71
72 docPaths = merged;
73 }
74 }
75 }
76
77 var adrLine = WorkspaceAdrMapResolver.BuildAdrIndicatorLine(docPaths, parsed?.Workspace?.Adr);
78 var firstRel = docPaths.Count > 0 ? docPaths[0] : null;
79 var firstAbs = firstRel is null ? null : WorkspaceAdrMapResolver.TryResolveAbsoluteDocPath(root, firstRel);
80
81 var templates = DocsTemplatesCatalogResolver.ResolveTemplatesFromWorkspaceToml(parsed, root);
82 var featureTemplate = templates.FirstOrDefault(t =>
83 string.Equals(t.Kind, "feature_doc", StringComparison.OrdinalIgnoreCase));
84 var moduleTemplate = templates.FirstOrDefault(t =>
85 string.Equals(t.Kind, "module_doc", StringComparison.OrdinalIgnoreCase));
86
87 var featureTemplateHint = featureTemplate?.RepoPath ?? "docs/templates/feature.md";
88 var moduleTemplateHint = moduleTemplate?.RepoPath ?? "docs/templates/module.md";
89
90 var docsCoverageLine =
91 docPaths.Count > 0
92 ? ""
93 : feature is not null
94 ? $"Docs: missing (feature has no ADRs) · template: {featureTemplateHint}"
95 : $"Docs: missing (no correspondence) · template: {moduleTemplateHint}";
96
97 return new(featureLine, featureDocs, docsCoverageLine, adrLine, firstAbs, docPaths.ToArray());
98 }
99 catch
100 {
101 return Empty;
102 }
103 }
104
105 private static Result Empty => new("", [], "", "", null, []);
106}
107
View only · write via MCP/CIDE