# Contract: Structured Audit Records Serialized form of the audit trail (FR-022, NFR-005, Principle V). Format is newline-delimited JSON, one record per line (OTD-006). All emission goes through a single `Write-PersonaAuditRecord` sink so a future transport can be added without touching call sites. ## Common envelope Every record carries: | Field | Type | Notes | | --- | --- | --- | | `timestamp` | string (ISO 8601 UTC) | | | `recordType` | string | `RunStart`, `UserEvent`, `Summary`, `RunComplete`, `EngineDefect` | | `runId` | string (GUID) | Constant for the run (NFR-005) | | `engineVersion` | string | | | `configVersion` | string | | | `configurationHash` | string | SHA-256 of the configuration file | | `mode` | string | `Preview` or `Enforce` | ## `UserEvent` Emitted once per processed user. 100% of these records carry `runId`, `userPrincipalName`, and `accountObjectId` (SC-006). ```json { "timestamp": "2026-08-20T09:14:02.187Z", "recordType": "UserEvent", "runId": "", "engineVersion": "1.0.0", "configVersion": "1.4.0", "configurationHash": "", "mode": "Preview", "accountObjectId": "", "userPrincipalName": "@", "outcome": "Matched", "matchedRuleId": "RULE-0100-TIER0", "storedPersona": "Employee", "calculatedPersona": "Tier0-Admin", "previousValue": "Employee", "action": "WouldUpdate", "rulesEvaluated": 4, "durationMs": 38, "evaluationErrorReason": null } ``` **Field requirements** | Field | Requirement | | --- | --- | | `outcome` | Exactly one of `Matched`, `Unclassified`, `EvaluationError` (SC-001) | | `matchedRuleId` | Non-null on every `Matched` record (SC-006) | | `previousValue` | **Captured at write time on every `Updated` record.** This is what makes OTD-010 rollback possible; omitting it in v1 makes rollback impossible retroactively | | `evaluationErrorReason` | Non-null exactly when `outcome` is `EvaluationError` | **Prohibited fields**: access tokens, `Authorization` headers, secrets, and full Graph responses MUST NEVER appear in any record. **Condition tracing**: a `conditionTrace` array may be added **only** under `-Debug` (`logging.traceConditionValues`). It contains diagnostic condition-level values and is therefore gated by acknowledgement (VR-003). ## `Summary` Emitted every `summaryInterval` users and once at completion. ```json { "recordType": "Summary", "runId": "", "summaryType": "Interim", "processed": 250, "matched": 231, "unclassified": 14, "evaluationError": 5, "unchanged": 220, "wouldUpdate": 11, "updated": 0, "updateFailed": 0, "reconciliationPassed": true, "ruleCounts": [ { "ruleId": "RULE-0100-TIER0", "name": "Tier 0 administrators", "enabled": true, "matches": 3 } ] } ``` `reconciliationPassed` is `processed == matched + unclassified + evaluationError` (FR-021, SC-007). `ruleCounts` lists **all** business rules, including disabled and zero-match rules — an absent rule is indistinguishable from a rule that never fired, and operators need that distinction. ## `RunComplete` ```json { "recordType": "RunComplete", "runId": "", "startedUtc": "2026-08-20T09:00:00.000Z", "completedUtc": "2026-08-20T09:12:44.913Z", "durationMs": 764913, "processed": 4820, "matched": 4611, "unclassified": 190, "evaluationError": 19, "unchanged": 4400, "wouldUpdate": 211, "updated": 0, "updateFailed": 0, "reconciliationPassed": true, "exitCode": 0 } ``` ## `EngineDefect` Emitted when reconciliation fails (FR-021) or an internal invariant is violated. Severity is always `Error`. A failed reconciliation is a defect in the engine, not a property of the data, and is reported as such rather than being folded into ordinary counters. ## Sanitization (SC-013) Committed artifacts — this contract, examples, fixtures, tests, and documentation — use placeholders only: ``, ``, ``, ``, ``, ``, ``, ``, ``. Runtime records naturally contain real UPNs and Object IDs — which are approved for logs — but no such value may ever be committed to this repository.