@multiplier-labs/stepflow 0.3.4 → 0.3.8

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.
Files changed (109) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-5XJBAPXF.js → chunk-4IDPWWGS.js} +15 -4
  3. package/dist/chunk-4IDPWWGS.js.map +1 -0
  4. package/dist/{chunk-U3PCVK6O.js → chunk-T6GAZBTQ.js} +70 -38
  5. package/dist/chunk-T6GAZBTQ.js.map +1 -0
  6. package/dist/{chunk-FMPJBK2N.js → chunk-TVSADUYU.js} +3 -5
  7. package/dist/{chunk-FMPJBK2N.js.map → chunk-TVSADUYU.js.map} +1 -1
  8. package/dist/events/index.d.ts +43 -3
  9. package/dist/events/index.js +7 -3
  10. package/dist/{index-Cd0IZ40L.d.ts → index-CdXDv30T.d.ts} +10 -2
  11. package/dist/index.d.ts +5 -4
  12. package/dist/index.js +159 -46
  13. package/dist/index.js.map +1 -1
  14. package/dist/scheduler/index.d.ts +2 -2
  15. package/dist/scheduler/index.js +1 -1
  16. package/dist/storage/index.d.ts +6 -4
  17. package/dist/storage/index.js +1 -1
  18. package/dist/{types-DSEjiLMj.d.ts → types-BfWe5yP6.d.ts} +2 -0
  19. package/package.json +26 -14
  20. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +0 -146
  21. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +0 -74
  22. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
  23. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +0 -161
  24. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +0 -34
  25. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
  26. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +0 -152
  27. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +0 -34
  28. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
  29. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +0 -93
  30. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
  31. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +0 -138
  32. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +0 -121
  33. package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
  34. package/.codekin/reports/security/2026-04-11_security-audit.md +0 -297
  35. package/.github/CODEOWNERS +0 -15
  36. package/.github/dependabot.yml +0 -23
  37. package/.github/workflows/ci.yml +0 -38
  38. package/.github/workflows/publish.yml +0 -42
  39. package/.github/workflows/security.yml +0 -29
  40. package/CHANGELOG.md +0 -34
  41. package/SECURITY.md +0 -40
  42. package/dist/chunk-5XJBAPXF.js.map +0 -1
  43. package/dist/chunk-U3PCVK6O.js.map +0 -1
  44. package/docs/github-packages-setup.md +0 -97
  45. package/docs/stepflow-api-reference.md +0 -1663
  46. package/src/core/engine/concurrency.ts +0 -229
  47. package/src/core/engine/lifecycle.ts +0 -176
  48. package/src/core/engine.test.ts +0 -1435
  49. package/src/core/engine.ts +0 -598
  50. package/src/core/index.ts +0 -7
  51. package/src/core/orchestrator/persistence.ts +0 -165
  52. package/src/core/orchestrator/plan-resolution.ts +0 -140
  53. package/src/core/orchestrator/state-transitions.ts +0 -109
  54. package/src/core/orchestrator/step-runner.ts +0 -427
  55. package/src/core/orchestrator.ts +0 -252
  56. package/src/core/types.ts +0 -220
  57. package/src/events/index.ts +0 -8
  58. package/src/events/memory.test.ts +0 -107
  59. package/src/events/memory.ts +0 -92
  60. package/src/events/socketio.test.ts +0 -378
  61. package/src/events/socketio.ts +0 -262
  62. package/src/events/types.ts +0 -118
  63. package/src/events/webhook.test.ts +0 -949
  64. package/src/events/webhook.ts +0 -807
  65. package/src/index.ts +0 -270
  66. package/src/planning/evaluate-condition.test.ts +0 -153
  67. package/src/planning/evaluate-condition.ts +0 -105
  68. package/src/planning/index.ts +0 -55
  69. package/src/planning/planner.ts +0 -494
  70. package/src/planning/planning.test.ts +0 -727
  71. package/src/planning/registry.test.ts +0 -398
  72. package/src/planning/registry.ts +0 -283
  73. package/src/planning/types.ts +0 -566
  74. package/src/scheduler/cron.test.ts +0 -479
  75. package/src/scheduler/cron.ts +0 -438
  76. package/src/scheduler/index.ts +0 -8
  77. package/src/scheduler/postgres-persistence.ts +0 -530
  78. package/src/scheduler/sqlite-persistence.test.ts +0 -235
  79. package/src/scheduler/sqlite-persistence.ts +0 -286
  80. package/src/scheduler/types.ts +0 -73
  81. package/src/storage/index.ts +0 -27
  82. package/src/storage/memory.test.ts +0 -349
  83. package/src/storage/memory.ts +0 -227
  84. package/src/storage/postgres-core.ts +0 -550
  85. package/src/storage/postgres-migration.test.ts +0 -69
  86. package/src/storage/postgres-transaction-pagination.test.ts +0 -107
  87. package/src/storage/postgres.ts +0 -835
  88. package/src/storage/sqlite-core.ts +0 -151
  89. package/src/storage/sqlite.test.ts +0 -642
  90. package/src/storage/sqlite.ts +0 -717
  91. package/src/storage/types.ts +0 -343
  92. package/src/utils/errors.test.ts +0 -150
  93. package/src/utils/errors.ts +0 -161
  94. package/src/utils/id.ts +0 -21
  95. package/src/utils/index.ts +0 -10
  96. package/src/utils/logger.test.ts +0 -121
  97. package/src/utils/logger.ts +0 -96
  98. package/src/utils/pinned-fetch.test.ts +0 -84
  99. package/src/utils/pinned-fetch.ts +0 -118
  100. package/src/utils/postgres-deps.ts +0 -79
  101. package/src/utils/retry.test.ts +0 -177
  102. package/src/utils/retry.ts +0 -121
  103. package/src/utils/safe-json.test.ts +0 -166
  104. package/src/utils/safe-json.ts +0 -107
  105. package/src/utils/webhook-signature.test.ts +0 -111
  106. package/src/utils/webhook-signature.ts +0 -74
  107. package/tsconfig.json +0 -21
  108. package/tsup.config.ts +0 -14
  109. package/vitest.config.ts +0 -9
@@ -1,161 +0,0 @@
1
- # Comment Assessment: stepflow
2
-
3
- **Date**: 2026-04-11T02:48:00.194Z
4
- **Repository**: /srv/repos/stepflow
5
- **Branch**: main
6
- **Workflow Run**: 98e67949-d200-491e-8d2e-333880bba263
7
- **Session**: 1f4952f0-14a9-4677-a164-d0cd05cc6d35
8
-
9
- ---
10
-
11
- ## Summary
12
-
13
- **Stepflow** is a TypeScript workflow orchestration library (~43 source files, ~30 non-test). Overall documentation quality is **Good to Excellent**, with approximately **67% of source files** carrying JSDoc/TSDoc comments. Public APIs are thoroughly documented; the core engine and utility modules set a high bar. Test files carry no comments, which is appropriate given self-descriptive `describe`/`it` naming. The most notable gaps are confined to a handful of interface implementation methods in the scheduler persistence layer, and two non-obvious semantic behaviors in the planning module that lack any explanatory comment.
14
-
15
- **Rating: 8/10** — above industry average; minor targeted additions would close all meaningful gaps.
16
-
17
- ---
18
-
19
- ## Well-Documented Areas
20
-
21
- ### `src/index.ts`
22
- A 56-line module-level JSDoc block enumerates every major feature (workflow registration, async execution, state management, event system, pluggable storage, retry/error handling) followed by a complete 45-line working code example. Serves as the library's living documentation entry point.
23
-
24
- ### `src/core/engine.ts` (693 lines)
25
- Every public method includes `@param`, `@returns`, and `@throws` tags. Logical sections are separated by banner dividers (`// ========…`). Example:
26
- ```ts
27
- /**
28
- * Register a workflow definition with the engine.
29
- * @param definition - The workflow definition to register.
30
- * @throws {WorkflowEngineError} If a workflow with the same kind is already registered.
31
- */
32
- registerWorkflow(definition: WorkflowDefinition): void
33
- ```
34
-
35
- ### `src/core/orchestrator.ts` — timeout / memory-leak rationale (lines 482–518)
36
- The `executeWithTimeout` helper explains *why* the `finally` block exists, not just what it does:
37
- ```ts
38
- /**
39
- * …The `finally` block is critical: without it, the losing promise's timer/listener
40
- * would remain active, leaking memory — especially problematic in long-running engines
41
- * processing many steps.
42
- */
43
- ```
44
- This is exemplary "why" commenting.
45
-
46
- ### `src/utils/errors.ts`
47
- Every custom error class carries a one-sentence JSDoc stating its purpose and the specific condition that triggers it.
48
-
49
- ### `src/utils/retry.ts`
50
- `RetryOptions` fields are individually documented. The exponential backoff formula includes an example showing calculated delays at each attempt.
51
-
52
- ### `src/core/types.ts`
53
- Every interface field has a one-line `/** … */` doc comment. `WorkflowContext`, `WorkflowDefinition`, `WorkflowHooks`, `RunStatus`, and `StepStatus` are all self-contained.
54
-
55
- ### `src/storage/postgres.ts` — schema type docs (lines 46–128)
56
- Kysely row-type interfaces (`WorkflowRunsTable`, `WorkflowRunStepsTable`, etc.) document every column with field-level comments explaining nullability, units, and JSON encoding:
57
- ```ts
58
- /** JSON-serialized output payload, null until run completes. */
59
- output_json: string | null;
60
- /** Execution priority (lower number = higher precedence). */
61
- priority: number;
62
- ```
63
-
64
- ### `src/scheduler/postgres-persistence.ts` — class-level JSDoc with two `@example` blocks
65
- Includes a basic usage example and a "Sharing connection pool" example, covering the two main integration patterns.
66
-
67
- ---
68
-
69
- ## Underdocumented Areas
70
-
71
- | File | Issue | Severity |
72
- |------|-------|----------|
73
- | `src/planning/planner.ts` | `evaluateCondition()` (line 44): uses `RE2` instead of native `RegExp` for ReDoS safety — security-relevant choice is entirely uncommented | **High** |
74
- | `src/planning/planner.ts` | `exists`/`notExists` operators (lines 95–99): treat empty string `''` as non-existent — opinionated semantic behaviour with no comment | **High** |
75
- | `src/scheduler/postgres-persistence.ts` | `loadSchedules()` (line 306), `saveSchedule()` (line 316), `updateSchedule()` (line 340), `deleteSchedule()` (line 402): public interface methods with no JSDoc | **Medium** |
76
- | `src/planning/planner.ts` | `applyConstraints()` (line 466): `perStepTimeout = maxDuration / steps.length` — evenly distributes budget across steps without noting the equal-duration assumption | **Medium** |
77
- | `src/planning/planner.ts` | `RuleBasedPlanner` class (line 170): single-line class JSDoc; scoring algorithm overview (condition-score vs priority-score vs forced/preferred-variant paths) not explained | **Medium** |
78
- | `src/scheduler/postgres-persistence.ts` | `rowToSchedule()` (line 480): dual JSON.parse path (`typeof … === 'string'` guard) is non-obvious with no comment explaining why it handles both string and pre-parsed JSONB | **Medium** |
79
- | `src/storage/postgres.ts` | `StepflowDatabase` interface (line 130): no doc comment explaining that this is the Kysely schema map tying table names to row types | **Low** |
80
- | `src/scheduler/postgres-persistence.ts` | `ensureInitialized()` (line 166): private guard method with no doc comment | **Low** |
81
- | `src/planning/planner.ts` | `scoreConditions()` scoring formula (lines 127–134) has inline comments but no JSDoc — callers of the public `RuleBasedPlanner` API cannot understand selection scoring from public docs alone | **Low** |
82
- | `src/storage/postgres.ts` | `stripStack()` helper (line 37): has a one-liner but does not explain *why* `stack` is stripped (prevent large stack strings from bloating persisted JSON) | **Low** |
83
- | `src/utils/id.ts` | `generateId()` explains ULID advantages but omits the practical consequence: IDs are lexicographically sortable by creation time | **Low** |
84
- | `src/events/memory.ts` | Channel naming convention explained in code (lines 34–41) but not in any exported type or JSDoc for consumers building custom transports | **Low** |
85
- | `src/scheduler/sqlite-persistence.ts` | `createTables()` has schema comments; `loadSchedules()`, `saveSchedule()`, `deleteSchedule()` interface methods have no JSDoc (same gap as postgres counterpart) | **Low** |
86
- | `src/planning/registry.ts` | `MemoryRecipeRegistry` class has no class-level JSDoc; only individual methods are commented | **Low** |
87
- | `src/core/orchestrator.ts` | `WorkflowCheckpoint` interface is documented, but no comment explains when/why checkpoints are created (only on step completion, not at arbitrary points) | **Low** |
88
-
89
- ---
90
-
91
- ## Comment Quality Issues
92
-
93
- No clearly **inaccurate** or **misleading** comments were found. The following are cases where comments exist but create a gap between stated and actual behaviour:
94
-
95
- ### 1. `src/planning/planner.ts:95–99` — `exists`/`notExists` undocumented empty-string treatment
96
- ```ts
97
- case 'exists':
98
- return fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
99
-
100
- case 'notExists':
101
- return fieldValue === undefined || fieldValue === null || fieldValue === '';
102
- ```
103
- The corresponding type definition in `src/planning/types.ts` describes `exists` as "field exists and is non-null." It does not mention that `''` is also treated as absent. A consumer who stores an empty string legitimately would get a surprising false from `exists`. No comment at either the operator definition or the implementation site flags this.
104
-
105
- ### 2. `src/planning/planner.ts:85–93` — RE2 usage is silent
106
- ```ts
107
- case 'matches':
108
- if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
109
- try {
110
- return new RE2(conditionValue).test(fieldValue);
111
- } catch {
112
- return false;
113
- }
114
- }
115
- ```
116
- `RE2` is a linear-time regex engine that prevents ReDoS attacks — a meaningful security property. The comment on `evaluateCondition` (line 42) says only "Evaluate a single condition against a value." Callers writing user-supplied patterns need to know RE2 syntax differs from ECMA regex (e.g., no lookaheads), and security-conscious reviewers need to know the DoS protection is intentional.
117
-
118
- ### 3. `src/scheduler/postgres-persistence.ts:387` — condition always true for `domainKey`
119
- ```ts
120
- if (updates[domainKey] !== undefined || merged[domainKey] !== undefined) {
121
- ```
122
- Because `merged` is `{ ...existing, ...updates }`, `merged[domainKey]` is always defined for fields present in the existing row. The inline comment above (lines 342–345) correctly describes intent ("only included if explicitly provided"), but the condition as written effectively includes every field on every update. The comment describes the goal, not the actual behaviour — a subtle mislead.
123
-
124
- ---
125
-
126
- ## Recommendations
127
-
128
- 1. **`src/planning/planner.ts:85–93` — Document RE2's ReDoS safety in `evaluateCondition`**
129
- Add to the `matches` case a brief comment: `// RE2 provides linear-time matching, preventing ReDoS attacks from user-supplied patterns.` Also note in the `evaluateCondition` JSDoc that RE2 syntax is used (no lookaheads, no backreferences), so callers know pattern compatibility constraints.
130
-
131
- 2. **`src/planning/planner.ts:95–99` and `src/planning/types.ts` — Align `exists` docs with empty-string behaviour**
132
- Either update the `ConditionOperator` type definition to read "field is non-null and non-empty-string," or add a `// NOTE:` inline explaining the empty-string-as-absent convention. Doing both ensures consumers reading either the type or the implementation get consistent expectations.
133
-
134
- 3. **`src/scheduler/postgres-persistence.ts:306–408` — Add JSDoc to all four public `SchedulePersistence` interface methods**
135
- `loadSchedules`, `saveSchedule`, `updateSchedule`, and `deleteSchedule` are part of the persistence contract but have no documentation. At minimum add `@returns` and `@throws` tags to match the documented `getSchedule`, `getDueSchedules`, and `getCompletionTriggers` below them. Apply the same fix to `src/scheduler/sqlite-persistence.ts` for consistency.
136
-
137
- 4. **`src/planning/planner.ts:170` — Expand `RuleBasedPlanner` class JSDoc**
138
- The current one-line class comment ("Rule-based planner that selects recipes based on condition matching") does not explain the selection algorithm. Add a paragraph describing the scoring hierarchy: forced recipe → preferred variant → condition scoring (50 + 10×N conditions, capped at 100) → default recipe → first available. This allows users to understand and predict selection without reading the source.
139
-
140
- 5. **`src/planning/planner.ts:462–468` — Document the per-step timeout distribution assumption**
141
- ```ts
142
- const perStepTimeout = constraints.maxDuration
143
- ? Math.floor(constraints.maxDuration / steps.length)
144
- : undefined;
145
- ```
146
- Add a comment: `// Budget is divided equally across steps; steps with inherently longer durations may time out even if the overall run would complete within maxDuration.` This sets expectations and helps users tune `maxDuration` correctly.
147
-
148
- 6. **`src/scheduler/postgres-persistence.ts:480` — Document the dual JSON.parse guard in `rowToSchedule`**
149
- The pattern `typeof row.X === 'string' ? JSON.parse(row.X) : row.X` appears six times without explanation. Add a single comment near the first occurrence: `// Kysely returns JSONB columns pre-parsed in some drivers; guard ensures correct deserialization regardless of driver behaviour.`
150
-
151
- 7. **`src/planning/types.ts` — Document each `ConditionOperator` value in the enum/union**
152
- The `ConditionOperator` type has a comment block, but individual values like `'matches'`, `'contains'`, `'exists'` have no per-value docs. Add a TSDoc `@remarks` or switch to a documented string union listing what each operator accepts (e.g., `'matches'` works on strings using RE2 syntax; `'contains'` works on both strings and arrays).
153
-
154
- 8. **`src/core/orchestrator.ts` — Document when checkpoints are written**
155
- `WorkflowCheckpoint` is documented, but `executeWorkflow` does not explain that checkpoints capture state only after a step fully completes. Add a note in the `ExecuteOptions.checkpoint` field doc: `// Checkpoint is built incrementally as steps complete; a partially executed step is NOT checkpointed and will re-run on resume.`
156
-
157
- 9. **`src/storage/postgres.ts:130` — Add JSDoc to `StepflowDatabase`**
158
- This interface is the Kysely schema map. A one-line comment — `/** Kysely database schema mapping table names to their row types. */` — makes the architectural role immediately clear to contributors adding new tables.
159
-
160
- 10. **`src/utils/id.ts` — Note sort-by-creation-time property in `generateId` JSDoc**
161
- The existing comment explains ULID advantages but omits the most practically useful one: generated IDs are lexicographically sortable by creation time, making them suitable as primary keys with implicit time-ordering. Adding this helps consumers decide whether to add a separate `created_at` sort column.
@@ -1,34 +0,0 @@
1
- # Comment Assessment: stepflow
2
-
3
- **Date**: 2026-04-14T01:18:27.171Z
4
- **Repository**: /srv/repos/stepflow
5
- **Branch**: main
6
- **Workflow Run**: af53bfdc-9706-4e22-bafa-e5954c9c9875
7
- **Session**: 27222af5-7c69-4c33-af75-7ad0318122a4
8
-
9
- ---
10
-
11
- ## Summary
12
-
13
- **Overall rating: A+ (Excellent)**
14
-
15
- The Stepflow codebase has outstanding documentation practices. The report has been saved to `.codekin/reports/comments/2026-04-14_comment-assessment.md`.
16
-
17
- Key findings:
18
-
19
- **Strengths (~94% coverage):**
20
- - Every exported function, interface, class, and type carries JSDoc
21
- - File-level module comments in 19/20 sampled files
22
- - Critical complex logic (abort handling, memory leak prevention, retry backoff) is thoroughly explained
23
- - Consistent use of `@param`, `@returns`, `@throws`, `@example` throughout
24
- - Zero stale, outdated, or misleading comments detected
25
- - External docs (`README.md`, `docs/stepflow-api-reference.md`) are comprehensive
26
-
27
- **Minor gaps (all Low/Medium severity):**
28
- - `QueuedRun` internal interface in `engine.ts` lacks JSDoc
29
- - `MemoryStorageAdapter` methods have one-liner summaries but no `@param`/`@returns`
30
- - Test files have no file-level comments
31
- - `postgres.ts` and persistence files not fully verified for comment parity
32
- - ID generation strategy in `utils/id.ts` undocumented
33
-
34
- Top recommendations: add JSDoc to `QueuedRun`, document the SQLite statement cache strategy, audit Postgres implementation for parity with SQLite docs, and add `@param`/`@returns` to storage implementation methods.
@@ -1,152 +0,0 @@
1
- # Complexity Report: stepflow
2
-
3
- **Date**: 2026-04-03T20:01:36.000Z
4
- **Repository**: /srv/repos/stepflow
5
- **Branch**: main
6
- **Workflow Run**: ba52b549-8f86-4537-b95d-af7bbe803812
7
- **Session**: 456e9325-f315-4a53-8b5f-e02d09d44a08
8
-
9
- ---
10
-
11
- Now I have sufficient data to produce the full report.
12
-
13
- ## Summary
14
-
15
- **Overall Complexity Rating: Medium**
16
-
17
- The codebase is a TypeScript workflow orchestration library (~8,700 source lines across 19 source files, 14,600 including tests). It is well-structured overall, with clear module boundaries and good separation between execution, storage, scheduling, and events. The main complexity hotspots are: (1) `postgres.ts` at 1,354 lines — a large god class with dual interface implementations, (2) `orchestrator.ts:executeStep` — deeply nested retry/cancellation logic, and (3) an unresolved migration from a legacy `StorageAdapter` interface to a newer `WorkflowStorage` interface that introduces duplicated type hierarchies and dead code paths.
18
-
19
- - **Largest file**: `src/storage/postgres.ts` — 1,354 lines
20
- - **Deepest nesting**: `orchestrator.ts:executeStep` — ~4 levels in the retry/cancellation catch block
21
- - **Most complex function**: `executeStep` in `orchestrator.ts` — ~185 lines, retry loop, 4 error paths, 5+ storage/event calls per iteration
22
-
23
- ---
24
-
25
- ## Largest Files
26
-
27
- | File | Lines | Primary Responsibility | Refactor Priority |
28
- |------|-------|------------------------|-------------------|
29
- | `src/storage/postgres.ts` | 1,354 | PostgreSQL storage adapter (DDL + CRUD + extended API) | **High** |
30
- | `src/core/engine.ts` | 692 | WorkflowEngine class (registry, run lifecycle, queue) | Medium |
31
- | `src/storage/sqlite.ts` | 621 | SQLite storage adapter | Medium |
32
- | `src/planning/planner.ts` | 576 | Rule-based recipe selection and plan generation | Low |
33
- | `src/planning/types.ts` | 566 | Planning type definitions | Low |
34
- | `src/core/orchestrator.ts` | 564 | Workflow + step execution logic | **High** |
35
- | `src/events/webhook.ts` | 557 | Webhook event transport + SSRF protection | Medium |
36
- | `src/scheduler/postgres-persistence.ts` | 512 | PostgreSQL schedule persistence | Medium |
37
- | `src/scheduler/cron.ts` | 414 | CronScheduler (polling + event triggers) | Low |
38
- | `src/planning/registry.ts` | 365 | Recipe and step handler registries | Low |
39
- | `src/storage/types.ts` | 342 | Storage interface definitions (legacy + new) | **High** |
40
- | `src/index.ts` | 270 | Public API re-exports | Low |
41
- | `src/events/socketio.ts` | 262 | Socket.IO event transport | Low |
42
- | `src/scheduler/sqlite-persistence.ts` | 261 | SQLite schedule persistence | Low |
43
- | `src/storage/memory.ts` | 227 | In-memory storage adapter | Low |
44
-
45
- ---
46
-
47
- ## Most Complex Functions
48
-
49
- | File:Function | Estimated Complexity | Issue Description | Refactor Suggestion |
50
- |---------------|---------------------|-------------------|---------------------|
51
- | `orchestrator.ts:executeStep` | High | ~185 lines; infinite `while(true)` retry loop; 4 distinct exit paths (success, skip, retry, fail); 5+ storage/event calls per iteration; 4-level nesting in catch block | Extract `handleStepError()` and `executeStepWithTimeout()` helper; separate retry orchestration from event emission |
52
- | `orchestrator.ts:executeWorkflow` | High | ~220 lines; manages hooks, checkpointing, timeouts, cancellation, success/failure divergence; complex error classification logic (lines 237–244) | Split into `runSteps()` + `finalizeRun()` helpers; extract error classification into a pure function |
53
- | `postgres.ts:createTables` | High | ~155 lines; 20+ sequential `sql\`...\`.execute(this.db)` calls; mixes initial DDL with alter-column migration; silent `.catch(() => {})` suppresses errors | Split into `createSchemaTables()` and `runMigrations()`; use a proper migration runner; avoid silent catch |
54
- | `postgres.ts:mapRunRow` | Medium | Repeated `typeof row.X === 'string' ? JSON.parse(row.X) : row.X` guard on every JSON column — 8 repetitions in one function | Extract a `parseJsonColumn(col: unknown): unknown` helper shared across all map methods |
55
- | `postgres.ts:createRun` | Medium | Accepts a union type `CreateRunInput \| Omit<WorkflowRunRecord, …>` — forces `'id' in run`, `'parentRunId' in run`, etc. inline discriminators throughout the method body | Consolidate to a single input type; bridge at the call site rather than inside the method |
56
- | `sqlite.ts:prepareStatements` | Medium | Bulk-prepares 15 SQL statements in one method; `listRuns` uses double-binding pattern (`kind, kind`, `statusJson, statusJson`) which is fragile and easily mis-ordered | Each statement can be lazily prepared on first use; document or encapsulate the double-bind idiom |
57
- | `planner.ts:generatePlan` | Medium | Applies three independent mutation passes (hints, constraints, config) over the steps array using nested loops; emits `PlanModification` records as a side effect | Extract `applyHints()`, `applyAdditionalConfig()` as pure functions returning `{ steps, modifications }` |
58
- | `engine.ts:startRun` | Medium | Handles both the immediate-start and queue paths, emits multiple events, performs queue insertion — 60 lines with branching and inline event building | Extract `enqueueOrExecute()` helper to isolate the concurrency decision; move event building to a factory |
59
- | `webhook.ts:sendWebhook` | Medium | Retry loop, timeout via AbortController, SSRF re-validation on every send, payload signing, concurrency queue interaction — all in one method | SSRF validation at `addEndpoint` time only (URL is static); extract `buildHeaders()` |
60
- | `planner.ts:evaluateCondition` | Low–Medium | 10-case switch statement; regex instantiated on every call via `new RE2(conditionValue)` inside the `matches` branch | Cache compiled RE2 instances per pattern; no structural issue but the hot-path instantiation is a performance concern |
61
-
62
- ---
63
-
64
- ## Coupling & Cohesion Issues
65
-
66
- 1. **Dual storage interface (`StorageAdapter` vs. `WorkflowStorage`) in `storage/types.ts`**
67
- - **Problem**: The file defines two nearly-identical storage interfaces: the legacy `StorageAdapter` (used by the engine) and a new `WorkflowStorage` (used by `PostgresStorageAdapter` internally). `PostgresStorageAdapter` implements both simultaneously. This doubles the number of types for runs/steps (`WorkflowRunRecord` vs. `ExtendedWorkflowRunRecord`, `RunStatus` vs. `ExtendedRunStatus`, `StepStatus` vs. `ExtendedStepStatus`, `ListRunsOptions` vs. `ExtendedListRunsOptions`).
68
- - **Suggested Fix**: Pick one interface as canonical and migrate all adapters to it; keep the other as a shim alias during transition. The duplicated status union types should be unified.
69
-
70
- 2. **`postgres.ts` is a god class**
71
- - **Problem**: `PostgresStorageAdapter` (1,354 lines) is responsible for: connection pool management, lazy dependency loading, DDL/migration, all CRUD operations for three tables, the extended `WorkflowStorage` API (7 additional methods), row-mapping helpers, atomic dequeue, and transaction wrapping. Unrelated responsibilities are co-located.
72
- - **Suggested Fix**: Extract `PostgresMigrationRunner` (DDL), `PostgresRunRepository`, and `PostgresStepRepository` as internal collaborators.
73
-
74
- 3. **Lazy-loaded module globals in `postgres.ts` and `postgres-persistence.ts`**
75
- - **Problem**: `let Kysely: any; let PostgresDialect: any; let sql: any; let pgModule: any;` are module-level mutable variables populated by `loadPostgresDeps()` during `initialize()`. These are effectively global singletons shared across all adapter instances. Concurrent `initialize()` calls could create a race condition. All type safety is lost (`any`).
76
- - **Suggested Fix**: Store loaded dependencies as instance properties populated in `initialize()`, or use a module-level `Promise` cache so the load happens exactly once and the result is strongly typed.
77
-
78
- 4. **`executeStep` is tightly coupled to storage, events, logger, and abort controller**
79
- - **Problem**: The function signature passes a 5-field `StepExecutionOptions` object, and inside it makes direct calls to `storage.createStep`, `storage.updateStep`, `events.emit` (×5–6 per iteration), `definition.hooks.*` (×3), and `abortController.signal`. It is essentially untestable in isolation without mocking all infrastructure.
80
- - **Suggested Fix**: Extract an `StepExecutionContext` interface that provides `onStarted`, `onSucceeded`, `onFailed`, `onCanceled` callbacks; the orchestrator wires these to storage/events; the step function only calls the context.
81
-
82
- 5. **`executeRun` is a no-op pass-through in `engine.ts`**
83
- - **Problem**: `executeRun` (lines 324–332) calls `launchRun` with identical arguments. It exists only as an indirection, adding a layer of abstraction with no logic.
84
- - **Suggested Fix**: Remove `executeRun`; call `launchRun` directly from `startRun` and `processQueue`.
85
-
86
- 6. **`SQLiteStorageAdapter.transaction()` misrepresents its async contract**
87
- - **Problem**: The `transaction()` method (marked `@deprecated`) has an `async` signature but actually runs synchronously via `better-sqlite3`. The JSDoc warns "only works for synchronous operations," but callers expecting async-safe semantics will be misled.
88
- - **Suggested Fix**: Remove the method (it is already deprecated); expose only `transactionSync()`.
89
-
90
- ---
91
-
92
- ## Refactoring Candidates
93
-
94
- 1. **Unify the dual storage interface hierarchy**
95
- - **Location**: `src/storage/types.ts`, `src/storage/postgres.ts`, `src/storage/sqlite.ts`, `src/storage/memory.ts`
96
- - **Problem**: Two parallel type families (`StorageAdapter`/`WorkflowStorage`, `WorkflowRunRecord`/`ExtendedWorkflowRunRecord`, `RunStatus`/`ExtendedRunStatus`) exist as an in-progress migration that was never completed. All consumers must understand both, and `PostgresStorageAdapter` implements both simultaneously.
97
- - **Suggested Approach**: Merge the two interfaces; remove the `Extended*` duplicates by upgrading `RunStatus` to include `'pending'` and `'timeout'` and adding `output` to the base record. Adapters can implement a single interface.
98
- - **Effort**: Large
99
-
100
- 2. **Extract `PostgresMigrationRunner` from `PostgresStorageAdapter`**
101
- - **Location**: `src/storage/postgres.ts:314–465` (`createTables`)
102
- - **Problem**: 155 lines of sequential DDL mixed with the adapter. It also silently swallows `ALTER TABLE` errors, making it hard to reason about migration state.
103
- - **Suggested Approach**: Move all DDL into a standalone `PostgresMigrationRunner` class with versioned migrations and explicit error handling. `PostgresStorageAdapter` calls `runner.migrate()` during `initialize()`.
104
- - **Effort**: Medium
105
-
106
- 3. **Decompose `executeStep` in the orchestrator**
107
- - **Location**: `src/core/orchestrator.ts:296–479`
108
- - **Problem**: ~185-line function with 4 exit paths, 4-level nesting, and direct coupling to all infrastructure. The retry delay sleep path duplicates cancellation handling from the outer path.
109
- - **Suggested Approach**: Extract `handleStepSuccess()`, `handleStepCancellation()`, and `handleStepRetry()` — each taking the step record and emitting the right events/storage calls. The main function becomes a loop calling these.
110
- - **Effort**: Medium
111
-
112
- 4. **Fix lazy-loaded dependency globals in postgres adapters**
113
- - **Location**: `src/storage/postgres.ts:13–16`, `src/scheduler/postgres-persistence.ts:11–15`
114
- - **Problem**: `let Kysely: any` etc. as module-level mutable globals. Both files duplicate this pattern. Type safety lost, race condition risk on concurrent initialization.
115
- - **Suggested Approach**: Create a shared `src/utils/postgres-deps.ts` module (it may partially exist as `loadPostgresDeps`) that returns a typed, promise-cached result; store the resolved deps as instance properties.
116
- - **Effort**: Small
117
-
118
- 5. **Cache RE2 instances in `evaluateCondition`**
119
- - **Location**: `src/planning/planner.ts:88–93`
120
- - **Problem**: `new RE2(conditionValue)` is called on every condition evaluation. In a hot planning path with many recipes and conditions, this allocates a new compiled regex object on each call.
121
- - **Suggested Approach**: Add a `Map<string, RE2>` cache at module scope or on the `RuleBasedPlanner` instance; look up or create before matching.
122
- - **Effort**: Small
123
-
124
- 6. **Remove `executeRun` pass-through in `WorkflowEngine`**
125
- - **Location**: `src/core/engine.ts:323–332`
126
- - **Problem**: `executeRun` adds a layer of indirection with zero logic, making the call chain harder to follow (`startRun → executeRun → launchRun`).
127
- - **Suggested Approach**: Delete `executeRun`; call `launchRun` directly from `startRun` (line 270) and `processQueue` (line 392).
128
- - **Effort**: Small
129
-
130
- 7. **Extract a `parseJsonColumn` helper in `postgres.ts` row mappers**
131
- - **Location**: `src/storage/postgres.ts:861–940` (four `map*Row` methods)
132
- - **Problem**: The guard `typeof row.X === 'string' ? JSON.parse(row.X) : row.X` is repeated ~10 times across four mapper methods, indicating that Kysely returns JSONB as either a string or a parsed object depending on configuration.
133
- - **Suggested Approach**: Extract `parseJsonColumn(col: unknown): unknown` and call it from all mappers. Alternatively, configure Kysely with a `pg.types.setTypeParser` for the JSONB OID to always return parsed objects, eliminating the guard entirely.
134
- - **Effort**: Small
135
-
136
- 8. **Encapsulate the SQLite double-binding pattern**
137
- - **Location**: `src/storage/sqlite.ts:329–341` (`listRuns`)
138
- - **Problem**: Prepared statements use `? IS NULL OR field = ?` which requires each parameter to be passed twice: `kind, kind` / `statusJson, statusJson` / `parentRunId, parentRunId`. This is fragile — if parameter order changes, bugs are silent.
139
- - **Suggested Approach**: Add a small helper `nullableParam(val)` returning `[val, val]` to make the doubling explicit and documented, or switch to Kysely for SQLite queries to get type-safe query building.
140
- - **Effort**: Small
141
-
142
- 9. **Remove or complete the deprecated `SQLiteStorageAdapter.transaction()` method**
143
- - **Location**: `src/storage/sqlite.ts:440–446`
144
- - **Problem**: The `async transaction()` method has a misleading contract — it wraps a synchronous transaction with an async signature, only works if the callback does no real async I/O, and is deprecated. It remains in the public API creating a footgun.
145
- - **Suggested Approach**: Remove the method in the next minor version (or immediately if semver allows). Consumers should use `transactionSync()`. Add a clear deprecation notice in the changelog.
146
- - **Effort**: Small
147
-
148
- 10. **Consolidate magic timeout/retry defaults into named constants**
149
- - **Location**: `src/core/orchestrator.ts:304–307`, `src/core/engine.ts:484`, `src/events/webhook.ts:145–151`
150
- - **Problem**: Default values like `maxRetries: 3`, `retryDelay: 1000`, `retryBackoff: 2`, `timeout: 60000`, `defaultTimeout: 5000` are magic numbers scattered across files. They are inconsistent (`orchestrator.ts` defaults `maxRetries` to 3 inline; `webhook.ts` defaults `defaultRetries` to 3 separately).
151
- - **Suggested Approach**: Collect all engine defaults into a `src/core/defaults.ts` constants file (`DEFAULT_MAX_RETRIES`, `DEFAULT_RETRY_DELAY_MS`, `DEFAULT_WAIT_TIMEOUT_MS`, etc.) and reference them everywhere.
152
- - **Effort**: Small
@@ -1,152 +0,0 @@
1
- # Complexity Report: stepflow
2
-
3
- **Date**: 2026-04-11T02:46:40.781Z
4
- **Repository**: /srv/repos/stepflow
5
- **Branch**: main
6
- **Workflow Run**: 7c4174ae-8cba-4ad8-9a30-b2f56cf4c98b
7
- **Session**: 474ba458-5157-4ff3-8ef4-802b1bc389e5
8
-
9
- ---
10
-
11
- Now I have enough information to produce the report.
12
-
13
- ## Summary
14
-
15
- **Overall Complexity Rating: Medium**
16
-
17
- The codebase is a TypeScript workflow orchestration library (~14,300 lines across source + tests). The implementation is generally clean and well-structured, but several files are oversized, the storage layer has two parallel interface lineages with significant duplication, and the orchestrator's `executeStep` function carries the highest cyclomatic complexity. No deeply pathological nesting exists, but coupling between the `postgres.ts` adapter and two different storage interfaces is the most acute maintenance risk.
18
-
19
- Key metrics:
20
- - **Largest file:** `src/storage/postgres.ts` — 1,354 lines
21
- - **Deepest nesting:** `executeStep` in `orchestrator.ts` — 4–5 levels (`while → try → if → if → try`)
22
- - **Most complex function:** `executeStep` — retry loop, cancellation guards, hooks, event emission, 4 error-handling branches
23
- - **Duplicate interface footprint:** `StorageAdapter` vs. `WorkflowStorage` — two complete CRUD contracts implemented in the same `postgres.ts` class
24
-
25
- ---
26
-
27
- ## Largest Files
28
-
29
- | File | Lines | Primary Responsibility | Refactor Priority |
30
- |------|-------|------------------------|-------------------|
31
- | `src/storage/postgres.ts` | 1,354 | PostgreSQL adapter (implements both `StorageAdapter` and `WorkflowStorage`) | **High** |
32
- | `src/core/engine.test.ts` | 1,364 | Engine integration tests | Low (test file) |
33
- | `src/planning/planning.test.ts` | 727 | Planner integration tests | Low (test file) |
34
- | `src/events/webhook.test.ts` | 657 | Webhook transport tests | Low (test file) |
35
- | `src/core/engine.ts` | 692 | `WorkflowEngine` class — run lifecycle, queue, resume | Medium |
36
- | `src/storage/sqlite.ts` | 621 | SQLite adapter | Medium |
37
- | `src/planning/planner.ts` | 576 | `RuleBasedPlanner` — recipe selection, plan generation | Low |
38
- | `src/planning/types.ts` | 566 | Planning domain types (Recipe, Plan, Planner interfaces) | Low |
39
- | `src/core/orchestrator.ts` | 564 | Step execution loop, retry logic, hooks | **High** |
40
- | `src/events/webhook.ts` | 557 | WebhookEventTransport — SSRF guards, signing, concurrency | Medium |
41
- | `src/scheduler/postgres-persistence.ts` | 512 | PostgreSQL schedule persistence | Low |
42
- | `src/scheduler/cron.ts` | 414 | CronScheduler — polling + event-driven triggers | Low |
43
- | `src/storage/types.ts` | 342 | Storage type definitions (two parallel interface families) | **High** |
44
- | `src/events/socketio.ts` | 262 | Socket.IO event transport | Low |
45
- | `src/index.ts` | 270 | Public API barrel file | Low |
46
-
47
- ---
48
-
49
- ## Most Complex Functions
50
-
51
- | File:Function | Estimated Complexity | Issue Description | Refactor Suggestion |
52
- |---------------|---------------------|-------------------|---------------------|
53
- | `orchestrator.ts:executeStep` | Very High (CC ~12) | Single `while(true)` loop wraps: `AbortSignal` check, step record creation, `beforeStep` hook, conditional timeout vs. `raceWithAbort`, `afterStep` hook, cancellation detection, `onStepError` hook, three error branches (`skip`/`retry`/`fail`), sleep-with-abort, and re-throw. 4–5 nesting levels. | Extract `handleStepError(error, attempt, strategy)` and `executeStepOnce(step, context, options)` as separate functions |
54
- | `orchestrator.ts:executeWorkflow` | High (CC ~8) | Combines run status update, event emission, context construction, timeout setup, `beforeRun` hook, step loop with skip checks, `afterRun` hook, success/failure/timeout/cancel branching in one try/catch | Split success path and failure classification into `finalizeSuccess()` / `finalizeFailure()` helpers |
55
- | `storage/postgres.ts:createTables` | High (CC ~7) | 150-line method issues 15+ raw SQL statements sequentially with ad-hoc `catch(() => {})` for ALTER TABLE idempotency. No migration versioning. | Replace with a proper migration runner (numbered migration files or Kysely-migrate) |
56
- | `storage/postgres.ts:mapRunRow` | Medium (CC ~6) | Repeated `typeof x === 'string' ? JSON.parse(x) : x` ternary for every JSONB column (6 occurrences in `mapRunRow` alone, duplicated in `mapExtendedRunRow`) | Extract `parseJsonColumn<T>(col: string | T): T` helper; used in all `mapXRow` methods |
57
- | `events/webhook.ts:sendWebhook` | Medium (CC ~6) | Retry loop with DNS validation re-run on every attempt, payload size check, dynamic header assembly, HMAC signing, and `AbortController` per attempt | Extract `deliverOnce(url, headers, body, timeout)` and call from `sendWebhookWithRetry` |
58
- | `events/webhook.ts:isBlockedHost` | Medium (CC ~9) | Flat function with 9 early-return branches testing IPv4 ranges, IPv6 literals, loopback, link-local — logic duplicated almost identically in `isBlockedIp` | Merge `isBlockedHost` and `isBlockedIp` into a single `isBlockedAddress(address: string)` that handles both hostname and resolved-IP cases |
59
- | `planner.ts:selectRecipe` | Medium (CC ~6) | Three sequential priority checks (forced, preferred, scored) each with early returns, plus a fallback chain of two more returns | Already reasonably structured; document the scoring ladder with inline comments |
60
- | `storage/postgres.ts:updateRun` | Medium (CC ~5) | Builds `updateData` object via 6 independent `if` guards covering two different input shapes (`UpdateRunInput` and `Partial<WorkflowRunRecord>`) simultaneously | Accept only `UpdateRunInput`; deprecate the `Partial<WorkflowRunRecord>` overload |
61
- | `engine.ts:waitForRun` | Medium (CC ~5) | Interleaves early-return for already-completed runs with Promise-based event subscription, dual cleanup via `cleanup()` closure, nested `try/catch` inside the event callback | Extract the Promise setup into a `awaitRunEvent(runId, timeout)` utility |
62
- | `planner.ts:evaluateCondition` | Low-Medium (CC ~11) | Large `switch` on 9 operators; each case is self-contained but the function is called in a hot loop for every condition on every recipe | Acceptable as-is; consider a `conditionEvaluators: Record<ConditionOperator, Evaluator>` map if operators are user-extensible |
63
-
64
- ---
65
-
66
- ## Coupling & Cohesion Issues
67
-
68
- **1. `storage/postgres.ts` implements two unrelated storage interfaces**
69
-
70
- `PostgresStorageAdapter` simultaneously implements `StorageAdapter` (the legacy interface with `WorkflowRunRecord`, `WorkflowRunStepRecord`) and `WorkflowStorage` (the newer interface with `ExtendedWorkflowRunRecord`, `StepResult`). This means `postgres.ts` contains two nearly identical sets of CRUD methods (`createRun`/`getRun`/`updateRun` in both shapes), two `mapRunRow` functions (`mapRunRow` and `mapExtendedRunRow`) that differ only in the target type, and two separate status type hierarchies (`RunStatus` vs. `ExtendedRunStatus`). Any schema change must be applied in multiple places.
71
-
72
- *Suggested fix:* Decide on one canonical interface. If `WorkflowStorage` is the future, mark `StorageAdapter` as deprecated and migrate callers. Provide an adapter shim for backward compatibility during the transition rather than carrying both implementations indefinitely.
73
-
74
- **2. `storage/types.ts` defines two parallel type families**
75
-
76
- `StorageAdapter` + `WorkflowRunRecord`/`WorkflowRunStepRecord` and `WorkflowStorage` + `ExtendedWorkflowRunRecord`/`StepResult` overlap heavily. `ExtendedRunStatus` adds `'pending'` and `'timeout'` to `RunStatus`; `ExtendedStepStatus` renames `'succeeded'` to `'completed'`. These differences are not documented as deliberate — they appear to be the result of incremental additions without consolidation.
77
-
78
- *Suggested fix:* Unify the status enums (`RunStatus` should include `'timeout'`; step status should use consistent terminology). Remove the `Extended*` types once the single interface is adopted.
79
-
80
- **3. `engine.ts` holds both scheduling concerns and run lifecycle concerns**
81
-
82
- `WorkflowEngine` manages the in-memory run queue with priority insertion (`queueRun`, `processQueue`), active-run tracking (`activeRuns` map), child spawning delegation, event subscription forwarding, and shutdown. This is reasonable but the queue and concurrency control logic (`hasCapacity`, `queueRun`, `processQueue`) could become an independent `RunQueue` class, making `WorkflowEngine` a thinner coordinator.
83
-
84
- **4. `events/webhook.ts` duplicates IP-range logic across two functions**
85
-
86
- `isBlockedHost` and `isBlockedIp` both check the same IPv4 private ranges (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x, 0.0.0.0) with near-identical code. A future range addition or bug fix must be applied twice.
87
-
88
- **5. `scheduler/postgres-persistence.ts` repeats the Kysely lazy-load pattern from `storage/postgres.ts`**
89
-
90
- Both files declare module-level `let Kysely`, `let PostgresDialect`, `let sql`, `let pgModule` and both call `loadPostgresDeps()` in `initialize()`. This cross-file duplication of boilerplate means any change to the lazy-loading pattern (e.g., adding a new exported symbol) must be applied in two places.
91
-
92
- *Suggested fix:* Centralize the Kysely instantiation in `utils/postgres-deps.ts` and return a ready-to-use `Kysely` instance, rather than returning the constructor for callers to instantiate manually.
93
-
94
- ---
95
-
96
- ## Refactoring Candidates
97
-
98
- **1. Consolidate dual storage interfaces (`StorageAdapter` / `WorkflowStorage`)**
99
-
100
- - **Location:** `src/storage/types.ts`, `src/storage/postgres.ts`
101
- - **Problem:** Two overlapping interface families with near-duplicate type definitions, status enums that differ by one or two values, and two sets of CRUD methods in `PostgresStorageAdapter`. Every schema or status change requires dual edits.
102
- - **Suggested approach:** Choose `WorkflowStorage` as the single target. Introduce a `LegacyStorageAdapterBridge` shim that wraps a `WorkflowStorage` into the `StorageAdapter` shape for backward compatibility. Unify `RunStatus` to include `'timeout'`; unify step status terminology to `'succeeded'`. Remove `Extended*` type aliases.
103
- - **Effort:** Large
104
-
105
- **2. Extract step execution helpers from `orchestrator.ts:executeStep`**
106
-
107
- - **Location:** `src/core/orchestrator.ts:296–479`
108
- - **Problem:** The `executeStep` function is a 183-line `while(true)` loop combining timeout handling, cancellation, hook invocation, event emission, and three error recovery strategies. High cyclomatic complexity makes it difficult to unit-test individual error paths.
109
- - **Suggested approach:** Extract `executeStepHandler(step, context, abortController)` (pure execution + timeout), `handleStepSuccess(stepRecord, ...)`, and `handleStepError(step, error, attempt, strategy, ...)` (retry/skip/fail branching). `executeStep` becomes a thin coordinator.
110
- - **Effort:** Medium
111
-
112
- **3. Merge `isBlockedHost` and `isBlockedIp` into one utility**
113
-
114
- - **Location:** `src/events/webhook.ts:487–555`
115
- - **Problem:** Two functions with near-identical IPv4-range checks. A missed range in one is a security regression.
116
- - **Suggested approach:** Consolidate into `isBlockedAddress(address: string): boolean` operating on dotted-decimal IPv4 or hostname strings. Call it from both `validateWebhookUrl` and `validateResolvedHost`.
117
- - **Effort:** Small
118
-
119
- **4. Extract a `parseJsonColumn` helper for storage mappers**
120
-
121
- - **Location:** `src/storage/postgres.ts:861–940`, `src/storage/sqlite.ts`
122
- - **Problem:** The pattern `typeof col === 'string' ? JSON.parse(col) : col` appears 10+ times across `mapRunRow`, `mapExtendedRunRow`, `mapStepRow`, `mapStepResultRow`, and `mapEventRow` in postgres.ts, and again in sqlite.ts's mapper.
123
- - **Suggested approach:** `function parseJsonColumn<T>(col: string | T | null | undefined, fallback?: T): T | undefined` — extracted to a shared `src/storage/utils.ts`. Eliminates all inline ternaries.
124
- - **Effort:** Small
125
-
126
- **5. Replace `createTables` ad-hoc migration with a versioned migration runner**
127
-
128
- - **Location:** `src/storage/postgres.ts:314–465`
129
- - **Problem:** The 150-line `createTables` method issues 15+ raw SQL statements and silently swallows `ALTER TABLE` errors as idempotency. There is no migration version tracking, so it is impossible to determine the current schema version or safely evolve the schema.
130
- - **Suggested approach:** Introduce a `schema_migrations` table tracking applied versions. Replace the `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` hack with numbered migration files applied sequentially. This also enables rollback logic.
131
- - **Effort:** Large
132
-
133
- **6. Refactor `WorkflowEngine` queue management into a `RunQueue` class**
134
-
135
- - **Location:** `src/core/engine.ts:112–399`
136
- - **Problem:** `WorkflowEngine` mixes run-lifecycle concerns (start, cancel, resume, wait) with an in-memory priority queue implementation (`queueRun`, `processQueue`, `hasCapacity`, `runQueue` array, `activeRuns` map). As concurrency features grow, this class will accumulate more queue-specific logic.
137
- - **Suggested approach:** Extract a `RunQueue` class that owns `runQueue`, `activeRuns`, `hasCapacity`, `queueRun`, `processQueue`, and the `QueuedRun` interface. `WorkflowEngine` delegates to it.
138
- - **Effort:** Medium
139
-
140
- **7. Centralize Kysely lazy-loading in `utils/postgres-deps.ts`**
141
-
142
- - **Location:** `src/utils/postgres-deps.ts`, `src/storage/postgres.ts:13–16`, `src/scheduler/postgres-persistence.ts:12–15`
143
- - **Problem:** Module-level `let Kysely`, `let PostgresDialect`, `let sql`, `let pgModule` declarations and `loadPostgresDeps()` calls are copy-pasted into two different files, and the callers then call `new Kysely(...)` themselves.
144
- - **Suggested approach:** Have `loadPostgresDeps` return an already-constructed `Kysely<any>` factory or a `buildKysely(pool)` helper. Callers receive a usable query builder, not raw constructors.
145
- - **Effort:** Small
146
-
147
- **8. Add a `retryUntilAbort` helper to eliminate duplicated sleep-abort patterns**
148
-
149
- - **Location:** `src/core/orchestrator.ts:462–473`, `src/utils/retry.ts`
150
- - **Problem:** The `sleep(delay, abortController.signal)` call inside the retry loop is followed by a catch that checks `WorkflowCanceledError` and updates the step record — a pattern that will be repeated if a second retry site is ever added.
151
- - **Suggested approach:** Provide a `sleepWithAbort(ms, signal, onAbort?)` utility in `src/utils/retry.ts` that handles the cancellation check and throws consistently, removing the need for a catch at the call site.
152
- - **Effort:** Small
@@ -1,34 +0,0 @@
1
- # Complexity Report: stepflow
2
-
3
- **Date**: 2026-04-14T01:04:23.188Z
4
- **Repository**: /srv/repos/stepflow
5
- **Branch**: main
6
- **Workflow Run**: 21addb61-3850-4a9f-9504-0c0627b6a8d5
7
- **Session**: 01abcef0-2ca7-4ee5-b46f-a7959609ab9a
8
-
9
- ---
10
-
11
- ## Code Complexity Report — Stepflow
12
-
13
- The report has been saved to `.codekin/reports/complexity/2026-04-14_complexity-report.md`.
14
-
15
- **Overall rating: Medium-High**
16
-
17
- Key findings:
18
-
19
- **Largest files** (top 3):
20
- - `src/storage/postgres.ts` — 1,354 lines (4.5× the 300-line guideline), a God object with 20+ methods across 8 distinct responsibilities
21
- - `src/core/engine.ts` — 692 lines, mixes queue management with lifecycle orchestration
22
- - `src/core/orchestrator.ts` — 564 lines, hosts the two most complex functions in the codebase
23
-
24
- **Hottest complexity spots:**
25
- - `orchestrator.ts: executeStep()` — CC ≈ 20, 7 levels of nesting in the retry loop
26
- - `orchestrator.ts: executeWorkflow()` — 217-line function with inline timeout scheduling, checkpointing, and lifecycle hooks all interleaved
27
- - `postgres.ts: createTables()` — 152 lines of undifferentiated SQL blocks
28
-
29
- **Highest-impact refactoring opportunities (quick wins):**
30
- 1. Extract a `parseJsonColumn<T>()` utility to eliminate the 8+ identical JSON parsing idioms duplicated across postgres and sqlite adapters (Small effort)
31
- 2. Replace `evaluateCondition()` switch with a strategy map (Small effort)
32
- 3. Extract event/status string literals into a `src/core/constants.ts` file (Small effort)
33
- 4. Replace the O(n) array-splice run queue with a priority heap (Small effort)
34
- 5. Introduce a shared `Semaphore` utility — both `engine.ts` and `webhook.ts` have hand-rolled concurrency controllers (Small effort)