Skip to content

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

Description

@atomantic

Slice audited: client-side hand mirrors of server sources of truth (client/src/lib, client/src/utils, client/src/components/*) — structural-drift audit, 2026-09-10.

Problem

Impact

  • generationToForm (commissionForm.js:145-154) fills an absent durationMode with 'auto', while the server's ability adapter treats absent as manual (server/services/creativeCommissions/abilityAdapters.js:177, 199, 250durationMode === 'auto' ? … : targetDurationSeconds). A commission created before feat: let creative director choose video duration #4494 (no key) opens showing "Creative Director chooses", runs as a fixed-length render, and a save with no edits silently flips it to auto.
  • A commission can be pinned to fal / reactor (video) or agy (image) only through the API; the form cannot express a value the schema accepts, and a record holding one renders a <select> with no matching option.
  • Every backend added to generationModes.js needs a second edit here — two of the last three additions skipped it.

Fix (decided)

  1. Move the spec to a pure leaf server/lib/creativeCommissionSpec.js (no zod, no Node built-ins — the creativeBriefLimits.js precedent both files already name): CREATIVE_COMMISSION_QUALITIES, CREATIVE_COMMISSION_ASPECT_RATIOS, CREATIVE_COMMISSION_IMAGE_MODES, CREATIVE_COMMISSION_VIDEO_MODES, COMMISSION_RENDER_MODEL_MAX, GENERATION_KEY_DEFS, ABILITY_GENERATION_SPEC. creativeCommissionValidation.js imports them and keeps building the Zod schema from GENERATION_KEY_DEFS exactly as today; abilityAdapters.js keeps consuming ABILITY_GENERATION_SPEC.
  2. Resolve the durationMode disagreement inside the spec: default: 'auto' (the seed for a NEW commission — the product choice feat: let creative director choose video duration #4494 made in the form) plus legacyAbsent: 'manual' (what a stored record with no key means — the adapter's existing read). generationToForm fills an absent key from legacyAbsent ?? default; the adapter reads gen.durationMode ?? def.legacyAbsent. One definition, both meanings explicit.
  3. commissionForm.js derives GENERATION_FIELDS_BY_ABILITY and GENERATION_DEFAULTS_BY_ABILITY from the leaf (type / min / max / values / default), keeping only presentation client-side in a FIELD_PRESENTATION map keyed by generation key (label, modelKind, modelModes, modelLabel, option display strings). IMAGE_BACKEND_OPTIONS / VIDEO_BACKEND_OPTIONS map over the server mode enums using MODE_LABELS from client/src/lib/imageGenModes.js (add fal / reactor labels there if missing).
  4. Parity test server/lib/creativeCommissionSpec.parity.test.js in the server workspace, importing the client module the way server/lib/reviewerConfig.test.js:395 does: every key in ABILITY_GENERATION_SPEC[ability].keys renders a field with matching bounds / options, every default matches, and every mode in the server enums is offered. Mutation-probe both directions before merging.
  5. Catalog rows (barrel + README) for the new leaf; commissionForm.test.js gains the legacy-absent projection case.

Acceptance criteria

  • server/lib/creativeCommissionSpec.js exists, imports nothing, and creativeCommissionValidation.js no longer declares GENERATION_KEY_DEFS, ABILITY_GENERATION_SPEC or the four enums.
  • commissionForm.js contains no literal min / max / default / option value for a generation key; they come from the leaf.
  • The form offers fal and reactor for video and agy for image, and a stored record pinned to one round-trips through toForm / toPayload.
  • A record with no durationMode projects as manual (matching abilityAdapters.js); a fresh commission seeds 'auto'.
  • The parity test fails when a mode is added to generationModes.js without a client label, or when a bound changes on one side only.
  • Server and client suites green; vite build succeeds.

Scope: medium

Dispatch: model:medium — multi-file work across lib, services and the client with an existing precedent to follow. effort:high — the Zod schema must stay equivalent for stored records and the default / legacy-absent split is a compatibility decision that has to be applied identically on both sides.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

code-qualityProposed from a cyclomatic-complexity auditeffort:highDispatch effort: highmodel:mediumRoutine multi-file workplanTracked by /do:replanplanner:fable-5-1Plan authored by the fable-5-1 modelstructural-driftProposed from a structural-drift audit

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions