feat(auditlog): GetExecutionTreeAsync recursive execution-chain query

This commit is contained in:
Joseph Doherty
2026-05-21 18:21:49 -04:00
parent d35551efc2
commit 255dd95cd9
9 changed files with 462 additions and 0 deletions
@@ -134,4 +134,45 @@ public interface IAuditLogRepository
TimeSpan window,
DateTime? nowUtc = null,
CancellationToken ct = default);
/// <summary>
/// Audit Log ParentExecutionId feature (Task 8) — given any
/// <paramref name="executionId"/> in an execution chain, returns the whole
/// chain rooted at the topmost ancestor: one <see cref="ExecutionTreeNode"/>
/// per distinct execution, summarising its <c>AuditLog</c> rows. The Central
/// UI renders the result as a tree.
/// </summary>
/// <remarks>
/// <para>
/// The input id may be any node in the chain — a leaf, the root, or a middle
/// node. The implementation first walks <em>up</em> via
/// <c>ParentExecutionId</c> to find the root, then walks <em>down</em> from
/// the root via a recursive CTE, so the full chain is returned regardless of
/// entry point.
/// </para>
/// <para>
/// The <c>ParentExecutionId</c> graph is a tree (acyclic by construction —
/// each execution is minted fresh and its parent always pre-exists). Both
/// the upward walk and the downward CTE are nonetheless bounded at 32 levels
/// as a guard against corrupt/pathological data: a depth that exceeds the
/// guard raises an error rather than hanging the server. Chains are shallow
/// (1-2 levels typical) so the guard is never reached in practice.
/// </para>
/// <para>
/// A "stub" node — an execution that emitted no rows of its own yet is
/// referenced by a child via <c>ParentExecutionId</c>, or whose rows have
/// been purged — still appears, with <see cref="ExecutionTreeNode.RowCount"/>
/// = 0. A purged/missing parent simply ends the upward walk.
/// </para>
/// <para>
/// When no <c>AuditLog</c> row carries <paramref name="executionId"/> in
/// either <c>ExecutionId</c> or <c>ParentExecutionId</c>, the result is a
/// single stub node for <paramref name="executionId"/> itself
/// (<see cref="ExecutionTreeNode.RowCount"/> = 0) — consistent with the
/// stub-node treatment of any other row-less execution.
/// </para>
/// </remarks>
Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync(
Guid executionId,
CancellationToken ct = default);
}