Forge
csharpdeeb25a2
1#nullable enable
2using CascadeIDE.Models.AgentChat;
3
4namespace CascadeIDE.Features.Chat;
5
6/// <summary>SEDM scope strip projection (T2 + T1 + decision one-liner) для UI и harness.</summary>
7public sealed record ChatSedmScopeStrip(
8 string? ContextOneLiner,
9 string? IntentOneLiner,
10 string? DecisionOneLiner,
11 string? DecisionStatus,
12 int OpenWorklineCount,
13 bool IntentIncomplete)
14{
15 public static ChatSedmScopeStrip Empty { get; } = new(null, null, null, null, 1, false);
16
17 public bool HasContent =>
18 !string.IsNullOrWhiteSpace(ContextOneLiner)
19 || !string.IsNullOrWhiteSpace(IntentOneLiner)
20 || !string.IsNullOrWhiteSpace(DecisionOneLiner);
21
22 public static ChatSedmScopeStrip FromProjection(
23 SedmEventProjector.WorklineProjection workline,
24 int openWorklineCount)
25 {
26 var context = FormatContextOneLiner(workline.ContextCard);
27 var intent = FormatIntentOneLiner(workline.IntentCard, out var incomplete);
28 var (decision, status) = FormatDecisionOneLiner(workline.ActiveDecision);
29 return new ChatSedmScopeStrip(context, intent, decision, status, Math.Max(1, openWorklineCount), incomplete);
30 }
31
32 public string FormatStripText()
33 {
34 if (!HasContent && OpenWorklineCount <= 1)
35 return "";
36
37 var parts = new List<string>(4);
38 if (OpenWorklineCount > 1)
39 parts.Add($"open: {OpenWorklineCount}");
40
41 if (!string.IsNullOrWhiteSpace(ContextOneLiner))
42 parts.Add(ContextOneLiner);
43
44 if (!string.IsNullOrWhiteSpace(IntentOneLiner))
45 {
46 var intent = IntentOneLiner;
47 if (IntentIncomplete)
48 intent += " (incomplete)";
49 parts.Add("intent: " + intent);
50 }
51
52 if (!string.IsNullOrWhiteSpace(DecisionOneLiner))
53 {
54 var decision = DecisionOneLiner;
55 if (!string.IsNullOrWhiteSpace(DecisionStatus) && !string.Equals(DecisionStatus, "active", StringComparison.OrdinalIgnoreCase))
56 decision += $" [{DecisionStatus}]";
57 parts.Add("decision: " + decision);
58 }
59
60 return parts.Count == 0 ? "" : string.Join(" · ", parts);
61 }
62
63 public string? BuildAgentContextPrefix()
64 {
65 if (!HasContent)
66 return null;
67
68 var lines = new List<string> { "[SEDM — сжатый срез активной workline]" };
69
70 if (!string.IsNullOrWhiteSpace(ContextOneLiner))
71 lines.Add("Here: " + ContextOneLiner);
72
73 if (!string.IsNullOrWhiteSpace(IntentOneLiner))
74 lines.Add("Intent: " + IntentOneLiner);
75
76 if (!string.IsNullOrWhiteSpace(DecisionOneLiner))
77 {
78 var line = "Decision: " + DecisionOneLiner;
79 if (!string.IsNullOrWhiteSpace(DecisionStatus) && !string.Equals(DecisionStatus, "active", StringComparison.OrdinalIgnoreCase))
80 line += $" ({DecisionStatus} — re-verify before trust)";
81 lines.Add(line);
82 }
83
84 if (OpenWorklineCount > 1)
85 lines.Add($"Other open worklines: {OpenWorklineCount - 1} (not expanded)");
86
87 lines.Add("---");
88 return string.Join(Environment.NewLine, lines);
89 }
90
91 private static string? FormatContextOneLiner(SedmContextCardMaterializedPayload? card)
92 {
93 if (card is null)
94 return null;
95
96 var path = card.Anchor.Path.Trim();
97 var symbol = string.IsNullOrWhiteSpace(card.Anchor.Symbol) ? null : card.Anchor.Symbol.Trim();
98 var here = symbol is null ? path : $"{path}::{symbol}";
99
100 var applies = card.Applies?.FirstOrDefault();
101 if (applies is null)
102 return here;
103
104 return $"{here} · {applies.Ref} — {Truncate(applies.OneLiner, 48)}";
105 }
106
107 private static string? FormatIntentOneLiner(SedmIntentCardRecordedPayload? card, out bool incomplete)
108 {
109 incomplete = false;
110 if (card is null)
111 return null;
112
113 incomplete = SedmCardCompleteness.IsIntentIncomplete(card);
114 var outcome = card.Card.Outcome.Trim();
115 var chosen = string.IsNullOrWhiteSpace(card.Card.ChosenApproach) ? null : card.Card.ChosenApproach.Trim();
116 return chosen is null ? Truncate(outcome, 72) : $"{Truncate(outcome, 40)} → {Truncate(chosen, 32)}";
117 }
118
119 private static (string? OneLiner, string? Status) FormatDecisionOneLiner(SedmEventProjector.DecisionState? decision)
120 {
121 if (decision is null)
122 return (null, null);
123
124 var outcome = decision.Payload.Card.Outcome.Trim();
125 var chosen = string.IsNullOrWhiteSpace(decision.Payload.Card.ChosenApproach)
126 ? null
127 : decision.Payload.Card.ChosenApproach.Trim();
128 var oneLiner = chosen is null ? Truncate(outcome, 72) : $"{Truncate(outcome, 40)} → {Truncate(chosen, 32)}";
129 return (oneLiner, decision.Status);
130 }
131
132 private static string Truncate(string text, int max) =>
133 text.Length <= max ? text : text[..max] + "…";
134}
135
136/// <summary>Валидация полноты intent/decision card (ADR 0173 §3).</summary>
137internal static class SedmCardCompleteness
138{
139 public static bool IsIntentIncomplete(SedmIntentCardRecordedPayload card)
140 {
141 if (string.IsNullOrWhiteSpace(card.Card.Outcome) || string.IsNullOrWhiteSpace(card.Card.Trigger))
142 return true;
143
144 if (string.IsNullOrWhiteSpace(card.Card.ChosenApproach))
145 return false;
146
147 if (string.IsNullOrWhiteSpace(card.Card.SelectionRationale))
148 return true;
149
150 return card.Considered is null || card.Considered.Count == 0;
151 }
152}
153
View only · write via MCP/CIDE