Files
Ready2Blob/.planning/phases/07-validation-ux-polish/07-RESEARCH.md
T
2026-03-31 09:13:53 +02:00

494 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 7: Validation & UX Polish - Research
**Researched:** 2026-03-31
**Domain:** React / react-hook-form / Zod v4 / Tailwind CSS inline validation and tooltip patterns
**Confidence:** HIGH
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
**Validation scope:**
- Azure account name (`account` field, azureblob): 3-24 lowercase alphanumeric characters — validate with regex `/^[a-z0-9]{3,24}$/`
- S3 region (`region` field, s3): valid AWS region format (lowercase letters, digits, hyphens) — validate with regex `/^[a-z][a-z0-9-]+[a-z0-9]$/`
- GCS project number (`project_number` field, gcs): digits only — validate with regex `/^\d+$/`
- S3-compatible endpoint: non-empty only (existing required behavior) — no URL format validation
- S3-compatible region: skip — optional field, no universal format rule
- No other backends/fields need format validation
**Validation rule location:**
- Add `validate?: { regex: RegExp; message: string }` to `FieldDef` in `src/schemas/registry.ts`
- `buildZodSchema()` in `src/schemas/index.ts` reads `field.validate` and adds `.regex(...)` to the Zod string
- Registry entries for the 3 validated fields get their `validate` rule inline
**Tooltip trigger:**
- Info icon (ⓘ) placed next to the field label — click/tap toggles explanation
- Inline reveal: explanation expands directly below the field input, pushing content down
- Toggle state is local to the field component (no global state needed)
- Tooltip stays visible until ⓘ is clicked again (not dismissed on blur)
**Tooltip data location:**
- Add optional `tooltipText?: string` to `FieldDef` interface
- `helpText` remains unchanged (brief hint, always visible below the field)
- `tooltipText` is the longer plain-language explanation shown on ⓘ click
- `FieldRenderer` and `PasswordField` render the ⓘ icon + inline expand when `tooltipText` is present
**Fields that get tooltips:**
- `azureblob sas_url`: explain SAS URL vs access key — SAS URL bundles endpoint + time-limited token, scoped to containers; access key is the full account credential
- `azureblob key` (access key): explain it's the full storage account key — different from SAS, full access
- `sftp` auth method: explain password vs key-based auth — SftpAuthToggle UI needs an ⓘ on the tab labels or above the toggle
- `onedrive token`: explain what the JSON token is and how to obtain it via `rclone authorize "onedrive"`
### Claude's Discretion
- Exact Tailwind styling of the ⓘ icon and inline tooltip panel
- Whether SftpAuthToggle gets its ⓘ on the tab label or as a standalone line above the tabs
- ⓘ icon source (Heroicons, inline SVG, or text character)
- Exact wording of tooltip content (stay close to existing helpText tone)
### Deferred Ideas (OUT OF SCOPE)
None — discussion stayed within phase scope.
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| VALID-01 | User sees inline validation error when a field value violates backend-specific format rules (Azure account name: 324 lowercase alphanumeric; S3 region: valid format; etc.) | Zod v4 `.regex()` chaining verified; `buildZodSchema()` extension pattern confirmed; error display already exists in FieldRenderer |
| UX-01 | User can view a contextual tooltip on sensitive or complex fields (SAS token vs. access key, region codes, SFTP auth method, OneDrive token) | React `useState` toggle pattern sufficient; `FieldDef` extension with `tooltipText?` is the correct approach; PasswordField and FieldRenderer both need ⓘ treatment |
</phase_requirements>
---
## Summary
Phase 7 is a narrowly-scoped surgical enhancement to an already-working wizard. Two independent concerns: (1) format validation wired through the schema registry into Zod, and (2) per-field inline tooltip panels wired through the registry into the field renderer components. Both concerns touch the same three-file chain (`registry.ts``index.ts``FieldRenderer.tsx`/`PasswordField.tsx`), plus `SftpAuthToggle.tsx` for the auth-method tooltip.
The project uses **Zod v4.3.6** (not v3). The `.regex()` method is available on `z.string()` and chains normally — verified by direct runtime test. However, `.optional()` in Zod v4 wraps the inner type, making `.regex()` unavailable after calling `.optional()`. The correct order when a field is optional but has a regex constraint is `z.string().regex(...).optional()`, not `z.string().optional().regex(...)`. All three validated fields (azureblob `account`, s3 `region`, gcs `project_number`) are `required: true`, so the simpler pattern applies: `z.string().min(1, ...).regex(regex, message)`.
The tooltip feature requires zero new dependencies. React `useState` per field is the right tool. The CSS toggle pattern already established in `AzureAuthToggle` and `SftpAuthToggle` (div.block / div.hidden) is the exact pattern to reuse for tooltip panel visibility. All existing tests pass (147/147); new tests must be added for VALID-01 regex rejection and UX-01 tooltip toggle behavior.
**Primary recommendation:** Implement as two sequential plans — Plan 01 adds validation (registry + schema + tests), Plan 02 adds tooltips (FieldDef extension + FieldRenderer + PasswordField + SftpAuthToggle + tests).
---
## Standard Stack
### Core (already installed — no new dependencies)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| zod | 4.3.6 | Schema validation | Already in use — `buildZodSchema()` already chains `.min(1)`, just extend with `.regex()` |
| react-hook-form | 7.72.0 | Form state + validation trigger | Already wired — `mode: 'onSubmit', reValidateMode: 'onChange'` means errors appear after first submit then live |
| @hookform/resolvers | 5.2.2 | zodResolver bridges Zod into RHF | Already wired in RemoteConfigStep |
| react | 18.3.1 | `useState` for tooltip toggle | useState already used in PasswordField for show/hide |
| tailwindcss | 4.2.2 | Styling | Inline tooltip panel follows existing helpText style (`text-xs`) |
**No new npm packages needed for this phase.**
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Local `useState` toggle | Global tooltip store | Local is correct here — each field is independent, no cross-field coordination |
| CSS div.hidden / div.block pattern | `{showTooltip && <div>...}` conditional render | Both work; CSS-hidden is the established project convention for toggles (AzureAuthToggle, SftpAuthToggle) — use CSS-hidden for consistency |
| Text character ⓘ | Heroicons InformationCircleIcon | No new dependency needed; text or inline SVG both work; Claude's discretion |
---
## Architecture Patterns
### Recommended File Modification Order
```
src/schemas/registry.ts # Step 1: extend FieldDef interface, add validate + tooltipText fields
src/schemas/index.ts # Step 2: extend buildZodSchema() to read field.validate
src/components/ui/FieldRenderer.tsx # Step 3: add ⓘ icon + tooltip panel for text/select renderers
src/components/ui/PasswordField.tsx # Step 3: add ⓘ icon + tooltip panel for password renderer
src/components/wizard/SftpAuthToggle.tsx # Step 4: add ⓘ above the tab toggle
src/components/wizard/RemoteConfigStep.test.tsx # Tests: VALID-01 + UX-01 coverage
```
### Pattern 1: FieldDef Interface Extension
**What:** Add two optional properties to the existing `FieldDef` interface.
**When to use:** Always — this is the single source of truth for both validate and tooltipText.
```typescript
// src/schemas/registry.ts — add to FieldDef interface
export interface FieldDef {
key: string;
label: string;
inputType: 'text' | 'password' | 'select' | 'toggle';
required: boolean;
placeholder?: string;
helpText?: string;
options?: { value: string; label: string }[];
validate?: { regex: RegExp; message: string }; // NEW: format validation rule
tooltipText?: string; // NEW: longer plain-language explanation
}
```
### Pattern 2: buildZodSchema() Extension
**What:** After the existing required/optional branch, chain `.regex()` if `field.validate` is present.
**Critical detail:** For required fields, chain `.regex()` AFTER `.min(1)`. For optional fields, chain `.regex()` BEFORE `.optional()` — though all three validated fields are required, so the optional case does not apply in this phase.
```typescript
// src/schemas/index.ts — updated loop body
for (const field of fields) {
let schema: z.ZodTypeAny = field.required
? z.string().min(1, `${field.label} is required`)
: z.string();
if (field.validate) {
schema = (schema as z.ZodString).regex(field.validate.regex, field.validate.message);
}
if (!field.required) {
schema = (schema as z.ZodString).optional();
}
shape[field.key] = schema;
}
```
**Zod v4 verification:** `z.string().min(1, ...).regex(/pattern/, 'message')` — confirmed working via runtime test. Regex fires after min(1), so empty string shows "required" error not "invalid format".
### Pattern 3: Tooltip Toggle in FieldRenderer
**What:** Add local `useState` for tooltip visibility; conditionally render the ⓘ button and panel.
**When to use:** When `field.tooltipText` is present — guard with `field.tooltipText &&`.
```typescript
// src/components/ui/FieldRenderer.tsx — text case (same pattern for select)
import { useState } from 'react';
// Inside the component function:
const [showTooltip, setShowTooltip] = useState(false);
// Label with optional ⓘ icon:
<label htmlFor={field.key} className="text-sm font-medium text-gray-700 flex items-center gap-1">
{field.label}
{field.required && <span className="ml-1 text-red-500">*</span>}
{field.tooltipText && (
<button
type="button"
onClick={() => setShowTooltip(v => !v)}
aria-label={`More info about ${field.label}`}
className="ml-1 text-blue-500 hover:text-blue-700 text-xs"
>
</button>
)}
</label>
// Tooltip panel (below input, above helpText/error):
{field.tooltipText && showTooltip && (
<p className="text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1.5">
{field.tooltipText}
</p>
)}
```
### Pattern 4: PasswordField Tooltip
**What:** PasswordField currently does not receive `field: FieldDef` — it receives individual props. Two options for adding tooltipText support:
**Option A (minimal change):** Add `tooltipText?: string` prop to PasswordFieldProps, same `useState` pattern inside.
**Option B (structural change):** Pass the full `FieldDef` to PasswordField so it receives all future FieldDef additions automatically.
**Recommendation:** Option A (minimal). The existing interface is stable and PasswordField is already used by both FieldRenderer and the auth toggle components — changing its interface to accept full FieldDef would require updating all call sites. Adding a single optional prop is lower risk.
```typescript
// Updated PasswordFieldProps:
interface PasswordFieldProps {
id: string;
label: string;
error?: FieldError;
registration: UseFormRegisterReturn;
placeholder?: string;
helpText?: string;
tooltipText?: string; // NEW
}
```
FieldRenderer passes `tooltipText={field.tooltipText}` when calling PasswordField. AzureAuthToggle call sites do not need tooltipText (azureblob key and sas_url get their tooltip via FieldRenderer in the registry loop — wait, see Pitfall 1 below).
### Pattern 5: SftpAuthToggle Auth-Method Tooltip
**What:** The auth-method tooltip is not field-level — it explains the Password vs Private Key choice. A standalone `useState` for one tooltip, placed above the segmented control.
```typescript
// SftpAuthToggle.tsx addition:
const [showAuthTip, setShowAuthTip] = useState(false);
// Before the segmented control div:
<div className="flex items-center gap-1">
<span className="text-sm font-medium text-gray-700">Authentication Method</span>
<button
type="button"
onClick={() => setShowAuthTip(v => !v)}
aria-label="More info about authentication methods"
className="text-blue-500 hover:text-blue-700 text-xs"
>
</button>
</div>
{showAuthTip && (
<p className="text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1.5">
Password auth sends your password directly to the SFTP server.
Key-based auth uses a private key file (more secure the server only stores your public key).
Paste the private key PEM content if using key-based auth.
</p>
)}
```
### Anti-Patterns to Avoid
- **Calling `.optional()` then `.regex()` in Zod v4:** `z.string().optional()` returns `ZodOptional<ZodString>` which has no `.regex()` method. Order must be `.regex().optional()` for optional+validated fields (not applicable here since all validated fields are required, but important to know).
- **Adding tooltip state to a parent component:** Each field manages its own tooltip state with `useState`. No lifting state up, no global tooltip context.
- **Rendering the ⓘ button when `tooltipText` is undefined:** Guard with `field.tooltipText &&` to avoid rendering an empty button for most fields.
- **Passing `tooltipText` to PasswordField from AzureAuthToggle for azureblob sas_url and key fields:** These fields are rendered inside AzureAuthToggle which calls PasswordField directly — not via FieldRenderer's registry loop. The `tooltipText` for azureblob.key and azureblob.sas_url must be passed explicitly through AzureAuthToggle (see Pitfall 1).
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Regex validation error display | Custom error state in component | Zod `.regex()` + zodResolver | RHF already captures Zod errors and makes them available as `errors[field.key]` — FieldRenderer already renders them |
| Tooltip positioning/overflow | CSS `position: absolute` tooltip popover | Inline expand (push content down) | Project decision: inline reveal, pushes content down — simpler, no overflow issues, no z-index conflicts |
| Validation trigger timing | Custom onChange handlers | RHF `reValidateMode: 'onChange'` | Already configured in RemoteConfigStep — regex errors appear live after first submit attempt |
---
## Common Pitfalls
### Pitfall 1: AzureAuthToggle bypasses FieldRenderer for sas_url and key
**What goes wrong:** `azureblob.sas_url` and `azureblob.key` are not rendered through the registry loop in RemoteConfigStep — they are rendered directly by `AzureAuthToggle` which calls `PasswordField` with hardcoded props. Adding `tooltipText` to the registry entries for these fields is not enough to make the tooltip appear automatically.
**Why it happens:** RemoteConfigStep uses a three-branch ternary. The `azureblob` branch renders the account field via FieldRenderer but delegates key+sas_url to `AzureAuthToggle`. AzureAuthToggle has hardcoded `helpText` strings and doesn't read from the registry.
**How to avoid:** Two options — (A) look up the registry entry inside AzureAuthToggle and pass `field.tooltipText` to PasswordField, or (B) hardcode the `tooltipText` prop in AzureAuthToggle's PasswordField calls. Option A is cleaner. If using Option A: `BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'sas_url')?.tooltipText`.
**Warning signs:** Registry has `tooltipText` on sas_url and key, but no tooltip appears in the Azure form. Test will catch this if asserting tooltip button presence.
### Pitfall 2: PasswordField used by both FieldRenderer and auth toggles — prop update needed in all call sites
**What goes wrong:** PasswordField is called in three places: FieldRenderer, AzureAuthToggle, and SftpAuthToggle. Adding `tooltipText?` to PasswordFieldProps is safe (optional prop), but only FieldRenderer and AzureAuthToggle need it populated. SftpAuthToggle's PasswordField calls (for `pass` and `key_pem`) do not need tooltipText — the auth method tooltip is above the toggle, not on each individual field.
**Why it happens:** PasswordField is a shared component used in multiple contexts.
**How to avoid:** Simply don't pass `tooltipText` in SftpAuthToggle's PasswordField calls — the prop is optional.
### Pitfall 3: Zod v4 `.regex()` returns a new ZodString — type must be cast for chaining
**What goes wrong:** In `buildZodSchema()`, the `schema` variable is typed as `z.ZodTypeAny`. After calling `.min(1, ...)` on a `z.string()`, the result type is still `ZodString`, but TypeScript may not know this through a `ZodTypeAny` variable.
**Why it happens:** `ZodTypeAny` is a broad union type. The `.regex()` method is only on `ZodString`.
**How to avoid:** Cast to `z.ZodString` before calling `.regex()`: `(schema as z.ZodString).regex(...)`. Alternatively, refactor the loop to keep a `ZodString` variable explicitly. Confirmed working via runtime test.
### Pitfall 4: Test assertions for tooltip — input type='password' accessibility
**What goes wrong:** When testing tooltip button presence for password fields (sas_url, key, onedrive token), the label must match what `getByLabelText` expects. The ⓘ button is inline with the label text, not the label itself.
**Why it happens:** If the ⓘ is a `<button>` inside the `<label>` element, `getByLabelText` behavior may differ depending on the DOM structure. The ⓘ should be OUTSIDE the `<label>` element to avoid accessibility issues.
**How to avoid:** Keep the ⓘ button as a sibling element after the label (wrapped in a flex container), not inside the `<label>` tag. Test with `screen.getByRole('button', { name: /more info about/i })` or `getByLabelText` on the input separately.
### Pitfall 5: S3 region regex edge cases
**What goes wrong:** The decided regex `/^[a-z][a-z0-9-]+[a-z0-9]$/` requires at least 3 characters (start + middle + end). This is correct for all current AWS regions. However, future AWS regions could theoretically be 2-character — though this has never happened.
**Why it happens:** The `+` quantifier on the middle group requires at least one character between start and end.
**How to avoid:** The decided regex is acceptable for the current AWS region list (verified by cross-checking against known regions like `us-east-1`, `eu-west-1`, `ap-southeast-2`). Document the regex choice in registry helpText or a comment.
---
## Code Examples
Verified patterns from codebase + Zod v4 runtime tests:
### Zod v4 regex chaining (verified 2026-03-31)
```typescript
// Required field with regex — order: min(1) then regex
z.string().min(1, 'Storage Account Name is required')
.regex(/^[a-z0-9]{3,24}$/, 'Must be 324 lowercase alphanumeric characters')
// Runtime verified:
// .safeParse('abc123') → { success: true }
// .safeParse('ABC') → { success: false, error.issues[0].message: 'Must be 324...' }
// .safeParse('') → { success: false, error.issues[0].message: 'Storage Account Name is required' }
```
### buildZodSchema() extended loop body
```typescript
function buildZodSchema(backendType: BackendType): z.ZodObject<Record<string, z.ZodTypeAny>> {
const fields = BACKEND_REGISTRY[backendType].fields;
const shape: Record<string, z.ZodTypeAny> = {};
for (const field of fields) {
let schema: z.ZodTypeAny = field.required
? z.string().min(1, `${field.label} is required`)
: z.string();
if (field.validate) {
schema = (schema as z.ZodString).regex(field.validate.regex, field.validate.message);
}
if (!field.required) {
schema = (schema as z.ZodString).optional();
}
shape[field.key] = schema;
}
return z.object(shape);
}
```
### FieldRenderer useState tooltip pattern
```typescript
import { useState } from 'react';
export function FieldRenderer({ field, register, error }: FieldRendererProps) {
const [showTooltip, setShowTooltip] = useState(false);
// ...
// Label row (flex, items-center):
// <label> text + required star + ⓘ button (if field.tooltipText)
// <input>
// tooltip panel (if field.tooltipText && showTooltip)
// helpText (if !error && !showTooltip, or always — Claude's discretion)
// error (if error)
}
```
### Registry entry with both validate and tooltipText
```typescript
// azureblob account field — validate only
{
key: 'account',
label: 'Storage Account Name',
inputType: 'text',
required: true,
placeholder: 'mystorageaccount',
helpText: 'The storage account name (not the full URL)',
validate: {
regex: /^[a-z0-9]{3,24}$/,
message: 'Must be 324 lowercase alphanumeric characters (no hyphens or uppercase)',
},
}
// azureblob sas_url field — tooltipText only
{
key: 'sas_url',
label: 'SAS URL',
inputType: 'password',
required: false,
placeholder: 'https://mystorageaccount.blob.core.windows.net/?sv=...',
helpText: 'Full SAS URL including account and container.',
tooltipText: 'A SAS URL (Shared Access Signature) bundles the storage endpoint with a time-limited, scope-limited token. It grants access only to the containers specified and expires automatically. Use this if you want limited-access credentials. If you have the full account key instead, switch to Access Key.',
}
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Hand-written Zod schemas per backend | `buildZodSchema()` from registry | Phase 5 (v1.1) | Adding a registry field automatically adds its Zod validation |
| Hardcoded field lists in components | Registry-driven `fields.map()` loop | Phase 5-6 | New field with `validate` property gets regex validation for free |
**Deprecated/outdated:**
- None for this phase — patterns are all already established.
---
## Open Questions
1. **helpText visibility when tooltip is showing**
- What we know: helpText is the brief always-visible hint; tooltipText is the expanded explanation
- What's unclear: Should helpText hide when the tooltip panel is open to reduce visual clutter, or always show?
- Recommendation: Keep helpText always visible (it's brief). Show tooltip panel below it. Claude's discretion on layout.
2. **AzureAuthToggle registry lookup vs hardcoded tooltipText**
- What we know: AzureAuthToggle bypasses FieldRenderer, so registry tooltipText won't appear automatically
- What's unclear: Whether to read from registry inside AzureAuthToggle or hardcode the tooltip text strings
- Recommendation: Read from registry (`BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'sas_url')?.tooltipText`) to keep registry as single source of truth. Hardcoding is acceptable if simpler.
---
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest 4.1.1 + @testing-library/react 16.3.2 |
| Config file | `vitest.config.ts` (environment: node; test files use `@vitest-environment jsdom` pragma) |
| Quick run command | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` |
| Full suite command | `npx vitest run` |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| VALID-01 | Azure account name rejects non-lowercase, non-alphanumeric, wrong length | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
| VALID-01 | Azure account name accepts valid 3-24 char lowercase alphanumeric | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
| VALID-01 | S3 region rejects invalid format (e.g., 'us_east_1', uppercase) | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
| VALID-01 | S3 region accepts valid format (e.g., 'us-east-1') | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
| VALID-01 | GCS project_number rejects non-digits | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
| VALID-01 | GCS project_number accepts digits | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
| UX-01 | ⓘ button present on azureblob sas_url field | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
| UX-01 | Clicking ⓘ on sas_url shows tooltip panel | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
| UX-01 | Clicking ⓘ again hides tooltip panel | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
| UX-01 | ⓘ button present on SFTP auth method section | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
| UX-01 | ⓘ button present on OneDrive token field | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
### Sampling Rate
- **Per task commit:** `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx`
- **Per wave merge:** `npx vitest run`
- **Phase gate:** Full suite green (currently 147 tests) before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] VALID-01 test stubs in `src/components/wizard/RemoteConfigStep.test.tsx` — covers regex rejection and acceptance for all 3 fields
- [ ] UX-01 test stubs in `src/components/wizard/RemoteConfigStep.test.tsx` — covers ⓘ button presence and toggle behavior for all 4 tooltip fields
*(Note: test FILE exists — new describes/its are added to the existing file)*
---
## Sources
### Primary (HIGH confidence)
- Direct Zod v4.3.6 runtime execution — `.regex()` chaining verified, `.optional().regex()` order issue verified
- Codebase read: `src/schemas/registry.ts`, `src/schemas/index.ts`, `src/components/ui/FieldRenderer.tsx`, `src/components/ui/PasswordField.tsx`, `src/components/wizard/AzureAuthToggle.tsx`, `src/components/wizard/SftpAuthToggle.tsx`, `src/components/wizard/RemoteConfigStep.tsx`, `src/components/wizard/RemoteConfigStep.test.tsx`
- Test suite run: `npx vitest run` — 147/147 pass as baseline
### Secondary (MEDIUM confidence)
- 07-CONTEXT.md — user locked decisions, regexes, field lists
- REQUIREMENTS.md — VALID-01, UX-01 success criteria
### Tertiary (LOW confidence)
- None — all findings are from direct codebase inspection and runtime verification.
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — all libraries already installed and in use; no new dependencies
- Architecture: HIGH — patterns directly observed in existing code; Zod v4 regex chaining runtime-verified
- Pitfalls: HIGH — AzureAuthToggle bypass confirmed by reading RemoteConfigStep; Zod optional+regex order confirmed by runtime test
**Research date:** 2026-03-31
**Valid until:** 2026-04-30 (stable codebase, no moving targets)