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:
@@ -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]**: US1–US9, 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, T101–T103) or an Azure Automation account (Phase 13,
|
||||
T115–T121). 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 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] 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 (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] 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 (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
|
||||
reads the audit shape US7 defines.
|
||||
|
||||
## Parallel opportunities
|
||||
|
||||
| Phase | Parallel set |
|
||||
| --- | --- |
|
||||
| 1 | T004, T005, T006, T007, T008 |
|
||||
| 2 | T011, T012, T013, T014 |
|
||||
| 3 | T016–T020 (all tests, separate files) |
|
||||
| 4 | T027, T028, T029 |
|
||||
| 5 | T034–T038 (tests); then T039, T040 |
|
||||
| 6 | T057–T060 |
|
||||
| 7 | T071, T072 |
|
||||
| 8 | T077, T078 |
|
||||
| 9 | T083, T084, T085 |
|
||||
| 10 | T091, T092 |
|
||||
| 11 | T095, T096, T097 |
|
||||
| 12 | T104–T110 |
|
||||
|
||||
After Phase 5, three tracks can run concurrently: validation/editor (Phases 6–7),
|
||||
observability (Phases 8–10), and the offline half of enforcement (T095–T100).
|
||||
|
||||
## Implementation strategy
|
||||
|
||||
**MVP = Phases 1–5** (T001–T056). 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 6–7**. Configuration safety and CI gating — the prerequisite for letting anyone
|
||||
other than the author edit rules.
|
||||
|
||||
**Increment 3 = Phases 8–10**. 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 | — | T001–T008 | 8 |
|
||||
| 2 Foundational | — | T009–T015 | 7 |
|
||||
| 3 | US2 (P1) | T016–T026 | 11 |
|
||||
| 4 | US3 (P1) | T027–T033 | 7 |
|
||||
| 5 | US1 (P1) 🎯 | T034–T056 | 23 |
|
||||
| 6 | US5 (P2) | T057–T070 | 14 |
|
||||
| 7 | US6 (P2) | T071–T076 | 6 |
|
||||
| 8 | US4 (P2) | T077–T082 | 6 |
|
||||
| 9 | US7 (P2) | T083–T090 | 8 |
|
||||
| 10 | US9 (P3) | T091–T094 | 4 |
|
||||
| 11 | US8 (P3) 🔒 | T095–T103 | 9 |
|
||||
| 12 Polish | — | T104–T114 | 11 |
|
||||
| 13 Stage B | — | T115–T121 | 7 (deferred) |
|
||||
| **Total** | | | **121** |
|
||||
Reference in New Issue
Block a user