Files
2026-08-21 01:18:19 -04:00

120 lines
9.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-21**: 400 offline tests passing; engine purity and sanitization gates passing;
no tenant-dependent item verified. The increase from 354 is FR-027FR-030 (rule add/edit/delete in
the interactive editor), added to spec.md and implemented in the same change (T122T129).
## 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`) |
| FR-027 | Add a rule interactively | `Add-PersonaConfigRule`; `[A]` command in `Edit-PersonaEngineConfig.ps1` | `EditorAddRule.Tests.ps1`, `EditorStructuralEdits.Tests.ps1` |
| FR-028 | Edit a rule interactively, including its condition tree | `Get/Add/Remove-PersonaConditionNode`, `Set-PersonaConditionLeaf`; `[E]` command | `EditorEditRule.Tests.ps1`, `EditorStructuralEdits.Tests.ps1` |
| FR-029 | Delete a rule interactively, with confirmation | `Remove-PersonaConfigRule`; `[D]` command | `EditorDeleteRule.Tests.ps1`, `EditorStructuralEdits.Tests.ps1` |
| FR-030 | Structural edits validated like a hand-edited file | `Test-PersonaCandidateEdit` in `Edit-PersonaEngineConfig.ps1` | `EditorStructuralEdits.Tests.ps1` |
## 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 (T115T121) | 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".