402 lines
16 KiB
Markdown
402 lines
16 KiB
Markdown
# Response Mapping Model
|
|
|
|
**Step 17.0 design document — established 2025**
|
|
|
|
This document records the canonical decisions about how action responses flow into
|
|
component state in the Conductor Preview runtime. It is the authoritative
|
|
reference for Steps 17.x implementation.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Why `project.bindings` is canonical](#1-why-projectbindings-is-canonical)
|
|
2. [Why actions own runtime responses](#2-why-actions-own-runtime-responses)
|
|
3. [Why `actions.<id>.response` is a virtual namespace](#3-why-actionsidresponse-is-a-virtual-namespace)
|
|
4. [Source path grammar](#4-source-path-grammar)
|
|
5. [Target path grammar](#5-target-path-grammar)
|
|
6. [Trigger semantics](#6-trigger-semantics)
|
|
7. [Label runtime-value behaviour](#7-label-runtime-value-behaviour)
|
|
8. [Compatibility — existing `onClick` bindings](#8-compatibility--existing-onclick-bindings)
|
|
9. [Deprecation of `action.responseMapping`](#9-deprecation-of-actionresponsemapping)
|
|
10. [Step 17.1 scope](#10-step-171-scope)
|
|
11. [Deferred capabilities](#11-deferred-capabilities)
|
|
12. [Required diagnostics](#12-required-diagnostics)
|
|
|
|
---
|
|
|
|
## 1. Why `project.bindings` is canonical
|
|
|
|
Two mechanisms could in principle move action response data into components:
|
|
|
|
| Mechanism | Location | Status |
|
|
|---|---|---|
|
|
| `action.responseMapping[]` | Inside each `Action` object | **Deprecated** |
|
|
| `project.bindings[]` | Top-level project array | **Canonical** |
|
|
|
|
`project.bindings` was chosen as the sole canonical mechanism for the following
|
|
reasons:
|
|
|
|
**Single locus of data-flow declarations.** All data movement in a Conductor
|
|
project — between components, from action responses, from variables — is
|
|
expressed in one flat array. Keeping response routing there avoids a split
|
|
where "inbound response routing" lives inside actions while all other data
|
|
movement lives at the project level.
|
|
|
|
**Symmetry with component-sourced bindings.** A binding from a TextInput value
|
|
to a Label uses the same structure as a binding from an action response to a
|
|
JsonViewer. Same schema, same Inspector rendering, same future transform field.
|
|
|
|
**Decoupled from action definition.** An action definition describes *how to
|
|
call* an endpoint (URL, method, auth). Where the response goes is a layout
|
|
concern, not an API concern. Separating them means a single action can feed
|
|
multiple targets without duplicating the action.
|
|
|
|
**Better Inspector and diagnostics surface.** A flat array of bindings is easy
|
|
to enumerate, validate cross-references for, and display in a panel.
|
|
Distributing response mapping rules across individual action objects makes
|
|
completeness checks harder.
|
|
|
|
**Incremental migration path.** Existing documents that carry
|
|
`action.responseMapping` values remain valid under the schema; they are simply
|
|
ignored at runtime. New documents use only `project.bindings`.
|
|
|
|
---
|
|
|
|
## 2. Why actions own runtime responses
|
|
|
|
Although `project.actions` is a design-time array of REST action definitions,
|
|
the Preview runtime must store ephemeral per-action state: the most recent
|
|
response, loading flag, and error. This state is:
|
|
|
|
- **Not stored in canonical project JSON.** The project document is the
|
|
design-time source of truth. Mutating it with runtime values would corrupt
|
|
the save/load round-trip.
|
|
- **Keyed by action ID.** Even though actions are stored as an ordered array
|
|
in the project document, the runtime resolves them by `id` for O(1) lookup.
|
|
- **Discarded on navigation or refresh.** Preview state is ephemeral React
|
|
state (`useState`).
|
|
|
|
The proposed runtime shape (to be implemented in Step 17.1) is:
|
|
|
|
```ts
|
|
type ActionRuntimeState = {
|
|
response?: ProxyResponse; // the full ProxyResponse envelope
|
|
loading?: boolean;
|
|
error?: string;
|
|
};
|
|
|
|
type ActionRuntimeStateMap = Record<string, ActionRuntimeState>;
|
|
```
|
|
|
|
This is a parallel structure to the existing `componentState` map in
|
|
`usePreviewRuntime`. In Step 17.1 it will be introduced alongside it.
|
|
|
|
> **Step 17.0 note:** This runtime shape is defined here for design purposes.
|
|
> It is not implemented in Step 17.0. The current runtime continues to store
|
|
> the full `ProxyResponse` envelope directly in `componentState[id].value`.
|
|
|
|
---
|
|
|
|
## 3. Why `actions.<id>.response` is a virtual namespace
|
|
|
|
In binding source and target expressions, paths of the form:
|
|
|
|
```
|
|
actions.<actionId>.response
|
|
actions.<actionId>.response.body
|
|
actions.<actionId>.response.body.<field>
|
|
```
|
|
|
|
refer to runtime action response data. This is a **virtual namespace** because:
|
|
|
|
1. `project.actions` is an **array** in the canonical document, not an object
|
|
keyed by ID. At design time, `actions.action_httpbin` does not exist as a
|
|
JSON key path.
|
|
|
|
2. The runtime resolves `<actionId>` via `Array.find((a) => a.id === actionId)`,
|
|
not by property access.
|
|
|
|
3. The path is only meaningful during Preview execution. It has no value in the
|
|
persisted project document.
|
|
|
|
The path syntax is chosen for readability and symmetry with `components.<name>.*`
|
|
target paths. It is not JSONPath; it is a **Conductor runtime dot-path** with
|
|
a fixed `actions.<id>.response[.body[.<fields>]]` prefix.
|
|
|
|
---
|
|
|
|
## 4. Source path grammar
|
|
|
|
Step 17.1 will support the following source path forms for action-response
|
|
bindings:
|
|
|
|
```
|
|
actions.<actionId>.response
|
|
actions.<actionId>.response.body
|
|
actions.<actionId>.response.body.<field>
|
|
actions.<actionId>.response.body.<nested>.<field>
|
|
```
|
|
|
|
### Resolution rules
|
|
|
|
| Path | Resolved value |
|
|
|---|---|
|
|
| `actions.<id>.response` | The full `ProxyResponse` envelope `{ ok, status, statusText, headers, body, durationMs }` |
|
|
| `actions.<id>.response.body` | `ProxyResponse.body` — parsed JSON object or text string |
|
|
| `actions.<id>.response.body.<field>` | `ProxyResponse.body.<field>` where `body` is an object |
|
|
| `actions.<id>.response.body.<nested>.<field>` | Deep dot-path traversal on `body` |
|
|
|
|
If a field in the path does not exist on the response body, the resolved value
|
|
is `undefined`. Step 17.1 will surface a runtime warning in that case; it
|
|
will not throw.
|
|
|
|
### Deferred source path forms
|
|
|
|
The following are explicitly deferred and must not be inferred from the above:
|
|
|
|
- `actions.<id>.response.headers.<header>` — reserved; not in Step 17.1
|
|
- `actions.<id>.response.status` — reserved; not in Step 17.1
|
|
- Wildcard `*` segments
|
|
- Filter expressions `[?(...)]`
|
|
- Array index syntax `[0]`, `[-1]`
|
|
- Keys containing literal `.` characters
|
|
|
|
---
|
|
|
|
## 5. Target path grammar
|
|
|
|
Step 17.1 will support only:
|
|
|
|
```
|
|
components.<componentName>.value
|
|
```
|
|
|
|
### Semantic enforcement
|
|
|
|
- `<componentName>` must match `component.name` (not `component.id`) in the
|
|
project's page components, consistent with the existing binding model.
|
|
- The property segment **must be** `.value`. The Inspector must emit a warning
|
|
for targets with any other property segment (e.g. `.data`, `.message`,
|
|
`.label`). The runtime must not silently remap these to `.value`.
|
|
|
|
### Property segment rationale
|
|
|
|
`.value` is the single runtime-mutable property exposed by `ComponentRuntimeState`
|
|
for display purposes. Until per-property runtime targeting is designed:
|
|
|
|
- `components.<name>.value` is valid.
|
|
- `components.<name>.data`, `.message`, `.label`, `.text`, etc. are unsupported.
|
|
They must produce an Inspector diagnostic and a runtime warning.
|
|
|
|
### Deferred target forms
|
|
|
|
- `variables.<variableName>` — variable runtime support is deferred
|
|
- `components.<name>.label` — Label runtime-value override is deferred to Step 17.1
|
|
(see §7)
|
|
- Per-property routing for non-`.value` segments
|
|
|
|
---
|
|
|
|
## 6. Trigger semantics
|
|
|
|
### Canonical trigger for action-response bindings: `onSuccess`
|
|
|
|
```json
|
|
{ "trigger": "onSuccess" }
|
|
```
|
|
|
|
`onSuccess` fires when the action completes and `ProxyResponse.ok === true`.
|
|
|
|
This is the trigger all new action-response bindings should use.
|
|
|
|
### Why not `onChange` (the schema default)?
|
|
|
|
The schema's `binding.trigger` default is `"onChange"`. This default is
|
|
appropriate for component-to-component data flow (e.g. a TextInput value driving
|
|
a downstream Label). It is not appropriate for action-response bindings because:
|
|
|
|
- Action responses are not "change" events on a component; they are completion
|
|
events on an async operation.
|
|
- `"onChange"` implies reactive/continuous behaviour; action execution is
|
|
discrete and user-triggered (or lifecycle-triggered).
|
|
|
|
The `"onChange"` default is therefore **not the correct default for
|
|
action-response bindings**. The Inspector should surface a warning when
|
|
`"onChange"` is found on a binding whose source is `actions.*`.
|
|
|
|
### Deferred triggers
|
|
|
|
| Trigger | Status |
|
|
|---|---|
|
|
| `onSuccess` | **Step 17.1** |
|
|
| `onError` | Deferred |
|
|
| `onLoad` | Deferred |
|
|
| `onChange` | Component-change bindings only; deferred |
|
|
| Chained actions | Deferred |
|
|
|
|
---
|
|
|
|
## 7. Label runtime-value behaviour
|
|
|
|
**Current (Step 17.0):** Label renders `properties.label` only. It has no
|
|
`runtimeState` wiring. Runtime action responses cannot update a Label.
|
|
|
|
**Planned (Step 17.1):**
|
|
|
|
- `properties.label` — design-time / configured text. Displayed when no
|
|
runtime value is set.
|
|
- `componentState[id].value` — runtime display value. If set (not `undefined`),
|
|
it overrides `properties.label` in the Preview renderer.
|
|
- A binding of the form:
|
|
```json
|
|
{
|
|
"source": "actions.<id>.response.body.<field>",
|
|
"target": "components.<labelName>.value",
|
|
"trigger": "onSuccess"
|
|
}
|
|
```
|
|
writes the resolved field value (coerced to string for display) into
|
|
`componentState[labelId].value`.
|
|
- **Runtime mutations must not modify canonical project JSON.** The override
|
|
is ephemeral; it lives only in `useState` for the Preview session.
|
|
|
|
This makes Label consistent with JsonViewer, which already reads
|
|
`runtimeState.value` in `PreviewComponent.tsx`.
|
|
|
|
---
|
|
|
|
## 8. Compatibility — existing `onClick` bindings
|
|
|
|
Steps 15 and 16 produced examples and docs using:
|
|
|
|
```json
|
|
{ "trigger": "onClick" }
|
|
```
|
|
|
|
on action-response bindings. These bindings fire when the button's `onClick`
|
|
event causes the action to execute.
|
|
|
|
**Step 17.0 treatment:** `"onClick"` bindings continue to work exactly as they
|
|
do today. The runtime (`usePreviewRuntime`) fires all bindings whose source
|
|
matches the action ID, regardless of their `trigger` value.
|
|
|
|
**Recommended migration:** New bindings should use `"onSuccess"` rather than
|
|
`"onClick"`. The Inspector may surface an info-level diagnostic recommending
|
|
migration on bindings that use `"onClick"` as the trigger on an
|
|
`actions.*` source. This is not yet implemented.
|
|
|
|
**Important:** `"onClick"` on an action-response binding is **legacy**, not
|
|
invalid. Documents using it will not fail schema validation and will not be
|
|
broken by Step 17.1.
|
|
|
|
---
|
|
|
|
## 9. Deprecation of `action.responseMapping`
|
|
|
|
`action.responseMapping` is **deprecated as of Step 17.0**.
|
|
|
|
| Property | Status |
|
|
|---|---|
|
|
| Present in schema | Yes — retained for backward compatibility |
|
|
| `x-deprecated: true` annotation | Added in Step 17.0 |
|
|
| Schema `description` | Updated to say `project.bindings` is canonical |
|
|
| TypeScript `@deprecated` JSDoc | Added in Step 17.0 |
|
|
| TS type narrowed | Yes — changed from `unknown[]` to `ResponseMappingRule[]` |
|
|
| Executed by Preview runtime | **No** — never was, never will be |
|
|
| Executed by backend proxy | **No** — never was, never will be |
|
|
| Shown by Inspector | **No** |
|
|
| Removal timeline | Not scheduled; retained for backward compatibility only |
|
|
|
|
**No new examples should use `action.responseMapping`.** Existing examples
|
|
(`valid-full.json`) that carry it are not updated; they remain valid because
|
|
the field is optional and the schema accepts (but ignores) its contents.
|
|
|
|
---
|
|
|
|
## 10. Step 17.1 scope
|
|
|
|
Step 17.1 will implement execution of `project.bindings` for
|
|
action-response-to-component data movement. Specifically:
|
|
|
|
1. **`ActionRuntimeStateMap`** — introduce alongside the existing
|
|
`componentState` map in `usePreviewRuntime`. After each proxy call,
|
|
write the `ProxyResponse` into `actionState[actionId].response`.
|
|
|
|
2. **Source path resolution** — implement dot-path traversal for
|
|
`actions.<id>.response`, `.response.body`, and `.response.body.<field...>`.
|
|
|
|
3. **Binding execution loop** — after a successful proxy call, iterate
|
|
`project.bindings` whose `trigger` is `"onSuccess"` (or `"onClick"` for
|
|
legacy compatibility) and whose `source` parses to the completed action ID.
|
|
Resolve the source path against `actionState[actionId].response`. Write the
|
|
resolved value to `componentState[targetId].value`.
|
|
|
|
4. **Label renderer update** — make `LabelRenderer` read `runtimeState?.value`
|
|
as an override over `properties.label`, consistent with `JsonViewerRenderer`.
|
|
|
|
5. **Inspector diagnostics** — add warnings for:
|
|
- Unsupported target property segments (anything other than `.value`)
|
|
- `"onChange"` trigger on action-response bindings (recommend `"onSuccess"`)
|
|
- Bindings whose source action ID does not exist
|
|
- Bindings whose target component name does not exist
|
|
|
|
6. **`valid-response-mapping-basic.json`** — the example created in Step 17.0
|
|
becomes the primary Step 17.1 integration test document.
|
|
|
|
---
|
|
|
|
## 11. Deferred capabilities
|
|
|
|
The following are explicitly out of scope until separately designed and scheduled:
|
|
|
|
| Capability | Reason deferred |
|
|
|---|---|
|
|
| `onError` trigger | Requires error-state routing design |
|
|
| `onLoad` trigger | Requires page lifecycle event system |
|
|
| Component-change data movement (`onChange`) | Requires reactive binding engine |
|
|
| Chained actions | Requires action dependency graph |
|
|
| Variable runtime support | Requires variable state store |
|
|
| Variable template interpolation in binding paths | Depends on variable runtime |
|
|
| Wildcard `*` in source paths | JSONPath-style engine not scoped |
|
|
| Filter expressions in source paths | Same |
|
|
| Array-index syntax `[0]` | Same |
|
|
| Keys with literal `.` in name | Requires path quoting syntax |
|
|
| `components.<name>.label` as writable target | Deferred to Step 17.1 label work |
|
|
| Any target property other than `.value` | Deferred to per-property routing design |
|
|
| Dropdown or Table runtime population | Separate step |
|
|
| `ComponentType` / `FullComponentType` cleanup | Separate step |
|
|
| Component-level `bindings[]` array | Schema field exists but is not wired |
|
|
| Duplicate component-name detection at save time | Future validation step |
|
|
| Transform expression execution | Deferred (`binding.transform` field retained) |
|
|
| Authentication credential injection | Separate step |
|
|
| `action.responseMapping` execution | Permanently deferred; field deprecated |
|
|
|
|
---
|
|
|
|
## 12. Required diagnostics
|
|
|
|
The following diagnostics must be implemented by Step 17.1. They are listed
|
|
here so the Inspector and runtime can be built consistently against this spec.
|
|
|
|
### Inspector (static, design-time)
|
|
|
|
| Condition | Severity | Message guidance |
|
|
|---|---|---|
|
|
| Binding `source` references action ID that does not exist in `project.actions` | Warning | "Source references action `<id>` which does not exist." |
|
|
| Binding `target` references component name that does not exist on any page | Warning | "Target references component `<name>` which does not exist." |
|
|
| Binding `target` uses property segment other than `.value` | Warning | "Target property `<segment>` is not supported. Use `.value`." |
|
|
| Two or more components share the same `name` on a page | Warning | "Duplicate component name `<name>`. Binding resolution is ambiguous." |
|
|
| `trigger: "onChange"` on an `actions.*` source binding | Info | "Consider using `onSuccess` for action-response bindings." |
|
|
| `trigger: "onClick"` on an `actions.*` source binding | Info | "Legacy trigger. Consider migrating to `onSuccess`." |
|
|
| `action.responseMapping` is non-empty | Info | "Use `project.bindings` instead. `responseMapping` is deprecated and not executed." |
|
|
|
|
### Runtime (execution-time, Preview only)
|
|
|
|
| Condition | Behaviour |
|
|
|---|---|
|
|
| Source path resolves to `undefined` (field not on body) | Write `undefined` to target; emit console warning |
|
|
| Target component not found at runtime | Skip write; emit console warning |
|
|
| `ProxyResponse.ok === false` and trigger is `onSuccess` | Do not fire binding; optionally fire `onError` bindings (deferred) |
|
|
| Action ID in binding source does not match any executing action | Skip silently |
|