@pikku/inspector 0.12.26 → 0.12.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/dist/add/add-functions.js +15 -4
- package/dist/add/add-rpc-invocations.js +27 -0
- package/dist/error-codes.d.ts +2 -1
- package/dist/error-codes.js +1 -0
- package/dist/inspector.js +18 -3
- package/dist/types.d.ts +14 -0
- package/dist/utils/extract-function-name.js +20 -3
- package/dist/utils/filter-inspector-state.js +7 -6
- package/dist/utils/post-process.js +8 -1
- package/dist/utils/resolve-deploy-target.d.ts +3 -2
- package/dist/utils/resolve-deploy-target.js +4 -3
- package/dist/utils/schema-generator.d.ts +1 -0
- package/dist/utils/schema-generator.js +76 -0
- package/dist/utils/workflow/derive-workflow-plan.d.ts +20 -0
- package/dist/utils/workflow/derive-workflow-plan.js +78 -0
- package/dist/utils/workflow/graph/finalize-workflows.js +15 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +14 -3
- package/dist/visit.d.ts +1 -0
- package/dist/visit.js +14 -1
- package/package.json +2 -2
- package/src/add/add-functions.ts +15 -4
- package/src/add/add-rpc-invocations.ts +41 -0
- package/src/add/pii-check.test.ts +3 -1
- package/src/add/rpc-type-cast.test.ts +123 -0
- package/src/error-codes.ts +2 -0
- package/src/inspector.ts +24 -2
- package/src/types.ts +17 -0
- package/src/utils/extract-function-name.ts +21 -3
- package/src/utils/filter-inspector-state.ts +13 -7
- package/src/utils/post-process.ts +8 -1
- package/src/utils/resolve-deploy-target.test.ts +30 -0
- package/src/utils/resolve-deploy-target.ts +5 -3
- package/src/utils/schema-generator.ts +92 -0
- package/src/utils/workflow/derive-workflow-plan.test.ts +122 -0
- package/src/utils/workflow/derive-workflow-plan.ts +90 -0
- package/src/utils/workflow/graph/finalize-workflows.ts +15 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +18 -3
- package/src/visit.ts +25 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
+
import { createHash } from 'crypto'
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
2
4
|
import { dirname, join, resolve } from 'path'
|
|
3
5
|
import { createGenerator, RootlessError } from 'ts-json-schema-generator'
|
|
4
6
|
import { register } from 'tsx/esm/api'
|
|
@@ -74,6 +76,74 @@ let cachedTsconfigPath: string | undefined
|
|
|
74
76
|
let cachedCustomTypesContent: string | undefined
|
|
75
77
|
let cachedTSSchemas: Record<string, JSONValue> | undefined
|
|
76
78
|
|
|
79
|
+
const SCHEMA_CACHE_VERSION = 1
|
|
80
|
+
|
|
81
|
+
// This package's own version — folded into the cache key so that upgrading
|
|
82
|
+
// @pikku/inspector (the channel through which a schema-format change ships)
|
|
83
|
+
// auto-invalidates every on-disk cache, without relying on someone remembering
|
|
84
|
+
// to bump SCHEMA_CACHE_VERSION. Read once; falls back to the constant if the
|
|
85
|
+
// package.json can't be located (e.g. an unexpected bundling layout).
|
|
86
|
+
const inspectorVersion: string = (() => {
|
|
87
|
+
try {
|
|
88
|
+
const pkgUrl = new URL('../../package.json', import.meta.url)
|
|
89
|
+
const pkg = JSON.parse(readFileSync(pkgUrl, 'utf-8'))
|
|
90
|
+
return typeof pkg.version === 'string' ? pkg.version : `v${SCHEMA_CACHE_VERSION}`
|
|
91
|
+
} catch {
|
|
92
|
+
return `v${SCHEMA_CACHE_VERSION}`
|
|
93
|
+
}
|
|
94
|
+
})()
|
|
95
|
+
|
|
96
|
+
// Key the TS-schema cache on everything that affects its output: the generated
|
|
97
|
+
// custom-types source, the generator options that change schema shape, and the
|
|
98
|
+
// inspector version (schema-format changes ship with a version bump).
|
|
99
|
+
function tsSchemaCacheKey(
|
|
100
|
+
customTypesContent: string,
|
|
101
|
+
config: { schemasFromTypes?: string[]; schema?: { additionalProperties?: boolean } }
|
|
102
|
+
): string {
|
|
103
|
+
return createHash('sha1')
|
|
104
|
+
.update(`v${SCHEMA_CACHE_VERSION}\0`)
|
|
105
|
+
.update(`pkg:${inspectorVersion}\0`)
|
|
106
|
+
.update(`ap:${config.schema?.additionalProperties ? 1 : 0}\0`)
|
|
107
|
+
.update(`ft:${(config.schemasFromTypes ?? []).join(',')}\0`)
|
|
108
|
+
.update(customTypesContent)
|
|
109
|
+
.digest('hex')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function schemaCacheFile(cacheDir: string): string {
|
|
113
|
+
return join(cacheDir, 'ts-schemas.json')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readDiskTSSchemas(
|
|
117
|
+
logger: InspectorLogger,
|
|
118
|
+
cacheDir: string,
|
|
119
|
+
key: string
|
|
120
|
+
): Record<string, JSONValue> | null {
|
|
121
|
+
const file = schemaCacheFile(cacheDir)
|
|
122
|
+
if (!existsSync(file)) return null
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(readFileSync(file, 'utf-8'))
|
|
125
|
+
if (parsed?.key === key && parsed.schemas) return parsed.schemas
|
|
126
|
+
} catch (e) {
|
|
127
|
+
logger.debug(`Ignoring unreadable TS-schema cache: ${(e as Error).message}`)
|
|
128
|
+
}
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function writeDiskTSSchemas(
|
|
133
|
+
logger: InspectorLogger,
|
|
134
|
+
cacheDir: string,
|
|
135
|
+
key: string,
|
|
136
|
+
schemas: Record<string, JSONValue>
|
|
137
|
+
): void {
|
|
138
|
+
const file = schemaCacheFile(cacheDir)
|
|
139
|
+
try {
|
|
140
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
141
|
+
writeFileSync(file, JSON.stringify({ key, schemas }))
|
|
142
|
+
} catch (e) {
|
|
143
|
+
logger.debug(`Failed to persist TS-schema cache: ${(e as Error).message}`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
77
147
|
function createProgramWithVirtualFile(
|
|
78
148
|
tsconfig: string,
|
|
79
149
|
virtualFilePath: string,
|
|
@@ -494,6 +564,7 @@ export async function generateAllSchemas(
|
|
|
494
564
|
tsconfig: string
|
|
495
565
|
schemasFromTypes?: string[]
|
|
496
566
|
schema?: { additionalProperties?: boolean }
|
|
567
|
+
cacheDir?: string
|
|
497
568
|
},
|
|
498
569
|
state: InspectorState
|
|
499
570
|
): Promise<Record<string, JSONValue>> {
|
|
@@ -509,11 +580,28 @@ export async function generateAllSchemas(
|
|
|
509
580
|
requiredTypes
|
|
510
581
|
)
|
|
511
582
|
|
|
583
|
+
// Fast path: same process, types unchanged — reuse the in-memory result.
|
|
512
584
|
if (cachedTSSchemas && cachedCustomTypesContent === customTypesContent) {
|
|
513
585
|
logger.debug('Reusing cached TS schemas (types unchanged)')
|
|
514
586
|
return { ...cachedTSSchemas, ...zodSchemas }
|
|
515
587
|
}
|
|
516
588
|
|
|
589
|
+
// Disk path: a prior `pikku all` left a cache whose key matches the current
|
|
590
|
+
// custom types — load it and skip ts-json-schema-generator (the dominant
|
|
591
|
+
// cold-run cost). Zod schemas are always regenerated (cheap, ~1ms/schema).
|
|
592
|
+
const cacheKey = config.cacheDir
|
|
593
|
+
? tsSchemaCacheKey(customTypesContent, config)
|
|
594
|
+
: null
|
|
595
|
+
if (config.cacheDir && cacheKey) {
|
|
596
|
+
const disk = readDiskTSSchemas(logger, config.cacheDir, cacheKey)
|
|
597
|
+
if (disk) {
|
|
598
|
+
logger.debug('Reusing on-disk TS schemas (types unchanged across runs)')
|
|
599
|
+
cachedCustomTypesContent = customTypesContent
|
|
600
|
+
cachedTSSchemas = disk
|
|
601
|
+
return { ...disk, ...zodSchemas }
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
517
605
|
const tsSchemas = generateTSSchemas(
|
|
518
606
|
logger,
|
|
519
607
|
config.tsconfig,
|
|
@@ -529,5 +617,9 @@ export async function generateAllSchemas(
|
|
|
529
617
|
cachedCustomTypesContent = customTypesContent
|
|
530
618
|
cachedTSSchemas = tsSchemas
|
|
531
619
|
|
|
620
|
+
if (config.cacheDir && cacheKey) {
|
|
621
|
+
writeDiskTSSchemas(logger, config.cacheDir, cacheKey, tsSchemas)
|
|
622
|
+
}
|
|
623
|
+
|
|
532
624
|
return { ...tsSchemas, ...zodSchemas }
|
|
533
625
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { strict as assert } from 'assert'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import type { WorkflowStepMeta } from '@pikku/core/workflow'
|
|
4
|
+
import { deriveWorkflowPlan } from './derive-workflow-plan.js'
|
|
5
|
+
|
|
6
|
+
const rpc = (stepName: string, rpcName = 'rpc.fn'): WorkflowStepMeta =>
|
|
7
|
+
({ type: 'rpc', stepName, rpcName }) as WorkflowStepMeta
|
|
8
|
+
const sleep = (stepName: string): WorkflowStepMeta =>
|
|
9
|
+
({ type: 'sleep', stepName }) as WorkflowStepMeta
|
|
10
|
+
const inline = (stepName: string): WorkflowStepMeta =>
|
|
11
|
+
({ type: 'inline', stepName }) as WorkflowStepMeta
|
|
12
|
+
|
|
13
|
+
describe('deriveWorkflowPlan', () => {
|
|
14
|
+
test('linear workflow is deterministic with every step listed in order', () => {
|
|
15
|
+
const plan = deriveWorkflowPlan([rpc('a'), sleep('b'), inline('c')])
|
|
16
|
+
assert.equal(plan.deterministic, true)
|
|
17
|
+
assert.deepEqual(plan.plannedSteps, [
|
|
18
|
+
{ stepName: 'a' },
|
|
19
|
+
{ stepName: 'b' },
|
|
20
|
+
{ stepName: 'c' },
|
|
21
|
+
])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('parallel group lists each child step', () => {
|
|
25
|
+
const plan = deriveWorkflowPlan([
|
|
26
|
+
rpc('a'),
|
|
27
|
+
{
|
|
28
|
+
type: 'parallel',
|
|
29
|
+
children: [rpc('p1'), rpc('p2')],
|
|
30
|
+
} as WorkflowStepMeta,
|
|
31
|
+
])
|
|
32
|
+
assert.equal(plan.deterministic, true)
|
|
33
|
+
assert.deepEqual(plan.plannedSteps, [
|
|
34
|
+
{ stepName: 'a' },
|
|
35
|
+
{ stepName: 'p1' },
|
|
36
|
+
{ stepName: 'p2' },
|
|
37
|
+
])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('branch is loopless: plannedSteps cover every arm but not deterministic', () => {
|
|
41
|
+
const plan = deriveWorkflowPlan([
|
|
42
|
+
rpc('start'),
|
|
43
|
+
{
|
|
44
|
+
type: 'branch',
|
|
45
|
+
branches: [{ condition: 'x', steps: [rpc('left')] }],
|
|
46
|
+
elseSteps: [rpc('right')],
|
|
47
|
+
} as WorkflowStepMeta,
|
|
48
|
+
rpc('end'),
|
|
49
|
+
])
|
|
50
|
+
assert.equal(plan.deterministic, false, 'a branch picks a path at runtime')
|
|
51
|
+
assert.deepEqual(plan.plannedSteps, [
|
|
52
|
+
{ stepName: 'start' },
|
|
53
|
+
{ stepName: 'left' },
|
|
54
|
+
{ stepName: 'right' },
|
|
55
|
+
{ stepName: 'end' },
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('switch is loopless: cases + default contribute steps, not deterministic', () => {
|
|
60
|
+
const plan = deriveWorkflowPlan([
|
|
61
|
+
{
|
|
62
|
+
type: 'switch',
|
|
63
|
+
expression: 'kind',
|
|
64
|
+
cases: [{ steps: [rpc('caseA')] }, { steps: [rpc('caseB')] }],
|
|
65
|
+
defaultSteps: [rpc('fallback')],
|
|
66
|
+
} as WorkflowStepMeta,
|
|
67
|
+
])
|
|
68
|
+
assert.equal(plan.deterministic, false)
|
|
69
|
+
assert.deepEqual(plan.plannedSteps, [
|
|
70
|
+
{ stepName: 'caseA' },
|
|
71
|
+
{ stepName: 'caseB' },
|
|
72
|
+
{ stepName: 'fallback' },
|
|
73
|
+
])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('fanout (loop) yields no plannedSteps and is not deterministic', () => {
|
|
77
|
+
const plan = deriveWorkflowPlan([
|
|
78
|
+
rpc('before'),
|
|
79
|
+
{
|
|
80
|
+
type: 'fanout',
|
|
81
|
+
stepName: 'each',
|
|
82
|
+
child: rpc('child'),
|
|
83
|
+
mode: 'parallel',
|
|
84
|
+
} as WorkflowStepMeta,
|
|
85
|
+
])
|
|
86
|
+
assert.equal(plan.deterministic, false)
|
|
87
|
+
assert.equal(plan.plannedSteps, undefined, 'loop count is runtime-dependent')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('a fanout nested inside a branch still suppresses the plan', () => {
|
|
91
|
+
const plan = deriveWorkflowPlan([
|
|
92
|
+
{
|
|
93
|
+
type: 'branch',
|
|
94
|
+
branches: [
|
|
95
|
+
{
|
|
96
|
+
condition: 'x',
|
|
97
|
+
steps: [
|
|
98
|
+
{
|
|
99
|
+
type: 'fanout',
|
|
100
|
+
stepName: 'each',
|
|
101
|
+
child: rpc('child'),
|
|
102
|
+
mode: 'serial',
|
|
103
|
+
} as WorkflowStepMeta,
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
} as WorkflowStepMeta,
|
|
108
|
+
])
|
|
109
|
+
assert.equal(plan.deterministic, false)
|
|
110
|
+
assert.equal(plan.plannedSteps, undefined)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('non-named steps (set/return/cancel) are skipped', () => {
|
|
114
|
+
const plan = deriveWorkflowPlan([
|
|
115
|
+
{ type: 'set' } as WorkflowStepMeta,
|
|
116
|
+
rpc('a'),
|
|
117
|
+
{ type: 'return' } as WorkflowStepMeta,
|
|
118
|
+
])
|
|
119
|
+
assert.equal(plan.deterministic, true)
|
|
120
|
+
assert.deepEqual(plan.plannedSteps, [{ stepName: 'a' }])
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkflowStepMeta,
|
|
3
|
+
WorkflowPlannedStep,
|
|
4
|
+
} from '@pikku/core/workflow'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Derive the static UI plan for a DSL workflow from its extracted step tree.
|
|
8
|
+
*
|
|
9
|
+
* `plannedSteps` is the ordered list of every named step the workflow can run,
|
|
10
|
+
* so a frontend can render the step skeleton up front without executing the
|
|
11
|
+
* workflow or hand-listing steps. It is populated whenever the workflow has NO
|
|
12
|
+
* loops (fanout) — loops make the step COUNT runtime-dependent, so a workflow
|
|
13
|
+
* containing any fanout gets neither field.
|
|
14
|
+
*
|
|
15
|
+
* `deterministic` is true only when the exact executed sequence is known up
|
|
16
|
+
* front: a flat list of named steps with no branches/switches (which pick a
|
|
17
|
+
* path at runtime) and no loops. A branchy-but-loopless workflow is therefore
|
|
18
|
+
* `deterministic: false` but still gets `plannedSteps` (the full set of
|
|
19
|
+
* possible steps, in source order).
|
|
20
|
+
*/
|
|
21
|
+
export function deriveWorkflowPlan(steps: WorkflowStepMeta[]): {
|
|
22
|
+
deterministic: boolean
|
|
23
|
+
plannedSteps?: WorkflowPlannedStep[]
|
|
24
|
+
} {
|
|
25
|
+
if (containsLoop(steps)) {
|
|
26
|
+
return { deterministic: false }
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
deterministic: !containsConditional(steps),
|
|
30
|
+
plannedSteps: collectNamedSteps(steps),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Any fanout (loop) anywhere in the tree — including inside branches/switches. */
|
|
35
|
+
function containsLoop(steps: WorkflowStepMeta[]): boolean {
|
|
36
|
+
return steps.some((step) => {
|
|
37
|
+
switch (step.type) {
|
|
38
|
+
case 'fanout':
|
|
39
|
+
return true
|
|
40
|
+
case 'branch':
|
|
41
|
+
return (
|
|
42
|
+
step.branches.some((b) => containsLoop(b.steps)) ||
|
|
43
|
+
(step.elseSteps ? containsLoop(step.elseSteps) : false)
|
|
44
|
+
)
|
|
45
|
+
case 'switch':
|
|
46
|
+
return (
|
|
47
|
+
step.cases.some((c) => containsLoop(c.steps)) ||
|
|
48
|
+
(step.defaultSteps ? containsLoop(step.defaultSteps) : false)
|
|
49
|
+
)
|
|
50
|
+
default:
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Any branch/switch — the run takes a runtime-decided path. */
|
|
57
|
+
function containsConditional(steps: WorkflowStepMeta[]): boolean {
|
|
58
|
+
return steps.some((step) => step.type === 'branch' || step.type === 'switch')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Flatten named steps (rpc/inline/sleep/parallel children) in source order. */
|
|
62
|
+
function collectNamedSteps(steps: WorkflowStepMeta[]): WorkflowPlannedStep[] {
|
|
63
|
+
const planned: WorkflowPlannedStep[] = []
|
|
64
|
+
for (const step of steps) {
|
|
65
|
+
switch (step.type) {
|
|
66
|
+
case 'rpc':
|
|
67
|
+
case 'inline':
|
|
68
|
+
case 'sleep':
|
|
69
|
+
planned.push({ stepName: step.stepName })
|
|
70
|
+
break
|
|
71
|
+
case 'parallel':
|
|
72
|
+
for (const child of step.children) {
|
|
73
|
+
planned.push({ stepName: child.stepName })
|
|
74
|
+
}
|
|
75
|
+
break
|
|
76
|
+
case 'branch':
|
|
77
|
+
for (const b of step.branches) planned.push(...collectNamedSteps(b.steps))
|
|
78
|
+
if (step.elseSteps) planned.push(...collectNamedSteps(step.elseSteps))
|
|
79
|
+
break
|
|
80
|
+
case 'switch':
|
|
81
|
+
for (const c of step.cases) planned.push(...collectNamedSteps(c.steps))
|
|
82
|
+
if (step.defaultSteps) {
|
|
83
|
+
planned.push(...collectNamedSteps(step.defaultSteps))
|
|
84
|
+
}
|
|
85
|
+
break
|
|
86
|
+
// set / return / cancel / filter / arrayPredicate produce no named step
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return planned
|
|
90
|
+
}
|
|
@@ -3,6 +3,7 @@ import { isVersionedId, formatVersionedId, parseVersionedId } from '@pikku/core'
|
|
|
3
3
|
import type { SerializedWorkflowGraph } from './workflow-graph.types.js'
|
|
4
4
|
import { canonicalJSON, hashString } from '../../hash.js'
|
|
5
5
|
import { convertDslToGraph } from './convert-dsl-to-graph.js'
|
|
6
|
+
import { deriveWorkflowPlan } from '../derive-workflow-plan.js'
|
|
6
7
|
import type { InspectorState } from '../../../types.js'
|
|
7
8
|
|
|
8
9
|
export function finalizeWorkflows(state: InspectorState): void {
|
|
@@ -14,6 +15,20 @@ export function finalizeWorkflows(state: InspectorState): void {
|
|
|
14
15
|
stampVersionsOnGraph(graph, functionsMeta)
|
|
15
16
|
computeStepHashes(graph, functionsMeta)
|
|
16
17
|
graph.graphHash = computeGraphHash(graph)
|
|
18
|
+
// Predictable (loopless) DSL workflows carry their full step list so a UI
|
|
19
|
+
// can render the skeleton up front without executing the run. Only DSL is
|
|
20
|
+
// gated: a complex workflow's step tree is incomplete (inline JS branches
|
|
21
|
+
// aren't captured) and flattens loops into plain steps, so its plan would
|
|
22
|
+
// lie about determinism.
|
|
23
|
+
if (graph.source === 'dsl') {
|
|
24
|
+
const { deterministic, plannedSteps } = deriveWorkflowPlan(meta.steps)
|
|
25
|
+
graph.deterministic = deterministic
|
|
26
|
+
// Omit an empty list — a deterministic workflow with no plannedSteps is
|
|
27
|
+
// simply one with no named steps (e.g. a bare return).
|
|
28
|
+
if (plannedSteps?.length) {
|
|
29
|
+
graph.plannedSteps = plannedSteps
|
|
30
|
+
}
|
|
31
|
+
}
|
|
17
32
|
workflows.graphMeta[name] = graph
|
|
18
33
|
}
|
|
19
34
|
|
|
@@ -87,8 +87,6 @@ export interface NodeOptions {
|
|
|
87
87
|
retryDelay?: string
|
|
88
88
|
/** Timeout for node execution (e.g., '30s', '5m') */
|
|
89
89
|
timeout?: string
|
|
90
|
-
/** If true, execute via queue (async). Default: false (inline) */
|
|
91
|
-
async?: boolean
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
/**
|
|
@@ -108,7 +106,11 @@ export type FlowType =
|
|
|
108
106
|
| 'set'
|
|
109
107
|
|
|
110
108
|
// Import and re-export context types from core
|
|
111
|
-
import type {
|
|
109
|
+
import type {
|
|
110
|
+
ContextVariable,
|
|
111
|
+
WorkflowContext,
|
|
112
|
+
WorkflowPlannedStep,
|
|
113
|
+
} from '@pikku/core/workflow'
|
|
112
114
|
|
|
113
115
|
export type { ContextVariable, WorkflowContext }
|
|
114
116
|
|
|
@@ -202,6 +204,19 @@ export interface SerializedWorkflowGraph {
|
|
|
202
204
|
entryNodeIds: string[]
|
|
203
205
|
/** Hash of graph topology (nodes, edges, input mappings) */
|
|
204
206
|
graphHash?: string
|
|
207
|
+
/**
|
|
208
|
+
* True when the exact executed step sequence is known up front: a loopless
|
|
209
|
+
* DSL workflow with no branches/switches. Lets a UI render the run as a fixed
|
|
210
|
+
* pipeline. Loops (fanout) → omitted; branchy-but-loopless → false.
|
|
211
|
+
*/
|
|
212
|
+
deterministic?: boolean
|
|
213
|
+
/**
|
|
214
|
+
* Every named step the workflow can run, in source order — so a frontend can
|
|
215
|
+
* render the step skeleton before the run starts. Populated for any loopless
|
|
216
|
+
* DSL workflow (a branchy one lists all possible steps); omitted when the step
|
|
217
|
+
* count is runtime-dependent (any fanout).
|
|
218
|
+
*/
|
|
219
|
+
plannedSteps?: WorkflowPlannedStep[]
|
|
205
220
|
/** Wire entry points (HTTP, channel, queue, etc.) that trigger this workflow */
|
|
206
221
|
wires?: WorkflowWires
|
|
207
222
|
}
|
package/src/visit.ts
CHANGED
|
@@ -100,6 +100,28 @@ export const visitSetup = (
|
|
|
100
100
|
)
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// Register every pikku function before transports/wirings are resolved, so that
|
|
104
|
+
// resolution (e.g. a channel handler referencing a function defined in another
|
|
105
|
+
// file) is independent of source-file traversal order. Runs between visitSetup
|
|
106
|
+
// and visitRoutes.
|
|
107
|
+
export const visitFunctions = (
|
|
108
|
+
logger: InspectorLogger,
|
|
109
|
+
checker: ts.TypeChecker,
|
|
110
|
+
node: ts.Node,
|
|
111
|
+
state: InspectorState,
|
|
112
|
+
options: InspectorOptions
|
|
113
|
+
) => {
|
|
114
|
+
const nextOptions = ts.isSourceFile(node)
|
|
115
|
+
? { ...options, sourceFile: node }
|
|
116
|
+
: options
|
|
117
|
+
|
|
118
|
+
addFunctions(logger, node, checker, state, nextOptions)
|
|
119
|
+
|
|
120
|
+
ts.forEachChild(node, (child) =>
|
|
121
|
+
visitFunctions(logger, checker, child, state, nextOptions)
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
103
125
|
export const visitRoutes = (
|
|
104
126
|
logger: InspectorLogger,
|
|
105
127
|
checker: ts.TypeChecker,
|
|
@@ -113,7 +135,9 @@ export const visitRoutes = (
|
|
|
113
135
|
|
|
114
136
|
checkAddonBans(logger, node, checker, state, nextOptions)
|
|
115
137
|
|
|
116
|
-
addFunctions
|
|
138
|
+
// NOTE: addFunctions runs in its own earlier pass (visitFunctions) so that
|
|
139
|
+
// every function is registered before any wiring (channels, CLI, etc.)
|
|
140
|
+
// resolves it — wiring resolution must not depend on source-file order.
|
|
117
141
|
addAuth(logger, node, checker, state, nextOptions)
|
|
118
142
|
addSecret(logger, node, checker, state, nextOptions)
|
|
119
143
|
addCredential(logger, node, checker, state, nextOptions)
|