Forge
csharpdeeb25a2
1using System.Diagnostics.CodeAnalysis;
2using System.Text.RegularExpressions;
3using CascadeIDE.Models.Editor;
4
5namespace CascadeIDE.Services;
6
7/// <summary>Разбор минимальных полей метанотации ADR в <see cref="MelodyRootEntry.TailSignature"/> и согласованность с <see cref="TailWireKind"/>.</summary>
8internal static class IntentMelodyTailSemantics
9{
10 /// <summary>Номера строк в параметрике и в args команд — 1-based (как в UI редактора). См. <see cref="LineNumber.MinimumOneBasedInclusive"/>.</summary>
11 internal const int MinEditorLineNumber = LineNumber.MinimumOneBasedInclusive;
12
13 private static readonly Regex IntSlots = new(
14 @"<[^>]+?:int>",
15 RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
16
17 /// <summary>Номер строки (1-based в пользовательском вводе); в TOML — <c>:ln</c> или <c>:linenumber</c>.</summary>
18 private static readonly Regex LineNumberSlots = new(
19 @"<[^>]+?:(?:ln|linenumber)>",
20 RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
21
22 internal static int CountIntSlots(string? tailSignature) =>
23 string.IsNullOrEmpty(tailSignature) ? 0 : IntSlots.Count(tailSignature);
24
25 internal static int CountLineNumberSlots(string? tailSignature) =>
26 string.IsNullOrEmpty(tailSignature) ? 0 : LineNumberSlots.Count(tailSignature);
27
28 /// <summary>Слоты цепочки «два числа в хвосте» (разделители из wire): <c>:int</c> и/или <c>:ln</c>.</summary>
29 internal static int CountDelimitedNumericSlots(string? tailSignature) =>
30 CountIntSlots(tailSignature) + CountLineNumberSlots(tailSignature);
31
32 internal static bool HasUrlSlot([NotNullWhen(true)] string? tailSignature)
33 {
34 if (string.IsNullOrEmpty(tailSignature))
35 return false;
36 return tailSignature.Contains("<url:url>", StringComparison.OrdinalIgnoreCase)
37 || tailSignature.Contains(":url>", StringComparison.OrdinalIgnoreCase);
38 }
39
40 internal static bool HasBracketCodeRefSlot([NotNullWhen(true)] string? tailSignature)
41 {
42 if (string.IsNullOrEmpty(tailSignature))
43 return false;
44 return tailSignature.Contains("<code_ref:bracket>", StringComparison.OrdinalIgnoreCase)
45 || tailSignature.Contains(":bracket>", StringComparison.OrdinalIgnoreCase);
46 }
47
48 internal static void ValidateMelodyAgainstWireClass(in MelodyRootEntry e, in TailWireClassEntry wire)
49 {
50 var numericSlots = CountDelimitedNumericSlots(e.TailSignature);
51 var url = HasUrlSlot(e.TailSignature);
52 var bracket = HasBracketCodeRefSlot(e.TailSignature);
53 if (numericSlots > 0 && (url || bracket))
54 {
55 throw new InvalidOperationException(
56 $"{IntentMelodyAliases.BundledRelativePath}: [[melody_root]] slug '{e.Slug}' — tail_signature смешивает url/bracket и числовые слоты (int/ln), не поддерживается.");
57 }
58
59 switch (wire.Kind)
60 {
61 case TailWireKind.SingleRemainder when (!url && !bracket) || numericSlots != 0:
62 throw new InvalidOperationException(
63 $"{IntentMelodyAliases.BundledRelativePath}: wire_class '{wire.Id}' ({nameof(TailWireKind.SingleRemainder)}) несогласован с tail_signature корня '{e.Slug}'.");
64 case TailWireKind.DelimitedSlots when url || numericSlots < 2:
65 throw new InvalidOperationException(
66 $"{IntentMelodyAliases.BundledRelativePath}: wire_class '{wire.Id}' ({nameof(TailWireKind.DelimitedSlots)}) ожидает ≥ двух слотов <:int> или <:ln>; корень '{e.Slug}'.");
67 }
68 }
69}
70
View only · write via MCP/CIDE