- Cases/Decisions in der Instanz-DB; CaseDesk fuehrt die Regeln, der Brett-Task traegt den Vorgang zum jeweiligen Halter - Eskalation nur nach oben, eine Ebene je Schritt; die Leitung entscheidet (mit Begruendung, bindet ihren Bereich), erledigt selbst oder reicht mit Einschaetzung weiter - Erst nachsehen: bindende Beschluesse werden vor dem Eskalieren gezeigt; dieselbe Frage aus derselben Einheit schliesst sich dem offenen Vorgang an - Tagesdeckel je Einheit; nach der Frist wird ein Mensch gerufen (Anschluss fuer #644) - Konflikt: zuerst die Gegenseite, dann Zustellung an die niedrigste gemeinsame Leitung; Ebenen dazwischen vermerkt, nicht befragt - Fremdinhalt-Stufe (#715): Eskalation ohne eigenen Text, kein Konflikt mit Position - Jede Ebene haengt an (#697), beim Halter gerahmt als Daten - Escalation ist ein implizites Werkzeug jedes Agenten; Briefing nennt den Weg - Scanner behaelt einen im Lauf gesetzten Status blockiert/abgebrochen Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
5.8 KiB
C#
119 lines
5.8 KiB
C#
using System.Globalization;
|
|
using System.Text.Json;
|
|
using ClawdDotNet.Core.Storage;
|
|
using Microsoft.Data.Sqlite;
|
|
|
|
namespace ClawdDotNet.Core.Cases;
|
|
|
|
public interface ICaseRepository
|
|
{
|
|
Task SaveAsync(CaseRecord record, CancellationToken ct);
|
|
Task<CaseRecord?> GetAsync(string id, CancellationToken ct);
|
|
Task<IReadOnlyList<CaseRecord>> ListOpenAsync(CancellationToken ct);
|
|
|
|
/// <summary>The newest cases first, open or closed.</summary>
|
|
Task<IReadOnlyList<CaseRecord>> ListRecentAsync(int limit, CancellationToken ct);
|
|
|
|
Task SaveDecisionAsync(Decision decision, CancellationToken ct);
|
|
Task<Decision?> GetDecisionAsync(string id, CancellationToken ct);
|
|
Task<IReadOnlyList<Decision>> ListDecisionsAsync(CancellationToken ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// #689: Cases and decisions in the instance's database. The record is stored whole as JSON —
|
|
/// its shape follows the rules and will grow — with the columns the queries need beside it.
|
|
/// </summary>
|
|
public sealed class SqliteCaseRepository(SqliteStorage storage) : ICaseRepository
|
|
{
|
|
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web)
|
|
{
|
|
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }
|
|
};
|
|
|
|
public Task SaveAsync(CaseRecord record, CancellationToken ct)
|
|
=> storage.WriteAsync(async conn =>
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = """
|
|
INSERT INTO Cases (Id, Kind, Status, Holder, OpenedUnitId, OpenedAt, DeadlineUtc, Json)
|
|
VALUES (@id, @kind, @status, @holder, @unit, @openedAt, @deadline, @json)
|
|
ON CONFLICT(Id) DO UPDATE SET
|
|
Kind = @kind, Status = @status, Holder = @holder, OpenedUnitId = @unit,
|
|
OpenedAt = @openedAt, DeadlineUtc = @deadline, Json = @json
|
|
""";
|
|
cmd.Parameters.AddWithValue("@id", record.Id);
|
|
cmd.Parameters.AddWithValue("@kind", record.Kind.ToString());
|
|
cmd.Parameters.AddWithValue("@status", record.Status.ToString());
|
|
cmd.Parameters.AddWithValue("@holder", record.Holder);
|
|
cmd.Parameters.AddWithValue("@unit", (object?)record.OpenedUnitId ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@openedAt", Format(record.OpenedAt));
|
|
cmd.Parameters.AddWithValue("@deadline", Format(record.DeadlineUtc));
|
|
cmd.Parameters.AddWithValue("@json", JsonSerializer.Serialize(record, Json));
|
|
await cmd.ExecuteNonQueryAsync(ct);
|
|
}, ct);
|
|
|
|
public async Task<CaseRecord?> GetAsync(string id, CancellationToken ct)
|
|
=> (await QueryAsync("SELECT Json FROM Cases WHERE Id = @id", ct, ("@id", id))).FirstOrDefault();
|
|
|
|
public Task<IReadOnlyList<CaseRecord>> ListOpenAsync(CancellationToken ct)
|
|
=> QueryAsync("SELECT Json FROM Cases WHERE Status = 'Open' ORDER BY OpenedAt", ct);
|
|
|
|
public Task<IReadOnlyList<CaseRecord>> ListRecentAsync(int limit, CancellationToken ct)
|
|
=> QueryAsync("SELECT Json FROM Cases ORDER BY OpenedAt DESC LIMIT @limit", ct, ("@limit", limit));
|
|
|
|
public Task SaveDecisionAsync(Decision decision, CancellationToken ct)
|
|
=> storage.WriteAsync(async conn =>
|
|
{
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = """
|
|
INSERT INTO Decisions (Id, ScopeUnitId, DecidedAt, Json)
|
|
VALUES (@id, @scope, @at, @json)
|
|
ON CONFLICT(Id) DO UPDATE SET ScopeUnitId = @scope, DecidedAt = @at, Json = @json
|
|
""";
|
|
cmd.Parameters.AddWithValue("@id", decision.Id);
|
|
cmd.Parameters.AddWithValue("@scope", (object?)decision.ScopeUnitId ?? DBNull.Value);
|
|
cmd.Parameters.AddWithValue("@at", Format(decision.DecidedAt));
|
|
cmd.Parameters.AddWithValue("@json", JsonSerializer.Serialize(decision, Json));
|
|
await cmd.ExecuteNonQueryAsync(ct);
|
|
}, ct);
|
|
|
|
public async Task<Decision?> GetDecisionAsync(string id, CancellationToken ct)
|
|
=> (await QueryDecisionsAsync("SELECT Json FROM Decisions WHERE Id = @id", ct, ("@id", id))).FirstOrDefault();
|
|
|
|
public Task<IReadOnlyList<Decision>> ListDecisionsAsync(CancellationToken ct)
|
|
=> QueryDecisionsAsync("SELECT Json FROM Decisions ORDER BY DecidedAt DESC", ct);
|
|
|
|
private async Task<IReadOnlyList<CaseRecord>> QueryAsync(string sql, CancellationToken ct, params (string Name, object Value)[] args)
|
|
{
|
|
var result = new List<CaseRecord>();
|
|
await foreach (var json in ReadJsonAsync(sql, ct, args))
|
|
if (JsonSerializer.Deserialize<CaseRecord>(json, Json) is { } record)
|
|
result.Add(record);
|
|
return result;
|
|
}
|
|
|
|
private async Task<IReadOnlyList<Decision>> QueryDecisionsAsync(string sql, CancellationToken ct, params (string Name, object Value)[] args)
|
|
{
|
|
var result = new List<Decision>();
|
|
await foreach (var json in ReadJsonAsync(sql, ct, args))
|
|
if (JsonSerializer.Deserialize<Decision>(json, Json) is { } decision)
|
|
result.Add(decision);
|
|
return result;
|
|
}
|
|
|
|
private async IAsyncEnumerable<string> ReadJsonAsync(string sql, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct,
|
|
params (string Name, object Value)[] args)
|
|
{
|
|
await using var conn = await storage.OpenConnectionAsync(ct);
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = sql;
|
|
foreach (var (name, value) in args)
|
|
cmd.Parameters.AddWithValue(name, value);
|
|
await using SqliteDataReader reader = await cmd.ExecuteReaderAsync(ct);
|
|
while (await reader.ReadAsync(ct))
|
|
yield return reader.GetString(0);
|
|
}
|
|
|
|
private static string Format(DateTime value) => value.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture);
|
|
}
|