You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
client/src/components/creative-commission/commissionForm.js:91-121 hand-mirrors GENERATION_KEY_DEFS / ABILITY_GENERATION_SPEC from server/lib/creativeCommissionValidation.js:85-114, and IMAGE_BACKEND_OPTIONS / VIDEO_BACKEND_OPTIONS (lines 63-74) hand-mirror CREATIVE_COMMISSION_IMAGE_MODES / CREATIVE_COMMISSION_VIDEO_MODES (server lines 63-72). Both files say so in comments ("keep this in sync with the server spec BY HAND"; "commissionForm.test.js asserts only the client-internal invariant … not server↔client parity"). No parity test exists: no server test imports commissionForm, and commissionForm.test.js covers toForm / toPayload only.
Already drifted, three ways:
durationMode default — client 'auto' (GENERATION_DEFAULTS_BY_ABILITY.video and 'music-video', lines 111 and 116) vs server default: 'manual' (creativeCommissionValidation.js:92). Both landed in ce01427 (feat: let creative director choose video duration #4494) already disagreeing.
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, 250 — durationMode === '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)
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.
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.
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).
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.
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.
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
client/src/components/creative-commission/commissionForm.js:91-121hand-mirrorsGENERATION_KEY_DEFS/ABILITY_GENERATION_SPECfromserver/lib/creativeCommissionValidation.js:85-114, andIMAGE_BACKEND_OPTIONS/VIDEO_BACKEND_OPTIONS(lines 63-74) hand-mirrorCREATIVE_COMMISSION_IMAGE_MODES/CREATIVE_COMMISSION_VIDEO_MODES(server lines 63-72). Both files say so in comments ("keep this in sync with the server spec BY HAND"; "commissionForm.test.js asserts only the client-internal invariant … not server↔client parity"). No parity test exists: no server test importscommissionForm, andcommissionForm.test.jscoverstoForm/toPayloadonly.durationModedefault — client'auto'(GENERATION_DEFAULTS_BY_ABILITY.videoand'music-video', lines 111 and 116) vs serverdefault: 'manual'(creativeCommissionValidation.js:92). Both landed in ce01427 (feat: let creative director choose video duration #4494) already disagreeing.CREATIVE_COMMISSION_VIDEO_MODES= auto +VIDEO_GEN_MODES= local, grok, fal, reactor (server/lib/generationModes.js:40-46; fal from e7d8921 [repo-study-julius-caesar-fal-video-api] Add fal.ai video generator API provider to MediaGen and FableLoom #6213, reactor from 6486822 [repo-study-julius-caesar-reactor-fasth3-provider] Add Reactor.inc fast-h3 video generation API provider to MediaGen and FableLoom #6214). ClientVIDEO_BACKEND_OPTIONS= auto, local, grok — last touched by 1161e87 (Let creative commissions pin an image/video render backend (local vs. Grok vs. Codex) per commission #3135);git log -S VIDEO_BACKEND_OPTIONS -- commissionForm.jsshows nothing since.CLOUD_IMAGE_GEN_MODES, ebbbba4 Fix lib-to-services layering inversions across server/lib #4901); clientIMAGE_BACKEND_OPTIONSstops at grok.Impact
generationToForm(commissionForm.js:145-154) fills an absentdurationModewith'auto', while the server's ability adapter treats absent as manual (server/services/creativeCommissions/abilityAdapters.js:177, 199, 250—durationMode === '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.<select>with no matching option.generationModes.jsneeds a second edit here — two of the last three additions skipped it.Fix (decided)
server/lib/creativeCommissionSpec.js(nozod, no Node built-ins — thecreativeBriefLimits.jsprecedent 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.jsimports them and keeps building the Zod schema fromGENERATION_KEY_DEFSexactly as today;abilityAdapters.jskeeps consumingABILITY_GENERATION_SPEC.durationModedisagreement 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) pluslegacyAbsent: 'manual'(what a stored record with no key means — the adapter's existing read).generationToFormfills an absent key fromlegacyAbsent ?? default; the adapter readsgen.durationMode ?? def.legacyAbsent. One definition, both meanings explicit.commissionForm.jsderivesGENERATION_FIELDS_BY_ABILITYandGENERATION_DEFAULTS_BY_ABILITYfrom the leaf (type / min / max / values / default), keeping only presentation client-side in aFIELD_PRESENTATIONmap keyed by generation key (label,modelKind,modelModes,modelLabel, option display strings).IMAGE_BACKEND_OPTIONS/VIDEO_BACKEND_OPTIONSmap over the server mode enums usingMODE_LABELSfromclient/src/lib/imageGenModes.js(add fal / reactor labels there if missing).server/lib/creativeCommissionSpec.parity.test.jsin the server workspace, importing the client module the wayserver/lib/reviewerConfig.test.js:395does: every key inABILITY_GENERATION_SPEC[ability].keysrenders a field with matching bounds / options, every default matches, and every mode in the server enums is offered. Mutation-probe both directions before merging.commissionForm.test.jsgains the legacy-absent projection case.Acceptance criteria
server/lib/creativeCommissionSpec.jsexists, imports nothing, andcreativeCommissionValidation.jsno longer declaresGENERATION_KEY_DEFS,ABILITY_GENERATION_SPECor the four enums.commissionForm.jscontains no literal min / max / default / option value for a generation key; they come from the leaf.toForm/toPayload.durationModeprojects as manual (matchingabilityAdapters.js); a fresh commission seeds'auto'.generationModes.jswithout a client label, or when a bound changes on one side only.vite buildsucceeds.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.