Doctrine: die Leitung steuert ihre Einheit, ohne jeden Auftragstext anzufassen (#698)

- SqliteDoctrineStore: je Einheit versioniert (Tabelle Doctrines: Text, Autor, Lauf, Grund),
  hoechstens 2000 Zeichen, leer entfernt; im Speicher gehalten fuer jeden Lauf
- OrgBriefing: die Doctrine der eigenen Einheit, nur eine Ebene tief, mit dem Satz,
  dass ein Widerspruch zur Aufgabe eskaliert wird; Leitungen erfahren doctrine_set
- Escalation: doctrine_read, doctrine_set (nur die Leitung, Grund Pflicht)
- IAgentTool.Vorgabe(operation, context): Einstufung mit dem Aufrufkontext; eine Leitung
  der Fremdinhalt-Stufe legt doctrine_set zur Freigabe vor
- API GET/PUT /organisation/units/{id}/doctrine; WebUI-Karte mit Verlauf und Markierung
  haeufiger Aenderungen; i18n en/de
- Tests: DoctrineTests (5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-01 09:38:44 +02:00
co-authored by Claude Opus 5.5
parent c608af1abb
commit 679333b382
15 changed files with 395 additions and 6 deletions
@@ -81,6 +81,28 @@ internal static class OrganisationEndpoints
? Results.Ok(ToDto(host.Organisation.PreviewMoveAgent(agentId, input.Target)))
: host.Organisation.MoveAgent(agentId, input.Target, input.Reason, input.DropExcessTools).ToResponse());
// #698: A unit's doctrine — every version, and the human setting it.
org.MapGet("/units/{unitId}/doctrine", async (AppHost host, string unitId, CancellationToken ct) =>
{
if (host.Doctrines is not { } store)
return Results.Ok(Array.Empty<DoctrineDto>());
return Results.Ok((await store.HistoryAsync(unitId, ct))
.Select(d => new DoctrineDto(d.Version, d.Text, d.SetBy, d.SetAt, d.RunId, d.Reason)).ToList());
});
org.MapPut("/units/{unitId}/doctrine", async (AppHost host, string unitId, DoctrineInputDto input, CancellationToken ct) =>
{
if (host.Doctrines is not { } store)
return Results.BadRequest(new OutcomeDto(false, "Doctrines are not available in this instance."));
if (host.Organisation.Current().Unit(unitId) is not { } unit)
return Results.BadRequest(new OutcomeDto(false, $"There is no unit '{unitId}'."));
var text = (input.Text ?? "").Trim();
if (text.Length > Core.Organisation.SqliteDoctrineStore.MaxLength)
return Results.BadRequest(new OutcomeDto(false, $"A doctrine has at most {Core.Organisation.SqliteDoctrineStore.MaxLength} characters."));
var set = await store.SetAsync(unit.Id, text, "human", null, input.Reason ?? "", ct);
return Results.Ok(new OutcomeDto(true, set.IsEmpty ? $"Doctrine of '{unit.Name}' removed." : $"Doctrine of '{unit.Name}' set (version {set.Version})."));
});
// #687: What the lead may approve instead of the human.
org.MapPut("/units/{unitId}/approvals", (AppHost host, string unitId, ApprovalsInputDto input) =>
host.Organisation.SetApprovals(unitId, input.Operations, input.PerDay, input.Hours, input.Reason).ToResponse());