# Issue 1099 Design: Rollup-Derived System Context

## Scope

This PR fixes the `#1099` platform defect narrowly: single-record rollup
recomputes may surface parent validation failures as opaque 500s, and metadata
has no explicit way to identify the platform-owned derived recompute save.

The merge unit is intentionally narrow:

- owner layer: `L0` records, rollup, formula-global, and REST/write-error
  classification substrate;
- affected path: `Insert` / `Update` / `Delete` post-save rollup fanout through
  `RollupService.ProcessRollups -> DerivedWriter.UpdateDerived`;
- validation surface: existing records-owned update lifecycle, including
  before-save automation and Validation Rules;
- out of scope: bulk-insert parent validation projection, rollup storage
  strategy, new rollup calculation authorities, tenant overlays, CRM-only
  branches, and unchanged `$Profile`-global compatibility for pre-existing
  metadata. That broader compatibility question is tracked separately in
  `#1129`.

## Failed-Attempt Context

PR `#915` tried to solve the broader `#517` bulk rollup parent validation
projection problem. The useful constraints from that attempt are:

- do not invent a second in-memory rollup calculator;
- do not introduce a new before-save preview mode for rollup writes;
- do not make a projected parent total an authority unless the real post-write
  storage contract is updated in the same transaction;
- keep implementation blocked on storage / bulk projection decisions when those
  decisions are the actual prerequisite.

`#1099` does not require any of that bulk projection machinery. The failing path
already uses the authoritative live rollup calculation and the existing
`UpdateDerived -> Update` lifecycle. The bug is the synthetic session and error
classification around that existing path.

## Current State

Before this PR, `RollupService.ProcessRollups` calculated the new aggregate,
then called:

```text
DerivedWriter.UpdateDerived(parentObject, parentID, updates, rollupSystemUser())
```

`UpdateDerived` sets `SystemContext` and the private derived-recompute marker,
then delegates to the normal records `Update` lifecycle. Validation Rules build
their formula globals from the supplied `UserSession` via the shared
`formula.BuildFormulaUserContext` path.

The old `rollupSystemUser()` helper only set `ID`, `Name`, and `Email`, with no
separate formula surface for the fact that the write is platform-owned. As a
result, metadata could only infer intent through `$Profile`/`$User` session
globals, which either made the actor look like a blank-profile user or tempted
the runtime to impersonate an admin profile.

If that validation rule fails, the error is wrapped by rollup/post-save hook
code as `InternalError`, and REST's intentionally internal-first classifier
returns `500 INTERNAL_ERROR`.

## Design Decisions

### 1. Preserve The Existing Derived-Write Lifecycle

Do not change `UpdateDerived` order of execution. Rollup-derived parent updates
continue to run through the existing records-owned update path. This preserves
the current source of truth for validation, formula recompute, before-save
automation, audit, outbox, and post-update rollup fanout.

### 2. Use A Greenfield-Safe Internal Actor And Marker

Rollup-owned derived writes should ask the records-owned writer for the
platform internal actor session. The rollup engine may depend on "there is an
internal actor", but it must not compile profile ID/name/admin facts into the
analytics package, and it must not require a persisted `User` row for
`constants.SystemUserID`.

`PersistenceService.LoadSystemUserSession` resolves:

- the stable synthetic actor ID/email used for attribution.

This keeps greenfield bootstrap safe because public-core does not seed a
`User/system` row. The actor remains non-privileged: it does not receive
`ProfileID`, `ProfileName`, or `IsSystemAdmin` fields for permission,
record-type, validation-super-user, flow-super-user, or admin-bypass checks.

`formula.BuildFormulaUserContext` remains the single construction path for
`$User`, `$Profile`, and `$UserRole`. For normal sessions it projects
`$Profile.Id`, `$Profile.Name`, and `$User.ProfileId` from the same
`UserSession` facts that permission, validation super-user detection, flow
formula evaluation, and record-type logic read. Synthetic platform actors do
not receive a second formula-only profile identity. This preserves one source
of truth for "is this actor an admin?" inside the save lifecycle.

The records-owned formula context also injects
`$SystemContext.IsDerivedRecompute` for `UpdateDerived` writes. Metadata that
needs to distinguish platform-owned rollup recomputes can use that dedicated
platform surface. Existing profile-gated metadata that should accept
platform-owned recomputes must be updated to include this marker explicitly;
the platform does not make those writes look like a real or formula-only
System Administrator.

The original broader `#1099` acceptance language required pre-existing
`$Profile.Id` / `$Profile.Name` gates to pass unchanged. That would require a
separate compatibility decision, now split to `#1129`, because the attempted
formula-only admin projection created a second profile/admin source of truth.
That follow-up decision is recorded in
`docs/design/issue-1129-derived-recompute-profile-global-contract.md`.

This is not a CRM object branch or tenant policy. It is a platform internal
write identity for a save lifecycle the platform already chose to run in
`SystemContext`.

### 3. Preserve Validation Classification Before Generic Wrapping

Keep REST's internal-first ordering unchanged. That ordering protects real
infrastructure failures where transaction rollback or IO wrappers include a
business error in the cause chain.

Instead, source wrappers around rollup fanout should avoid converting direct
validation-class errors into `InternalError`:

- `ValidationRuleError`
- `ValidationError`
- FLS-denied write errors

If an error is already an `InternalError`, leave it internal. This keeps genuine
infra failures as 500 while allowing parent validation-rule failures from the
records save lifecycle to reach `WriteError` as 400
`FIELD_CUSTOM_VALIDATION_EXCEPTION`.

## Non-Goals

- No new rollup aggregation implementation.
- No new dry-run or preview persistence mode.
- No bulk insert projection behavior.
- No Salesforce CRM object-name checks.
- No tenant-specific metadata, seed data, or overlay payload.

## Verification Plan

- Unit-test that `ProcessRollups` loads the records-owned system session and
  passes that loaded session to `UpdateDerived` without a persisted
  `User/system` row or admin/profile identity.
- Unit-test that `UpdateDerived` injects `$SystemContext.IsDerivedRecompute`
  while leaving behavior-bearing and formula-visible permission/profile fields
  non-privileged.
- Unit-test that derived recompute metadata can use
  `$SystemContext.IsDerivedRecompute` to allow platform-owned rollup writes,
  and that plain `$Profile` gates still fail for the blank-profile internal
  actor.
- Unit-test that direct validation-class rollup failures are not wrapped into
  `InternalError`, while true internal errors remain internal.
- Run focused backend tests for `analytics`, `records`, `errors`, and REST error
  response coverage, then run broader backend verification as time allows.
