Repository navigation
Description
Problem
Seeded records carry no usable actor on the audit trail. Every audit event written during seed --profile demo|simulation is stamped ActorEmail = "(unresolved)".
Measured against the demo seeder (probe run, 2026-08-10):
DailyEntry.Create x248 actor='(unresolved)'
Flock.Create x3 actor='(unresolved)'
Product.Create x3 actor='(unresolved)'
SalesOrder.Create x2 actor='(unresolved)'
Why it happens
AuditWriter (src/Cluckwork.Infrastructure/Repositories/AuditWriter.cs) resolves its actor from ICurrentUser:
var actorId = user.IsResolved ? user.UserId : Guid.Empty;
var actorEmail = user.IsResolved ? user.Email : "(unresolved)";Both seeders (DemoDataSeeder, SimulationDataSeeder) call tenant.Resolve(accountId) before invoking handlers — which is why the tenant guard doesn't throw — but neither ever calls currentUser.Resolve(...). CurrentUserContext.IsResolved is only ever set by TenantResolutionMiddleware on an HTTP request, so the CLI seed path always takes the fallback.
Why it matters now
This predates #494 — Product.Create and User.Create were already writing (unresolved) — but it was only ever visible on the admin-only /audit page.
#494 surfaces it on five main screens. The new "History" column on Flocks, Egg grades, Daily entry history, Sales and Expenses renders it verbatim, so demo data now reads "Created by (unresolved) on 2026-…" on ~256 rows. Anyone demoing the product sees it immediately.
Scope
- Resolve
ICurrentUserin both seeders before invoking any handler.- Demo: attribute to the default account's Owner.
- Simulation: attribute to the persona that would realistically have done it — the seeder already creates
sim-manager-N@,sim-sales-N@,sim-worker-N@and an Owner, so daily entries can be attributed to workers, sales orders to sales personas, etc. Higher fidelity is the whole point of that fixture.
- Add a test pinning that seeded rows carry a real actor — i.e. that no seeded audit event has
ActorEmail = "(unresolved)". This is the guard that stops the fallback silently returning. - Consider whether
"(unresolved)"should remain reachable at all for a tenant-resolved caller, or become a loud failure — a real request can never hit it (auth precedes tenant resolution), so today it is exclusively a seeder artifact.
Out of scope
- Backfilling provenance for records created before Show "created by / when" and "last changed by / when" inline on record pages #494 (explicitly declined there).
- Changing
AuditWriter's fallback for genuinely unauthenticated non-HTTP callers (e.g. future background jobs), which may legitimately have no user.
Notes
Not a regression and nothing is broken: seeders don't throw, the full backend suite passes, and the k6 / Playwright harnesses are unaffected (k6 checks are presence-based; the one index-based Playwright selector is on the Audit table, untouched by #494).
Found while implementing #494; deliberately split out to keep that PR scoped.
Activity
feat(api,web): show who created and last changed a record, inline on …
fix(api): seeded demo and simulation records name a real person
fix(infra): hold the simulation cast to the Owner's standard (#500)
fix(infra): tell a dropped-roles schema to restore, not to bootstrap (#…
fix(api): seeded demo and simulation records name a real person
fix(infra): hold the simulation cast to the Owner's standard (#500)
fix(infra): tell a dropped-roles schema to restore, not to bootstrap (#…
docs: compress AGENTS.md to one paragraph per rule, and draw the two …
docs: fix k6 prep ordering and rate-limit cleanup in the fixture runbook
docs: fix k6 prep ordering and rate-limit cleanup in the fixture runbook
docs: add k6 preparation steps to the dev-database fixture runbook (#643
feat(cli): rename-account verb to change a farm code (#732) (#733)
docs(mcp): record the MCP server design and what the spike got wrong (#…