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:
Paul Huliganga 2026-05-14 11:16:28 -04:00
parent c22d26bcf6
commit ba6bc90964
3 changed files with 236 additions and 14 deletions

View File

@ -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).*

View File

@ -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.

View File

@ -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")