M1: audit log (Governance) + edit-distance metric
SharedKernel: - IAuditLog/AuditEvent — append-only audit contract any module writes through. - EditDistance (Levenshtein + normalized) — the north-star metric, available from day one; consumed at edit-and-approve in M5. Governance module (references SharedKernel only): - AuditEntry entity; internal GovernanceDbContext (schema "governance") + InitialGovernance migration; AuditLog implements IAuditLog. - GET /api/governance/audit — owner-only (ViewAuditLog), returns recent entries. Wiring (via the SharedKernel IAuditLog interface — no module references Governance): - OrgBoard records team.created, task.created, task.moved, task.assigned. - Identity records invitation.created, member.joined. Verified: build green; ArchitectureTests 8/8 (Governance references only SharedKernel; audit flows through the shared interface); IntegrationTests 20/20 — board flow now asserts task.created/task.moved appear in the audit log, plus EditDistance unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using TeamUp.Modules.OrgBoard.Domain;
|
||||
using TeamUp.Modules.OrgBoard.Persistence;
|
||||
using TeamUp.SharedKernel.Access;
|
||||
using TeamUp.SharedKernel.Auditing;
|
||||
using TeamUp.SharedKernel.Modularity;
|
||||
|
||||
namespace TeamUp.Modules.OrgBoard.Endpoints;
|
||||
@@ -61,8 +62,8 @@ internal static class OrgBoardEndpoints
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateTeam(
|
||||
CreateTeamRequest request, IPermissionService permissions,
|
||||
OrgBoardDbContext db, TimeProvider clock, CancellationToken ct)
|
||||
CreateTeamRequest request, ICurrentUser user, IPermissionService permissions,
|
||||
IAuditLog audit, OrgBoardDbContext db, TimeProvider clock, CancellationToken ct)
|
||||
{
|
||||
if (!permissions.Has(Capability.CreateProductsAndTeams, ScopeRef.Org(request.OrganizationId)))
|
||||
{
|
||||
@@ -82,6 +83,7 @@ internal static class OrgBoardEndpoints
|
||||
var team = new Team(request.OrganizationId, request.Name.Trim(), clock.GetUtcNow());
|
||||
db.Teams.Add(team);
|
||||
await db.SaveChangesAsync(ct);
|
||||
await audit.WriteAsync(new AuditEvent("team.created", "Team", team.Id, user.MemberId, team.Name), ct);
|
||||
return Results.Ok(new TeamResponse(team.Id, team.OrganizationId, team.Name));
|
||||
}
|
||||
|
||||
@@ -104,7 +106,7 @@ internal static class OrgBoardEndpoints
|
||||
|
||||
private static async Task<IResult> CreateTask(
|
||||
CreateTaskRequest request, ICurrentUser user, IPermissionService permissions,
|
||||
OrgBoardDbContext db, TimeProvider clock, CancellationToken ct)
|
||||
IAuditLog audit, OrgBoardDbContext db, TimeProvider clock, CancellationToken ct)
|
||||
{
|
||||
var team = await db.Teams.FirstOrDefaultAsync(t => t.Id == request.TeamId, ct);
|
||||
if (team is null)
|
||||
@@ -125,6 +127,7 @@ internal static class OrgBoardEndpoints
|
||||
var item = new WorkItem(team.Id, request.Title.Trim(), request.Description, request.Type, user.MemberId, clock.GetUtcNow());
|
||||
db.WorkItems.Add(item);
|
||||
await db.SaveChangesAsync(ct);
|
||||
await audit.WriteAsync(new AuditEvent("task.created", "WorkItem", item.Id, user.MemberId, item.Title), ct);
|
||||
return Results.Ok(ToResponse(item));
|
||||
}
|
||||
|
||||
@@ -153,8 +156,8 @@ internal static class OrgBoardEndpoints
|
||||
}
|
||||
|
||||
private static async Task<IResult> MoveTask(
|
||||
Guid id, MoveTaskRequest request, IPermissionService permissions,
|
||||
OrgBoardDbContext db, TimeProvider clock, CancellationToken ct)
|
||||
Guid id, MoveTaskRequest request, ICurrentUser user, IPermissionService permissions,
|
||||
IAuditLog audit, OrgBoardDbContext db, TimeProvider clock, CancellationToken ct)
|
||||
{
|
||||
var (item, team, error) = await LoadItemWithTeam(db, id, ct);
|
||||
if (error is not null)
|
||||
@@ -169,12 +172,13 @@ internal static class OrgBoardEndpoints
|
||||
|
||||
item!.MoveTo(request.Status, clock.GetUtcNow());
|
||||
await db.SaveChangesAsync(ct);
|
||||
await audit.WriteAsync(new AuditEvent("task.moved", "WorkItem", item.Id, user.MemberId, request.Status.ToString()), ct);
|
||||
return Results.Ok(ToResponse(item));
|
||||
}
|
||||
|
||||
private static async Task<IResult> AssignTask(
|
||||
Guid id, AssignTaskRequest request, IPermissionService permissions,
|
||||
OrgBoardDbContext db, TimeProvider clock, CancellationToken ct)
|
||||
Guid id, AssignTaskRequest request, ICurrentUser user, IPermissionService permissions,
|
||||
IAuditLog audit, OrgBoardDbContext db, TimeProvider clock, CancellationToken ct)
|
||||
{
|
||||
var (item, team, error) = await LoadItemWithTeam(db, id, ct);
|
||||
if (error is not null)
|
||||
@@ -189,6 +193,7 @@ internal static class OrgBoardEndpoints
|
||||
|
||||
item!.AssignToMember(request.MemberId, clock.GetUtcNow());
|
||||
await db.SaveChangesAsync(ct);
|
||||
await audit.WriteAsync(new AuditEvent("task.assigned", "WorkItem", item.Id, user.MemberId, request.MemberId.ToString()), ct);
|
||||
return Results.Ok(ToResponse(item));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user