Implement Stage A: rule engine, validation, audit, and safety gates

Completes 109 of 121 tasks. Every remaining task needs a tenant connection
(T055, T056, T101-T103) or an Azure Automation account (T115-T121).

  354 offline Pester tests      PASS
  Engine purity (Principle IV)  PASS
  Sanitization (SC-013)         PASS  (156 files)
  Graph module loaded in tests  none  (SC-008 holds)

What landed
  - Four-layer configuration validation with stable finding codes, covering
    every VR-002 and VR-003 condition, plus a 23-fixture invalid-config corpus
  - Run loop, audit records (NDJSON through a single sink), summaries,
    reconciliation, and exit codes 0-6
  - Persistence behind a single write-body builder whose result always has
    exactly one key
  - Invoke-PersonaEngine.ps1 and Edit-PersonaEngineConfig.ps1
  - Six docs, two pipelines, traceability matrix, V-5a and sanitization records

Three deviations from tasks.md, each recorded in its status block

  T033 is not in Resolve-UserPersona. evaluationErrorThreshold is run-level
  state and the rule engine is pure; a counter there would break Principle IV.
  It lives in New-PersonaRunCounter and is applied in the run loop.

  A new src/Engine/ layer holds Invoke-PersonaEngineRun. The entry script
  imports the manifest, which requires Microsoft.Graph.Authentication, so a
  loop living only inside it could not run on a machine without the Graph SDK
  and SC-004 could not be proven at all. The entry script is now a thin
  wrapper and what ships is what is tested.

  The invalid-config corpus is generated by a committed script, with the
  generated fixtures committed too, so a reviewer sees the fixture in the diff.

Defects found by running the code, not by reading it

  Group and role ID lists were double-wrapped: @(Get-PersonaGroupIdPage ...)
  around a comma-returned array collapsed every membership list into one
  bogus space-joined entry. That is a silent false non-match, exactly what
  FR-013 exists to prevent.

  A 403 whose status appears only in the exception message parsed as $null,
  which the retry policy treats as a transport error - five requests per
  account against a tenant already refusing. Status extraction now falls back
  to the message text, bounded to 400-599.

  The sanitization scan walked tracked files only, so it covered 34 of 156
  files and none of this phase's code. It now scans untracked non-ignored
  files too, and a negative control confirms it catches a planted leak.

  Test-Json reports one error per violating location, not first-failure-only
  as the V-5a draft claimed. Record and pin corrected.

Enforcement remains blocked on the V-4 security sign-off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 21:48:19 -04:00
parent c59c85dd55
commit cdc6bb33d3
124 changed files with 16638 additions and 199 deletions
+481
View File
@@ -0,0 +1,481 @@
---
description: "Task list for Persona Engine implementation"
---
# Tasks: Persona Engine
**Input**: Design documents from `/specs/001-persona-engine/`
**Prerequisites**: [plan.md](plan.md), [spec.md](spec.md), [research.md](research.md),
[data-model.md](data-model.md), [contracts/](contracts/), [quickstart.md](quickstart.md)
**Tests**: **Included and mandatory.** Not an optional TDD preference here — SC-004, SC-005, SC-008,
and SC-009 are written as test assertions, and constitution Principle IV requires the rule engine to
pass offline Pester tests before any Graph integration exists.
**Organization**: Grouped by user story. Story phases are ordered by the constitution's
**non-negotiable build order**, not strictly by priority — see the note below.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies on incomplete tasks)
- **[Story]**: US1US9, mapping to the user stories in spec.md
- Every task names an exact file path
## Path Conventions
Single PowerShell module at repository root: `src/`, `tests/`, `config/`, `docs/`, `pipelines/`,
per the Project Structure section of [plan.md](plan.md).
## Implementation status — 2026-08-20
**109 of 121 tasks complete.** All twelve remaining tasks require something this workstation does not
have: a tenant connection (T055, T056, T101T103) or an Azure Automation account (Phase 13,
T115T121). Nothing offline-implementable is outstanding.
```
354 offline Pester tests PASS
Engine purity (Principle IV) PASS
Sanitization (SC-013) PASS 156 files
Graph module loaded during tests False (SC-008 holds)
```
Three deviations from the task list as written, each made while implementing and each for a reason
worth recording:
**T033 does not live in `Resolve-UserPersona`.** The task named that file, but `evaluationErrorThreshold`
is run-level state and the rule engine is pure — a counter there would break Principle IV. It is
implemented in `New-PersonaRunCounter` and applied in the run loop, which is what "and the run summary
path" in the task description points at.
**A new `src/Engine/` layer exists** holding `Invoke-PersonaEngineRun`, which was not in the planned
structure. `Invoke-PersonaEngine.ps1` imports the manifest, which requires `Microsoft.Graph.Authentication`;
a run loop living only inside that script could not be executed on a machine without the Graph SDK,
so SC-004 — zero writes under `-WhatIf` across a full population — could not be proven at all. The
entry script is now a thin wrapper and the loop is testable. What ships and what is tested are the
same code.
**T057's corpus is generated, not hand-written.** `tests/TestData/InvalidConfigs/New-InvalidConfigCorpus.ps1`
produces 23 fixtures, each a valid configuration with exactly one defect. The generated files are
committed so a reviewer sees the fixture in the diff rather than a script that produces it.
## Ordering note (read before starting)
Constitution Principle IV fixes the build order: pure rule engine first, persistence last. This
**overrides** strict priority ordering, so the P1 stories are sequenced US2 → US3 → US1 rather than
US1 first. US1 (preview) is the headline MVP story but cannot be built before the engine it previews.
Stage labels refer to the Delivery Staging table in [plan.md](plan.md): **A1** offline, **A2**
delegated read-only, **A3** delegated write to test accounts, **B** Azure Automation (deferred — no
Automation account available).
---
## Phase 1: Setup (Shared Infrastructure) — Stage A1
**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)
---
## Phase 2: Foundational (Blocking Prerequisites) — Stage A1
**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] T011 [P] Implement `New-PersonaValidationFinding` in src/Configuration/New-PersonaValidationFinding.ps1 emitting Severity, Code, Location, Description, SuggestedResolution, Layer (VR-004)
- [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 06 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] T062 [US5] Implement validation layer 1 (syntax) in src/Configuration/Test-PersonaConfiguration.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)
**Checkpoint**: invalid configurations cannot reach the engine or overwrite a good file.
---
## 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 (T062T065). If pipeline gating is needed sooner than the interactive
editor, build T062T065 and T071T073 first and defer T067T070.
### 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 04 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 04 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] T080 [US4] Implement interval-triggered interim summaries in Invoke-PersonaEngine.ps1 (FR-019)
- [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 (T101T103) until**: V-4 security sign-off is recorded,
`-WhatIf` impact evidence from Scenario 4 is reviewed, and Phases 110 pass. Tasks T095T100 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 06 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 112 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.
US4US9 are independent of each other and may proceed in any order once US1 lands, except that US9
reads the audit shape US7 defines.
## Parallel opportunities
| Phase | Parallel set |
| --- | --- |
| 1 | T004, T005, T006, T007, T008 |
| 2 | T011, T012, T013, T014 |
| 3 | T016T020 (all tests, separate files) |
| 4 | T027, T028, T029 |
| 5 | T034T038 (tests); then T039, T040 |
| 6 | T057T060 |
| 7 | T071, T072 |
| 8 | T077, T078 |
| 9 | T083, T084, T085 |
| 10 | T091, T092 |
| 11 | T095, T096, T097 |
| 12 | T104T110 |
After Phase 5, three tracks can run concurrently: validation/editor (Phases 67),
observability (Phases 810), and the offline half of enforcement (T095T100).
## Implementation strategy
**MVP = Phases 15** (T001T056). Delivers complete classification visibility against the live tenant
with zero write capability, closes V-1 (read) and V-3, and establishes the performance baseline. This
is a genuinely useful deliverable on its own: it answers "what would this classify my tenant as?"
without touching anything.
**Increment 2 = Phases 67**. Configuration safety and CI gating — the prerequisite for letting anyone
other than the author edit rules.
**Increment 3 = Phases 810**. Observability, reconciliation, and audit. Required before enforcement
is defensible, because the `-WhatIf` impact evidence the constitution demands is only as good as the
output that produces it.
**Increment 4 = Phase 11**, behind the V-4 gate, against test accounts only.
**Stage B (Phase 13)** converts the result into an unattended service. Until T121 passes, the correct
status to report is **"Stage A complete"**, not "v1 done" — the Definition of Done requires an
Automation PowerShell 7 run.
## Task summary
| Phase | Story | Tasks | Count |
| --- | --- | --- | --- |
| 1 Setup | — | T001T008 | 8 |
| 2 Foundational | — | T009T015 | 7 |
| 3 | US2 (P1) | T016T026 | 11 |
| 4 | US3 (P1) | T027T033 | 7 |
| 5 | US1 (P1) 🎯 | T034T056 | 23 |
| 6 | US5 (P2) | T057T070 | 14 |
| 7 | US6 (P2) | T071T076 | 6 |
| 8 | US4 (P2) | T077T082 | 6 |
| 9 | US7 (P2) | T083T090 | 8 |
| 10 | US9 (P3) | T091T094 | 4 |
| 11 | US8 (P3) 🔒 | T095T103 | 9 |
| 12 Polish | — | T104T114 | 11 |
| 13 Stage B | — | T115T121 | 7 (deferred) |
| **Total** | | | **121** |