ALL-1873 PR-2: Data audit profile + 3 initial scanner checks
queuedAgent: will-engineer
Priority: 3
Branch:
wilbo/all-1873-audit-profile-checksPR: #12486
Linear: ALL-1873
## ALL-1873 PR-2: Audit profile + initial checks
**Linear issue:** ALL-1873 — Feature: Data audit entrypoint for Data Review Center
**Branch:** wilbo/all-1873-audit-profile-checks
**Depends on:** PR-1 (DATA_AUDIT source type must be merged first)
### What to do
Create a new profile under domains/identity/src/services/staging/profiles/dataAudit.ts and register it. The audit process differs from bulk-upload: instead of validating new rows, it scans existing workspace production data and emits issues. Each check receives a workspace-scoped DB query and returns ProfileIssue[].
#### 1. Define the AuditRecord schema and profile
Audit records are lightweight references — the rawData for each staged record is just { entityType, entityId, workspaceId }. Profile schema:
AuditRecordSchema = z.object({
entityType: z.enum(["contact", "site", "device"]),
entityId: z.string().min(1),
workspaceId: z.string().min(1),
});
Register the profile with key "dataAudit".
#### 2. Implement 3 checks (Phase 1 from Linear issue)
a. duplicateContactCheck — entityType === "contact": query ThirdParty (canonical contact entity) for records in the same workspace with matching email or name+address. Use prisma from @/common/db. Emit type: "DUPLICATE_CONTACT", severity: "warning".
b. orphanDeviceCheck — entityType === "device": check no associated site link (siteId IS NULL or equivalent join). Emit type: "ORPHAN_DEVICE", severity: "warning".
c. missingGeocodeCheck — entityType === "site": latitude IS NULL OR longitude IS NULL. Emit type: "MISSING_GEOCODE", severity: "warning".
IMPORTANT: Before writing checks, verify exact Prisma table/field names by reading domains/identity/prisma/schema.prisma. Confirm ThirdParty, CustomerSite, CustomerDevice (or whatever the real model names are).
#### 3. Register in profiles/index.ts
Append: import "./dataAudit";
#### 4. Unit tests
Add profiles/dataAudit.test.ts covering each check:
- returns [] when no anomaly present
- returns correct ProfileIssue when anomaly present
Mock prisma using existing test patterns from profileRegistry.test.ts / commonChecks.test.ts.
### Acceptance test
- getProfile("dataAudit") returns the registered profile
- Three checks exist and are individually unit-tested (pass + fail cases)
- TypeScript compiles in domains/identity
- All existing tests still pass
### Gotchas
- Each check must return [] early when entityType does not match its target domain.
- Use __unregisterProfileForTesting in test teardown to avoid duplicate-key throws across test files.
- prisma calls in checks are async — mark checks async.
- Do NOT hard-code table names without verifying schema.prisma first.
### Out of scope
- Audit runner service / GraphQL mutation (PR-3)
- DRC UI button (PR-4)
- Phase 2/3 checks (orphan sites, stale devices, enrollment mismatches, etc.)
- Cross-domain queries (requires cross-database joins — Phase 2)
Event Timeline
created
status_change
queued → in_progress
failed
lease expired — re-queued for retry
in_progress → queued
progress
PR-2 opened: https://github.com/TextureHQ/mono/pull/12486. CI pending. 22 tests green locally.
status_change
queued → in_progress
failed
lease expired — re-queued for retry
in_progress → queued