# Issue 802 Flow Typed Literal Resolution Design

## Goal

Fix and pin the generic L0 Flow runtime contract for Salesforce-style typed
literal wrappers in record update assignments.

When Flow metadata contains an assignment value such as:

```json
{"stringValue":"Candidate"}
```

the runtime must pass the literal payload `"Candidate"` to the persistence
layer. It must not reinterpret the unwrapped string as a variable name,
ContextData key, dotted reference, condition result, or truthy boolean.

## Background

Issue #802 was exposed while validating a scheduled AutoLaunchedFlow through the
public API against local TiDB. The stored Flow metadata still contained a valid
typed string literal, but execution attempted to persist boolean `true` into a
picklist field. The picklist validator correctly rejected that value.

The failure was not caused by corrupted Flow metadata, a bad picklist validator,
or tenant-specific pack content. The generic Flow executor resolved a typed
literal too late in the pipeline: after unwrapping `{"stringValue":"..."}`, it
continued to treat the resulting string as a possible ContextData reference. If
ContextData already contained a key matching the literal string, for example a
Decision outcome named `"Candidate"` with value `true`, recordUpdate wrote that
ContextData value instead of the authored literal.

The same risk applies to all concrete typed literal wrappers:

- `stringValue`
- `numberValue`
- `booleanValue`
- `dateValue`
- `dateTimeValue`

`elementReference` is intentionally different. It is a reference wrapper and
must continue resolving through ContextData, system variables, and nested record
paths.

## Boundary

This is an L0 runtime bug. The fix must stay metadata-driven and app-neutral.

Allowed:

- Improve the generic Flow value resolver.
- Add synthetic Flow metadata tests that use neutral object and field names.
- Keep picklist validation strict.
- Preserve Salesforce-style typed value wrapper semantics.

Not allowed:

- Special-case any concrete app object or tenant custom field.
- Accept invalid picklist values to mask runtime corruption.
- Add business picklist values, workflow stages, or tenant metadata to platform
  code.
- Treat scheduled Flow as a separate interpreter if the shared executor path can
  express the contract.

## Root Cause

The problematic execution path is:

1. `executeRecordUpdate` reads `models.FlowInputAssignment.Value`.
2. It calls `resolveValueCtxWithPresence`.
3. `resolveValueCtxWithPresence` calls `extractTypedValue`.
4. `extractTypedValue` unwraps the map and returns the payload.
5. The resolver sees the unwrapped payload is a string.
6. Before the fix, the resolver could continue through reference lookup logic
   for that string.
7. If `FlowInterview.ContextData` had a matching key, that ContextData value won
   over the authored typed literal.

The core missing state was whether the input was a concrete typed literal before
unwrapping. Once that provenance is lost, `"Candidate"` from a literal wrapper
and `"Candidate"` as a bare reference candidate are indistinguishable.

## Design

`extractTypedValue` returns two values:

```go
func extractTypedValue(v interface{}) (interface{}, bool)
```

The second return value means `wasLiteral`.

- `wasLiteral=true` for `stringValue`, `numberValue`, `booleanValue`,
  `dateValue`, and `dateTimeValue` wrappers with non-nil payloads.
- `wasLiteral=false` for `elementReference`.
- `wasLiteral=false` for non-map values and unknown map shapes.

`resolveValueCtxWithPresence` must preserve that provenance:

```go
value, wasLiteral := extractTypedValue(value)
ref, ok := value.(string)
if !ok {
    return value, true
}
if wasLiteral {
    return value, true
}
```

This makes concrete typed literal wrappers authoritative. A literal remains a
literal even if:

- its string matches a ContextData key;
- its string resembles a variable name;
- its string contains dots;
- its string equals `"true"` or `"false"`;
- its value is a number/date/datetime represented as a JSON string.

`elementReference` keeps `wasLiteral=false`, so reference resolution remains
available for wrappers such as:

```json
{"elementReference":"$Record.OwnerId"}
```

This keeps the resolver split simple:

- typed literal wrappers produce values;
- `elementReference` wrappers produce references;
- bare strings preserve existing legacy behavior.

## Test Strategy

The regression suite needs two layers because the bug is a resolver bug that
manifested through recordUpdate.

### Resolver Contract Test

`TestResolveValueCtxWithPresence_TypedLiteralsDoNotResolveContextNameCollisions`
constructs a `FlowInterview` whose `ContextData` intentionally contains keys
that match typed literal payloads:

- `"Candidate"` maps to `true`;
- `"42"` maps to another string;
- `"false"` maps to another string;
- date and datetime strings map to unrelated values.

Each concrete typed wrapper must resolve to its payload, not to the matching
ContextData entry.

### recordUpdate Regression Test

`TestRecordUpdate_InputAssignmentStringValueFromHydratedMetadataWritesLiteral`
hydrates Flow metadata from JSON to mirror the API/DB shape:

```json
{
  "recordUpdates": [{
    "name": "Update_Target",
    "object": "XRuntimeTarget__c",
    "inputReference": "$Record",
    "inputAssignments": [
      {"field": "Status__c", "value": {"stringValue": "Candidate"}}
    ],
    "connector": {"targetReference": "Done"}
  }]
}
```

The interview includes both:

- `$Record`, the update target;
- `Candidate: true`, the collision that previously corrupted the write.

The assertion is made at the persistence boundary. The captured update payload
must contain:

```go
updates["Status__c"] == "Candidate"
```

This test proves that recordUpdate preserves the literal through the same value
resolution path used by scheduled and non-scheduled Flow execution.

## Verification

Focused verification:

```bash
(cd backend && go test ./internal/features/automation -run 'TestRecordUpdate_InputAssignmentStringValueFromHydratedMetadataWritesLiteral|TestResolveValueCtxWithPresence_TypedLiteralsDoNotResolveContextNameCollisions' -count=1)
```

Requested package verification:

```bash
env NEXUSCRM_OFFICIAL_APP_PACKS_ROOT=/path/to/nexuscrm-app-packs \
  bash -lc 'cd backend && go test ./internal/features/automation ./pkg/validation'
```

Repository hygiene:

```bash
git diff --check
```

The validation package is included because issue #802 surfaced as a picklist
validation failure. The fix must not relax validation. Validation should still
reject genuinely invalid picklist payloads.

The full automation package command requires `NEXUSCRM_OFFICIAL_APP_PACKS_ROOT`
to point at a local checkout of `nexuscrm-app-packs`, because some integration
tests load official pack fixtures during runtime setup.

## Confidence Checklist

- The root cause is at the generic resolver boundary, before validation.
- The design carries literal provenance instead of inferring it from the
  unwrapped value.
- The fix covers every concrete typed literal wrapper, not only string values.
- `elementReference` keeps reference semantics.
- The recordUpdate test hydrates metadata from JSON, so it exercises the same
  map shape produced by API/DB metadata storage.
- Tests use synthetic metadata and do not encode tenant or app behavior.
- The persistence boundary assertion catches the original corruption before a
  validator can obscure it.
- Picklist validation remains strict and out of the fix path.

## Implementation Status

Implemented by PR #808, `Fix Flow typed literal recordUpdate resolution`.

The implementation changed:

- `backend/internal/features/automation/executor_helpers.go`
- `backend/internal/features/automation/executor_helpers_test.go`
- `backend/internal/features/automation/executor_record_ops_test.go`

The PR was merged on 2026-05-14 and closed issue #802.
