# Offers — Agent Guidance

Offers-specific rules. Read alongside `../CLAUDE.md`.

## Form isolation invariant

Every form section owns its own `useForm` instance, its own `FormProvider`, and its own Zod resolver. Sections **never** share form state. If you find yourself reading a field from a sibling section's form, that is a bug — expose the value through a prop or a shared hook return instead.

## Section hook contract

All section hooks must return `{ form, onSave, isLoading, ...domainData }`. The `onSave` signature is always `(cb?: (showAlert: boolean) => void) => (e?: BaseSyntheticEvent) => void`. This is the standard used by `useJobForm` to compose the wizard — do not deviate.

## `useJobForm` is CREATE-only

`useJobForm` orchestrates the four-step wizard. It is not used on edit pages. Edit pages instantiate section hooks directly (e.g. `JobOfferEdit` calls `useGeneralConfigSection` directly). Do not try to reuse `useJobForm` for edit flows.

## Application Form visibility is not an RHF form

Field-visibility state is managed as a delta map (`Record<number, JobApplicationFormFieldVisibilityValue>`) via `useApplicationFormChanges`. It is **not** a React Hook Form instance. The delta only tracks fields whose visibility differs from the backend value; missing keys inherit the BE value. Call `resetApplicationFormChanges()` after a successful save.

## `ApplicationFormTab` ↔ `profileDisplayRules` coupling

`ApplicationFormTab` (inside `PublishStep`) imports `HIDDEN_SECTION_CODES` from `JobApplicationDetail/constants/profileDisplayRules.ts`. This cross-directory import is intentional — both views must agree on which sections are unsupported. If you add a new hidden section code, update `profileDisplayRules.ts` in one place.

## `summaryTypes.ts` invariant

`Section` is a discriminated union: it holds either `items` or `subsections`, never both. This invariant is enforced by the TypeScript type. Do not add an optional second field — use the union correctly.

## `ProfileTab` resume override

In `ProfileTab`, `application.resume` overrides the form-answer value for the `resume` field. This is intentional: the resume can be updated via the edit drawer without re-fetching form answers. Do not remove this override.

## `useOffersFilters` — URL as state

Filter state (search, departments, brands) lives in URL search params, not in component state. All setters use `replace: true` to avoid polluting history. List params are encoded as `|`-separated strings. When adding a new filter, follow this pattern — do not introduce useState for filter state.

## `useJobPosting` constructs the public URL client-side

The public posting URL is assembled from `VITE_CAREERS_SITE_URL` + `jobBoard.siteSlug` + `data.jobId`. The backend does not return the full URL. Do not expect a `url` field from the API response.

## `useApplicantUpdateDrawerContent` biome-ignore

This hook returns a factory function (a hook that returns a function). Biome incorrectly flags the inner hooks as top-level violations. The `biome-ignore-all lint/correctness/useHookAtTopLevel` comment at the top is a documented false positive — do not remove it.

## Query invalidation

After mutating application form field visibility, always invalidate `jobKeys.applicationForm(jobId)`. After updating a candidate, invalidate `jobKeys.detail(jobId)`, `applicantsKeys.applicationDetail(jobId, applicationId)`, and `applicantsKeys.formAnswers(jobId, applicationId)`.

## `usePublicDescriptionWarningModal` vs `useJobOpenFromPublishModal`

- `usePublicDescriptionWarningModal` — shown when the user edits a **live** posting (not just any save)
- `useJobOpenFromPublishModal` — shown when the job is still in Draft and the user tries to publish

Do not conflate these two modal triggers.
