fix: tighten conditional parent tab allowlist + add conditional logic design doc
- compose: replace fragile exclusion list (_INVALID_PARENT_TAB_TYPES) with
an allowlist (_VALID_PARENT_TAB_TYPES = {listTabs, radioGroupTabs, checkboxTabs}).
Previous list missed textTabs/numberTabs/dateTabs/companyTabs/stampTabs and had
a wrong name (emailTabs vs emailAddressTabs), allowing invalid conditionalParentLabel
references to pass through to the API and cause CONDITIONALTAB_HAS_INVALID_PARENT 400s.
- field-mapping.md: update invalid parent tab description to state the real DocuSign
rule (only list/radio/checkbox are valid parents) and regenerate PDF.
- docs/CONDITIONAL-LOGIC-DESIGN.md: new design doc covering all five conditional logic
gaps, the DocuSign flags technique and its real limits, solvable vs. fundamental gaps,
and proposed Phases 24-27 (NOT_EQUALS expansion, ANY fan-out, richer guidance, Maestro flag).
137/137 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c22d26bcf6
commit
ba6bc90964
|
|
@ -0,0 +1,218 @@
|
||||||
|
# Conditional Logic: Gap Analysis & Design
|
||||||
|
|
||||||
|
This document captures the structural differences between Adobe Sign's and DocuSign's
|
||||||
|
conditional field models, which gaps are solvable in the migrator, and the design of
|
||||||
|
proposed solutions. It complements `field-mapping.md` (current-state reference) and
|
||||||
|
`IMPLEMENTATION-PLAN.md` (feature history).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background: The Two Models
|
||||||
|
|
||||||
|
**Adobe Sign** supports full conditional logic per field:
|
||||||
|
- `conditionalAction.predicates[]` — one or more `{fieldName, operator, value}` tuples
|
||||||
|
- `anyOrAll` — ANY (OR) or ALL (AND) combining semantics
|
||||||
|
- `action` — SHOW or HIDE the field
|
||||||
|
- `operator` — EQUALS, NOT_EQUALS, GT, LT, CONTAINS, etc.
|
||||||
|
- The trigger field can belong to any recipient; cross-recipient conditions are valid
|
||||||
|
|
||||||
|
**DocuSign** supports a single, simple reveal condition per tab:
|
||||||
|
- `conditionalParentLabel` — the `tabLabel` or `groupName` of the trigger tab
|
||||||
|
- `conditionalParentValue` — the exact string value that reveals the tab
|
||||||
|
- The trigger tab must be a `listTab`, `radioGroupTab`, or `checkboxTab`
|
||||||
|
- The trigger and the dependent tab must belong to the **same recipient**
|
||||||
|
- Only one condition per tab, always EQUALS, always a SHOW (reveal)
|
||||||
|
|
||||||
|
This is the deepest structural asymmetry in the migration. Every gap below flows from
|
||||||
|
the gap between these two models.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gap Catalog
|
||||||
|
|
||||||
|
### Gap 1 — HIDE action
|
||||||
|
**Adobe**: `action: HIDE` — field is visible by default and hides when the condition is met.
|
||||||
|
**DocuSign**: No equivalent. DocuSign only supports revealing a tab that starts hidden.
|
||||||
|
**Current behavior**: Condition dropped; field always visible. `HIDE_ACTION` issue emitted.
|
||||||
|
|
||||||
|
### Gap 2 — NOT_EQUALS and other operators
|
||||||
|
**Adobe**: Supports NOT_EQUALS, GT, LT, CONTAINS, etc.
|
||||||
|
**DocuSign**: Only EQUALS is supported.
|
||||||
|
**Current behavior**: Condition dropped; field always visible. `UNSUPPORTED_OPERATOR` issue emitted.
|
||||||
|
|
||||||
|
The most common real-world case is `NOT_EQUALS ""` (show this field if the user
|
||||||
|
entered *anything* in another field). This is doubly broken in DocuSign:
|
||||||
|
1. NOT_EQUALS has no equivalent
|
||||||
|
2. Text fields can't be conditional parents at all (see Gap 5)
|
||||||
|
|
||||||
|
### Gap 3 — Cross-recipient conditionals
|
||||||
|
**Adobe**: Field B can show/hide based on the value of Field A even if A and B
|
||||||
|
belong to different recipients.
|
||||||
|
**DocuSign**: `conditionalParentLabel` resolves only within the same recipient's tab
|
||||||
|
set. The API silently ignores or rejects cross-recipient references.
|
||||||
|
**Current behavior**: Condition dropped. `CROSS_RECIPIENT_CONDITIONAL` issue emitted.
|
||||||
|
|
||||||
|
### Gap 4 — Multi-predicate ANY (OR) logic
|
||||||
|
**Adobe**: `anyOrAll: ANY` with multiple predicates — show if condition 1 OR condition 2 OR …
|
||||||
|
**DocuSign**: One `conditionalParentValue` per tab — no native OR.
|
||||||
|
**Current behavior**: Only the first EQUALS predicate is mapped; rest are dropped.
|
||||||
|
`MULTI_PREDICATE` issue emitted.
|
||||||
|
|
||||||
|
### Gap 5 — Invalid conditional parent tab types
|
||||||
|
**Adobe**: Any field can be a conditional trigger — text fields, signature fields, anything.
|
||||||
|
**DocuSign**: Only `listTabs`, `radioGroupTabs`, and `checkboxTabs` may be conditional
|
||||||
|
parents. All other types cause `CONDITIONALTAB_HAS_INVALID_PARENT` (400).
|
||||||
|
**Current behavior**: Strip pass removes conditions referencing invalid parent types.
|
||||||
|
`INVALID_PARENT_TAB` issue emitted.
|
||||||
|
|
||||||
|
### Gap 6 — Multi-predicate ALL (AND) logic
|
||||||
|
**Adobe**: `anyOrAll: ALL` — show only if ALL conditions are simultaneously true.
|
||||||
|
**DocuSign**: No AND logic at the template conditional level.
|
||||||
|
**Current behavior**: Only the first EQUALS predicate is mapped. `MULTI_PREDICATE` issue emitted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The DocuSign "Flags" Technique
|
||||||
|
|
||||||
|
DocuSign's "Building Advanced Templates" guide describes using **hidden intermediate
|
||||||
|
tabs** ("flags") to work around the platform's single-parent conditional model. The
|
||||||
|
pattern:
|
||||||
|
|
||||||
|
1. Create a hidden `checkboxTab` or `listTab` — the "flag" — placed out of the
|
||||||
|
visible form area or given `locked: true, selected: false`.
|
||||||
|
2. Make the flag conditionally revealed by one condition (the flag itself is shown
|
||||||
|
when condition A is true).
|
||||||
|
3. Make the dependent field conditionally revealed when the flag is in a specific state.
|
||||||
|
|
||||||
|
This creates two-level conditional depth. However, DocuSign tabs don't auto-set their
|
||||||
|
own value — a checkbox flag only "activates" when the signer checks it, or when the
|
||||||
|
sender pre-fills it before sending. This limits the technique to two scenarios:
|
||||||
|
|
||||||
|
**Scenario A — Sender-prefill flags**: The sender manually checks/sets flags before
|
||||||
|
routing the envelope, making explicit business logic decisions that the template cannot
|
||||||
|
express automatically. Useful for bespoke workflows but not for automated migration.
|
||||||
|
|
||||||
|
**Scenario B — Checkbox-group section selectors**: For OR-style logic driven by a
|
||||||
|
single dropdown, radio group, or checkbox, the flag technique is not needed — the
|
||||||
|
multi-copy approach (Gap 4 solution below) handles this cleanly.
|
||||||
|
|
||||||
|
**Bottom line**: The flag technique is a UI/manual-workflow tool, not an automated
|
||||||
|
migration output. It doesn't solve AND logic or cross-recipient conditions without
|
||||||
|
human intervention at sending time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solvable Gaps: Proposed Designs
|
||||||
|
|
||||||
|
### Solution A — ANY multi-predicate fan-out (addresses Gap 4, partially Gap 2)
|
||||||
|
|
||||||
|
**Scope**: When `anyOrAll: ANY` and all predicates share the same parent field and all
|
||||||
|
use `EQUALS`, the OR logic can be emitted as **one copy of the dependent tab per
|
||||||
|
predicate value**, all at the same coordinates with the same `tabLabel`.
|
||||||
|
|
||||||
|
At signing time, DocuSign shows exactly the copies whose condition is satisfied.
|
||||||
|
Because the copies share a `tabLabel`, their values sync — the signer fills one and
|
||||||
|
DocuSign treats them as the same logical field.
|
||||||
|
|
||||||
|
**Before** (current output — only first predicate mapped):
|
||||||
|
```json
|
||||||
|
{ "tabLabel": "Notes", "conditionalParentLabel": "Category",
|
||||||
|
"conditionalParentValue": "Option A" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**After** (fan-out):
|
||||||
|
```json
|
||||||
|
{ "tabLabel": "Notes", "conditionalParentLabel": "Category",
|
||||||
|
"conditionalParentValue": "Option A" },
|
||||||
|
{ "tabLabel": "Notes", "conditionalParentLabel": "Category",
|
||||||
|
"conditionalParentValue": "Option B" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOT_EQUALS expansion** (Gap 2, dropdown parents only): When the operator is
|
||||||
|
NOT_EQUALS and the parent is a `listTab`, look up the parent field's option list and
|
||||||
|
compute the complement — emit one copy per remaining value. This covers the common
|
||||||
|
"show unless user picked X" pattern.
|
||||||
|
|
||||||
|
**Limitations**:
|
||||||
|
- Only works when all predicates reference the **same parent field**
|
||||||
|
- Mixed-parent ANY (field A = x OR field B = y) is still unsupported
|
||||||
|
- NOT_EQUALS expansion only works for `listTab` parents with a finite option set
|
||||||
|
- Text field parents remain invalid regardless of operator (Gap 5)
|
||||||
|
|
||||||
|
**New issue codes proposed**:
|
||||||
|
- `MULTI_PREDICATE_EXPANDED` (info) — OR logic successfully fanned out
|
||||||
|
- `NOT_EQUALS_EXPANDED` (info) — NOT_EQUALS resolved via complement enumeration
|
||||||
|
- `NOT_EQUALS_UNEXPANDABLE` (warning) — NOT_EQUALS on a non-list parent; condition dropped
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Solution B — Richer guidance messages for unsolvable gaps
|
||||||
|
|
||||||
|
All currently unsolvable gaps emit a generic "condition dropped" message. Each gap
|
||||||
|
warrants a specific, actionable message explaining the constraint and what to do
|
||||||
|
manually.
|
||||||
|
|
||||||
|
| Gap | Improved guidance |
|
||||||
|
|-----|------------------|
|
||||||
|
| `HIDE_ACTION` | "Restructure as a SHOW condition: start the field hidden (remove it from the base layout) and show it when the inverse condition is met. If the trigger is a dropdown, this requires listing all values that should reveal the field." |
|
||||||
|
| `CROSS_RECIPIENT_CONDITIONAL` | "Add a prefill recipient (routing order 0) that holds the controlling field. Downstream signers' conditional fields can then reference the prefill tab. The sender sets the value before routing." |
|
||||||
|
| `INVALID_PARENT_TAB` (text parent) | "DocuSign requires a dropdown, radio group, or checkbox as the condition trigger. Replace the text field trigger with a dropdown containing expected values, then the condition can be mapped." |
|
||||||
|
| `MULTI_PREDICATE` (mixed parents) | "DocuSign supports one condition per field. Split this field into two separate positioned fields, each conditional on one predicate." |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unsolvable Gaps (Fundamental Limits of DocuSign Templates)
|
||||||
|
|
||||||
|
These gaps cannot be addressed at the template layer regardless of technique:
|
||||||
|
|
||||||
|
| Gap | Why |
|
||||||
|
|-----|-----|
|
||||||
|
| HIDE action (general) | DocuSign has no "start visible, hide when X" model. Inversion via complement enumeration only works for dropdown triggers with finite option sets. |
|
||||||
|
| Text field as conditional parent | DocuSign's API flatly rejects any tab type other than `listTabs`, `radioGroupTabs`, `checkboxTabs` as a parent — this is a server-side constraint, not a schema quirk. |
|
||||||
|
| AND logic (multi-predicate ALL) | DocuSign template conditionals have no AND. The flag chaining technique requires a human to set intermediate flags at sending time, making it unsuitable for automated migration output. |
|
||||||
|
| Mixed-parent ANY | "Show if field A = x OR field B = y" requires two independent conditions on the same tab, which the single-parent model cannot express. |
|
||||||
|
| Cross-recipient without prefill restructure | True cross-recipient logic (signer 2's visibility driven by signer 1's choice) requires restructuring the recipient list to add a prefill signer — a structural change, not a tab-level fix. |
|
||||||
|
|
||||||
|
**The right platform for these cases is DocuSign Maestro (Workflow automation)**, which
|
||||||
|
supports proper conditional branching, cross-step data references, and decision gateways.
|
||||||
|
Templates with significant AND logic or cross-recipient conditionals should be flagged as
|
||||||
|
Maestro candidates in the migration report.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Implementation Phases
|
||||||
|
|
||||||
|
### Phase 24 — NOT_EQUALS expansion for dropdown parents
|
||||||
|
- Detect `operator: NOT_EQUALS` where parent field is a `listTab`
|
||||||
|
- Look up parent's `hiddenOptions`/`visibleOptions` from the Adobe source fields
|
||||||
|
- Compute complement set (all options except the excluded value)
|
||||||
|
- Emit one tab copy per remaining option with `conditionalParentValue = option`
|
||||||
|
- Emit `NOT_EQUALS_EXPANDED` info issue
|
||||||
|
- Emit `NOT_EQUALS_UNEXPANDABLE` warning for non-list parents (unchanged drop behavior)
|
||||||
|
- Tests: unit tests for complement computation; regression snapshots updated
|
||||||
|
|
||||||
|
### Phase 25 — ANY multi-predicate fan-out
|
||||||
|
- Detect `anyOrAll: ANY`, multiple predicates, all referencing the **same parent field**,
|
||||||
|
all using `EQUALS`
|
||||||
|
- Emit one tab copy per predicate value at the same coordinates with the same `tabLabel`
|
||||||
|
- Emit `MULTI_PREDICATE_EXPANDED` info issue
|
||||||
|
- Cases with mixed parents or non-EQUALS operators keep `MULTI_PREDICATE` warning
|
||||||
|
- Tests: unit tests covering fan-out; mixed-parent case stays as warning
|
||||||
|
|
||||||
|
### Phase 26 — Richer issue guidance text
|
||||||
|
- Update `HIDE_ACTION`, `CROSS_RECIPIENT_CONDITIONAL`, `INVALID_PARENT_TAB`,
|
||||||
|
and `MULTI_PREDICATE` message strings to include specific manual fix instructions
|
||||||
|
- No behavioral change — UI display only
|
||||||
|
- Tests: update any tests that assert on exact message strings
|
||||||
|
|
||||||
|
### Phase 27 — Maestro escalation advisory
|
||||||
|
- Add template-level `MAESTRO_RECOMMENDED` advisory (distinct from per-field issues) when
|
||||||
|
a template contains unresolvable AND logic, cross-recipient conditionals, or HIDE actions
|
||||||
|
that affect more than N fields (threshold TBD)
|
||||||
|
- Surface in the Issues tab of the template detail view
|
||||||
|
- No code generation — guidance only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Created: 2026-05-14. Companion documents: `field-mapping.md` (current-state reference),
|
||||||
|
`docs/IMPLEMENTATION-PLAN.md` (feature history), `tests/EDGE-CASES.md` (known edge cases).*
|
||||||
|
|
@ -120,7 +120,7 @@ Adobe Sign `conditionalAction` → DocuSign `conditionalParentLabel` + `conditio
|
||||||
| Other operators (NOT_EQUALS, etc.)| **Not supported** | Dropped | Condition skipped. `UNSUPPORTED_OPERATOR` issue emitted. |
|
| Other operators (NOT_EQUALS, etc.)| **Not supported** | Dropped | Condition skipped. `UNSUPPORTED_OPERATOR` issue emitted. |
|
||||||
| Multiple predicates (ANY/ALL) | **Partial** — first EQUALS only | Partial | `MULTI_PREDICATE` issue emitted; remaining predicates ignored |
|
| Multiple predicates (ANY/ALL) | **Partial** — first EQUALS only | Partial | `MULTI_PREDICATE` issue emitted; remaining predicates ignored |
|
||||||
| Trigger field on a different recipient | **Not supported** | Dropped | DocuSign `conditionalParentLabel` only works within the same recipient's tab set. `CROSS_RECIPIENT_CONDITIONAL` issue emitted. |
|
| Trigger field on a different recipient | **Not supported** | Dropped | DocuSign `conditionalParentLabel` only works within the same recipient's tab set. `CROSS_RECIPIENT_CONDITIONAL` issue emitted. |
|
||||||
| Parent is signature/auto-fill tab | **Not supported** | Stripped | DocuSign forbids signature, initial, dateSign, fullName, email, title tabs as conditional parents. `INVALID_PARENT_TAB` issue emitted. |
|
| Parent is non-interactive tab | **Not supported** | Stripped | DocuSign only accepts `listTabs`, `radioGroupTabs`, and `checkboxTabs` as conditional parents. All other types — including `textTabs`, `numberTabs`, `dateTabs`, `signHereTabs`, `initialHereTabs`, `dateSignedTabs`, `fullNameTabs`, `emailAddressTabs`, `companyTabs`, `titleTabs`, `stampTabs`, `signerAttachmentTabs` — are rejected by the API with `CONDITIONALTAB_HAS_INVALID_PARENT` (400). `INVALID_PARENT_TAB` issue emitted. |
|
||||||
|
|
||||||
## Known Gaps
|
## Known Gaps
|
||||||
|
|
||||||
|
|
@ -132,11 +132,13 @@ Adobe Sign `conditionalAction` → DocuSign `conditionalParentLabel` + `conditio
|
||||||
the value of field A even when A and B belong to different recipients. DocuSign's
|
the value of field A even when A and B belong to different recipients. DocuSign's
|
||||||
`conditionalParentLabel` only works within a single recipient's tab set.
|
`conditionalParentLabel` only works within a single recipient's tab set.
|
||||||
Emits a `CROSS_RECIPIENT_CONDITIONAL` field issue; the condition is dropped.
|
Emits a `CROSS_RECIPIENT_CONDITIONAL` field issue; the condition is dropped.
|
||||||
- **Invalid or forbidden conditional parents**: If the trigger field maps to a signature,
|
- **Invalid or forbidden conditional parents**: DocuSign only accepts `listTabs`,
|
||||||
initial, dateSign, fullName, email, or title tab — DocuSign forbids these as conditional
|
`radioGroupTabs`, and `checkboxTabs` as conditional parents. Any other tab type used
|
||||||
parents and returns `CONDITIONALTAB_HAS_INVALID_PARENT` (400). The compose pipeline
|
as a trigger — including plain `textTabs`, `numberTabs`, `dateTabs`, and all
|
||||||
strips these conditions in a post-processing pass and emits an `INVALID_PARENT_TAB`
|
auto-filled/action types — is rejected with `CONDITIONALTAB_HAS_INVALID_PARENT`
|
||||||
field issue.
|
(400). This means Adobe conditions like "show X when text field Y is not empty" have
|
||||||
|
no DocuSign equivalent. The compose pipeline strips these in a post-processing pass
|
||||||
|
and emits an `INVALID_PARENT_TAB` field issue.
|
||||||
- **Multi-predicate conditions**: Adobe Sign supports ANY/ALL of multiple predicates.
|
- **Multi-predicate conditions**: Adobe Sign supports ANY/ALL of multiple predicates.
|
||||||
DocuSign only supports a single parent condition per tab. Only the first EQUALS
|
DocuSign only supports a single parent condition per tab. Only the first EQUALS
|
||||||
predicate is mapped; complex conditions require manual rework.
|
predicate is mapped; complex conditions require manual rework.
|
||||||
|
|
|
||||||
|
|
@ -370,26 +370,28 @@ def merge_tabs(acc: dict, new: dict) -> dict:
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
|
|
||||||
# Tab types DocuSign forbids as conditional parents (auto-filled or action tabs)
|
# Only these three tab types may serve as conditional parents in DocuSign.
|
||||||
_INVALID_PARENT_TAB_TYPES = {
|
# Everything else (textTabs, signHereTabs, dateSignedTabs, fullNameTabs,
|
||||||
"signHereTabs", "initialHereTabs", "dateSignedTabs",
|
# emailAddressTabs, numberTabs, dateTabs, companyTabs, titleTabs, stampTabs,
|
||||||
"fullNameTabs", "emailTabs", "titleTabs", "signerAttachmentTabs",
|
# signerAttachmentTabs, initialHereTabs) is rejected by the API with
|
||||||
}
|
# CONDITIONALTAB_HAS_INVALID_PARENT (400).
|
||||||
|
_VALID_PARENT_TAB_TYPES = {"listTabs", "radioGroupTabs", "checkboxTabs"}
|
||||||
|
|
||||||
|
|
||||||
def _strip_invalid_conditionals(signers: list, warnings: list, issues: list) -> None:
|
def _strip_invalid_conditionals(signers: list, warnings: list, issues: list) -> None:
|
||||||
"""
|
"""
|
||||||
Remove conditionalParentLabel/Value from any tab whose parent label either
|
Remove conditionalParentLabel/Value from any tab whose parent label either
|
||||||
doesn't exist in the template or points to a tab type DocuSign forbids as a
|
doesn't exist in the template or points to a tab type DocuSign forbids as a
|
||||||
parent (signature, initial, auto-filled). Mutates signers in place.
|
parent. Only listTabs, radioGroupTabs, and checkboxTabs are valid parents.
|
||||||
|
Mutates signers in place.
|
||||||
"""
|
"""
|
||||||
for signer in signers:
|
for signer in signers:
|
||||||
tabs = signer.get("tabs", {})
|
tabs = signer.get("tabs", {})
|
||||||
|
|
||||||
# Collect valid parent labels: only tab types allowed as parents
|
# Collect valid parent labels from the only three permitted parent types
|
||||||
valid_labels: set[str] = set()
|
valid_labels: set[str] = set()
|
||||||
for tab_type, tab_list in tabs.items():
|
for tab_type, tab_list in tabs.items():
|
||||||
if tab_type in _INVALID_PARENT_TAB_TYPES:
|
if tab_type not in _VALID_PARENT_TAB_TYPES:
|
||||||
continue
|
continue
|
||||||
for tab in tab_list:
|
for tab in tab_list:
|
||||||
lbl = tab.get("tabLabel") or tab.get("groupName")
|
lbl = tab.get("tabLabel") or tab.get("groupName")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue