# Implementation Plan: Persona Engine **Branch**: `main` (feature directory `001-persona-engine`) | **Date**: 2026-08-20 | **Spec**: [spec.md](spec.md) **Input**: Feature specification from `/specs/001-persona-engine/spec.md` ## Summary Deterministic, configuration-driven persona classification for Microsoft Entra ID user objects. The engine enumerates in-scope users, evaluates each against an ordered JSON rule set, assigns exactly one persona, and updates a single approved directory attribute only when the calculated value differs from the stored value. **Technical approach**: a PowerShell 7 module (`PersonaEngine`) whose rule engine is a pure function over normalized records, with Graph access, persistence, and presentation isolated behind adapters. Directory access uses `Invoke-MgGraphRequest` (direct REST over the `Microsoft.Graph.Authentication` module) so the write body is explicitly constructed and test-assertable. The persona value is stored in a **directory (schema) extension** on the user object, consumed downstream by dynamic membership groups. Configuration is validated with the built-in `Test-Json -SchemaFile` against a draft-07 schema. See [research.md](research.md) for the decisions and their rationale. ## Delivery Staging **Constraint (2026-08-20)**: no Azure Automation account is available. All development and testing proceeds on a local PowerShell 7 workstation using **user accounts and delegated authentication**. This changes sequencing, not architecture. The adapter boundaries that make the engine testable offline (Principle IV) are the same boundaries that make the Automation runtime a late, additive step — so the deferral costs nothing structurally. | Stage | Environment | Auth | Status | | --- | --- | --- | --- | | **A1** — offline | Local PS7, synthetic fixtures | None | Available now. Covers the rule engine, all four validation layers, and the safety suites. No tenant, no network. | | **A2** — connected read-only | Local PS7, tenant | Delegated (`Connect-MgGraph -Scopes`) | Available now. Covers enumeration, membership, roles, normalization, presentation, reconciliation, and `-WhatIf`. | | **A3** — connected write | Local PS7, **test accounts only** | Delegated | Gated on V-4. Test accounts only — the baseline's read-only-during-early-development assumption still stands for the general population. | | **B** — Automation | Azure Automation PS7 | Managed identity | **Deferred.** Additive: a second authentication adapter, a runbook wrapper, and a schedule. | **Consequences, stated plainly:** 1. **v1 cannot be declared complete while Stage B is deferred.** The Definition of Done requires an Azure Automation PowerShell 7 run to pass. Deferring it does not violate the constitution — it defers *completion*. The correct milestone to claim in the meantime is "Stage A complete", not "v1 done". Do not quietly redefine done. 2. **`Connect-PersonaGraphManagedIdentity` will ship unexercised.** `Connect-MgGraph -Identity` cannot run on a workstation. The mitigation is to keep the authentication adapter's surface minimal — one function, returning the same handle shape as the interactive path, with no engine-visible difference — so that the untested code is a few lines rather than a subsystem. 3. **Delegated authorization behaves differently from application permissions.** Effective access is the intersection of the requested scope and the signed-in user's directory roles. This makes V-3 *more* meaningful when run as an ordinary user account, and meaningless when run as a Global Administrator. See research.md V-3. 4. **Automation-specific risk stays open**: runtime PowerShell version, module availability, and sandbox behaviour are unverified until Stage B. The one-module dependency decision (OTD-004) is what keeps that risk small. ## Technical Context **Language/Version**: PowerShell 7.4 locally. The Automation runtime version is unverified and remains so until Stage B (verification item V-5b in research.md). Avoid any construct newer than PS 7.2 so the eventual Automation runtime is not a constraint discovered late. **Primary Dependencies**: `Microsoft.Graph.Authentication` (token acquisition and `Invoke-MgGraphRequest`) is the only runtime dependency. `Pester` 5.x and `PSScriptAnalyzer` are development/CI-only. No full Microsoft Graph SDK dependency — see OTD-004. Module availability in the Automation sandbox is unverified until Stage B. **Storage**: JSON configuration file on disk; no database. Persona values live in the directory itself. Audit output is newline-delimited JSON to a file plus the Automation output stream. **Testing**: Pester 5.x. Unit and rule-engine suites run fully offline against synthetic fixtures (SC-008); integration suites require a read-only tenant identity, satisfied in Stage A2 by a delegated connection; safety suites assert zero writes under `-WhatIf` (SC-004) and single-attribute write bodies (SC-005). The safety suites mock the write adapter, so they are fully available now and are **not** gated on Stage A3 or B — the zero-write guarantee is proven against the adapter contract, not against a tenant. **Target Platform**: PowerShell 7 on a local workstation (Stages A1–A3). The Azure Automation PowerShell 7 runtime remains the eventual production target but is out of the current stage. **Project Type**: PowerShell module plus two CLI entry-point scripts. **Performance Goals**: None fixed. NFR-002 explicitly defers a hard target until representative tenant testing. The plan requires per-user and total duration to be recorded from the first connected run so a baseline exists before any target is set. **Constraints**: Rule engine must be free of Graph, authentication, Automation, and console dependencies (Principle IV). `-WhatIf` must issue zero writes (Principle III). Write payloads carry exactly one attribute (Principle III). All artifacts sanitized to placeholders (Principle V, SC-013). **Scale/Scope**: In-scope population size is tenant-specific and unknown at planning time. Full enumeration with pagination is the v1 processing model (OTD-008); delta processing is deferred. The read-only pilot establishes the population size and run duration baseline. ## Constitution Check *GATE: Must pass before Phase 0 research. Re-checked after Phase 1 design.* Evaluated against [constitution.md](../../.specify/memory/constitution.md) v1.0.0. | Gate | Principle | Pre-research | Post-design | Notes | | --- | --- | --- | --- | --- | | Deterministic, single-persona result | I (NON-NEGOTIABLE) | PASS | PASS | Ordered priority evaluation, first-match stop, no clock/random/unordered inputs in the engine. Rejecting `extensionAttributeN` (research OTD-001) removes a population-dependent failure mode that would have broken determinism across a hybrid population. | | Configuration-driven rules | II | PASS | PASS | No persona, priority, group ID, role ID, or attribute name in source. Four-layer validation ordering preserved in the config contract. | | Fail-safe, idempotent persistence | III (NON-NEGOTIABLE) | PASS | PASS | `EvaluationError` preserves stored value; `SupportsShouldProcess` on both write paths; changed-values-only comparison; single-attribute body construction isolated in one function. | | Pure rule engine, offline-tested first | IV | PASS | PASS | Rule engine depends only on normalized records. Build order enforced in the task sequencing below; persistence adapter is last. | | Explainable, sanitized observability | V | PASS | PASS | Run ID, UPN, Account Object ID, and matched rule ID on every user event; condition-value tracing gated behind `-Debug`; placeholders only in all artifacts. | **Security and least-privilege constraints**: PASS with a mandatory condition. Research OTD-003 establishes that Microsoft Graph application permissions **cannot** be scoped to an individual user attribute for the selected mechanism. The constitution anticipates exactly this outcome and makes the compensating controls mandatory rather than optional; they are carried into the design as testable requirements (see the persistence contract). This is a documented and approved-by-design condition, not a constitution violation. Security approval of the compensating controls is a gate before enforcement, per the Development Workflow section. **Automation deferral (Stage B)**: PASS. Every principle is satisfiable on a local workstation — determinism, configuration-driven rules, fail-safe persistence, engine purity, and observability are all properties of the code, not of the hosting environment. Two constitution items are *deferred, not waived*: the Definition of Done's Azure Automation PowerShell 7 run, and the release-pipeline stages that deploy to it. Both are recorded in the Delivery Staging table and gate the v1 completion claim. **Result**: no unjustified violations. Complexity Tracking is empty. ## Project Structure ### Documentation (this feature) ```text specs/001-persona-engine/ ├── plan.md # This file ├── research.md # Phase 0 output — OTD-001..010 decisions ├── data-model.md # Phase 1 output — entity contracts ├── quickstart.md # Phase 1 output — validation scenarios ├── contracts/ # Phase 1 output │ ├── persona-engine.schema.json # Configuration JSON Schema (draft-07) │ ├── cli-invoke-persona-engine.md # Engine CLI contract │ ├── cli-edit-persona-engine-config.md # Editor CLI contract │ ├── graph-data-provider.md # Directory read/write contract │ └── audit-record.md # Structured log record contracts └── tasks.md # Phase 2 output (/speckit-tasks — NOT created here) ``` ### Source Code (repository root) ```text PersonaEngine.psd1 # Module manifest PersonaEngine.psm1 # Module loader Invoke-PersonaEngine.ps1 # Engine entry point (CmdletBinding, SupportsShouldProcess) Edit-PersonaEngineConfig.ps1 # Configuration validation / editor entry point config/ ├── persona-engine.example.json # Placeholder-only example └── persona-engine.schema.json # Shipped schema (from contracts/) src/ ├── Configuration/ # Import-PersonaConfiguration, Test-PersonaConfiguration, Resolve-TargetAttribute ├── Authentication/ # Connect-PersonaGraphInteractive, Connect-PersonaGraphManagedIdentity ├── DataProviders/ # Get-PersonaUsers, Get-PersonaGroupMembership, Get-PersonaDirectoryRoles ├── Normalization/ # ConvertTo-PersonaUserRecord, ConvertTo-PersonaMembershipRecord ├── RuleEngine/ # Test-PersonaCondition, Test-PersonaConditionGroup, Test-PersonaRule, │ # Resolve-UserPersona <-- no Graph/auth/console dependency ├── Persistence/ # Compare-PersonaValue, New-PersonaWriteBody, Set-UserPersonaAttribute ├── Presentation/ # Write-UserPersonaResult, Write-PersonaSummary └── Audit/ # New-PersonaAuditRecord, Export-PersonaRunReport tests/ ├── Unit/ # Per-function offline tests ├── RuleEngine/ # Rule evaluation matrix against synthetic fixtures ├── Configuration/ # Schema, semantic (VR-002), and safety (VR-003) validation ├── Integration/ # Read-only tenant tests ├── Safety/ # SC-004 zero-write, SC-005 single-attribute-body assertions └── TestData/ # Obviously fictional synthetic users, memberships, configs docs/ # Architecture, BusinessRules, ConfigurationReference, Logging, # SecurityModel, OperationsRunbook pipelines/ # validate.yml, test.yml, release.yml ``` **Structure Decision**: single PowerShell module with two CLI entry points, matching the layout already published in [README.md](../../README.md). The directory split is the enforcement mechanism for Principle IV — `src/RuleEngine/` may import nothing from `src/Authentication/`, `src/DataProviders/`, `src/Persistence/`, or `src/Presentation/`, and a CI check asserts this. ### Build order (Principle IV, non-negotiable sequencing) 1. Normalized record contracts and synthetic fixtures. 2. Pure rule engine + offline Pester suite (no tenant connectivity). 3. Configuration import, four-layer validation, and non-interactive pipeline mode. 4. `Edit-PersonaEngineConfig.ps1` interactive editor and synthetic rule testing. 5. Graph authentication and **read** adapters; normalization wiring. 6. Presentation, summaries, reconciliation, and structured audit output. 7. Persistence adapter **last**, with `ShouldProcess` and the zero-write/single-attribute suites. Steps 1–4 are Stage A1 (offline). Steps 5–6 are Stage A2 (delegated read-only). Step 7 is built and fully unit-tested in Stage A1/A2 against a mocked adapter, and only *exercised against the tenant* in Stage A3, behind V-4. Adding the managed-identity adapter and runbook wrapper is Stage B and touches nothing in steps 1–7 — that is the test of whether the boundaries were drawn correctly. ## Complexity Tracking > No Constitution Check violations. This section is intentionally empty.