| 1 | #nullable enable |
| 2 | |
| 3 | using CascadeIDE.Features.Workspace; |
| 4 | using CascadeIDE.Features.Workspace.DataAcquisition; |
| 5 | using CascadeIDE.Services; |
| 6 | |
| 7 | namespace CascadeIDE.Features.WorkspaceNavigation.Application; |
| 8 | |
| 9 | /// <summary>ADR/feature/doc correspondence для якоря карты навигации (ADR 0061).</summary> |
| 10 | public 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 | |