# Requirements traceability Every functional requirement, non-functional requirement, and success criterion in [spec.md](spec.md), mapped to the code that implements it and the test that holds it there. A row with no test is a requirement nobody is checking. Those are listed explicitly at the bottom rather than left out, because an incomplete matrix that looks complete is worse than no matrix. **Status as at 2026-08-20**: 354 offline tests passing; engine purity and sanitization gates passing; no tenant-dependent item verified. ## Functional requirements | ID | Requirement | Implementation | Test | | --- | --- | --- | --- | | FR-001 | Load JSON configuration | `Import-PersonaConfiguration` | `LayerOrdering.Tests.ps1` | | FR-002 | Validate before connecting | `Test-PersonaConfiguration`; entry script exits 1 before `Connect-` | `LayerOrdering.Tests.ps1`, `ExitCodes.Tests.ps1` | | FR-003 | Adapter-isolated authentication | `Connect-PersonaGraphInteractive` | Purity gate; `ShouldProcessGate.Tests.ps1` | | FR-004 | Enumerate all users with pagination | `Get-PersonaUsers` | `Pagination.Tests.ps1` | | FR-005 | Select only required properties | `Get-PersonaRequiredProperties` | `Pagination.Tests.ps1` | | FR-006 | Retrieve and cache related data | `Get-PersonaGroupMembership`, `Get-PersonaCachedMembership` | `Pagination.Tests.ps1`, `OutcomeExclusivity.Tests.ps1` | | FR-007 | Normalize before evaluation | `ConvertTo-PersonaUserRecord`, `ConvertTo-PersonaMembershipRecord` | `RecordContracts.Tests.ps1` | | FR-008 | Evaluate rules in priority order | `Resolve-UserPersona` | `Ordering.Tests.ps1` | | FR-009 | Stop at first match | `Resolve-UserPersona` | `Ordering.Tests.ps1` | | FR-010 | `Unclassified` when nothing matches | `Resolve-UserPersona` | `Unclassified.Tests.ps1` | | FR-011 | Disabled accounts stay in scope | `New-PersonaUserRecord` exposes `AccountEnabled` | `Operators.Tests.ps1` | | FR-012 | Null treated as empty | `Test-PersonaCondition` | `Operators.Tests.ps1` | | FR-013 | Unretrievable group data yields `EvaluationError` | Tri-state evaluation; facet retrieval flags | `UnknownNotFalse.Tests.ps1`, `UnknownPropagation.Tests.ps1` | | FR-014 | Preserve stored value on failure | `Compare-PersonaValue` sets `Skipped` first | `EvaluationError.Tests.ps1`, `WriteGate.Tests.ps1` | | FR-015 | Compare stored and calculated | `Compare-PersonaValue`, ordinal | `WriteGate.Tests.ps1` | | FR-016 | Write only changed values, four conditions | `Compare-PersonaValue` + run-loop gate | `WriteGate.Tests.ps1` | | FR-017 | Preview issues no write request | `Invoke-PersonaEngineRun` gate | `WhatIfZeroWrites.Tests.ps1` | | FR-018 | Immediate per-user output | `Write-UserPersonaResult` | Exercised by every run-loop suite | | FR-019 | Periodic summary | `Write-PersonaSummary`, interval check | `SummaryInterval.Tests.ps1` | | FR-020 | Interval semantics, final always shown | `Invoke-PersonaEngineRun` | `SummaryInterval.Tests.ps1` | | FR-021 | Reconciliation at every summary | `Test-PersonaReconciliation` | `Reconciliation.Tests.ps1` | | FR-022 | Structured audit records | `New-PersonaAuditRecord`, `Write-PersonaAuditRecord` | `AuditRecordShape.Tests.ps1` | | FR-023 | Configuration editor | `Edit-PersonaEngineConfig.ps1` | `ExitCodes.Tests.ps1` (editor) | | FR-024 | Non-interactive validation with exit codes | `-NonInteractive` short-circuit | `NonInteractive.Tests.ps1` | | FR-025 | Synthetic rule testing, no tenant | `Invoke-SyntheticRuleTest` | `NonInteractive.Tests.ps1` | | FR-026 | Validate and back up before save | `Save-PersonaConfiguration` | `Safety.Tests.ps1` (`PE-SAF-007`) | ## Rule engine requirements | ID | Requirement | Implementation | Test | | --- | --- | --- | --- | | RE-001 | Required rule fields | Schema `definitions/rule` | `LayerOrdering.Tests.ps1` | | RE-002 | Unique priorities, lower first | `Resolve-UserPersona`; `PE-SEM-002` | `Ordering.Tests.ps1`, `Semantic.Tests.ps1` | | RE-003 | `all` / `any` with nesting | `Test-PersonaConditionGroup` | `Composition.Tests.ps1` | | RE-004 | Depth limit and hard ceiling | `Test-PersonaConditionGroup`; `PE-SEM-012`, `PE-SEM-013` | `Composition.Tests.ps1`, `Semantic.Tests.ps1` | | RE-005 | Thirteen operators | `Test-PersonaCondition` | `Operators.Tests.ps1` | | RE-006 | Case-insensitive; regex validated first | `Test-PersonaCondition`; `PE-SEM-016` | `Operators.Tests.ps1`, `Semantic.Tests.ps1` | | RE-007 | Per-condition membership mode | Three-facet `MembershipRecord` | `RecordContracts.Tests.ps1`, `UnknownPropagation.Tests.ps1` | | RE-008 | Combined identity sources | `Get-PersonaRequiredFacets` | `OutcomeExclusivity.Tests.ps1` | | RE-009 | Special accounts by Object ID | Example configuration; no hard-coded path | Purity gate | ## Validation requirements | ID | Requirement | Implementation | Test | | --- | --- | --- | --- | | VR-001 | Four ordered layers, fail-fast | `Test-PersonaConfiguration` | `LayerOrdering.Tests.ps1` | | VR-002 | Sixteen semantic conditions | `Test-PersonaConfigurationSemantic` | `Semantic.Tests.ps1` — one test per code | | VR-003 | Seven safety conditions | `Test-PersonaConfigurationSafety` | `Safety.Tests.ps1`, `TraceAcknowledgement.Tests.ps1` | | VR-004 | Finding shape | `New-PersonaValidationFinding` | `Semantic.Tests.ps1`, `Safety.Tests.ps1`, `RecordContracts.Tests.ps1` | | VR-005 | Warnings block only on request | Editor exit-code mapping | `ExitCodes.Tests.ps1` (editor) | ## Non-functional requirements | ID | Requirement | Implementation | Test | | --- | --- | --- | --- | | NFR-001 | PowerShell 7 | `#Requires -Version 7.2`; manifest floor | Runs on 7.6.5 | | NFR-002 | Caching, per-user and total duration | `New-PersonaDataCache`; stopwatch in `Resolve-UserPersona`; `RunComplete.durationMs` | `AuditCompleteness.Tests.ps1` — **no target set** | | NFR-003 | Pagination, bounded retry, backoff | `Invoke-PersonaGraphRequest` | `RetryPolicy.Tests.ps1`, `Pagination.Tests.ps1` | | NFR-004 | Comment-based help on public functions | Every function in `src/` | Manual review | | NFR-005 | Run ID, UPN, Object ID, config hash | `New-PersonaAuditContext` | `AuditCompleteness.Tests.ps1` | | NFR-006 | Least privilege, single attribute, no secrets | Six OTD-003 controls | `WriteBody.Tests.ps1`, `WriteBodyRejection.Tests.ps1`, sanitization gate | | NFR-007 | Engine and validation run without the platform | Purity; layer dot-sourcing | Purity gate; whole offline suite | | NFR-008 | No Windows PowerShell-only dependencies | `Microsoft.Graph.Authentication` only | **Unverified in Automation (V-5b)** | ## Success criteria | ID | Criterion | Test | Status | | --- | --- | --- | --- | | SC-001 | Exactly one outcome per user | `OutcomeExclusivity.Tests.ps1` | Passing | | SC-002 | Second run proposes zero changes | `Idempotence.Tests.ps1` | Passing | | SC-003 | Determinism across shuffled input | `Determinism.Tests.ps1` | Passing | | SC-004 | Zero writes under `-WhatIf` | `WhatIfZeroWrites.Tests.ps1` | Passing | | SC-005 | Single-attribute request body | `WriteBody.Tests.ps1` | Passing | | SC-006 | Audit completeness | `AuditCompleteness.Tests.ps1` | Passing | | SC-007 | Reconciliation, and its failure path | `Reconciliation.Tests.ps1` | Passing | | SC-008 | Everything runs offline | Whole offline suite; `validate.yml` gate 7 | Passing | | SC-009 | Every VR-002 and VR-003 condition detected | `Semantic.Tests.ps1`, `Safety.Tests.ps1` | Passing | | SC-010 | Non-interactive never prompts or hangs | `NonInteractive.Tests.ps1` | Passing | | SC-011 | Every exit code reachable | `ExitCodes.Tests.ps1` (both) | Passing | | SC-012 | Per-user output is immediate | `Write-UserPersonaResult` emits in `process` | Structural, not timed | | SC-013 | No tenant data committed | `Test-Sanitization.ps1` | Passing | ## Gaps, stated plainly | Item | Why it is not covered | What would close it | | --- | --- | --- | | NFR-002 performance | No target exists until representative tenant testing | A timed run against a real population | | NFR-004 help coverage | Reviewed by eye, not asserted | A test parsing every exported function for a help block | | NFR-008 Automation compatibility | No Automation account available | V-5b (T116) | | SC-012 timing | Asserted structurally, not measured | A timed harness — low value against the cost | | V-1, V-2, V-3 | Require a tenant | Stage A2 and A3 runs | | V-4 | Requires a person | Written security sign-off | | Phase 13 (T115–T121) | Requires an Automation account | Stage B | ## How to keep this honest When a requirement's implementation moves, this table moves with it. When a test is deleted, the row it backed becomes a gap and belongs in the gaps table, not silently in the main one. A matrix that is allowed to drift is worse than none, because it converts "we do not know" into "we checked".