V17 / d6d373561692
ReleaseRuntime.Approvals.cs
Interfaces/Gui/LilithGui.Worker/ReleaseRuntime.Approvals.cs
Unchanged from the preceding versionCS
using System.Text.Json;
using System.Security.Cryptography;
using System.Text;
using static Lilith.Gui.Worker.ReflectionApi;
namespace Lilith.Gui.Worker;
internal sealed partial class ReleaseRuntime
{
private sealed record Approval(string Id,string Service,string Title,JsonElement Review,string Fingerprint,string Approve,string? Reject);
private readonly HashSet<string> _declinedExecution=new();
private static JsonElement Field(JsonElement value,string name)=>value.EnumerateObject().FirstOrDefault(p=>p.Name.Equals(name,StringComparison.OrdinalIgnoreCase)).Value;
private static string Fingerprint(JsonElement review)=>Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(review.GetRawText())));
private async Task<List<Approval>> Approvals(CancellationToken ct)
{
var list=new List<Approval>();
var boardItems=Get(Get(_provider,"Kanban"),"Store") is {} boardStore&&await Call(boardStore,"List") is System.Collections.IEnumerable savedBoards?savedBoards.Cast<object>().ToArray():[];
var boardDrafts=boardItems.Select(b=>Get(b,"DraftId")?.ToString()).Where(id=>id is not null).ToHashSet();
async Task Add(string service,string pending,string review,string approve,string reject)
{
var commands=Get(_agent,"ChatCommands");if(commands is null)return;
var result=await Call(commands,"ExecuteAsync",new Dictionary<string,object?>{{"input",pending},{"cancellationToken",ct}});
if(Get(result,"Handled") is not true||Get(result,"Output") is not string output)return;
JsonElement data;try{data=JsonDocument.Parse(output).RootElement.Clone();}catch(JsonException){return;}
if(data.ValueKind==JsonValueKind.Object&&data.TryGetProperty("Data",out var items))data=items;
if(data.ValueKind!=JsonValueKind.Array)return;
foreach(var item in data.EnumerateArray())
{
if(!item.TryGetProperty("Id",out var idValue))continue;string id=idValue.GetString()!;
var exact=review.Length==0?item.Clone():JsonDocument.Parse((string)Get((await Call(commands,"ExecuteAsync",new Dictionary<string,object?>{{"input",review+id},{"cancellationToken",ct}}))!,"Output")!).RootElement.Clone();
string fingerprint=Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(exact.GetRawText())));
list.Add(new(id,service,"Review "+service+" change",exact,fingerprint,approve+id,reject+id));
}
}
if(Get(_provider,"Workspace") is not null)await Add("workspace","/workspace","/workspace review ","/approve ","/reject ");
if(Get(_provider,"Schedules") is not null)await Add("schedules","/schedules pending","/schedules review ","/schedule-approve ","/schedule-reject ");
if(Get(_provider,"Calendar") is not null)await Add("calendar","/calendar pending","/calendar review ","/calendar-approve ","/calendar-reject ");
if(Get(_provider,"Home") is not null)await Add("home","/home-pending","","/home-approve ","/home-reject ");
if(core.GetType("Lilith.Core.Projects.ProjectDraftStore") is {} draftType&&Get(_provider,"Workspace") is {} workspace)
{
object store=Activator.CreateInstance(draftType,workspace)!;
if(await Call(store,"List") is System.Collections.IEnumerable drafts)foreach(var draft in drafts.Cast<object>())
{
if(Get(draft,"State")?.ToString()!="awaiting_test_approval")continue;
string id=Get(draft,"Id")!.ToString()!;if(boardDrafts.Contains(id))continue;var review=JsonSerializer.SerializeToElement(await Call(store,"Load",new Dictionary<string,object?>{{"id",id}}));
string fingerprint=Fingerprint(review);if(_declinedExecution.Contains(id+fingerprint))continue;
list.Add(new(id,"project execution","Execute generated project and bounded test/repair rounds",review,fingerprint,"/project-test "+id,null));
}
}
foreach(var board in boardItems)
{
string state=Get(board,"State")!.ToString()!,id=Get(board,"Id")!.ToString()!;
if(state is not("waiting_plan" or "waiting_test"))continue;
var review=JsonSerializer.SerializeToElement(board);string fingerprint=Fingerprint(review);
if(_declinedExecution.Contains(id+fingerprint))continue;
list.Add(new(id,"kanban",state=="waiting_plan"?"Approve the exact Kanban plan":"Execute generated Kanban project and bounded tests",review,fingerprint,state=="waiting_plan"?"/kanban-approve "+id+" "+Get(board,"Revision"):"/kanban-test "+id,state=="waiting_plan"?"/kanban-cancel "+id:null));
}
var bridge=core.GetType("Lilith.Core.Messaging.MessageBridgeClient");
bool pendingMessages=(bool?)(_provider?.GetType().GetField("PendingMessageApproval",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic)?.GetValue(_provider))==true;
if(bridge?.GetProperty("Configured")?.GetValue(null) is true&&pendingMessages)
{
async Task<JsonElement> Request(string action,object payload){var task=(Task<string>)bridge.GetMethod("RequestAsync")!.Invoke(null,[action,payload,ct])!;return JsonDocument.Parse(await task).RootElement.Clone();}
var channels=await Request("channels",new{key=""});
foreach(var channel in Field(channels,"channels").EnumerateArray())
{
string nickname=Field(channel,"nickname").GetString()!;var status=await Request("status",new{nickname});
foreach(var proposal in Field(status,"proposals").EnumerateArray())
{string id=Field(proposal,"id").GetString()!;var review=JsonSerializer.SerializeToElement(new{nickname,proposal});list.Add(new(id,"messaging","Review exact destination and outgoing message",review,Fingerprint(review),"/message-approve "+nickname+" "+id,"/message-reject "+nickname+" "+id));}
}
}
return list;
}
internal async Task PublishApprovals(CancellationToken ct)=>emit("approvals",await Approvals(ct));
private async Task<object?> DecideApproval(JsonElement payload,CancellationToken ct)
{
var proposal=(await Approvals(ct)).SingleOrDefault(p=>p.Id==Text(payload,"id")&&p.Service==Text(payload,"service"))??throw new InvalidOperationException("This proposal is no longer pending. Refresh and review again.");
if(proposal.Fingerprint!=Text(payload,"fingerprint"))throw new InvalidOperationException("The proposal changed. Review it again before deciding.");
bool approve=payload.GetProperty("approve").GetBoolean();
if(!approve&&proposal.Reject is null){_declinedExecution.Add(proposal.Id+proposal.Fingerprint);return new{message="Execution was not approved. The private draft remains untested and unpublished."};}
return await Command(approve?proposal.Approve:proposal.Reject!,ct);
}
}