Skip to content

feat: RPEI Outcome Graph — Causal Chain Tracking Per Object (#363)#373

Merged
JayVDZ merged 84 commits intomainfrom
feature/rpei-outcome-graph-phase1
Mar 4, 2026
Merged

feat: RPEI Outcome Graph — Causal Chain Tracking Per Object (#363)#373
JayVDZ merged 84 commits intomainfrom
feature/rpei-outcome-graph-phase1

Conversation

@JayVDZ
Copy link
Contributor

@JayVDZ JayVDZ commented Mar 4, 2026

Summary

  • Implements the complete RPEI Outcome Graph feature across all phases (1–7 + 4b)
  • Each Run Profile Execution Item now records a structured tree of causal outcomes showing the full chain of consequences from processing a single Connected System Object
  • Replaces flat ObjectChangeType-based statistics with outcome-type aggregates derived from the outcome tree
  • Adds configurable tracking levels (None/Standard/Detailed) via service setting
  • Adds outcome stat chips, outcome type filter, and causality tree to the Activity Detail UI
  • Removes redundant Change Type filter and column from web UI (superseded by outcome graph)
  • Adds snapshot fields on RPEIs for historical display name and object type preservation
  • 10 NUnit repository-level integration tests covering dual-path stats derivation
  • PowerShell integration test assertions added to Scenario 1 (Joiner, Mover, Leaver)
  • All phases validated against Medium template (1,000 users, 8/8 tests passing)

Test plan

  • dotnet build JIM.sln — 0 errors
  • dotnet test JIM.sln — all tests pass
  • Integration test Scenario 1 (Micro) — 8/8 passed
  • Integration test Scenario 1 (Medium) — 8/8 passed with all outcome assertions
  • All 6 Phase 4b outcome assertions validated at Medium scale

🤖 Generated with Claude Code

JayVDZ and others added 30 commits March 1, 2026 22:04
Transitive dependency of puppeteer in the diagram export tooling.
Patches a critical path traversal vulnerability in downloadToDir().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1 of the RPEI Outcome Graph feature. Adds the data model,
EF configuration, database migration, service setting, and bulk
insert support for structured causal outcome trees on RPEIs.

New types:
- ActivityRunProfileExecutionItemSyncOutcome entity (self-referential tree)
- ActivityRunProfileExecutionItemSyncOutcomeType enum (15 outcome types)
- ActivityRunProfileExecutionItemSyncOutcomeTrackingLevel enum (None/Standard/Detailed)

Infrastructure:
- OutcomeSummary denormalised column on ActivityRunProfileExecutionItem
- EF cascade delete from RPEI, SET NULL for self-referential parent
- Composite index on RPEI FK + OutcomeType for aggregate stats queries
- BulkInsertRpeisAsync extended to flatten and bulk insert outcome trees
- ChangeTracking.SyncOutcomes.Level service setting (default: Detailed)
- GetSyncOutcomeTrackingLevelAsync() on ServiceSettingsServer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 2 of the RPEI Outcome Graph feature. Adds SyncOutcomeBuilder
helper and wires causal outcome nodes at all sync integration points:
projection, join, attribute flow, disconnection, deletion, export
evaluation, and cross-page reference resolution. Respects the
None/Standard/Detailed tracking level service setting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
)

SyncOutcomeBuilder adds child outcomes to both parent.Children and
rpei.SyncOutcomes (flat list). FlattenSyncOutcomes was iterating the
full flat list AND recursing into Children, causing each child to be
visited twice and inserted with the same GUID — triggering a PK
violation on the SyncOutcomes table during full sync.

Fix: filter to root outcomes only before recursing, so each child is
visited exactly once via its parent's Children collection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
)

Phase 3: Import processor now records CsoAdded, CsoUpdated, CsoDeleted,
and ExportFailed outcomes on RPEIs. Export processor records Exported
and Deprovisioned outcomes. Both load tracking level once at start and
build OutcomeSummary before bulk insert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The integration test runner now computes a content hash of
post-provision.sh and start-samba.sh, embeds it as a Docker image label
during build, and compares it on subsequent runs. Stale images are
automatically rebuilt, preventing cryptic failures when schema extensions
(e.g. extensionAttribute1-15) are added but the image hasn't been
rebuilt.

Also adds early validation in Setup-Scenario1 and Setup-Scenario8 to
check that all required LDAP attributes exist in the imported schema
before attempting to configure attribute flow mappings, with a clear
error message pointing to the stale image as the cause.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce a two-document workflow for new features: developers write a PRD
(what & why), then Claude generates an implementation plan (how). Adds a
customised PRD template in docs/prd/, a jim-prd shell alias that prompts
for a feature name and scaffolds the PRD file, and updates documentation
to reflect the new workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
#363)

Update stats derivation to use outcome nodes when available, falling
back to RPEI ObjectChangeType counting for legacy data or when outcome
tracking is disabled. This gives richer stats — e.g. multi-system
exports count each target system separately.

- Repository GetActivityRunProfileExecutionStatsAsync uses hybrid
  approach: outcome-based GROUP BY when outcomes exist, RPEI-based
  otherwise
- Worker CalculateActivitySummaryStats flattens outcome trees for
  in-memory counting with same hybrid fallback
- Add OutcomeSummary to ActivityRunProfileExecutionItemHeader DTO
  for stat chip rendering in list views
- RPEI-only stats (OutOfScopeRetainJoin, DriftCorrection, Created,
  NoChange) and error counts always derived from RPEIs
- 7 new unit tests for outcome-based path, all 22 stats tests pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…il (#363)

Phase 5 of the RPEI Outcome Graph feature. Adds per-row outcome stat
chips to the Activity Detail table (parsed from OutcomeSummary) and a
new Outcome Type filter for filtering RPEIs by their recorded outcomes.

- Add Outcomes column to RPEI table with coloured chips per outcome type
- Add outcome type filter chip set (Projected, Attr Flow, Exported, etc.)
- Add ParseOutcomeSummary, GetOutcomeTypeMudBlazorColor, and
  GetOutcomeTypeDisplayName helpers
- Thread outcomeTypeFilter parameter through repository, application, and
  interface layers with OutcomeSummary string matching
- Add 17 unit tests for outcome summary parsing and display helpers
- Move Phase 4b to correct position in plan document

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…#363)

RPEI header projection relied on live CSO navigation properties for Display
Name and Object Type. When a CSO is deleted (FK set to NULL via
DeleteBehaviour.SetNull), these fields showed as dashes in the Activity Detail
view. Add DisplayNameSnapshot and ObjectTypeSnapshot fields alongside the
existing ExternalIdSnapshot, populated at all CSO-to-RPEI linking sites and
centralised in flush methods as defence-in-depth. The header projection now
falls back to snapshots when the live CSO is unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…card (#363)

BuildOutcomeSummary now counts all outcome nodes (root + children) instead
of only roots, matching how Activity-level stats are derived via
FlattenOutcomes. This ensures RPEI outcome chips show the full causal
chain (e.g. Projected + AttributeFlow) rather than just root outcomes.

Also removes the confusing "Total" stat card from the Activity Detail page
since it duplicated RPEI count alongside outcome-type counts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… confirmations (#363)

Previously, successful export confirmations during confirming imports only
updated the activity-level PendingExportsConfirmed counter but did not
create individual RPEIs. This meant the RPEI table showed "CSO Updated"
(from the normal import phase) without any ExportConfirmed outcome chip.

Now each successfully confirmed export gets an RPEI with the ExportConfirmed
sync outcome type, making the confirming import results visible per-row in
the Activity Detail table.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, confirming imports created separate RPEIs for export
confirmation outcomes (ExportConfirmed/ExportFailed), breaking the
one-RPEI-per-CSO rule and producing duplicate rows in the Activity
Detail table.

Now reconciliation looks up the existing import RPEI for each CSO and
merges outcomes onto it. If no import RPEI exists (pure confirming
import with no attribute changes), a new RPEI is created as before.

Key changes:
- Add BulkUpdateRpeiOutcomesAsync repository method for updating
  OutcomeSummary and error fields on already-persisted RPEIs while
  inserting new sync outcome nodes
- Build CSO-to-RPEI lookup from persisted import RPEIs and pass to
  ReconcilePendingExportsAsync
- Reconciliation merges ExportConfirmed/ExportFailed outcomes onto
  existing RPEIs via SyncOutcomeBuilder.AddRootOutcome, then
  bulk-updates the modified RPEIs after the reconciliation loop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… changed (#363)

Confirming imports (import after export) transition CSOs from PendingProvisioning
to Normal status. When imported attribute values are identical to existing values,
hasAttributeChanges is false but statusTransitioned is true, causing a CsoUpdated
outcome to be incorrectly recorded. This test proves the bug by importing 3 CSOs
with identical attribute values and asserting no CsoUpdated outcomes exist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…363)

The PendingProvisioning → Normal status transition during confirming imports
was triggering a CsoUpdated sync outcome even when no attribute values changed.
Added hasAttributeChanges guard to the outcome recording condition so status-only
transitions create the RPEI slot (for reconciliation to merge ExportConfirmed
onto) without misrepresenting the outcome.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d CSOs (#363)

When a joined CSO is obsoleted during sync, produce one RPEI with
ObjectChangeType.Disconnected and both Disconnected and CsoDeleted as
sibling root outcomes, instead of two separate RPEIs. This follows the
one-RPEI-per-CSO rule from the outcome graph design.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
)

Import-phase RPEIs no longer record CsoDeleted outcomes since the CSO is
only marked Obsolete (not actually deleted). The CsoDeleted outcome is now
exclusive to sync-phase RPEIs where the CSO is actually deleted.

- Remove CsoDeleted outcome from full import and delta import deletion paths
- Add early SnapshotCsoDisplayFields call before CSO reference is nulled
- Supplement stats with RPEI-based ObjectChangeType.Deleted count for imports
- Add context-aware change type labels (Deletion Detected vs Deletion Processed)
- Hide CsoDeleted outcome filter chip for import activities (no matching RPEIs)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents the relationship between ObjectChangeType, SyncOutcomeType,
stat box labels, filter chip labels, and row change chips across all
three run profile types (Import, Sync, Export). Includes key design
decisions like import deletions having no CsoDeleted outcome and the
one-RPEI-per-CSO rule with examples.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…#363)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…363)

SyncOutcomeBuilder.AddChildOutcome adds children to both parent.Children
AND rpei.SyncOutcomes (flat list). FlattenOutcomes then recursively
walked Children, yielding child outcomes a second time. This caused
denormalised stats (e.g. TotalAttributeFlows) to be inflated on the
Activity model, while the repository query (which counts DB rows) was
correct — producing a mismatch between the list and detail pages.

Fix: use rpei.SyncOutcomes directly (already flat) instead of
FlattenOutcomes. Update test helper to mirror production behaviour
where all outcomes are in the flat SyncOutcomes list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… and accuracy fixes (#363)

- Fix LOC counts across architecture diagram to match post-#363 codebase
- Add SyncOutcomeBuilder and PostgresDataRepository LOC to diagram
- Correct JimApplication server count from 16 to 17 (includes DriftDetectionService)
- Clarify ISyncPersistence status: bulk SQL delivered but interface never formalised
- Update ISyncEngine extraction estimate from ~3,050 to ~3,330 LOC
- Add RPEI Outcome Graph (#363) progress section
- Note ExportResult rename and ObjectChangeType.Provisioned consolidation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…back double-counting (#363)

Add stat assertions to CsoProjectToMvoTestAsync, MvoAttributeFlowOnNewJoinTestAsync,
and AttributeRecallExpressionWorkflowTests to guard against future stats double-counting
bugs. Fix _allPersistedRpeis accumulation in SyncTaskProcessorBase to only occur when
raw SQL is used, preventing double-counting in the EF fallback test path. Update
HelpersOutcomeSummaryTests for the "Attribute Flow" rename.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…#363)

Pending Export was incorrectly in the "Filter by Change Type" section, which
filters by ObjectChangeType. Since pending exports are outcomes on RPEIs with
other change types (e.g., Joined + PendingExportCreated), clicking the filter
found zero results. Move it to the "Filter by Outcome Type" section where it
correctly matches against OutcomeSummary. Also align the stat box colour from
orange/warning to teal/info to match the outcome chip colour.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document the rationale, phased approach, prerequisites, and considerations
for removing the redundant ObjectChangeType display from the Activity Detail
page once the outcome graph is fully established.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add recursive OutcomeTree/OutcomeTreeNode Razor components that render
the sync outcome graph as an indented tree with CSS connector lines.
Include SyncOutcomes in the repository query, add outcome type icons
to Helpers, and integrate the tree section into the RPEI detail page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JayVDZ and others added 28 commits March 3, 2026 16:33
The raw SQL bulk insert reads the FK property directly, bypassing EF
Core's nav-property-to-FK resolution. The sync processor was only
setting the ConnectedSystemObject nav property, leaving
ConnectedSystemObjectId null in the database. This caused the
Causality Tree to miss the CS/CSO header on sync RPEIs.

All 9 RPEI creation sites in SyncTaskProcessorBase now set both the
nav property and the FK, matching the pattern used by the import
processor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
For provisioned CSOs, the display name snapshot is empty at export time
because attributes are only populated during the confirming import.
Now falls back to the live CSO's DisplayNameOrId property when the
snapshot is null, using the existing model property instead of
duplicating the lookup logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
)

CsoDeleted was incorrectly added as a sibling root outcome alongside
Disconnected. It should be a child, reflecting the causal chain:
CSO is disconnected, then deleted as a consequence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
)

- Page titles now include the CSO object type name (e.g. "User Added to
  Staging" instead of "Object Added to Staging", "Projected User to the
  Metaverse" instead of "Projected New Metaverse Object")
- Collapse cumulative bottom padding from deeply nested last-child nodes
  so the tree's bottom spacing matches the top

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… outcomes (#363)

Disconnected now shows as "CSO Disconnected" instead of falling through
to the default ToString(). Also adds explicit entries for Joined and
Provisioned to avoid relying on the fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…isconnected (#363)

When a CSO is deleted during disconnection processing, the RPEI's
ConnectedSystemObjectId is nulled (FK constraint). The outcome tree now
falls back to the parent Activity's ConnectedSystemId to populate the
CS/RP root node header.

Also renames "Disconnected" display name to "CSO Disconnected" and adds
explicit display names for Joined and Provisioned outcome types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d nodes (#363)

The ::after pseudo-element that hides the vertical branch line below the
last child's connector stub was also hiding the line alongside expanded
attribute tables and nested children. Now uses :has() to only apply the
cover when the node has no nested .outcome-children container.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…vel (#363)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e has children (#363)

When an outcome node has both an expanded attribute table and child
nodes below, the table area now shows a continuation of the branch
line connecting down to the children. Applied via a conditional CSS
class that matches the .outcome-children border positioning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adjusted outcome-children margin-left from 20px to 11px so the
vertical branch line aligns with the centre of the parent's 24px
MudIcon. Updated padding-left, connector stub, and last-child cover
positions to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Captures analysis of ULID, NanoID, TSID, Sqids, and Base62-encoded
GUIDs as potential alternatives, with trade-offs and effort assessment.
Also documents PostgreSQL sequence reuse behaviour for int PKs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tcome tree (#363)

- Change CsoDeleted icon from RemoveCircle to Delete (trash can)
- Centre-align icons with text labels instead of top-aligning with manual offset

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…or (#363)

The provisioning CSO intentionally lacks the ConnectedSystem nav property
(to avoid EF Core tracker conflicts), so TargetEntityDescription was
always null. Resolve the CS name from the export evaluation cache instead.

Add an em dash separator before the CS link in the outcome tree UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rename "MVO Projected" to "Projected" and "MVO Attribute Flow" to
  "Attribute Flow"
- Move Joined target description inline: "Joined — to Amelia Sullivan"
- Add em dash separator for all inline target descriptions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Import-phase deletion detection and sync-phase drift correction now
record structured outcomes in the causality tree, closing the remaining
gaps where ObjectChangeType had no corresponding outcome.

- Add DeletionDetected and DriftCorrection to SyncOutcomeType enum
- Record outcomes in import and sync processors
- Update stats derivation to use outcome-based counts with legacy fallback
- Add display names, icons, and colours for new outcome types
- Add outcome filter chips for DeletionDetected (import) and DriftCorrection (sync)
- Improve RPEI detail page headings with MVO type context
- Update plan doc mapping tables, examples, and stats derivation section
- Add unit tests for new outcome types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tity-prefixed naming (#363)

Rename all stat/outcome labels to consistently prefix with CSO/MVO entity context
(e.g. "Projected" → "CSOs Projected", "Attribute Flow" → "MVOs with Attribute Flow").
Separate Disconnected and Deleted into individual stat boxes. Fold Out of Scope
outcome filter into CSO Disconnected chip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… example

- Upgrade Export Evaluation (PendingExport) from Partial to Done — 67
  dedicated unit tests now exist across three files in
  JIM.Worker.Tests/OutboundSync/
- Fix integration test example to use Run-IntegrationTests.ps1 entry
  point instead of directly invoking scenario scripts with -ApiKey

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…363)

Phase 7 of the RPEI Outcome Graph feature. The outcome type filter and
per-row outcome chips provide a strict superset of Change Type information,
making the Change Type filter section and table column redundant.

- Remove "Filter by Change Type" chip set and "Change Type" table column
- Remove changeTypeFilter parameter from repository/application stack
- Remove dead helper methods (GetChangeTypesForRunType, GetStatCountForChangeType, GetChangeTypeDisplayName)
- Relocate "Unchanged" informational chip to outcome filter section
- Fix pre-existing test assertions for entity-prefixed display names

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…aces badge

- Add "What Makes JIM Different" section with capabilities comparison table
- Replace "Getting Started" with "Quick Start" split into Admins (Deploy) and Developers (Contribute)
- Add concrete deployment steps for admins with SSO prerequisite
- Add GitHub Codespaces one-click badge and devcontainer instructions for developers
- Add Codespaces badge to top badge row

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove Entitlement Management from scenarios as it is not yet supported.
Update planned connectors list with Entra ID / Microsoft Graph API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Google Cloud Identity and AWS Identity Center/Cognito to the list
of supported identity providers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tity-prefixed naming (#363)

- Rename "CSO Projected" to "MVO Projected" (projection creates an MVO)
- Rename "Provisioned" to "CSO Provisioned" (consistent entity prefix)
- Rename "CSO Exports" to "CSOs Exported" (consistent plural past-tense)
- Add TotalProvisioned stat across full pipeline: model, repository,
  worker, API DTO, stat boxes, filter chips, sidebar, and list chips
- Add database migration for Activity.TotalProvisioned column
- Reorder stat boxes and sidebar to follow causal sync flow:
  Joined → Projected → Attribute Flow → Provisioned → Pending Exports
  → Disconnected → Deleted → Drift Corrected
- Update plan doc mapping table with corrected labels and Provisioned row
- Add unit tests for MVO Projected and CSO Provisioned display names

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…#363)

- Add DeletionDetected to import outcome tests (was missing)
- Add Provisioned to sync nested tree test chain
- Split sync tests into distinct chains: join, disconnection, drift
- Add multi-system provisioning stats test
- Move DriftCorrection out of RPEI-only stats (it has an outcome type)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 4b: 10 NUnit repository-level tests covering outcome-based stats
derivation, legacy fallback, nested outcome trees, disconnections, drift
correction, exports, multi-system provisioning, RPEI-only types, error
counting, and OutcomeSummary header projection.

Add PowerShell assertion helpers (Assert-ActivityOutcomeStats,
Assert-ActivityItemsHaveOutcomeSummary) and outcome validations to
Scenario 1 at Joiner, Mover, and Leaver steps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Design document for adding `-Scenario All` to Run-IntegrationTests.ps1,
enabling all implemented scenarios to run in a single invocation with
lightweight resets between scenarios.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All phases (1-7 + 4b) are implemented and validated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@JayVDZ JayVDZ merged commit 7ed08ec into main Mar 4, 2026
5 checks passed
@JayVDZ JayVDZ deleted the feature/rpei-outcome-graph-phase1 branch March 4, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant