I have a work-around to my own question.
According to the documentation (https://www.jenkins.io/doc/book/pipeline/syntax/#declarative-matrix) what I am looking for is not possible. "Each cell in a matrix can include one or more stages to be run sequentially using the configuration for that cell. Note that a stage must have one and only one of steps, stages, parallel, or matrix. It is not possible to nest a parallel or matrix block within a stage directive if that stage directive is nested within a parallel or matrix block itself."
But.... you can cheat. I was able to turn the matrix into a generic dispatch queue. i.e. each combination invokes N stages - for me, 2 and I call them "prepare" and "execute". I pass an additional "matrixtype" argument in. The matrixtype gets 1 value for the matrix itself, as well as additional values for each line of the non-matrix. I then use the matrix 'excludes' to insure that the non-matrix lines execute only once. Effectively, this folds the non matrix into the matrix differentiated by matrix type.
Original (parallel-stages in series with a subsequent matrix)
stage {
parallel {
stage("alpha") {
alpha(..)
}
stage("beta") {
beta(..)
}
// etc
}
}
stage {
matrix {
axes {
axis {
name 'ORIGAXIS'
values 'FOO','BAR','BAZ'
}
}
stages {
stage("First") {
originalFirst( ...)
}
stage("Second") {
originalSecond(...)
}
}
}
}
Replacement (parallel folded into matrix)
stage {
matrix {
axes {
axis {
name 'MATRIXTYPE
values 'ORIGINAL', 'ALPHA', 'BETA'
axis {
name 'ORIGAXIS'
values 'FOO','BAR','BAZ'
}
excludes {
// Execute Alpha and Beta only once (during 'FOO')
exclude {
axis {
name 'MATRIXTYPE'
values 'ALPHA', 'BETA'
}
axis {
name 'ORIGAXIS'
values 'BAR','BAZ'
}
}
}
}
stages {
stage("First") {
dispatchFirst( "${MATRIXTYPE}", ...)
}
stage("Second") {
dispatchSecond( "${MATRIXTYPE}", ...)
}
}
}
}
The dispatchFirst(..) and dispatchSecond(..) are then simple dispatch methods in the shared lib that examine matrixtype, and invoke originalFirst(..), originalSecond(..), alpha(..), beta(..) or a no-op as appropriate. It's slightly clumsy, and amounts to shoehorning the parallel stages into the matrix, but it works. And, you get the benefit of parallelization (build speed-optimization)
Hopefully in the future, there will be something more elegant.
Updated (2022-09-22): What the 'replacement' does is brute-force the heck out of it. It creates a matrix with 9 permutations:
ORIGINAL-FOO, ORIGINAL-BAR, ORIGINAL-BAZ, ALPHA-FOO, ALPHA-BAR, ALPHA-BAZ, BETA-FOO, BETA-BAR, BETA-BAZ. The exclude then gets rid of ALPHA-BAR, ALPHA-BAZ, BETA-BAR, BETA-BAZ, leaving 5 executable elements (emphazied above) in the matrix. For each of the 5 elements, the matrix will call the "dispatchFirst" then "dispatchSecond" method, passing the matrix variables over to it.
You then implement dispatchFirst such that:
if matrixType == ALPHA, invoke alpha() [only gets called once]
if matrixType == BETA, invoke beta() [only gets called once]
otherwise, invoke originalFirst(..) using FOO/BAR/BAZ as passed in
And dispatchSecond only operates if matrixType=ORIGINAL, and it calls originalSecond(..) using FOO/BAR/BAZ as passed in.