@@ -18,8 +18,33 @@ export const ENGINE_V2_CUSTOM_EXTENSION_HOOKS = Object.freeze({
1818export const ENGINE_V2_CUSTOM_EXTENSION_HOOK_LIST = Object . freeze ( Object . values ( ENGINE_V2_CUSTOM_EXTENSION_HOOKS ) ) ;
1919
2020export const ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS = Object . freeze ( {
21- APPROVED : "approved" ,
22- UNAPPROVED : "unapproved" ,
21+ DRAFT : "draft" ,
22+ PRIVATE : "private" ,
23+ SUBMITTED : "submitted" ,
24+ AI_VALIDATED : "aiValidated" ,
25+ AI_REJECTED : "aiRejected" ,
26+ HUMAN_APPROVED : "humanApproved" ,
27+ HUMAN_REJECTED : "humanRejected" ,
28+ PROMOTED_CANDIDATE : "promotedCandidate" ,
29+ } ) ;
30+
31+ export const ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS = Object . freeze ( {
32+ SUBMIT : "submit" ,
33+ MARK_PRIVATE : "markPrivate" ,
34+ RETURN_TO_DRAFT : "returnToDraft" ,
35+ RECORD_AI_VALIDATED : "recordAiValidated" ,
36+ RECORD_AI_REJECTED : "recordAiRejected" ,
37+ PROMOTE_CANDIDATE : "promoteCandidate" ,
38+ HUMAN_APPROVE : "humanApprove" ,
39+ HUMAN_REJECT : "humanReject" ,
40+ } ) ;
41+
42+ export const ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_BOUNDARY = Object . freeze ( {
43+ boundaryName : "Admin Custom Extension Approval" ,
44+ aiValidationAdvisoryOnly : true ,
45+ humanApprovalRequiredForPublishEligibility : true ,
46+ noOpenAiIntegrationImplementation : true ,
47+ noAdminUiImplementation : true ,
2348} ) ;
2449
2550export const ENGINE_V2_CUSTOM_EXTENSION_ALLOWED_CONTEXT_KEYS = Object . freeze ( [
@@ -73,9 +98,53 @@ export const ENGINE_V2_CUSTOM_EXTENSION_ERRORS = Object.freeze({
7398 FIELD_FORBIDDEN : "ENGINE_V2_CUSTOM_EXTENSION_FIELD_FORBIDDEN" ,
7499 RUNTIME_INVALID : "ENGINE_V2_CUSTOM_EXTENSION_RUNTIME_INVALID" ,
75100 HOOK_NAME_INVALID : "ENGINE_V2_CUSTOM_EXTENSION_HOOK_NAME_INVALID" ,
101+ APPROVAL_ACTION_INVALID : "ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_ACTION_INVALID" ,
102+ APPROVAL_TRANSITION_INVALID : "ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_TRANSITION_INVALID" ,
76103} ) ;
77104
78105const APPROVAL_STATUS_LIST = Object . freeze ( Object . values ( ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS ) ) ;
106+ const APPROVAL_ACTION_LIST = Object . freeze ( Object . values ( ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS ) ) ;
107+ const AI_ADVISORY_APPROVAL_STATUSES = new Set ( [
108+ ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . AI_VALIDATED ,
109+ ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . AI_REJECTED ,
110+ ] ) ;
111+ const PUBLISH_ELIGIBLE_APPROVAL_STATUSES = new Set ( [
112+ ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . HUMAN_APPROVED ,
113+ ] ) ;
114+ const ADMIN_APPROVAL_TRANSITIONS = Object . freeze ( {
115+ [ ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . DRAFT ] : Object . freeze ( {
116+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . MARK_PRIVATE ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . PRIVATE ,
117+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . SUBMIT ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . SUBMITTED ,
118+ } ) ,
119+ [ ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . PRIVATE ] : Object . freeze ( {
120+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . RETURN_TO_DRAFT ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . DRAFT ,
121+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . SUBMIT ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . SUBMITTED ,
122+ } ) ,
123+ [ ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . SUBMITTED ] : Object . freeze ( {
124+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . RECORD_AI_VALIDATED ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . AI_VALIDATED ,
125+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . RECORD_AI_REJECTED ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . AI_REJECTED ,
126+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . HUMAN_APPROVE ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . HUMAN_APPROVED ,
127+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . HUMAN_REJECT ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . HUMAN_REJECTED ,
128+ } ) ,
129+ [ ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . AI_VALIDATED ] : Object . freeze ( {
130+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . PROMOTE_CANDIDATE ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . PROMOTED_CANDIDATE ,
131+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . HUMAN_APPROVE ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . HUMAN_APPROVED ,
132+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . HUMAN_REJECT ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . HUMAN_REJECTED ,
133+ } ) ,
134+ [ ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . AI_REJECTED ] : Object . freeze ( {
135+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . SUBMIT ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . SUBMITTED ,
136+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . HUMAN_REJECT ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . HUMAN_REJECTED ,
137+ } ) ,
138+ [ ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . PROMOTED_CANDIDATE ] : Object . freeze ( {
139+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . HUMAN_APPROVE ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . HUMAN_APPROVED ,
140+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . HUMAN_REJECT ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . HUMAN_REJECTED ,
141+ } ) ,
142+ [ ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . HUMAN_REJECTED ] : Object . freeze ( {
143+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . MARK_PRIVATE ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . PRIVATE ,
144+ [ ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_ACTIONS . SUBMIT ] : ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . SUBMITTED ,
145+ } ) ,
146+ [ ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . HUMAN_APPROVED ] : Object . freeze ( { } ) ,
147+ } ) ;
79148
80149export function registerEngineV2CustomExtensionHooks ( { extensionDefinitions } ) {
81150 const errors = [ ] ;
@@ -94,25 +163,29 @@ export function registerEngineV2CustomExtensionHooks({ extensionDefinitions }) {
94163 }
95164
96165 const registeredHooks = extensionDefinitions . flatMap ( ( definition ) => {
97- const approved = definition . approvalStatus === ENGINE_V2_CUSTOM_EXTENSION_APPROVAL_STATUS . APPROVED ;
166+ const approvalState = createApprovalState ( definition . approvalStatus ) ;
98167
99168 return definition . hookRegistrations . map ( ( registration ) => Object . freeze ( {
100169 extensionId : definition . extensionId ,
101170 displayName : definition . displayName ,
102171 hookName : registration . hookName ,
103172 extensionMode : registration . extensionMode ,
104173 allowedContextKeys : Object . freeze ( [ ...registration . allowedContextKeys ] ) ,
105- approved,
106- creatorPrivate : ! approved ,
107- publishEligible : approved ,
174+ approvalStatus : approvalState . approvalStatus ,
175+ humanApproved : approvalState . humanApproved ,
176+ aiValidationAdvisory : approvalState . aiValidationAdvisory ,
177+ humanApprovalRequiredForPublishEligibility : approvalState . humanApprovalRequiredForPublishEligibility ,
178+ creatorPrivate : approvalState . creatorPrivate ,
179+ publishEligible : approvalState . publishEligible ,
108180 } ) ) ;
109181 } ) ;
110182 const publishEligible = registeredHooks . every ( ( hook ) => hook . publishEligible ) ;
111183 const runtime = Object . freeze ( {
112184 registeredHooks : Object . freeze ( registeredHooks ) ,
113185 publishEligibility : Object . freeze ( {
114186 eligible : publishEligible ,
115- blockedByExtensionIds : Object . freeze ( registeredHooks . filter ( ( hook ) => ! hook . publishEligible ) . map ( ( hook ) => hook . extensionId ) ) ,
187+ humanApprovalRequired : ! publishEligible ,
188+ blockedByExtensionIds : freezeUnique ( registeredHooks . filter ( ( hook ) => ! hook . publishEligible ) . map ( ( hook ) => hook . extensionId ) ) ,
116189 } ) ,
117190 } ) ;
118191
@@ -124,6 +197,52 @@ export function registerEngineV2CustomExtensionHooks({ extensionDefinitions }) {
124197 } ) ;
125198}
126199
200+ export function resolveEngineV2AdminCustomExtensionApprovalBoundary ( { extensionDefinition, approvalAction } ) {
201+ const errors = [ ] ;
202+ const definitionErrors = validateExtensionDefinition ( extensionDefinition , "extensionDefinition" ) ;
203+ definitionErrors . forEach ( ( error ) => errors . push ( error ) ) ;
204+
205+ const actionType = isRecord ( approvalAction ) ? approvalAction . actionType : approvalAction ;
206+
207+ if ( ! APPROVAL_ACTION_LIST . includes ( actionType ) ) {
208+ errors . push ( createCustomExtensionError ( ENGINE_V2_CUSTOM_EXTENSION_ERRORS . APPROVAL_ACTION_INVALID , "Admin Custom Extension Approval requires an approved approval action." , "approvalAction.actionType" ) ) ;
209+ }
210+
211+ if ( errors . length > 0 ) {
212+ return createApprovalBoundaryResult ( {
213+ previousApprovalStatus : isRecord ( extensionDefinition ) ? extensionDefinition . approvalStatus : null ,
214+ nextApprovalStatus : null ,
215+ publishEligibility : null ,
216+ errors,
217+ } ) ;
218+ }
219+
220+ const nextApprovalStatus = ADMIN_APPROVAL_TRANSITIONS [ extensionDefinition . approvalStatus ] [ actionType ] ;
221+
222+ if ( ! nextApprovalStatus ) {
223+ errors . push ( createCustomExtensionError ( ENGINE_V2_CUSTOM_EXTENSION_ERRORS . APPROVAL_TRANSITION_INVALID , "Admin Custom Extension Approval action is not valid for the current approval status." , "approvalAction.actionType" ) ) ;
224+ return createApprovalBoundaryResult ( {
225+ previousApprovalStatus : extensionDefinition . approvalStatus ,
226+ nextApprovalStatus : null ,
227+ publishEligibility : null ,
228+ errors,
229+ } ) ;
230+ }
231+
232+ const approvalState = createApprovalState ( nextApprovalStatus ) ;
233+
234+ return createApprovalBoundaryResult ( {
235+ previousApprovalStatus : extensionDefinition . approvalStatus ,
236+ nextApprovalStatus,
237+ publishEligibility : Object . freeze ( {
238+ eligible : approvalState . publishEligible ,
239+ humanApprovalRequired : approvalState . humanApprovalRequiredForPublishEligibility ,
240+ blockedByExtensionIds : approvalState . publishEligible ? Object . freeze ( [ ] ) : Object . freeze ( [ extensionDefinition . extensionId ] ) ,
241+ } ) ,
242+ errors,
243+ } ) ;
244+ }
245+
127246export function dispatchEngineV2CustomExtensionHook ( { runtime, hookName, runtimeContext } ) {
128247 const errors = [ ] ;
129248
@@ -200,6 +319,19 @@ function validateHookRegistration(registration, path) {
200319 return errors ;
201320}
202321
322+ function createApprovalState ( approvalStatus ) {
323+ const humanApproved = PUBLISH_ELIGIBLE_APPROVAL_STATUSES . has ( approvalStatus ) ;
324+
325+ return Object . freeze ( {
326+ approvalStatus,
327+ humanApproved,
328+ aiValidationAdvisory : AI_ADVISORY_APPROVAL_STATUSES . has ( approvalStatus ) ,
329+ humanApprovalRequiredForPublishEligibility : ! humanApproved ,
330+ creatorPrivate : ! humanApproved ,
331+ publishEligible : humanApproved ,
332+ } ) ;
333+ }
334+
203335function validateNoForbiddenFields ( value , path ) {
204336 return ENGINE_V2_CUSTOM_EXTENSION_FORBIDDEN_FIELDS
205337 . filter ( ( field ) => Object . hasOwn ( value , field ) )
@@ -246,6 +378,22 @@ function createCustomExtensionDispatchResult({ hookInvocations, errors }) {
246378 } ) ;
247379}
248380
381+ function createApprovalBoundaryResult ( { previousApprovalStatus, nextApprovalStatus, publishEligibility, errors } ) {
382+ const nextApprovalState = nextApprovalStatus ? createApprovalState ( nextApprovalStatus ) : null ;
383+
384+ return Object . freeze ( {
385+ valid : errors . length === 0 ,
386+ boundary : ENGINE_V2_ADMIN_CUSTOM_EXTENSION_APPROVAL_BOUNDARY ,
387+ previousApprovalStatus,
388+ nextApprovalStatus,
389+ aiValidationAdvisory : nextApprovalState ? nextApprovalState . aiValidationAdvisory : false ,
390+ humanApprovalRequiredForPublishEligibility : nextApprovalState ? nextApprovalState . humanApprovalRequiredForPublishEligibility : true ,
391+ creatorPrivate : nextApprovalState ? nextApprovalState . creatorPrivate : true ,
392+ publishEligibility,
393+ errors : Object . freeze ( errors ) ,
394+ } ) ;
395+ }
396+
249397function createCustomExtensionError ( code , message , path ) {
250398 return Object . freeze ( { code, message, path } ) ;
251399}
@@ -254,6 +402,10 @@ function freezeJsonClone(value) {
254402 return Object . freeze ( JSON . parse ( JSON . stringify ( value ) ) ) ;
255403}
256404
405+ function freezeUnique ( values ) {
406+ return Object . freeze ( [ ...new Set ( values ) ] ) ;
407+ }
408+
257409function isRecord ( value ) {
258410 return value !== null && typeof value === "object" && ! Array . isArray ( value ) ;
259411}
0 commit comments