@@ -29,18 +29,37 @@ export function resolveContainerToSentinelStart(blockId: string, dag: DAG): stri
2929export interface RunFromBlockValidation {
3030 valid : boolean
3131 error ?: string
32+ /** Why the start was refused, by id, for callers that phrase their own guidance. */
33+ refusal ?: RunFromBlockRefusal
3234}
3335
36+ /**
37+ * The structured reason behind a refused run-from-block start. `blockId` is always the
38+ * requested start block.
39+ */
40+ export type RunFromBlockRefusal =
41+ | { kind : 'block_not_found' ; blockId : string }
42+ | {
43+ kind : 'inside_subflow'
44+ blockId : string
45+ subflowId : string
46+ subflowType : 'loop' | 'parallel'
47+ }
48+ | { kind : 'upstream_not_executed' ; blockId : string ; upstreamBlockId : string }
49+
3450/**
3551 * A run-from-block start the executor refused before running anything: the block is
3652 * missing, sits inside a loop or parallel, or has an upstream dependency the source
3753 * snapshot never executed. Its own class so the application layer can hand the reason
3854 * to the caller as a validation failure instead of the generic system-error fallback.
3955 */
4056export class RunFromBlockValidationError extends Error {
41- constructor ( message : string ) {
57+ readonly refusal ?: RunFromBlockRefusal
58+
59+ constructor ( message : string , refusal ?: RunFromBlockRefusal ) {
4260 super ( message )
4361 this . name = 'RunFromBlockValidationError'
62+ this . refusal = refusal
4463 }
4564}
4665
@@ -158,39 +177,50 @@ export function computeExecutionSets(dag: DAG, startBlockId: string): ExecutionS
158177 * @param blockId - The block ID to validate
159178 * @param dag - The workflow DAG
160179 * @param executedBlocks - Set of blocks that were executed in the source run
180+ * @param blockNames - Display names by block id, so the message names blocks instead of ids
161181 * @returns Validation result with error message if invalid
162182 */
163183export function validateRunFromBlock (
164184 blockId : string ,
165185 dag : DAG ,
166- executedBlocks : Set < string >
186+ executedBlocks : Set < string > ,
187+ blockNames : ReadonlyMap < string , string > = new Map ( )
167188) : RunFromBlockValidation {
189+ const label = ( id : string ) => blockNames . get ( id ) ?? id
190+ const insideSubflow = (
191+ subflowId : string | undefined ,
192+ subflowType : 'loop' | 'parallel'
193+ ) : RunFromBlockValidation => {
194+ if ( ! subflowId ) {
195+ return { valid : false , error : `Cannot run from block inside ${ subflowType } : ${ blockId } ` }
196+ }
197+ return {
198+ valid : false ,
199+ error : `Cannot run from block inside ${ subflowType } : ${ label ( subflowId ) } . Run the ${ subflowType } or the whole workflow instead.` ,
200+ refusal : { kind : 'inside_subflow' , blockId, subflowId, subflowType } ,
201+ }
202+ }
203+
168204 const node = dag . nodes . get ( blockId )
169205 const isLoopContainer = dag . loopConfigs . has ( blockId )
170206 const isParallelContainer = dag . parallelConfigs . has ( blockId )
171207 const isContainer = isLoopContainer || isParallelContainer
172208 let validationNode = node
173209
174210 if ( ! node && ! isContainer ) {
175- return { valid : false , error : `Block not found in workflow: ${ blockId } ` }
211+ return {
212+ valid : false ,
213+ error : `Block not found in workflow: ${ blockId } ` ,
214+ refusal : { kind : 'block_not_found' , blockId } ,
215+ }
176216 }
177217
178218 if ( isContainer ) {
179219 const parentLoopId = findParentLoop ( blockId , dag )
180- if ( parentLoopId ) {
181- return {
182- valid : false ,
183- error : `Cannot run from block inside loop: ${ parentLoopId } ` ,
184- }
185- }
220+ if ( parentLoopId ) return insideSubflow ( parentLoopId , 'loop' )
186221
187222 const parentParallelId = findParentParallel ( blockId , dag )
188- if ( parentParallelId ) {
189- return {
190- valid : false ,
191- error : `Cannot run from block inside parallel: ${ parentParallelId } ` ,
192- }
193- }
223+ if ( parentParallelId ) return insideSubflow ( parentParallelId , 'parallel' )
194224
195225 const sentinelStartId = resolveContainerToSentinelStart ( blockId , dag )
196226 if ( ! sentinelStartId || ! dag . nodes . has ( sentinelStartId ) ) {
@@ -203,19 +233,8 @@ export function validateRunFromBlock(
203233 }
204234
205235 if ( node ) {
206- if ( node . metadata . isLoopNode ) {
207- return {
208- valid : false ,
209- error : `Cannot run from block inside loop: ${ node . metadata . subflowId } ` ,
210- }
211- }
212-
213- if ( node . metadata . isParallelBranch ) {
214- return {
215- valid : false ,
216- error : `Cannot run from block inside parallel: ${ node . metadata . subflowId } ` ,
217- }
218- }
236+ if ( node . metadata . isLoopNode ) return insideSubflow ( node . metadata . subflowId , 'loop' )
237+ if ( node . metadata . isParallelBranch ) return insideSubflow ( node . metadata . subflowId , 'parallel' )
219238
220239 if ( node . metadata . isSentinel ) {
221240 return { valid : false , error : 'Cannot run from sentinel node' }
@@ -236,7 +255,8 @@ export function validateRunFromBlock(
236255 if ( ! executedBlocks . has ( sourceId ) ) {
237256 return {
238257 valid : false ,
239- error : `Upstream dependency not executed: ${ sourceId } ` ,
258+ error : `Upstream dependency not executed: ${ label ( sourceId ) } ` ,
259+ refusal : { kind : 'upstream_not_executed' , blockId, upstreamBlockId : sourceId } ,
240260 }
241261 }
242262 }
0 commit comments