# Configuration reference Every field in `persona-engine.json`, and every finding code the validator can produce. The authoritative schema is [`config/persona-engine.schema.json`](../config/persona-engine.schema.json) (JSON Schema draft-07). A working example is [`config/persona-engine.example.json`](../config/persona-engine.example.json), which is validated against its own schema before every commit — if the example the documentation points at could not pass, every reader's first run would fail. For a task-oriented walkthrough of every condition type and operator with worked JSON examples, see [RuleAuthoringGuide.md](RuleAuthoringGuide.md). This document is the terse reference; that one teaches by example. ## Top level | Field | Required | Notes | | --- | --- | --- | | `configVersion` | yes | Semantic version, `major.minor.patch`. A downgrade is a safety finding. | | `metadata` | no | `owner`, `changeReference`, `description`. Free-form; not read by the engine. | | `engine` | yes | Engine behaviour. | | `dataSources` | yes | Which directory data may be retrieved. | | `logging` | no | Audit output. | | `personas` | yes | The declared persona catalogue. | | `rules` | yes | Ordered business rules. | ## `engine` | Field | Required | Default | Notes | | --- | --- | --- | --- | | `targetAttribute` | yes | — | The single attribute the engine may write. Must be a directory extension property and must appear in `approvedWritableAttributes`. | | `approvedWritableAttributes` | yes | — | The allow-list. Comparison is **ordinal** — extension property names are case-sensitive in Graph. | | `maxConditionDepth` | no | `5` | RE-004. Minimum 1, hard ceiling 10. | | `summaryInterval` | no | `25` | Interim summary frequency. `0` suppresses interim summaries; a final summary always appears. | | `defaultMembershipMode` | no | `direct` | Mode for membership conditions that do not specify one. | | `evaluationErrorThreshold` | no | unset | Count of `EvaluationError` results above which the run reports exit code 4. Unset means report, do not fail. | Setting `evaluationErrorThreshold` to `0` makes a single transient lookup failure fail the run. That is occasionally what you want; it is rarely what you meant. ## `dataSources` | Field | Required | Notes | | --- | --- | --- | | `groups.enabled` | yes | When false, no membership facet is retrieved. Rules needing it become `EvaluationError`. | | `groups.membershipMode` | no | Pins a mode globally. **Leave it out unless you mean to restrict** — absent means "any mode is acceptable", and RE-007 makes mode a per-condition choice. Pinning it makes every per-condition override a `PE-SEM-014` warning. | | `roles.enabled` | yes | Directory role assignments. | | `roles.includeEligible` | no | PIM-eligible assignments. **Out of scope for v1**; no provider is implemented. | The engine retrieves only the facets enabled rules actually reference. A configuration with no role conditions never calls the role endpoint, so a tenant where role reads are unavailable can still run property-only rules. ## `logging` | Field | Required | Default | Notes | | --- | --- | --- | --- | | `destination` | no | `both` | `file`, `stream`, `both`, or `none`. `stream` writes records to the PowerShell Information stream. | | `path` | no | `/logs/persona-engine-audit.ndjson` | NDJSON output file. One record per line. | | `traceConditionValues` | no | `false` | Writes evaluated attribute values into audit records. | | `acknowledgeConditionTracing` | no | `false` | **Required whenever `traceConditionValues` is true** (VR-003). | ## `personas` The declared catalogue. A rule assigning a persona absent from this list is a `PE-SEM-010` error — the catalogue is what stops a typo from writing a new persona value into the directory. `Unclassified` and `EvaluationError` are processing results and may never be declared or assigned. ## `rules` | Field | Required | Notes | | --- | --- | --- | | `id` | yes | Unique. Appears in every audit record; this is how a decision is traced to its rule. | | `name` | yes | Human-readable. | | `description` | yes | Why the rule exists. Required, because a rule nobody can explain cannot be safely changed. | | `enabled` | yes | Disabled rules are excluded from evaluation but still appear in summaries with zero matches. | | `priority` | yes | Unique integer. **Lower evaluates first.** | | `persona` | yes | Must appear in `personas`. | | `match` | yes | The root condition group. | | `tags`, `owner`, `changeReference`, `effectiveDate`, `notes`, `testCases` | no | Metadata. `effectiveDate` is **not** evaluated — a date-dependent decision would break determinism. | Priorities must be unique among enabled rules. The engine breaks ties by rule ID so results stay deterministic, but the resulting order is an accident rather than a decision, so `PE-SEM-002` blocks it. ## Condition groups and conditions A group has `operator` (`all` or `any`) and a `conditions` array. Each entry is either another group or a condition. | Field | Applies to | Notes | | --- | --- | --- | | `type` | all | `property`, `membership`, or `role`. | | `property` | `property` | One of the supported names below, or an extension property. | | `operator` | all | See the operator table. | | `value` | most | Single comparison value. | | `values` | `in`, `notIn` | Comparison set. | | `groupObjectIds` | `membership` | Group Object IDs. Names are mutable; IDs are not (RE-009). | | `roleIds` | `role` | Role **template** IDs, which are stable across tenants. | | `membershipMode` | `membership` | `direct` or `transitive`, per condition. | | `caseSensitive` | — | Reserved. Not implemented in v1; the schema accepts the key so a later version needs no breaking change. | ### Supported properties `AccountObjectId` · `UserPrincipalName` · `DisplayName` · `UserType` · `AccountEnabled` · `CompanyName` · `JobTitle` · `Department` Plus any directory extension property named `extension_<32-hex-app-id>_`. Anything else is `PE-SEM-015`: unsupported properties are never retrieved, so the condition would compare against a permanently absent value and quietly never match. ### Operators (RE-005) | Operator | Applies to | Notes | | --- | --- | --- | | `equals`, `notEquals` | property | Case-insensitive (RE-006). | | `contains`, `notContains` | property | Case-insensitive substring. | | `startsWith`, `endsWith` | property | Case-insensitive. | | `matchesRegex` | property | Pattern compiled at validation time. An invalid pattern is `PE-SEM-016`, not a runtime failure. | | `in`, `notIn` | property | Requires `values`. | | `isNull`, `isNotNull` | property | Tests presence. **Must not carry a value** — it would be silently ignored (`PE-SEM-009`). | | `memberOf`, `notMemberOf` | membership, role | Requires `groupObjectIds` or `roleIds`. | Null and absent properties are treated as empty for ordinary comparisons and never cause an evaluation failure (FR-012). Intentional null matching uses `isNull` / `isNotNull`. ## Tri-state evaluation Conditions return `True`, `False`, or `Unknown`. `Unknown` means required data could not be retrieved, and it propagates: | Group | Contains | Result | | --- | --- | --- | | `all` | any `False` | `False` | | `all` | only `True` plus at least one `Unknown` | `Unknown` | | `any` | any `True` | `True` | | `any` | only `False` plus at least one `Unknown` | `Unknown` | An `Unknown` at a rule's root makes the account `EvaluationError`: the stored persona is preserved and no write is attempted (FR-013, FR-014). ## Validation layers Run in order, stopping at the first that produces `Error` findings. Running semantic checks over a structurally invalid document yields noise, not signal. | Layer | Mechanism | Codes | | --- | --- | --- | | 1 Syntax | `ConvertFrom-Json` | `PE-SYN-nnn` | | 2 Schema | `Test-Json -SchemaFile` | `PE-SCH-nnn` | | 3 Semantic | PowerShell checks | `PE-SEM-nnn` | | 4 Safety | PowerShell checks | `PE-SAF-nnn` | Codes are stable. Pipelines and runbooks match on them, so a code is never reused for a different condition and never renumbered. ### Syntax — `PE-SYN` | Code | Condition | | --- | --- | | `PE-SYN-001` | Configuration file not found, or is not a file | | `PE-SYN-002` | File exists but could not be read | | `PE-SYN-003` | File is not valid JSON | ### Schema — `PE-SCH` | Code | Condition | | --- | --- | | `PE-SCH-001` | Document violates the schema | | `PE-SCH-002` | Schema file not found | | `PE-SCH-003` | Schema file exists but is not valid JSON Schema | `PE-SCH-003` exists because of V-5a: `Test-Json` returns `$true` when the schema itself cannot be parsed. A wrapper trusting the return value would report every configuration as schema-valid against a schema that never ran. ### Semantic — `PE-SEM` (VR-002) | Code | Condition | Severity | | --- | --- | --- | | `PE-SEM-001` | Duplicate rule ID | Error | | `PE-SEM-002` | Duplicate priority among enabled rules | Error | | `PE-SEM-003` | No rules, or no enabled rules | Error | | `PE-SEM-004` | Blank target attribute | Error | | `PE-SEM-005` | Target attribute absent from the approved list | Error | | `PE-SEM-006` | Rule references a disabled data source | Error | | `PE-SEM-007` | `memberOf` / `notMemberOf` with no group or role IDs | Error | | `PE-SEM-008` | `in` / `notIn` with no `values` | Error | | `PE-SEM-009` | `isNull` / `isNotNull` carrying a comparison value | Error | | `PE-SEM-010` | Persona not in the declared catalogue | Error | | `PE-SEM-011` | `Unclassified` used as a rule persona | Error | | `PE-SEM-012` | Nesting deeper than `maxConditionDepth` | Error | | `PE-SEM-013` | `maxConditionDepth` outside 1–10 | Error | | `PE-SEM-014` | Condition mode differs from an explicitly pinned global mode | Warning | | `PE-SEM-015` | Unsupported property name | Error | | `PE-SEM-016` | Invalid regular expression | Error | Several of these are also enforced by the schema. The overlap is deliberate: layer 2 can be bypassed with `-SchemaPath`, and V-5a showed an unparseable schema passes silently. Anything that can misclassify a privileged account is checked twice. ### Safety — `PE-SAF` (VR-003) | Code | Condition | Severity | | --- | --- | --- | | `PE-SAF-001` | Blank target attribute | Error enforcing, Warning in preview | | `PE-SAF-002` | Approved list contains a non-extension attribute | Error | | `PE-SAF-003` | Enabled rules need data the data sources do not provide | Error | | `PE-SAF-004` | `configVersion` lower than the deployed version | Error enforcing, Warning in preview | | `PE-SAF-005` | Rules removed or reordered with no version change | Error enforcing, Warning in preview | | `PE-SAF-006` | Tracing enabled without acknowledgement | Error | | `PE-SAF-007` | Save would overwrite an existing configuration with no backup | Error | `PE-SAF-004` and `PE-SAF-005` need `-PreviousConfigPath`. Without it they are **skipped**, and an `Information` finding says so — silence would be read as approval. ## Escalation (VR-005) `Error` blocks execution and saving. `Warning` blocks only under `-TreatWarningsAsErrors`. `Information` never blocks. Passing `-TreatWarningsAsErrors` does not change a finding's severity; it changes the caller's tolerance for it.