diff --git a/docs/ConfigurationReference.md b/docs/ConfigurationReference.md index 88bb3b6..f429f63 100644 --- a/docs/ConfigurationReference.md +++ b/docs/ConfigurationReference.md @@ -57,7 +57,7 @@ property-only rules. | --- | --- | --- | --- | | `destination` | no | `both` | `file`, `stream`, `both`, or `none`. `stream` writes records to the PowerShell Information stream. | | `path` | no | `/logs/persona-engine-audit.ndjson` | NDJSON output file. One record per line. | -| `resultsFileName` | no | `results.csv` | Per-account results CSV, written next to `path`'s directory. Lists `AccountObjectId`, `UserPrincipalName`, `PersonaStatus` for every account processed so far. Overwritten on every summary, interim and final. | +| `resultsFileName` | no | `results.csv` | Per-account results CSV, written next to `path`'s directory. Lists `AccountObjectId`, `UserPrincipalName`, `PersonaStatus`, `CompanyName`, `Department` for every account processed so far. Overwritten on every summary, interim and final. | | `traceConditionValues` | no | `false` | Writes evaluated attribute values into audit records. | | `acknowledgeConditionTracing` | no | `false` | **Required whenever `traceConditionValues` is true** (VR-003). | diff --git a/docs/Logging.md b/docs/Logging.md index 7204e0f..986caaf 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -42,9 +42,10 @@ or the warning that matters is buried in the noise it generates. Alongside the NDJSON audit log, every summary — interim and final — (re)writes a plain CSV listing every account processed so far: `AccountObjectId`, `UserPrincipalName`, `PersonaStatus` (the assigned -persona for `Matched` accounts, otherwise `Unclassified` or `EvaluationError`). It is overwritten in -full each time, not appended, so it always reflects the whole run to that point rather than only the -accounts since the last summary. +persona for `Matched` accounts, otherwise `Unclassified` or `EvaluationError`), `CompanyName`, and +`Department` (as retrieved from the directory, blank when absent). It is overwritten in full each +time, not appended, so it always reflects the whole run to that point rather than only the accounts +since the last summary. It is written next to the audit log — same directory as `logging.path` — under `logging.resultsFileName` (default `results.csv`). Export failure never ends a run, for the same reason a sink failure doesn't: diff --git a/docs/OperationsRunbook.md b/docs/OperationsRunbook.md index 887eeea..d5ea2b6 100644 --- a/docs/OperationsRunbook.md +++ b/docs/OperationsRunbook.md @@ -47,8 +47,8 @@ configuration look identical if zero-match rules are omitted, and that distincti you are looking for. Its header shows elapsed wall-clock time since the run started. Each summary also (re)writes `logging.resultsFileName` (default `results.csv`, next to the audit log) -with one row per account processed so far — Object ID, UPN, and assigned persona/status — for an -operator who wants the current population breakdown without parsing NDJSON. +with one row per account processed so far — Object ID, UPN, assigned persona/status, company name, +and department — for an operator who wants the current population breakdown without parsing NDJSON. ## Exit codes diff --git a/specs/001-persona-engine/contracts/cli-invoke-persona-engine.md b/specs/001-persona-engine/contracts/cli-invoke-persona-engine.md index 8d48042..b074042 100644 --- a/specs/001-persona-engine/contracts/cli-invoke-persona-engine.md +++ b/specs/001-persona-engine/contracts/cli-invoke-persona-engine.md @@ -61,7 +61,7 @@ Failing any of these yields `Unchanged`, `WouldUpdate`, or `Skipped` — never a (FR-020). - **Every summary, interim and final**: `logging.resultsFileName` (default `results.csv`, written next to the audit log) is overwritten with one row per account processed so far — Account Object - ID, UPN, and the assigned persona or outcome status. + ID, UPN, the assigned persona or outcome status, company name, and department. - **Reconciliation** at every summary: `Processed = Matched + Unclassified + EvaluationError` (FR-021). A mismatch is logged as an engine defect, at `Error` severity. - **Audit records**: see [audit-record.md](audit-record.md). diff --git a/src/Engine/Invoke-PersonaEngineRun.ps1 b/src/Engine/Invoke-PersonaEngineRun.ps1 index b5bd51a..dbb3cf0 100644 --- a/src/Engine/Invoke-PersonaEngineRun.ps1 +++ b/src/Engine/Invoke-PersonaEngineRun.ps1 @@ -196,7 +196,7 @@ function Invoke-PersonaEngineRun { Write-UserPersonaResult -Result $result - Add-PersonaRunResult -Counters $counters -Result $result + Add-PersonaRunResult -Counters $counters -Result $result -UserRecord $record New-PersonaAuditRecord -Context $Context -RecordType 'UserEvent' -Result $result ` -PreviousValue $previousValue -IncludeTrace:$Tracing | diff --git a/src/Presentation/New-PersonaRunCounter.ps1 b/src/Presentation/New-PersonaRunCounter.ps1 index 229bc15..8cd1052 100644 --- a/src/Presentation/New-PersonaRunCounter.ps1 +++ b/src/Presentation/New-PersonaRunCounter.ps1 @@ -96,11 +96,18 @@ function Add-PersonaRunResult { .PARAMETER Result A PersonaDecisionResult with both Outcome and Action populated. + + .PARAMETER UserRecord + The normalized UserRecord the decision was made from. Optional, and used + only to carry CompanyName/Department onto the results CSV row - it is never + written to counters or audit records, so this does not widen what a + PersonaDecisionResult itself carries. #> [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Counters, - [Parameter(Mandatory)] [object] $Result + [Parameter(Mandatory)] [object] $Result, + [object] $UserRecord ) $Counters.Processed++ @@ -124,11 +131,24 @@ function Add-PersonaRunResult { 'Skipped' { $Counters.Skipped++ } } + # TryGetValue rather than the indexer: Properties is a case-insensitive + # Dictionary, whose indexer throws on a missing key rather than returning + # $null. CompanyName/Department are always present when UserRecord comes from + # ConvertTo-PersonaUserRecord, but this stays safe for any other caller too. + $companyName = $null + $department = $null + if ($UserRecord) { + $null = $UserRecord.Properties.TryGetValue('CompanyName', [ref]$companyName) + $null = $UserRecord.Properties.TryGetValue('Department', [ref]$department) + } + # Matched carries the assigned persona; Unclassified/EvaluationError carry # their outcome name, since CalculatedPersona is 'Unclassified' or $null there. $Counters.Results.Add([pscustomobject]@{ AccountObjectId = [string]$Result.AccountObjectId UserPrincipalName = [string]$Result.UserPrincipalName PersonaStatus = [string]$Result.Outcome -eq 'Matched' ? [string]$Result.CalculatedPersona : [string]$Result.Outcome + CompanyName = [string]$companyName + Department = [string]$department }) }