Skip to content

Derive commission form generation fields from a zod-free server spec leaf - #6823

Merged
atomantic merged 6 commits into
mainfrom
claim/issue-6816
Sep 10, 2026
Merged

Derive commission form generation fields from a zod-free server spec leaf#6823
atomantic merged 6 commits into
mainfrom
claim/issue-6816

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

  • Move the Creative Commission generation spec (GENERATION_KEY_DEFS, ABILITY_GENERATION_SPEC, and the quality/aspect-ratio/image-mode/video-mode enums) out of server/lib/creativeCommissionValidation.js (which pulls zod) into a new zod-free leaf, server/lib/creativeCommissionSpec.js — the same pattern as creativeBriefLimits.js.
  • Rewrite client/src/components/creative-commission/commissionForm.js to derive its generation fields, bounds, defaults, and backend-mode options directly from that leaf instead of hand-copying them, keeping only display labels and the render-backend-pin wiring as client-side presentation metadata.
  • The form now offers agy (image) and fal/reactor (video) — backends the client picker was missing entirely — and every future backend added to generationModes.js reaches the form automatically via the shared modeLabel lookup.
  • Resolve the durationMode compatibility split explicitly: the descriptor now carries both default ('auto', the fresh-commission seed from feat: let creative director choose video duration #4494) and legacyAbsent ('manual', what a stored record with no key has always meant). The server's ability-adapter sanitizer and the client's record projection both resolve an absent value through this same split, so a legacy record still projects/renders as a fixed length while a brand-new commission still seeds "Creative Director chooses."
  • Add server/lib/creativeCommissionSpec.parity.test.js, importing the client module from the server workspace, asserting every ability field's bounds/options/defaults match the spec, every non-auto backend mode has a real label, and the durationMode split resolves identically on both sides.

Test plan

  • server/lib/creativeCommissionSpec.parity.test.js, server/lib/index.test.js, server/lib/importScoping.test.js, server/lib/validation.test.js, server/services/creativeCommissions/{abilityAdapters,store,directive,scheduler}.test.js, server/routes/creativeCommissions.test.js — all green (536 tests).
  • client/src/components/creative-commission/{commissionForm,CommissionConfigForm}.test.jsx, client/src/pages/{CreativeCommissionDetail,CreativeCommissions}.test.jsx, client/src/lib/index.test.js — all green (63 tests), including new coverage for the durationMode legacy split and the agy/fal/reactor round-trip.
  • vite build succeeds; the commission-form bundle chunk carries no zod/child_process/node:fs references.
  • Every new assertion was verified to fail against the pre-fix code (temporarily reverted, run, restored) before being accepted, including the parity test against the unmodified spec-leaf-less tree.

Closes #6816

…eaf (#6816)

server/lib/creativeCommissionValidation.js pulls zod, so commissionForm.js
could only mirror its per-key type/bounds/default spec by hand. That hand
copy had already drifted three ways: durationMode's default (client 'auto'
vs server 'manual'), the video backend enum (client missing fal/reactor),
and the image backend enum (client missing agy).

Move GENERATION_KEY_DEFS, ABILITY_GENERATION_SPEC, and the quality/aspect
ratio/backend enums into a new dependency-free leaf,
server/lib/creativeCommissionSpec.js (the creativeBriefLimits.js precedent).
creativeCommissionValidation.js now imports and re-exports them, still
building the same Zod schema from GENERATION_KEY_DEFS.

Also resolves the durationMode compatibility split explicitly: the
descriptor now carries both `default` ('auto', the fresh-commission seed
from #4494) and `legacyAbsent` ('manual', what a record with no key at all
has always meant). abilityAdapters.js's coerceGenerationValue and its three
raw duration reads now resolve an absent value through this split
(resolvedDefault / GENERATION_KEY_DEFS.durationMode.legacyAbsent) instead of
a bare `.default`, so stored-record behavior is unchanged.
…ec leaf (#6816)

commissionForm.js hand-mirrored server/lib/creativeCommissionValidation.js's
generation-key bounds/defaults/options because that module pulled zod. Now
that the spec lives in the zod-free creativeCommissionSpec.js, the form
imports GENERATION_KEY_DEFS / ABILITY_GENERATION_SPEC directly and builds
GENERATION_FIELDS_BY_ABILITY / GENERATION_DEFAULTS_BY_ABILITY from it,
keeping only display labels and the render-backend-pin wiring as client-side
presentation metadata.

IMAGE_BACKEND_OPTIONS / VIDEO_BACKEND_OPTIONS now map over the server's mode
enums via the shared MODE_LABELS (client/src/lib/imageGenModes.js), so the
form immediately offers agy (image) and fal/reactor (video) instead of
needing a second hand-edit whenever generationModes.js gains a backend.

generationToForm now projects a REAL record's absent key through the spec's
legacyAbsent reading (falls back to ABILITY_GENERATION_SPEC's defaults,
which prefer legacyAbsent over default) rather than the fresh-commission
default, so an existing record with no durationMode still projects as
'manual' — matching abilityAdapters.js. A brand-new/blank commission
(toForm({}) / blankForm()) is unaffected: it seeds every key from
GENERATION_DEFAULTS_BY_ABILITY's plain default ('auto' for durationMode),
replacing the old ad hoc video/music-video-only Object.hasOwn check with a
general mechanism that works for any key a future spec entry marks
legacyAbsent.

Also switches the RENDER_BACKEND_AUTO import from imageGenBackends.js (which
pulls lucide-react) to its node-safe half, imageGenModes.js — required for
the server-workspace parity test landing in the next commit to import this
module at all.
… spec (#6816)

No test previously compared commissionForm.js's hand-copied generation
fields/defaults/backend options against the server's actual spec — that gap
is how the durationMode default, the video backend enum (fal/reactor), and
the image backend enum (agy) drifted silently for months.

creativeCommissionSpec.parity.test.js imports commissionForm.js from the
server workspace (the reviewerConfig.test.js pattern) and asserts every
ability field's bounds/options/defaults match GENERATION_KEY_DEFS, every
non-auto backend mode has a real client label, and an absent durationMode
resolves identically on the ability adapter and the client form.

Raises the server suite's static-import budget (importScoping.test.js) by
the standard ~1.5k allowance: the new creativeCommissionSpec.js leaf adds
+1 to every closure already reaching creativeCommissionValidation.js (itself
widely reached through validation.js's flat re-export) without pulling in
anything those closures didn't already have — the same shape as #6590/#6617.
- commissionForm.js: reuse the shared modeLabel() helper (client/src/lib/
  imageGenModes.js) instead of reaching into MODE_LABELS directly, and drop
  a redundant `|| 'video'` fallback toForm() no longer needs now that
  abilityOr() already covers it.
- abilityAdapters.js: isAutoDuration() now delegates to the same
  coerceGenerationValue() every other key's absent-value resolution already
  goes through, instead of a bespoke `??` on the raw value — also makes it
  correctly resolve an INVALID (not just absent) durationMode.
- creativeCommissionSpec.parity.test.js: drop assertions that could only
  ever pass or fail together (blankForm() vs toForm({}); video vs
  music-video for a per-key, ability-agnostic resolution), and add coverage
  for the plain-select option labels (quality/durationMode) alongside the
  existing backend-mode label check.
- commissionForm.test.js: add the agy/fal/reactor round-trip case the
  acceptance criteria named explicitly.
…tion as legacy, not fresh

toForm()'s blank-vs-real branch keyed only on `c.generation == null`, so a
persisted commission whose generation object happened to be absent or null
would have been mis-seeded with the fresh-commission default (durationMode
'auto') instead of the legacy reading ('manual') abilityAdapters.js already
applies to such a record. Every real API response always sends a populated
generation object today, so this wasn't reachable through the app, but the
helper's own contract should not depend on that.

Add `&& !c.id` to the blank check — ids are minted server-side and never
present on an unsaved commission, so this distinguishes "not yet persisted"
from "persisted but sparse" without relying on `generation` alone. Verified
against the existing CommissionConfigForm harness that pre-seeds a NEW
commission with just a brief (no id, no generation) — that still correctly
seeds the fresh default.
… defaults (#6816)

mergeGenerationForAbility was a bare alias of generationToForm, so once that
projection became legacy-aware a form switched from Image to Video seeded
durationMode as 'manual' — on a brand-new commission too — where #4494's
product choice is 'Creative Director chooses'. A key the switch introduces was
never on the record, so the legacy reading does not describe it: the switch now
falls back to the same fresh defaults a blank form gets, while a stored record
with the key missing still projects as 'manual'. The two projections share one
filler and differ only in which defaults they fall back to.
@atomantic
atomantic merged commit 2d8f978 into main Sep 10, 2026
12 checks passed
@atomantic
atomantic deleted the claim/issue-6816 branch September 10, 2026 07:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[structural-drift-commission-generation-spec] Derive the commission form's generation fields from a zod-free server spec leaf

1 participant