// dotnet run scripts/CheckDocs.cs (dalla root del repo; exit 1 se i doc sono fuori allineamento) // Controlla anche che ogni doc di docs/ e docs/decisioni/ sia nell'indice. // Controlla: link relativi (file e #ancora), del csproj == ultima voce del CHANGELOG, // ogni .cs di PrepStep/ e scripts/ citato in docs/architettura.md. using System.Text.RegularExpressions; var errori = new List(); var mdFiles = Directory.EnumerateFiles(".", "*.md", SearchOption.AllDirectories) .Where(f => !f.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}") && !f.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}") && !f.Contains($"{Path.DirectorySeparatorChar}raw{Path.DirectorySeparatorChar}") && !f.Contains($"{Path.DirectorySeparatorChar}step{Path.DirectorySeparatorChar}")).ToList(); // slug delle intestazioni come lo calcola GitHub/Gitea static HashSet Ancore(string path) => File.ReadLines(path).Where(l => l.StartsWith('#')) .Select(l => Regex.Replace(Regex.Replace(l.TrimStart('#').Trim().ToLowerInvariant(), @"[`*]", ""), @"[^\p{L}\p{N}\- _]", "").Replace(' ', '-')) .ToHashSet(); // 1. link relativi foreach (var f in mdFiles) { var testo = Regex.Replace(File.ReadAllText(f), @"```.*?```", "", RegexOptions.Singleline); foreach (Match m in Regex.Matches(testo, @"\]\(([^)\s]+)\)")) { var url = m.Groups[1].Value; if (Regex.IsMatch(url, @"^[a-z]+:")) continue; // http:, mailto:, ... var parti = url.Split('#', 2); var target = parti[0] == "" ? f : Path.GetFullPath(Path.Combine(Path.GetDirectoryName(f)!, parti[0])); if (!File.Exists(target) && !Directory.Exists(target)) { errori.Add($"{f}: link rotto -> {url}"); continue; } if (parti.Length == 2 && target.EndsWith(".md") && !Ancore(target).Contains(parti[1].ToLowerInvariant())) errori.Add($"{f}: ancora inesistente -> {url}"); } } // 2. versione var version = Regex.Match(File.ReadAllText("PrepStep/PrepStep.csproj"), @"(.+?)").Groups[1].Value; var ultima = Regex.Match(File.ReadAllText("CHANGELOG.md"), @"^## \[(\d[^\]]*)\]", RegexOptions.Multiline).Groups[1].Value; if (version != ultima) errori.Add($"CHANGELOG.md: ultima voce {ultima} != {version} del csproj"); // 3. ogni sorgente citato nella mappa file-per-file var arch = File.ReadAllText("docs/architettura.md"); foreach (var cs in Directory.EnumerateFiles("PrepStep", "*.cs").Concat(Directory.EnumerateFiles("scripts", "*.cs"))) { var nome = Path.GetFileName(cs); if (!arch.Contains(nome) && !nome.EndsWith(".Designer.cs")) errori.Add($"docs/architettura.md: {cs} non è descritto"); } // 4. ogni doc in docs/ è raggiungibile dall'indice della sua cartella foreach (var dir in new[] { "docs", Path.Combine("docs", "decisioni") }) { var indice = File.ReadAllText(Path.Combine(dir, "README.md")) + (dir == "docs" ? File.ReadAllText(Path.Combine(dir, "decisioni", "README.md")) : ""); foreach (var md in Directory.EnumerateFiles(dir, "*.md").Where(f => Path.GetFileName(f) != "README.md")) if (!indice.Contains(Path.GetFileName(md))) errori.Add($"{dir}/README.md: {Path.GetFileName(md)} non è nell'indice"); } errori.ForEach(Console.Error.WriteLine); Console.WriteLine(errori.Count == 0 ? $"Doc OK ({mdFiles.Count} file md)" : $"{errori.Count} problemi"); return errori.Count == 0 ? 0 : 1;