**Purpose**: Repository scaffolding and tooling. No behaviour.
- [X] T001 Create the directory tree (`src/{Configuration,Authentication,DataProviders,Normalization,RuleEngine,Persistence,Presentation,Audit}`, `tests/{Unit,RuleEngine,Configuration,Integration,Safety,TestData}`, `config/`, `docs/`, `pipelines/`) per the Project Structure section of specs/001-persona-engine/plan.md
- [X] T002 Create the module manifest PersonaEngine.psd1 with `PowerShellVersion = '7.2'` and `RequiredModules = @('Microsoft.Graph.Authentication')`
- [X] T003 Create the module loader PersonaEngine.psm1 that dot-sources every `src/**/*.ps1` and exports only public functions
- [X] T004 [P] Add PSScriptAnalyzer settings in PSScriptAnalyzerSettings.psd1 enabling PSUseShouldProcessForStateChangingFunctions and PSAvoidUsingPlainTextForPassword
- [X] T005 [P] Add Pester configuration in tests/PesterConfiguration.ps1 with separate tags for Offline, Integration, and Safety suites
- [X] T006 [P] Copy specs/001-persona-engine/contracts/persona-engine.schema.json to config/persona-engine.schema.json as the shipped schema
- [X] T007 [P] Create the placeholder-only sample configuration config/persona-engine.example.json using `<APPROVED-PERSONA-ATTRIBUTE-NAME>` and `<GROUP-OBJECT-ID>` tokens only
- [X] T008 [P] Write the sanitization scan script tests/Test-Sanitization.ps1 that fails on real-looking GUIDs, domains, UPNs, or secrets in any tracked file (SC-013)
**Purpose**: The normalized contracts and guardrails every story depends on.
**⚠️ CRITICAL**: No user story work begins until this phase is complete.
- [X] T009 Implement `New-PersonaUserRecord` in src/Normalization/New-PersonaUserRecord.ps1 producing the UserRecord shape in data-model.md, with case-insensitive `Properties` lookup and mandatory `AccountObjectId`/`UserPrincipalName`
- [X] T010 Implement `New-PersonaMembershipRecord` in src/Normalization/New-PersonaMembershipRecord.ps1 with three independently-retrieved facets (direct, transitive, roles), every `*Retrieved` flag defaulting to `$false` so an unset record is never mistaken for "not a member" (revised during implementation — see data-model.md)
- [X] T012 [P] Create synthetic fixtures in tests/TestData/Users/ covering enabled, disabled, guest, null-property, and missing-property accounts — obviously fictional, placeholders only
- [X] T013 [P] Create synthetic membership fixtures in tests/TestData/Memberships/ including a partial-failure record (one facet failed) and a total-failure record
- [X] T014 [P] Write Pester contract tests for both record types in tests/Unit/RecordContracts.Tests.ps1
- [X] T015 Write the engine-purity CI check tests/Test-EnginePurity.ps1 asserting no file under src/RuleEngine/ references Graph, `Connect-`, `Invoke-MgGraphRequest`, `Write-Host`, or any function from src/Authentication|DataProviders|Persistence|Presentation (Principle IV)
**Checkpoint**: normalized contracts exist and purity is enforced by CI. Story work can begin.
---
## Phase 3: User Story 2 — Define classification rules without changing code (Priority: P1) 🎯 Engine core
**Goal**: Ordered, JSON-defined rules evaluate against normalized records with no code changes and no
tenant.
**Independent Test**: Author a multi-rule configuration, evaluate it against synthetic fixtures
offline with the network disconnected, and confirm the expected persona for each fixture.
### Tests for User Story 2
> Write these first and confirm they fail before implementing.
- [X] T016 [P] [US2] Operator matrix tests for all thirteen RE-005 operators in tests/RuleEngine/Operators.Tests.ps1, including case-insensitivity (RE-006) and null-as-empty (FR-012)
- [X] T017 [P] [US2] Ordering tests in tests/RuleEngine/Ordering.Tests.ps1 asserting ascending priority evaluation, first-match stop, and that a priority-10 match beats a priority-20 match
- [X] T018 [P] [US2] Composition tests in tests/RuleEngine/Composition.Tests.ps1 for nested `all`/`any` within the depth limit, and rejection beyond `maxConditionDepth` and the hard ceiling of 10 (RE-004)
- [X] T019 [P] [US2] Determinism tests in tests/RuleEngine/Determinism.Tests.ps1 asserting identical results across shuffled fixture order and repeated runs (SC-003)
- [X] T020 [P] [US2] Unclassified test in tests/RuleEngine/Unclassified.Tests.ps1 asserting that zero matches with all rules evaluating successfully yields `Unclassified` (FR-010)
### Implementation for User Story 2
- [X] T021 [US2] Implement `Import-PersonaConfiguration` in src/Configuration/Import-PersonaConfiguration.ps1 converting JSON to BusinessRule/ConditionGroup/Condition objects and computing the SHA-256 `ConfigurationHash`
- [X] T022 [US2] Implement `Test-PersonaCondition` in src/RuleEngine/Test-PersonaCondition.ps1 covering all RE-005 operators, with regex patterns validated before execution (RE-006)
- [X] T023 [US2] Implement `Test-PersonaConditionGroup` in src/RuleEngine/Test-PersonaConditionGroup.ps1 with `all`/`any` composition and depth enforcement
- [X] T024 [US2] Implement `Test-PersonaRule` in src/RuleEngine/Test-PersonaRule.ps1 evaluating a rule's root condition group
- [X] T025 [US2] Implement `Resolve-UserPersona` in src/RuleEngine/Resolve-UserPersona.ps1 with priority sort, first-match stop, `Unclassified` fallback, `RulesEvaluated`, and `DurationMs`
- [X] T026 [US2] Implement membership and role condition types in src/RuleEngine/Test-PersonaCondition.ps1 reading only from the MembershipRecord, honouring per-condition `direct`/`transitive` mode (RE-007)
**Checkpoint**: the rule engine classifies synthetic users offline. Disconnect the network and re-run
tests/RuleEngine — SC-008 holds or the phase is not done.
---
## Phase 4: User Story 3 — Preserve existing values when evaluation cannot be trusted (Priority: P1)
**Goal**: Unretrievable required data yields `EvaluationError`, preserves the stored persona, and
never becomes a silent non-match.
**Independent Test**: Inject a group-lookup failure for one synthetic user; that user receives
`EvaluationError`, no write is attempted, and the run continues.
### Tests for User Story 3
- [X] T027 [P] [US3] Tri-state propagation tests in tests/RuleEngine/UnknownPropagation.Tests.ps1 covering all four rows of the propagation table in data-model.md
- [X] T028 [P] [US3] Preservation tests in tests/RuleEngine/EvaluationError.Tests.ps1 asserting stored persona is retained, `Action` is `Skipped`, and processing continues to the next user (FR-014)
- [X] T029 [P] [US3] Regression test in tests/RuleEngine/UnknownNotFalse.Tests.ps1 asserting a failed membership lookup never satisfies `notMemberOf` — the specific misclassification hazard FR-013 exists to prevent
### Implementation for User Story 3
- [X] T030 [US3] Extend condition evaluation in src/RuleEngine/Test-PersonaCondition.ps1 to return `True`/`False`/`Unknown` instead of a boolean
- [X] T031 [US3] Implement the propagation rules in src/RuleEngine/Test-PersonaConditionGroup.ps1: `all` with any `False` is `False`; `all` with only `True` plus `Unknown` is `Unknown`; `any` with any `True` is `True`; `any` with only `False` plus `Unknown` is `Unknown`
- [X] T032 [US3] Map a root-level `Unknown` to the `EvaluationError` outcome with `EvaluationErrorReason` in src/RuleEngine/Resolve-UserPersona.ps1
- [X] T033 [US3] Add the `evaluationErrorThreshold` counter and final-status effect in src/RuleEngine/Resolve-UserPersona.ps1 and the run summary path
**Checkpoint**: unknown data degrades to preserve-and-report. Principle III is satisfied in the pure
engine, before any tenant exists.
---
## Phase 5: User Story 1 — Preview classification without changing the directory (Priority: P1) 🎯 MVP
**Goal**: Every in-scope user is retrieved, evaluated, and reported with stored value, calculated
value, and matched rule — with zero directory writes.
**Independent Test**: Run with `-WhatIf` against the tenant using a delegated read-only connection;
verify a result appears for every account and the write adapter receives zero calls.
**Stage**: A2. Requires an app registration with admin consent for the three delegated scopes.
### Tests for User Story 1
- [X] T034 [P] [US1] Zero-write safety test in tests/Safety/WhatIfZeroWrites.Tests.ps1 mocking the write adapter and asserting call count is exactly 0 across the full synthetic population (SC-004)
- [X] T035 [P] [US1] Mode-derivation test in tests/Safety/ShouldProcessGate.Tests.ps1 asserting `-Debug` without `-WhatIf` still reaches the write path — `-Debug` is not a safety control
- [X] T036 [P] [US1] Pagination test in tests/Unit/Pagination.Tests.ps1 asserting `@odata.nextLink` is followed to exhaustion and a truncated enumeration raises rather than returning a partial population
- [X] T037 [P] [US1] Idempotence test in tests/Safety/Idempotence.Tests.ps1 asserting a second consecutive run over unchanged input proposes zero changes (SC-002)
- [X] T038 [P] [US1] Exactly-one-outcome test in tests/Unit/OutcomeExclusivity.Tests.ps1 asserting every processed user lands in exactly one bucket (SC-001)
### Implementation for User Story 1
- [X] T039 [P] [US1] Implement `Connect-PersonaGraphInteractive` in src/Authentication/Connect-PersonaGraphInteractive.ps1 requesting only `User.Read.All`, `GroupMember.Read.All`, `RoleManagement.Read.Directory`, returning an opaque handle that never exposes a token
- [X] T040 [P] [US1] Implement the retry helper `Invoke-PersonaGraphRequest` in src/DataProviders/Invoke-PersonaGraphRequest.ps1 per the OTD-007 table: retry 429/500/502/503/504/timeout, never 400/401/403/404/409, honour `Retry-After`, max 5 attempts, exponential backoff with full jitter capped at 60s
- [X] T041 [US1] Implement `Get-PersonaUsers` in src/DataProviders/Get-PersonaUsers.ps1 with `$select` built from FR-005 plus rule-referenced properties plus the target attribute, `$top=999`, and full `@odata.nextLink` pagination
- [X] T042 [US1] Add single-user retrieval to src/DataProviders/Get-PersonaUsers.ps1 for the `-UserObjectId` path
- [X] T043 [US1] Implement `Get-PersonaGroupMembership` in src/DataProviders/Get-PersonaGroupMembership.ps1 selecting `memberOf` or `transitiveMemberOf` by mode, returning a MembershipRecord with `RetrievalSucceeded = $false` on failure rather than throwing or returning an empty list
- [X] T044 [US1] Implement `Get-PersonaDirectoryRoles` in src/DataProviders/Get-PersonaDirectoryRoles.ps1 via `/roleManagement/directory/roleAssignments`
- [X] T045 [US1] Implement the run-scoped group/role cache in src/DataProviders/PersonaDataCache.ps1, never persisted between runs
- [X] T046 [US1] Implement `ConvertTo-PersonaUserRecord` in src/Normalization/ConvertTo-PersonaUserRecord.ps1 mapping raw Graph responses to UserRecord, including the persona extension property
- [X] T047 [US1] Implement `ConvertTo-PersonaMembershipRecord` in src/Normalization/ConvertTo-PersonaMembershipRecord.ps1
- [X] T048 [US1] Implement `Compare-PersonaValue` in src/Persistence/Compare-PersonaValue.ps1 performing ordinal comparison of stored versus calculated (FR-015)
- [X] T049 [US1] Implement `Write-UserPersonaResult` in src/Presentation/Write-UserPersonaResult.ps1 emitting one line per user immediately with UPN, Account Object ID, outcome, matched rule ID, stored value, calculated value, and action (FR-018, SC-012)
- [X] T050 [US1] Create Invoke-PersonaEngine.ps1 with `CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')` and the parameter set from contracts/cli-invoke-persona-engine.md
- [X] T051 [US1] Derive execution mode solely from `$PSCmdlet.ShouldProcess()` in Invoke-PersonaEngine.ps1 — no separate preview boolean, per the contract's prohibition on two sources of truth for the write gate
- [X] T052 [US1] Implement the `WouldUpdate` reporting path in Invoke-PersonaEngine.ps1 so preview reports intended changes without constructing a request (FR-017)
- [X] T053 [US1] Implement exit codes 0–6 in Invoke-PersonaEngine.ps1 per the contract table
- [X] T054 [US1] Record per-user and total duration in Invoke-PersonaEngine.ps1 to establish the NFR-002 baseline no target yet exists for
- [ ] T055 [US1] Run quickstart Scenario 4 as a **non-privileged** account and record results in specs/001-persona-engine/verification/V-3.md — a Global Administrator run invalidates this item
- [ ] T056 [US1] Close the V-1 read half against a cloud-only, a currently-synced, and a formerly-synced account; record in specs/001-persona-engine/verification/V-1.md
**Checkpoint**: 🎯 **MVP complete.** Full classification visibility against the live tenant with zero
write risk. Everything after this point adds observability, safety tooling, or enforcement.
---
## Phase 6: User Story 5 — Validate and edit configuration safely (Priority: P2)
**Goal**: Four-layer validation with structured findings, plus an interactive editor that cannot save
an invalid configuration.
**Independent Test**: Run the invalid-configuration corpus and confirm each file produces its
documented finding code, severity, and location.
### Tests for User Story 5
- [X] T057 [P] [US5] Build the invalid-configuration corpus in tests/TestData/InvalidConfigs/ — one file per VR-002 and VR-003 condition
- [X] T058 [P] [US5] Semantic validation tests in tests/Configuration/Semantic.Tests.ps1 asserting code, severity, and location for every VR-002 condition (SC-009)
- [X] T059 [P] [US5] Safety validation tests in tests/Configuration/Safety.Tests.ps1 asserting every VR-003 condition
- [X] T060 [P] [US5] Layer-ordering test in tests/Configuration/LayerOrdering.Tests.ps1 asserting a structurally invalid document stops before semantic checks run
### Implementation for User Story 5
- [X] T061 [US5] Close V-5a: observe and record whether `Test-Json -SchemaFile` returns `$false`, writes a non-terminating error, or throws on this PowerShell build; record in specs/001-persona-engine/verification/V-5a.md and pin the observation in tests/Configuration/TestJsonBehaviour.Tests.ps1
- [X] T063 [US5] Implement validation layer 2 (schema) in src/Configuration/Test-PersonaConfiguration.ps1 wrapping `Test-Json -SchemaFile` with `-ErrorAction SilentlyContinue -ErrorVariable` and converting collected errors into ValidationFinding objects per the V-5a observation
- [X] T064 [US5] Implement validation layer 3 (semantic) in src/Configuration/Test-PersonaConfigurationSemantic.ps1 covering every VR-002 condition with stable `PE-SEM-nnn` codes
- [X] T065 [US5] Implement validation layer 4 (safety) in src/Configuration/Test-PersonaConfigurationSafety.ps1 covering every VR-003 condition with stable `PE-SAF-nnn` codes
- [X] T066 [US5] Implement `Resolve-TargetAttribute` in src/Configuration/Resolve-TargetAttribute.ps1 rejecting any attribute absent from `approvedWritableAttributes`
- [X] T067 [US5] Create Edit-PersonaEngineConfig.ps1 with the parameter set from contracts/cli-edit-persona-engine-config.md
- [X] T068 [US5] Implement the interactive editor loop in Edit-PersonaEngineConfig.ps1 with re-validation before save
- [X] T069 [US5] Implement the timestamped backup and Save-As path in Edit-PersonaEngineConfig.ps1 (FR-026)
- [X] T070 [US5] Implement synthetic rule testing via `-TestDataPath` in Edit-PersonaEngineConfig.ps1, reusing the rule engine with no tenant connectivity (FR-025)
### Rule CRUD in the interactive editor (US5 addition — FR-027–FR-030)
Added after the initial Phase 6 build. The editor originally shipped deliberately scoped to
toggle/priority only (see the `Invoke-InteractiveEditor` comment header); spec.md now requires
add/edit/delete, including full condition-tree editing, so that scope is reversed here rather than
worked around.
#### Tests for rule CRUD
- [X] T122 [P] [US5] Add-rule tests in tests/Configuration/EditorAddRule.Tests.ps1 covering the full field/condition-tree prompt sequence and rejection of a duplicate `id` or `priority` before the rule is added (FR-027)
- [X] T123 [P] [US5] Edit-rule tests in tests/Configuration/EditorEditRule.Tests.ps1 covering top-level field changes and condition-tree add/edit/remove/renest operations within `MaxConditionDepth` (FR-028)
- [X] T124 [P] [US5] Delete-rule tests in tests/Configuration/EditorDeleteRule.Tests.ps1 covering the `id`/`name`/`priority` confirmation prompt and removal, including a declined confirmation leaving the rule in place (FR-029)
- [X] T125 [P] [US5] Structural-edit validation tests in tests/Configuration/EditorStructuralEdits.Tests.ps1 asserting add/edit/delete changes stay in memory until save, re-validate through all four layers on `[V]`/`[S]`, and that a depth-limit violation is reported immediately at edit time rather than deferred (FR-030)
#### Implementation for rule CRUD
- [X] T126 [US5] Implement rule addition (prompt for every RE-001 field plus the condition tree, reject a colliding `id`/`priority`) in Edit-PersonaEngineConfig.ps1 and wire the `[A]` command into the interactive loop (FR-027)
- [X] T127 [US5] Implement rule editing (top-level fields plus add/edit/remove/renest on the condition tree) in Edit-PersonaEngineConfig.ps1 and wire the `[E]` command into the interactive loop (FR-028)
- [X] T128 [US5] Implement rule deletion with an `id`/`name`/`priority` confirmation prompt in Edit-PersonaEngineConfig.ps1 and wire the `[D]` command into the interactive loop (FR-029)
- [X] T129 [US5] Route add/edit/delete through the existing `$dirty` tracking and `Invoke-ConfigurationValidation` re-validation path in Edit-PersonaEngineConfig.ps1, and surface a depth-limit finding at edit time using the same check the runtime validator uses (FR-030)
**Checkpoint**: invalid configurations cannot reach the engine or overwrite a good file, and rules can be added, edited, and deleted entirely from the editor.
## Phase 7: User Story 6 — Block invalid configuration in a pipeline (Priority: P2)
**Goal**: Non-interactive validation that returns exit codes and never prompts.
**Independent Test**: Run `-ValidateOnly -NonInteractive` against an invalid file with stdin closed;
confirm a non-zero exit code, no prompt, and no hang.
**Depends on**: US5's validator (T062–T065). If pipeline gating is needed sooner than the interactive
editor, build T062–T065 and T071–T073 first and defer T067–T070.
### Tests for User Story 6
- [X] T071 [P] [US6] Non-interactive test in tests/Configuration/NonInteractive.Tests.ps1 running with stdin closed and asserting no prompt and no hang (SC-010)
- [X] T072 [P] [US6] Exit-code test in tests/Configuration/ExitCodes.Tests.ps1 covering codes 0–4 from the editor contract
### Implementation for User Story 6
- [X] T073 [US6] Implement `-NonInteractive` and `-ValidateOnly` short-circuits in Edit-PersonaEngineConfig.ps1 that never call a prompting cmdlet
- [X] T074 [US6] Implement `-TreatWarningsAsErrors` escalation in Edit-PersonaEngineConfig.ps1 (VR-005)
- [X] T075 [US6] Implement the editor exit codes 0–4 in Edit-PersonaEngineConfig.ps1
- [X] T076 [US6] Add the validation stage to pipelines/validate.yml invoking sanitization, PSScriptAnalyzer, engine purity, schema validation, and the offline Pester suites
**Checkpoint**: CI blocks a bad configuration before it can reach a tenant.
---
## Phase 8: User Story 4 — Observe progress and reconcile results (Priority: P2)
**Goal**: Interim and final summaries with a reconciliation check that treats a mismatch as an engine
defect.
**Independent Test**: Run over a fixture population with `summaryInterval` set to 5, then to 0;
confirm interim summaries appear at the interval, are suppressed at 0, and a final summary appears in
both cases.
### Tests for User Story 4
- [X] T077 [P] [US4] Interval-semantics tests in tests/Unit/SummaryInterval.Tests.ps1 covering default 25, a custom interval, and the `0` case that still produces a final summary (FR-020)
- [X] T078 [P] [US4] Reconciliation tests in tests/Unit/Reconciliation.Tests.ps1 asserting `Processed = Matched + Unclassified + EvaluationError` at every summary and that a forced mismatch raises an engine defect (SC-007)
### Implementation for User Story 4
- [X] T079 [US4] Implement `Write-PersonaSummary` in src/Presentation/Write-PersonaSummary.ps1 rendering all business rules with match counts — including disabled and zero-match rules, so an absent rule is distinguishable from one that never fired
- [X] T081 [US4] Implement `Test-PersonaReconciliation` in src/Presentation/Test-PersonaReconciliation.ps1 and invoke it at every summary (FR-021)
- [X] T082 [US4] Emit an `EngineDefect` record and exit code 5 on reconciliation failure in Invoke-PersonaEngine.ps1
**Checkpoint**: operators can watch a long run and trust the counters.
---
## Phase 9: User Story 7 — Audit any classification decision (Priority: P2)
**Goal**: Structured, audit-friendly records that explain every decision and make rollback possible.
**Independent Test**: Run over fixtures and confirm 100% of user events carry run ID, UPN, and
Account Object ID, and 100% of matched results carry a rule ID.
### Tests for User Story 7
- [X] T083 [P] [US7] Completeness tests in tests/Unit/AuditCompleteness.Tests.ps1 asserting SC-006 across every record
- [X] T084 [P] [US7] Redaction tests in tests/Unit/AuditRedaction.Tests.ps1 asserting no token, `Authorization` header, secret, or raw Graph response can appear in any record
- [X] T085 [P] [US7] Schema tests in tests/Unit/AuditRecordShape.Tests.ps1 validating each record type against contracts/audit-record.md
### Implementation for User Story 7
- [X] T086 [US7] Implement `New-PersonaAuditRecord` in src/Audit/New-PersonaAuditRecord.ps1 building the common envelope plus each `recordType`
- [X] T087 [US7] Implement the single sink `Write-PersonaAuditRecord` in src/Audit/Write-PersonaAuditRecord.ps1 emitting NDJSON to file and/or stream per `logging.destination`, as the only emission point so a future transport needs no call-site changes
- [X] T088 [US7] Implement `Export-PersonaRunReport` in src/Audit/Export-PersonaRunReport.ps1 producing the `RunComplete` record with all counters and the exit code
- [X] T089 [US7] Wire the run ID from `-CorrelationId` or a generated GUID through every record in Invoke-PersonaEngine.ps1 (NFR-005)
- [X] T090 [US7] Record `configVersion` and `configurationHash` on every record in src/Audit/New-PersonaAuditRecord.ps1
**Checkpoint**: every decision is explainable after the fact.
---
## Phase 10: User Story 9 — Trace the values behind every rule decision (Priority: P3)
**Goal**: Condition-level diagnostic tracing, available only when explicitly enabled.
**Independent Test**: Run one user with `-Debug` and confirm a condition trace appears; run without
`-Debug` and confirm no condition values are emitted anywhere.
### Tests for User Story 9
- [X] T091 [P] [US9] Gating tests in tests/Unit/ConditionTrace.Tests.ps1 asserting `conditionTrace` is absent without `-Debug` and present with it
- [X] T092 [P] [US9] Acknowledgement test in tests/Configuration/TraceAcknowledgement.Tests.ps1 asserting `traceConditionValues` without explicit acknowledgement produces a VR-003 safety finding
### Implementation for User Story 9
- [X] T093 [US9] Populate `ConditionTrace` on the decision result in src/RuleEngine/Resolve-UserPersona.ps1, built only when tracing is active
- [X] T094 [US9] Add the `conditionTrace` array to user events in src/Audit/New-PersonaAuditRecord.ps1, gated on `-Debug` or `logging.traceConditionValues`
**Checkpoint**: rule authors can debug a decision without loosening default logging.
---
## Phase 11: User Story 8 — Enforce changes in production (Priority: P3) 🔒 Gated
**Goal**: Write the calculated persona, only when changed, only the approved attribute.
**Independent Test**: Against test accounts, confirm only changed values are written, every request
body has exactly one key, and every `Updated` record carries `previousValue`.
**⚠️ Do not begin the tenant-facing tasks (T101–T103) until**: V-4 security sign-off is recorded,
`-WhatIf` impact evidence from Scenario 4 is reviewed, and Phases 1–10 pass. Tasks T095–T100 are
offline and may proceed at any time.
### Tests for User Story 8
- [X] T095 [P] [US8] Single-attribute body test in tests/Safety/WriteBody.Tests.ps1 asserting every captured body has exactly one key equal to `engine.targetAttribute` (SC-005)
- [X] T096 [P] [US8] Rejection tests in tests/Safety/WriteBodyRejection.Tests.ps1 asserting `New-PersonaWriteBody` throws for any other attribute name and for an attribute absent from `approvedWritableAttributes`
- [X] T097 [P] [US8] Write-gate tests in tests/Safety/WriteGate.Tests.ps1 covering all four FR-016 conditions, including that an `EvaluationError` user is never written
### Implementation for User Story 8
- [X] T098 [US8] Implement `New-PersonaWriteBody` in src/Persistence/New-PersonaWriteBody.ps1 as the only function permitted to construct a write body, returning a hashtable whose `Count` is exactly 1
- [X] T099 [US8] Implement `Set-UserPersonaAttribute` in src/Persistence/Set-UserPersonaAttribute.ps1 issuing `PATCH /v1.0/users/{id}`, reachable only when `ShouldProcess` returned true
- [X] T100 [US8] Capture `previousValue` at write time into the audit record in src/Persistence/Set-UserPersonaAttribute.ps1 — missing this in v1 makes OTD-010 rollback impossible retroactively
- [ ] T101 [US8] Record the V-4 security sign-off in specs/001-persona-engine/verification/V-4.md before any enforcement run
- [ ] T102 [US8] Close the V-1 write half against purpose-created test accounts of each origin type; append to specs/001-persona-engine/verification/V-1.md
- [ ] T103 [US8] Close V-2 by building a dynamic membership group on the persona extension and confirming a CA policy assigned to it applies; record in specs/001-persona-engine/verification/V-2.md
**Checkpoint**: enforcement works against test accounts with every safety invariant proven.
---
## Phase 12: Polish & Cross-Cutting Concerns
- [X] T104 [P] Add comment-based help to every public function across src/ (NFR-004)
- [X] T105 [P] Write docs/Architecture.md describing the adapter boundaries and why the rule engine is pure
- [X] T106 [P] Write docs/ConfigurationReference.md documenting every schema field and finding code
- [X] T107 [P] Write docs/SecurityModel.md recording the OTD-003 trade-off, the six compensating controls, and the V-4 sign-off
- [X] T108 [P] Write docs/OperationsRunbook.md including the kill switch and the rollback procedure
- [X] T109 [P] Write docs/BusinessRules.md and docs/Logging.md
- [X] T110 [P] Add the test stage to pipelines/test.yml publishing Pester results
- [X] T111 Build the FR/NFR traceability matrix in specs/001-persona-engine/traceability.md mapping every requirement ID to its implementing task and test
- [X] T112 Verify every exit code 0–6 is reachable via fault injection in tests/Unit/ExitCodes.Tests.ps1 (SC-011)
- [X] T113 Run tests/Test-Sanitization.ps1 across all tracked files and record the result in specs/001-persona-engine/verification/sanitization.md (SC-013)
- [X] T114 Update specs/001-persona-engine/spec.md Clarification Register to mark OTD-001 through OTD-005 resolved, citing research.md
---
## Phase 13: Stage B — Azure Automation (DEFERRED)
**Blocked**: no Automation account is available. Listed so the remaining entry cost stays visible and
nothing is lost. Nothing in Phases 1–12 depends on these.
- [ ] T115 Implement `Connect-PersonaGraphManagedIdentity` in src/Authentication/Connect-PersonaGraphManagedIdentity.ps1 returning the same handle shape as the interactive path
- [ ] T116 Close V-5b: confirm `Test-Json -SchemaFile` behaviour in the Automation runtime and record its exact PowerShell version in specs/001-persona-engine/verification/V-5b.md — run this first, it is the cheapest item most likely to surprise
- [ ] T117 Close V-3b: confirm the three read scopes work as application permissions on the managed identity; record in specs/001-persona-engine/verification/V-3b.md
- [ ] T118 Create the runbook wrapper pipelines/runbook/Invoke-PersonaEngineRunbook.ps1
- [ ] T119 Build pipelines/release.yml deploying to Automation with the schedule shipped **disabled**, a `-WhatIf` validation stage, and an approval gate before enforcement
- [ ] T120 Implement the run-start concurrency check in Invoke-PersonaEngine.ps1 per OTD-009
- [ ] T121 Run quickstart Scenario 5 and record results in specs/001-persona-engine/verification/Scenario5.md — **this is what allows v1 to be declared complete**
---
## Dependencies
```text
Phase 1 (Setup)
└─> Phase 2 (Foundational) ← blocks everything
└─> Phase 3 US2 Rule engine ← blocks US3, US1
└─> Phase 4 US3 Fail-safe ← blocks US1
└─> Phase 5 US1 Preview 🎯 MVP
├─> Phase 6 US5 Validation + editor
│ └─> Phase 7 US6 Pipeline mode
├─> Phase 8 US4 Summaries
│ └─> Phase 9 US7 Audit
│ └─> Phase 10 US9 Tracing
└─> Phase 11 US8 Enforcement 🔒 (also gated on V-4)
└─> Phase 12 Polish
└─> Phase 13 Stage B (deferred)
```
**Story independence, honestly stated**: US2 and US3 are genuinely independent of everything except
the foundation. US1 depends on both — the constitution's build order makes that unavoidable, and
pretending otherwise would produce a task list that cannot be executed in the mandated sequence.
US4–US9 are independent of each other and may proceed in any order once US1 lands, except that US9