groundswell 0.0.1
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/.claude/settings.local.json +9 -0
- package/.claude/system_prompts/task-breakdown.md +100 -0
- package/PRPs/001-hierarchical-workflow-engine.md +2438 -0
- package/PRPs/PRDs/001-hierarchical-workflow-engine.md +543 -0
- package/PRPs/PRDs/002-agent-prompt.md +390 -0
- package/PRPs/PRDs/003-agent-prompt.md +943 -0
- package/PRPs/PRDs/004-agent-prompt.md +1136 -0
- package/PRPs/PRDs/tasks-001.json +492 -0
- package/PRPs/README.md +83 -0
- package/PRPs/templates/prp_base.md +222 -0
- package/README.md +218 -0
- package/docs/agent.md +422 -0
- package/docs/prompt.md +419 -0
- package/docs/workflow.md +600 -0
- package/examples/README.md +244 -0
- package/examples/examples/01-basic-workflow.ts +100 -0
- package/examples/examples/02-decorator-options.ts +217 -0
- package/examples/examples/03-parent-child.ts +241 -0
- package/examples/examples/04-observers-debugger.ts +340 -0
- package/examples/examples/05-error-handling.ts +387 -0
- package/examples/examples/06-concurrent-tasks.ts +352 -0
- package/examples/examples/07-agent-loops.ts +432 -0
- package/examples/examples/08-sdk-features.ts +667 -0
- package/examples/examples/09-reflection.ts +573 -0
- package/examples/examples/10-introspection.ts +550 -0
- package/examples/index.ts +143 -0
- package/examples/utils/helpers.ts +57 -0
- package/llms_full.txt +5890 -0
- package/package.json +63 -0
- package/plan/P1P2/PRP.md +527 -0
- package/plan/P1P2/research/LRU_CACHE_BEST_PRACTICES.md +1929 -0
- package/plan/P1P2/research/LRU_CACHE_CODE_PATTERNS.md +857 -0
- package/plan/P1P2/research/LRU_CACHE_INTEGRATION_GUIDE.md +738 -0
- package/plan/P1P2/research/LRU_CACHE_RESEARCH_INDEX.md +424 -0
- package/plan/P1P2/research/REFLECTION_INDEX.md +291 -0
- package/plan/P1P2/research/REFLECTION_RESEARCH_REPORT.md +1342 -0
- package/plan/P1P2/research/RESEARCH_SUMMARY.md +342 -0
- package/plan/P1P2/research/anthropic-sdk.md +174 -0
- package/plan/P1P2/research/async-local-storage.md +200 -0
- package/plan/P1P2/research/reflection-code-patterns.md +1205 -0
- package/plan/P1P2/research/reflection-decision-matrix.md +421 -0
- package/plan/P1P2/research/reflection-implementation-guide.md +1341 -0
- package/plan/P1P2/research/reflection-integration-guide.md +834 -0
- package/plan/P1P2/research/reflection-patterns.md +1468 -0
- package/plan/P1P2/research/reflection-quick-reference.md +558 -0
- package/plan/P1P2/research/zod-schema.md +152 -0
- package/plan/P3P4/PRP.md +1388 -0
- package/plan/P3P4/research/caching-lru.md +116 -0
- package/plan/P3P4/research/introspection-tools.md +177 -0
- package/plan/P3P4/research/reflection-patterns.md +117 -0
- package/plan/P4P5/PRP.md +1136 -0
- package/plan/P4P5/research/RESEARCH_SUMMARY.md +151 -0
- package/plan/architecture/external_deps.md +358 -0
- package/plan/architecture/system_context.md +242 -0
- package/plan/backlog.json +867 -0
- package/plan/research/INTROSPECTION_RESEARCH_SUMMARY.md +378 -0
- package/plan/research/README-INTROSPECTION.md +352 -0
- package/plan/research/agent-introspection-patterns.md +1085 -0
- package/plan/research/introspection-security-guide.md +928 -0
- package/plan/research/introspection-tool-examples.md +875 -0
- package/scripts/generate-llms-full.ts +206 -0
- package/src/__tests__/integration/agent-workflow.test.ts +256 -0
- package/src/__tests__/integration/tree-mirroring.test.ts +114 -0
- package/src/__tests__/unit/agent.test.ts +169 -0
- package/src/__tests__/unit/cache-key.test.ts +182 -0
- package/src/__tests__/unit/cache.test.ts +172 -0
- package/src/__tests__/unit/context.test.ts +138 -0
- package/src/__tests__/unit/decorators.test.ts +100 -0
- package/src/__tests__/unit/introspection-tools.test.ts +277 -0
- package/src/__tests__/unit/prompt.test.ts +135 -0
- package/src/__tests__/unit/reflection.test.ts +210 -0
- package/src/__tests__/unit/tree-debugger.test.ts +85 -0
- package/src/__tests__/unit/workflow.test.ts +81 -0
- package/src/cache/cache-key.ts +244 -0
- package/src/cache/cache.ts +236 -0
- package/src/cache/index.ts +8 -0
- package/src/core/agent.ts +573 -0
- package/src/core/context.ts +119 -0
- package/src/core/event-tree.ts +260 -0
- package/src/core/factory.ts +123 -0
- package/src/core/index.ts +17 -0
- package/src/core/logger.ts +87 -0
- package/src/core/mcp-handler.ts +184 -0
- package/src/core/prompt.ts +150 -0
- package/src/core/workflow-context.ts +349 -0
- package/src/core/workflow.ts +302 -0
- package/src/debugger/index.ts +1 -0
- package/src/debugger/tree-debugger.ts +210 -0
- package/src/decorators/index.ts +3 -0
- package/src/decorators/observed-state.ts +95 -0
- package/src/decorators/step.ts +139 -0
- package/src/decorators/task.ts +96 -0
- package/src/examples/index.ts +2 -0
- package/src/examples/tdd-orchestrator.ts +65 -0
- package/src/examples/test-cycle-workflow.ts +64 -0
- package/src/index.ts +140 -0
- package/src/reflection/index.ts +5 -0
- package/src/reflection/reflection.ts +407 -0
- package/src/tools/index.ts +36 -0
- package/src/tools/introspection.ts +464 -0
- package/src/types/agent.ts +90 -0
- package/src/types/decorators.ts +25 -0
- package/src/types/error-strategy.ts +13 -0
- package/src/types/error.ts +20 -0
- package/src/types/events.ts +74 -0
- package/src/types/index.ts +55 -0
- package/src/types/logging.ts +24 -0
- package/src/types/observer.ts +18 -0
- package/src/types/prompt.ts +40 -0
- package/src/types/reflection.ts +117 -0
- package/src/types/sdk-primitives.ts +128 -0
- package/src/types/snapshot.ts +14 -0
- package/src/types/workflow-context.ts +163 -0
- package/src/types/workflow.ts +37 -0
- package/src/utils/id.ts +11 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/observable.ts +77 -0
- package/tasks.json +0 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,2438 @@
|
|
|
1
|
+
# PRP-001: Hierarchical Workflow Engine with Full Observability
|
|
2
|
+
|
|
3
|
+
> **PRP**: Product Requirements Package - Comprehensive implementation guide for Groundswell workflow orchestration engine
|
|
4
|
+
|
|
5
|
+
**Version**: 1.0
|
|
6
|
+
**Status**: Implementation-Ready
|
|
7
|
+
**Source PRD**: `./PRD.md`
|
|
8
|
+
|
|
9
|
+
## Pre-Implementation Checklist
|
|
10
|
+
|
|
11
|
+
Before implementing, verify you have:
|
|
12
|
+
- [ ] Read the full PRD at `./PRD.md`
|
|
13
|
+
- [ ] Node.js 18+ LTS installed
|
|
14
|
+
- [ ] TypeScript 5.2+ available
|
|
15
|
+
- [ ] Understanding of TypeScript decorators (see `DECORATOR_QUICK_REFERENCE.md`)
|
|
16
|
+
- [ ] Familiarity with async/await patterns and Promises
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. Goal
|
|
21
|
+
|
|
22
|
+
### Feature Goal
|
|
23
|
+
Build a TypeScript workflow orchestration engine that provides hierarchical workflows with automatic parent/child attachment, high-resolution observability (logs, events, snapshots), and a real-time tree debugger API for terminal visualization.
|
|
24
|
+
|
|
25
|
+
### Deliverable
|
|
26
|
+
A complete npm-publishable TypeScript library exporting:
|
|
27
|
+
- `Workflow` abstract base class
|
|
28
|
+
- `@Step`, `@Task`, `@ObservedState` decorators
|
|
29
|
+
- `WorkflowLogger` class
|
|
30
|
+
- `WorkflowTreeDebugger` class
|
|
31
|
+
- All TypeScript interfaces and types
|
|
32
|
+
- Example workflows demonstrating usage
|
|
33
|
+
|
|
34
|
+
### Success Definition
|
|
35
|
+
1. All TypeScript compiles with strict mode enabled
|
|
36
|
+
2. Example `TDDOrchestrator` workflow runs successfully with child `TestCycleWorkflow`
|
|
37
|
+
3. `WorkflowTreeDebugger` produces accurate ASCII tree visualization
|
|
38
|
+
4. All logs and events form a **perfect 1:1 tree mirror** of workflow execution
|
|
39
|
+
5. Errors contain full state snapshots and log history
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. Context
|
|
44
|
+
|
|
45
|
+
### External Documentation
|
|
46
|
+
```yaml
|
|
47
|
+
primary_docs:
|
|
48
|
+
- url: "https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#decorators"
|
|
49
|
+
purpose: "Modern TC39 Stage 3 decorator syntax and behavior"
|
|
50
|
+
key_sections:
|
|
51
|
+
- "Decorators"
|
|
52
|
+
- "Decorator Metadata"
|
|
53
|
+
- "Auto-Accessors"
|
|
54
|
+
|
|
55
|
+
- url: "https://tc39.es/proposal-decorators/"
|
|
56
|
+
purpose: "Official TC39 decorator specification"
|
|
57
|
+
key_sections:
|
|
58
|
+
- "Method Decorators"
|
|
59
|
+
- "Field Decorators"
|
|
60
|
+
- "Class Decorators"
|
|
61
|
+
|
|
62
|
+
- url: "https://docs.temporal.io/develop/typescript/child-workflows"
|
|
63
|
+
purpose: "Parent-child workflow patterns and lifecycle management"
|
|
64
|
+
key_sections:
|
|
65
|
+
- "executeChild vs startChild"
|
|
66
|
+
- "Parent Close Policy"
|
|
67
|
+
- "Cancellation Scopes"
|
|
68
|
+
|
|
69
|
+
- url: "https://nodejs.org/api/events.html"
|
|
70
|
+
purpose: "EventEmitter patterns for observer system"
|
|
71
|
+
key_sections:
|
|
72
|
+
- "emitter.on(eventName, listener)"
|
|
73
|
+
- "Memory leak warnings"
|
|
74
|
+
|
|
75
|
+
reference_implementations:
|
|
76
|
+
- url: "https://github.com/temporalio/sdk-typescript"
|
|
77
|
+
purpose: "Production workflow engine patterns"
|
|
78
|
+
files_to_study:
|
|
79
|
+
- "packages/workflow/src/workflow.ts"
|
|
80
|
+
- "packages/common/src/interfaces/workflow.ts"
|
|
81
|
+
|
|
82
|
+
- url: "https://github.com/danielgerlag/workflow-es"
|
|
83
|
+
purpose: "TypeScript workflow step patterns"
|
|
84
|
+
files_to_study:
|
|
85
|
+
- "src/workflow-builder.ts"
|
|
86
|
+
- "src/step-body.ts"
|
|
87
|
+
|
|
88
|
+
local_research:
|
|
89
|
+
- file: "./DECORATOR_QUICK_REFERENCE.md"
|
|
90
|
+
purpose: "Decorator implementation patterns for this project"
|
|
91
|
+
|
|
92
|
+
- file: "./DECORATOR_EXAMPLES.ts"
|
|
93
|
+
purpose: "Production-ready decorator code to adapt"
|
|
94
|
+
|
|
95
|
+
- file: "./TREE_VISUALIZATION_QUICK_REF.md"
|
|
96
|
+
purpose: "ASCII tree rendering patterns and status indicators"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Codebase Context
|
|
100
|
+
```yaml
|
|
101
|
+
existing_patterns:
|
|
102
|
+
# This is a greenfield project - no existing patterns
|
|
103
|
+
# Follow patterns from research documents
|
|
104
|
+
|
|
105
|
+
project_structure:
|
|
106
|
+
root: "./"
|
|
107
|
+
source: "./src"
|
|
108
|
+
tests: "./src/__tests__"
|
|
109
|
+
|
|
110
|
+
naming_conventions:
|
|
111
|
+
files: "kebab-case.ts (e.g., workflow-logger.ts, tree-debugger.ts)"
|
|
112
|
+
classes: "PascalCase (e.g., WorkflowLogger, WorkflowTreeDebugger)"
|
|
113
|
+
interfaces: "PascalCase with descriptive names (e.g., WorkflowNode, LogEntry)"
|
|
114
|
+
types: "PascalCase for type aliases (e.g., WorkflowStatus, LogLevel)"
|
|
115
|
+
constants: "SCREAMING_SNAKE_CASE (e.g., OBSERVED_STATE_FIELDS)"
|
|
116
|
+
functions: "camelCase (e.g., generateId, getObservedState)"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Technical Constraints
|
|
120
|
+
```yaml
|
|
121
|
+
typescript:
|
|
122
|
+
version: "5.2+"
|
|
123
|
+
config_requirements:
|
|
124
|
+
- "target: ES2022"
|
|
125
|
+
- "module: ES2022"
|
|
126
|
+
- "strict: true"
|
|
127
|
+
- "useDefineForClassFields: true"
|
|
128
|
+
- "DO NOT use experimentalDecorators flag - use modern Stage 3 decorators"
|
|
129
|
+
|
|
130
|
+
dependencies:
|
|
131
|
+
required: [] # Zero runtime dependencies - pure TypeScript
|
|
132
|
+
|
|
133
|
+
dev_dependencies:
|
|
134
|
+
- name: "typescript"
|
|
135
|
+
version: "^5.2.0"
|
|
136
|
+
purpose: "TypeScript compiler with modern decorator support"
|
|
137
|
+
- name: "vitest"
|
|
138
|
+
version: "^1.0.0"
|
|
139
|
+
purpose: "Fast unit testing framework"
|
|
140
|
+
- name: "@types/node"
|
|
141
|
+
version: "^20.0.0"
|
|
142
|
+
purpose: "Node.js type definitions"
|
|
143
|
+
|
|
144
|
+
avoid:
|
|
145
|
+
- name: "reflect-metadata"
|
|
146
|
+
reason: "Legacy pattern - use Symbol.metadata instead"
|
|
147
|
+
- name: "rxjs"
|
|
148
|
+
reason: "Too heavy - implement lightweight Observable"
|
|
149
|
+
|
|
150
|
+
runtime:
|
|
151
|
+
node_version: "18+"
|
|
152
|
+
target: "ES2022"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Known Gotchas
|
|
156
|
+
```yaml
|
|
157
|
+
pitfalls:
|
|
158
|
+
- issue: "Arrow functions in decorators lose 'this' context"
|
|
159
|
+
solution: "Always use regular function declarations in decorator wrappers"
|
|
160
|
+
example: |
|
|
161
|
+
// WRONG
|
|
162
|
+
return (...args) => original.call(this, ...args);
|
|
163
|
+
// CORRECT
|
|
164
|
+
return function(...args) { return original.call(this, ...args); };
|
|
165
|
+
|
|
166
|
+
- issue: "Decorator not preserving async behavior"
|
|
167
|
+
solution: "Ensure wrapper function is async and properly awaits original"
|
|
168
|
+
example: |
|
|
169
|
+
async function wrapper(this: This, ...args: Args): Promise<any> {
|
|
170
|
+
return await originalMethod.call(this, ...args);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
- issue: "Child workflow parent not set correctly"
|
|
174
|
+
solution: "Set parent in constructor AND call attachChild on parent"
|
|
175
|
+
|
|
176
|
+
- issue: "Events not reaching root observer"
|
|
177
|
+
solution: "Always traverse up to root via getRootObservers() method"
|
|
178
|
+
|
|
179
|
+
- issue: "State snapshot missing fields"
|
|
180
|
+
solution: "Use WeakMap keyed by prototype, not instance"
|
|
181
|
+
|
|
182
|
+
- issue: "Tree debugger showing stale data"
|
|
183
|
+
solution: "Emit 'treeUpdated' event after every structural change"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 3. Implementation Tasks
|
|
189
|
+
|
|
190
|
+
> Complete tasks in order. Each task builds on previous tasks.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### Task 1: Project Setup and Configuration
|
|
195
|
+
**Depends on**: None
|
|
196
|
+
|
|
197
|
+
**Input**: Empty `./src` directory
|
|
198
|
+
|
|
199
|
+
**Steps**:
|
|
200
|
+
1. Create directory structure:
|
|
201
|
+
```
|
|
202
|
+
src/
|
|
203
|
+
├── types/
|
|
204
|
+
├── core/
|
|
205
|
+
├── decorators/
|
|
206
|
+
├── debugger/
|
|
207
|
+
├── utils/
|
|
208
|
+
├── examples/
|
|
209
|
+
└── __tests__/
|
|
210
|
+
├── unit/
|
|
211
|
+
└── integration/
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
2. Create `./package.json`:
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"name": "groundswell",
|
|
218
|
+
"version": "1.0.0",
|
|
219
|
+
"description": "Hierarchical workflow orchestration engine with full observability",
|
|
220
|
+
"type": "module",
|
|
221
|
+
"main": "./dist/index.js",
|
|
222
|
+
"module": "./dist/index.js",
|
|
223
|
+
"types": "./dist/index.d.ts",
|
|
224
|
+
"exports": {
|
|
225
|
+
".": {
|
|
226
|
+
"import": "./dist/index.js",
|
|
227
|
+
"types": "./dist/index.d.ts"
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
"scripts": {
|
|
231
|
+
"build": "tsc",
|
|
232
|
+
"test": "vitest run",
|
|
233
|
+
"test:watch": "vitest",
|
|
234
|
+
"lint": "tsc --noEmit"
|
|
235
|
+
},
|
|
236
|
+
"devDependencies": {
|
|
237
|
+
"typescript": "^5.2.0",
|
|
238
|
+
"vitest": "^1.0.0",
|
|
239
|
+
"@types/node": "^20.0.0"
|
|
240
|
+
},
|
|
241
|
+
"engines": {
|
|
242
|
+
"node": ">=18"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
3. Create `./tsconfig.json`:
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"compilerOptions": {
|
|
251
|
+
"target": "ES2022",
|
|
252
|
+
"module": "ES2022",
|
|
253
|
+
"moduleResolution": "bundler",
|
|
254
|
+
"lib": ["ES2022"],
|
|
255
|
+
"outDir": "./dist",
|
|
256
|
+
"rootDir": "./src",
|
|
257
|
+
"declaration": true,
|
|
258
|
+
"declarationMap": true,
|
|
259
|
+
"sourceMap": true,
|
|
260
|
+
"strict": true,
|
|
261
|
+
"useDefineForClassFields": true,
|
|
262
|
+
"esModuleInterop": true,
|
|
263
|
+
"skipLibCheck": true,
|
|
264
|
+
"forceConsistentCasingInFileNames": true,
|
|
265
|
+
"resolveJsonModule": true,
|
|
266
|
+
"isolatedModules": true
|
|
267
|
+
},
|
|
268
|
+
"include": ["src/**/*"],
|
|
269
|
+
"exclude": ["node_modules", "dist"]
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
4. Run `npm install` to install dependencies
|
|
274
|
+
|
|
275
|
+
**Output**:
|
|
276
|
+
- Complete directory structure
|
|
277
|
+
- Configured package.json and tsconfig.json
|
|
278
|
+
- Dependencies installed
|
|
279
|
+
|
|
280
|
+
**Validation**:
|
|
281
|
+
- `npx tsc --version` shows 5.2+
|
|
282
|
+
- `npm run lint` runs without errors (empty project)
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
### Task 2: Core Type Definitions
|
|
287
|
+
**Depends on**: Task 1
|
|
288
|
+
|
|
289
|
+
**Input**: Empty `src/types/` directory
|
|
290
|
+
|
|
291
|
+
**Steps**:
|
|
292
|
+
|
|
293
|
+
1. Create `./src/types/workflow.ts`:
|
|
294
|
+
```typescript
|
|
295
|
+
/**
|
|
296
|
+
* Workflow status representing the current execution state
|
|
297
|
+
*/
|
|
298
|
+
export type WorkflowStatus =
|
|
299
|
+
| 'idle'
|
|
300
|
+
| 'running'
|
|
301
|
+
| 'completed'
|
|
302
|
+
| 'failed'
|
|
303
|
+
| 'cancelled';
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Represents a node in the workflow execution tree
|
|
307
|
+
* This is the data structure, not the Workflow class
|
|
308
|
+
*/
|
|
309
|
+
export interface WorkflowNode {
|
|
310
|
+
/** Unique identifier for this workflow instance */
|
|
311
|
+
id: string;
|
|
312
|
+
/** Human-readable name */
|
|
313
|
+
name: string;
|
|
314
|
+
/** Parent node reference (null for root) */
|
|
315
|
+
parent: WorkflowNode | null;
|
|
316
|
+
/** Child workflow nodes */
|
|
317
|
+
children: WorkflowNode[];
|
|
318
|
+
/** Current execution status */
|
|
319
|
+
status: WorkflowStatus;
|
|
320
|
+
/** Log entries for this node */
|
|
321
|
+
logs: LogEntry[];
|
|
322
|
+
/** Events emitted by this node */
|
|
323
|
+
events: WorkflowEvent[];
|
|
324
|
+
/** Optional serialized state snapshot */
|
|
325
|
+
stateSnapshot: SerializedWorkflowState | null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Forward declarations - import from their respective files
|
|
329
|
+
import type { LogEntry } from './logging.js';
|
|
330
|
+
import type { WorkflowEvent } from './events.js';
|
|
331
|
+
import type { SerializedWorkflowState } from './snapshot.js';
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
2. Create `./src/types/logging.ts`:
|
|
335
|
+
```typescript
|
|
336
|
+
/**
|
|
337
|
+
* Log severity levels
|
|
338
|
+
*/
|
|
339
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* A single log entry in the workflow
|
|
343
|
+
*/
|
|
344
|
+
export interface LogEntry {
|
|
345
|
+
/** Unique identifier for this log entry */
|
|
346
|
+
id: string;
|
|
347
|
+
/** ID of the workflow that created this log */
|
|
348
|
+
workflowId: string;
|
|
349
|
+
/** Unix timestamp in milliseconds */
|
|
350
|
+
timestamp: number;
|
|
351
|
+
/** Severity level */
|
|
352
|
+
level: LogLevel;
|
|
353
|
+
/** Log message */
|
|
354
|
+
message: string;
|
|
355
|
+
/** Optional structured data */
|
|
356
|
+
data?: unknown;
|
|
357
|
+
/** ID of parent log entry (for hierarchical logging) */
|
|
358
|
+
parentLogId?: string;
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
3. Create `./src/types/snapshot.ts`:
|
|
363
|
+
```typescript
|
|
364
|
+
/**
|
|
365
|
+
* Serialized workflow state as key-value pairs
|
|
366
|
+
*/
|
|
367
|
+
export type SerializedWorkflowState = Record<string, unknown>;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Metadata for observed state fields
|
|
371
|
+
*/
|
|
372
|
+
export interface StateFieldMetadata {
|
|
373
|
+
/** If true, field is not included in snapshots */
|
|
374
|
+
hidden?: boolean;
|
|
375
|
+
/** If true, value is shown as '***' in snapshots */
|
|
376
|
+
redact?: boolean;
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
4. Create `./src/types/error.ts`:
|
|
381
|
+
```typescript
|
|
382
|
+
import type { LogEntry } from './logging.js';
|
|
383
|
+
import type { SerializedWorkflowState } from './snapshot.js';
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Rich error object containing workflow context
|
|
387
|
+
*/
|
|
388
|
+
export interface WorkflowError {
|
|
389
|
+
/** Error message */
|
|
390
|
+
message: string;
|
|
391
|
+
/** Original thrown error */
|
|
392
|
+
original: unknown;
|
|
393
|
+
/** ID of workflow where error occurred */
|
|
394
|
+
workflowId: string;
|
|
395
|
+
/** Stack trace if available */
|
|
396
|
+
stack?: string;
|
|
397
|
+
/** State snapshot at time of error */
|
|
398
|
+
state: SerializedWorkflowState;
|
|
399
|
+
/** Logs from the failing workflow node */
|
|
400
|
+
logs: LogEntry[];
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
5. Create `./src/types/events.ts`:
|
|
405
|
+
```typescript
|
|
406
|
+
import type { WorkflowNode } from './workflow.js';
|
|
407
|
+
import type { WorkflowError } from './error.js';
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Discriminated union of all workflow events
|
|
411
|
+
*/
|
|
412
|
+
export type WorkflowEvent =
|
|
413
|
+
| { type: 'childAttached'; parentId: string; child: WorkflowNode }
|
|
414
|
+
| { type: 'stateSnapshot'; node: WorkflowNode }
|
|
415
|
+
| { type: 'stepStart'; node: WorkflowNode; step: string }
|
|
416
|
+
| { type: 'stepEnd'; node: WorkflowNode; step: string; duration: number }
|
|
417
|
+
| { type: 'error'; node: WorkflowNode; error: WorkflowError }
|
|
418
|
+
| { type: 'taskStart'; node: WorkflowNode; task: string }
|
|
419
|
+
| { type: 'taskEnd'; node: WorkflowNode; task: string }
|
|
420
|
+
| { type: 'treeUpdated'; root: WorkflowNode };
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
6. Create `./src/types/observer.ts`:
|
|
424
|
+
```typescript
|
|
425
|
+
import type { LogEntry } from './logging.js';
|
|
426
|
+
import type { WorkflowEvent } from './events.js';
|
|
427
|
+
import type { WorkflowNode } from './workflow.js';
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Observer interface for subscribing to workflow events
|
|
431
|
+
* Observers attach to the root workflow and receive all events
|
|
432
|
+
*/
|
|
433
|
+
export interface WorkflowObserver {
|
|
434
|
+
/** Called when a log entry is created */
|
|
435
|
+
onLog(entry: LogEntry): void;
|
|
436
|
+
/** Called when any workflow event occurs */
|
|
437
|
+
onEvent(event: WorkflowEvent): void;
|
|
438
|
+
/** Called when a node's state is updated */
|
|
439
|
+
onStateUpdated(node: WorkflowNode): void;
|
|
440
|
+
/** Called when the tree structure changes */
|
|
441
|
+
onTreeChanged(root: WorkflowNode): void;
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
7. Create `./src/types/decorators.ts`:
|
|
446
|
+
```typescript
|
|
447
|
+
/**
|
|
448
|
+
* Configuration options for @Step decorator
|
|
449
|
+
*/
|
|
450
|
+
export interface StepOptions {
|
|
451
|
+
/** Custom step name (defaults to method name) */
|
|
452
|
+
name?: string;
|
|
453
|
+
/** If true, capture state snapshot after step completion */
|
|
454
|
+
snapshotState?: boolean;
|
|
455
|
+
/** If true, track and emit step duration */
|
|
456
|
+
trackTiming?: boolean;
|
|
457
|
+
/** If true, log message at step start */
|
|
458
|
+
logStart?: boolean;
|
|
459
|
+
/** If true, log message at step end */
|
|
460
|
+
logFinish?: boolean;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Configuration options for @Task decorator
|
|
465
|
+
*/
|
|
466
|
+
export interface TaskOptions {
|
|
467
|
+
/** Custom task name (defaults to method name) */
|
|
468
|
+
name?: string;
|
|
469
|
+
/** If true, run returned workflows concurrently */
|
|
470
|
+
concurrent?: boolean;
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
8. Create `./src/types/error-strategy.ts`:
|
|
475
|
+
```typescript
|
|
476
|
+
import type { WorkflowError } from './error.js';
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Strategy for merging multiple errors from concurrent operations
|
|
480
|
+
*/
|
|
481
|
+
export interface ErrorMergeStrategy {
|
|
482
|
+
/** Enable error merging (default: false, first error wins) */
|
|
483
|
+
enabled: boolean;
|
|
484
|
+
/** Maximum depth to merge errors */
|
|
485
|
+
maxMergeDepth?: number;
|
|
486
|
+
/** Custom function to combine multiple errors */
|
|
487
|
+
combine?(errors: WorkflowError[]): WorkflowError;
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
9. Create `./src/types/index.ts`:
|
|
492
|
+
```typescript
|
|
493
|
+
// Core types
|
|
494
|
+
export type { WorkflowStatus, WorkflowNode } from './workflow.js';
|
|
495
|
+
export type { LogLevel, LogEntry } from './logging.js';
|
|
496
|
+
export type { SerializedWorkflowState, StateFieldMetadata } from './snapshot.js';
|
|
497
|
+
export type { WorkflowError } from './error.js';
|
|
498
|
+
export type { WorkflowEvent } from './events.js';
|
|
499
|
+
export type { WorkflowObserver } from './observer.js';
|
|
500
|
+
export type { StepOptions, TaskOptions } from './decorators.js';
|
|
501
|
+
export type { ErrorMergeStrategy } from './error-strategy.js';
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Output**: Complete type system in `src/types/`
|
|
505
|
+
|
|
506
|
+
**Validation**:
|
|
507
|
+
- `npm run lint` passes with no errors
|
|
508
|
+
- All imports resolve correctly
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
### Task 3: Utility Functions
|
|
513
|
+
**Depends on**: Task 2
|
|
514
|
+
|
|
515
|
+
**Input**: Type definitions from Task 2
|
|
516
|
+
|
|
517
|
+
**Steps**:
|
|
518
|
+
|
|
519
|
+
1. Create `./src/utils/id.ts`:
|
|
520
|
+
```typescript
|
|
521
|
+
/**
|
|
522
|
+
* Generate a unique identifier
|
|
523
|
+
* Uses crypto.randomUUID if available, falls back to timestamp + random
|
|
524
|
+
*/
|
|
525
|
+
export function generateId(): string {
|
|
526
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
527
|
+
return crypto.randomUUID();
|
|
528
|
+
}
|
|
529
|
+
// Fallback for environments without crypto.randomUUID
|
|
530
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 11)}`;
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
2. Create `./src/utils/observable.ts`:
|
|
535
|
+
```typescript
|
|
536
|
+
/**
|
|
537
|
+
* Lightweight Observable implementation for event streaming
|
|
538
|
+
* No external dependencies
|
|
539
|
+
*/
|
|
540
|
+
export interface Subscription {
|
|
541
|
+
unsubscribe(): void;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export interface Observer<T> {
|
|
545
|
+
next?: (value: T) => void;
|
|
546
|
+
error?: (error: unknown) => void;
|
|
547
|
+
complete?: () => void;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export class Observable<T> {
|
|
551
|
+
private subscribers: Set<Observer<T>> = new Set();
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Subscribe to this observable
|
|
555
|
+
* @returns Subscription with unsubscribe method
|
|
556
|
+
*/
|
|
557
|
+
subscribe(observer: Observer<T>): Subscription {
|
|
558
|
+
this.subscribers.add(observer);
|
|
559
|
+
return {
|
|
560
|
+
unsubscribe: () => {
|
|
561
|
+
this.subscribers.delete(observer);
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Emit a value to all subscribers
|
|
568
|
+
*/
|
|
569
|
+
next(value: T): void {
|
|
570
|
+
for (const subscriber of this.subscribers) {
|
|
571
|
+
try {
|
|
572
|
+
subscriber.next?.(value);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
console.error('Observable subscriber error:', err);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Signal an error to all subscribers
|
|
581
|
+
*/
|
|
582
|
+
error(err: unknown): void {
|
|
583
|
+
for (const subscriber of this.subscribers) {
|
|
584
|
+
try {
|
|
585
|
+
subscriber.error?.(err);
|
|
586
|
+
} catch (e) {
|
|
587
|
+
console.error('Observable error handler failed:', e);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Signal completion to all subscribers
|
|
594
|
+
*/
|
|
595
|
+
complete(): void {
|
|
596
|
+
for (const subscriber of this.subscribers) {
|
|
597
|
+
try {
|
|
598
|
+
subscriber.complete?.();
|
|
599
|
+
} catch (err) {
|
|
600
|
+
console.error('Observable complete handler failed:', err);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
this.subscribers.clear();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Get current subscriber count
|
|
608
|
+
*/
|
|
609
|
+
get subscriberCount(): number {
|
|
610
|
+
return this.subscribers.size;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
3. Create `./src/utils/index.ts`:
|
|
616
|
+
```typescript
|
|
617
|
+
export { generateId } from './id.js';
|
|
618
|
+
export { Observable } from './observable.js';
|
|
619
|
+
export type { Subscription, Observer } from './observable.js';
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
**Output**: Utility functions in `src/utils/`
|
|
623
|
+
|
|
624
|
+
**Validation**: `npm run lint` passes
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
### Task 4: WorkflowLogger Implementation
|
|
629
|
+
**Depends on**: Task 3
|
|
630
|
+
|
|
631
|
+
**Input**: Types and utilities from previous tasks
|
|
632
|
+
|
|
633
|
+
**Steps**:
|
|
634
|
+
|
|
635
|
+
1. Create `./src/core/logger.ts`:
|
|
636
|
+
```typescript
|
|
637
|
+
import type { WorkflowNode, LogEntry, LogLevel, WorkflowObserver } from '../types/index.js';
|
|
638
|
+
import { generateId } from '../utils/id.js';
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Logger that emits log entries to workflow node and observers
|
|
642
|
+
*/
|
|
643
|
+
export class WorkflowLogger {
|
|
644
|
+
constructor(
|
|
645
|
+
private readonly node: WorkflowNode,
|
|
646
|
+
private readonly observers: WorkflowObserver[]
|
|
647
|
+
) {}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Emit a log entry to the node and all observers
|
|
651
|
+
*/
|
|
652
|
+
private emit(entry: LogEntry): void {
|
|
653
|
+
this.node.logs.push(entry);
|
|
654
|
+
for (const obs of this.observers) {
|
|
655
|
+
try {
|
|
656
|
+
obs.onLog(entry);
|
|
657
|
+
} catch (err) {
|
|
658
|
+
console.error('Observer onLog error:', err);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Create a log entry with the given level
|
|
665
|
+
*/
|
|
666
|
+
private log(level: LogLevel, message: string, data?: unknown): void {
|
|
667
|
+
const entry: LogEntry = {
|
|
668
|
+
id: generateId(),
|
|
669
|
+
workflowId: this.node.id,
|
|
670
|
+
timestamp: Date.now(),
|
|
671
|
+
level,
|
|
672
|
+
message,
|
|
673
|
+
data,
|
|
674
|
+
};
|
|
675
|
+
this.emit(entry);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Log a debug message
|
|
680
|
+
*/
|
|
681
|
+
debug(message: string, data?: unknown): void {
|
|
682
|
+
this.log('debug', message, data);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Log an info message
|
|
687
|
+
*/
|
|
688
|
+
info(message: string, data?: unknown): void {
|
|
689
|
+
this.log('info', message, data);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Log a warning message
|
|
694
|
+
*/
|
|
695
|
+
warn(message: string, data?: unknown): void {
|
|
696
|
+
this.log('warn', message, data);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Log an error message
|
|
701
|
+
*/
|
|
702
|
+
error(message: string, data?: unknown): void {
|
|
703
|
+
this.log('error', message, data);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Create a child logger that includes parentLogId
|
|
708
|
+
*/
|
|
709
|
+
child(parentLogId: string): WorkflowLogger {
|
|
710
|
+
const childLogger = new ChildWorkflowLogger(
|
|
711
|
+
this.node,
|
|
712
|
+
this.observers,
|
|
713
|
+
parentLogId
|
|
714
|
+
);
|
|
715
|
+
return childLogger;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Child logger that includes parent log ID in entries
|
|
721
|
+
*/
|
|
722
|
+
class ChildWorkflowLogger extends WorkflowLogger {
|
|
723
|
+
constructor(
|
|
724
|
+
node: WorkflowNode,
|
|
725
|
+
observers: WorkflowObserver[],
|
|
726
|
+
private readonly parentLogId: string
|
|
727
|
+
) {
|
|
728
|
+
super(node, observers);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private emit(entry: LogEntry): void {
|
|
732
|
+
const entryWithParent: LogEntry = {
|
|
733
|
+
...entry,
|
|
734
|
+
parentLogId: this.parentLogId,
|
|
735
|
+
};
|
|
736
|
+
// Access parent's emit through node
|
|
737
|
+
(this as any).node.logs.push(entryWithParent);
|
|
738
|
+
for (const obs of (this as any).observers) {
|
|
739
|
+
try {
|
|
740
|
+
obs.onLog(entryWithParent);
|
|
741
|
+
} catch (err) {
|
|
742
|
+
console.error('Observer onLog error:', err);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**Output**: `src/core/logger.ts`
|
|
750
|
+
|
|
751
|
+
**Validation**: TypeScript compiles without errors
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
### Task 5: @ObservedState Decorator
|
|
756
|
+
**Depends on**: Task 2
|
|
757
|
+
|
|
758
|
+
**Input**: Type definitions
|
|
759
|
+
|
|
760
|
+
**Steps**:
|
|
761
|
+
|
|
762
|
+
1. Create `./src/decorators/observed-state.ts`:
|
|
763
|
+
```typescript
|
|
764
|
+
import type { StateFieldMetadata, SerializedWorkflowState } from '../types/index.js';
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* WeakMap storing field metadata keyed by class prototype
|
|
768
|
+
* Structure: Map<propertyKey, StateFieldMetadata>
|
|
769
|
+
*/
|
|
770
|
+
const OBSERVED_STATE_FIELDS = new WeakMap<object, Map<string, StateFieldMetadata>>();
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* @ObservedState decorator
|
|
774
|
+
* Marks a class field for inclusion in state snapshots
|
|
775
|
+
*
|
|
776
|
+
* @example
|
|
777
|
+
* class MyWorkflow extends Workflow {
|
|
778
|
+
* @ObservedState()
|
|
779
|
+
* currentStep!: string;
|
|
780
|
+
*
|
|
781
|
+
* @ObservedState({ redact: true })
|
|
782
|
+
* sensitiveData!: string;
|
|
783
|
+
*
|
|
784
|
+
* @ObservedState({ hidden: true })
|
|
785
|
+
* internalState!: object;
|
|
786
|
+
* }
|
|
787
|
+
*/
|
|
788
|
+
export function ObservedState(meta: StateFieldMetadata = {}) {
|
|
789
|
+
return function (
|
|
790
|
+
_value: undefined,
|
|
791
|
+
context: ClassFieldDecoratorContext
|
|
792
|
+
): void {
|
|
793
|
+
const propertyKey = String(context.name);
|
|
794
|
+
|
|
795
|
+
// Use addInitializer to register field when class is instantiated
|
|
796
|
+
context.addInitializer(function (this: object) {
|
|
797
|
+
const proto = Object.getPrototypeOf(this);
|
|
798
|
+
let map = OBSERVED_STATE_FIELDS.get(proto);
|
|
799
|
+
if (!map) {
|
|
800
|
+
map = new Map();
|
|
801
|
+
OBSERVED_STATE_FIELDS.set(proto, map);
|
|
802
|
+
}
|
|
803
|
+
map.set(propertyKey, meta);
|
|
804
|
+
});
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Get all observed state from an object instance
|
|
810
|
+
* Applies hidden and redact transformations
|
|
811
|
+
*/
|
|
812
|
+
export function getObservedState(obj: object): SerializedWorkflowState {
|
|
813
|
+
const proto = Object.getPrototypeOf(obj);
|
|
814
|
+
const map = OBSERVED_STATE_FIELDS.get(proto);
|
|
815
|
+
|
|
816
|
+
if (!map) {
|
|
817
|
+
return {};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const result: SerializedWorkflowState = {};
|
|
821
|
+
|
|
822
|
+
for (const [key, meta] of map) {
|
|
823
|
+
// Skip hidden fields
|
|
824
|
+
if (meta.hidden) {
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
let value = (obj as Record<string, unknown>)[key];
|
|
829
|
+
|
|
830
|
+
// Redact sensitive fields
|
|
831
|
+
if (meta.redact) {
|
|
832
|
+
value = '***';
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
result[key] = value;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return result;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Check if a field is observed on an object
|
|
843
|
+
*/
|
|
844
|
+
export function isFieldObserved(obj: object, fieldName: string): boolean {
|
|
845
|
+
const proto = Object.getPrototypeOf(obj);
|
|
846
|
+
const map = OBSERVED_STATE_FIELDS.get(proto);
|
|
847
|
+
return map?.has(fieldName) ?? false;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Get metadata for a specific field
|
|
852
|
+
*/
|
|
853
|
+
export function getFieldMetadata(obj: object, fieldName: string): StateFieldMetadata | undefined {
|
|
854
|
+
const proto = Object.getPrototypeOf(obj);
|
|
855
|
+
const map = OBSERVED_STATE_FIELDS.get(proto);
|
|
856
|
+
return map?.get(fieldName);
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
**Output**: `src/decorators/observed-state.ts`
|
|
861
|
+
|
|
862
|
+
**Validation**: TypeScript compiles without errors
|
|
863
|
+
|
|
864
|
+
---
|
|
865
|
+
|
|
866
|
+
### Task 6: Workflow Base Class
|
|
867
|
+
**Depends on**: Tasks 4, 5
|
|
868
|
+
|
|
869
|
+
**Input**: Logger and ObservedState decorator
|
|
870
|
+
|
|
871
|
+
**Steps**:
|
|
872
|
+
|
|
873
|
+
1. Create `./src/core/workflow.ts`:
|
|
874
|
+
```typescript
|
|
875
|
+
import type {
|
|
876
|
+
WorkflowNode,
|
|
877
|
+
WorkflowStatus,
|
|
878
|
+
WorkflowEvent,
|
|
879
|
+
WorkflowObserver,
|
|
880
|
+
} from '../types/index.js';
|
|
881
|
+
import { generateId } from '../utils/id.js';
|
|
882
|
+
import { WorkflowLogger } from './logger.js';
|
|
883
|
+
import { getObservedState } from '../decorators/observed-state.js';
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Abstract base class for all workflows
|
|
887
|
+
* Provides parent/child management, logging, events, and state snapshots
|
|
888
|
+
*/
|
|
889
|
+
export abstract class Workflow {
|
|
890
|
+
/** Unique identifier for this workflow instance */
|
|
891
|
+
public readonly id: string;
|
|
892
|
+
|
|
893
|
+
/** Parent workflow (null for root workflows) */
|
|
894
|
+
public parent: Workflow | null = null;
|
|
895
|
+
|
|
896
|
+
/** Child workflows */
|
|
897
|
+
public children: Workflow[] = [];
|
|
898
|
+
|
|
899
|
+
/** Current execution status */
|
|
900
|
+
public status: WorkflowStatus = 'idle';
|
|
901
|
+
|
|
902
|
+
/** Logger instance for this workflow */
|
|
903
|
+
protected readonly logger: WorkflowLogger;
|
|
904
|
+
|
|
905
|
+
/** The node representation of this workflow */
|
|
906
|
+
protected readonly node: WorkflowNode;
|
|
907
|
+
|
|
908
|
+
/** Observers (only populated on root workflow) */
|
|
909
|
+
private observers: WorkflowObserver[] = [];
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Create a new workflow instance
|
|
913
|
+
* @param name Human-readable name (defaults to class name)
|
|
914
|
+
* @param parent Optional parent workflow
|
|
915
|
+
*/
|
|
916
|
+
constructor(name?: string, parent?: Workflow) {
|
|
917
|
+
this.id = generateId();
|
|
918
|
+
this.parent = parent ?? null;
|
|
919
|
+
|
|
920
|
+
// Create the node representation
|
|
921
|
+
this.node = {
|
|
922
|
+
id: this.id,
|
|
923
|
+
name: name ?? this.constructor.name,
|
|
924
|
+
parent: parent?.node ?? null,
|
|
925
|
+
children: [],
|
|
926
|
+
status: 'idle',
|
|
927
|
+
logs: [],
|
|
928
|
+
events: [],
|
|
929
|
+
stateSnapshot: null,
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// Create logger with root observers
|
|
933
|
+
this.logger = new WorkflowLogger(this.node, this.getRootObservers());
|
|
934
|
+
|
|
935
|
+
// Attach to parent if provided
|
|
936
|
+
if (parent) {
|
|
937
|
+
parent.attachChild(this);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Get observers from the root workflow
|
|
943
|
+
* Traverses up the tree to find the root
|
|
944
|
+
*/
|
|
945
|
+
private getRootObservers(): WorkflowObserver[] {
|
|
946
|
+
if (this.parent) {
|
|
947
|
+
return this.parent.getRootObservers();
|
|
948
|
+
}
|
|
949
|
+
return this.observers;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Get the root workflow
|
|
954
|
+
*/
|
|
955
|
+
protected getRoot(): Workflow {
|
|
956
|
+
if (this.parent) {
|
|
957
|
+
return this.parent.getRoot();
|
|
958
|
+
}
|
|
959
|
+
return this;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Add an observer to this workflow (must be root)
|
|
964
|
+
* @throws Error if called on non-root workflow
|
|
965
|
+
*/
|
|
966
|
+
public addObserver(observer: WorkflowObserver): void {
|
|
967
|
+
if (this.parent) {
|
|
968
|
+
throw new Error('Observers can only be added to root workflows');
|
|
969
|
+
}
|
|
970
|
+
this.observers.push(observer);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Remove an observer from this workflow
|
|
975
|
+
*/
|
|
976
|
+
public removeObserver(observer: WorkflowObserver): void {
|
|
977
|
+
const index = this.observers.indexOf(observer);
|
|
978
|
+
if (index !== -1) {
|
|
979
|
+
this.observers.splice(index, 1);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Attach a child workflow
|
|
985
|
+
* Called automatically in constructor when parent is provided
|
|
986
|
+
*/
|
|
987
|
+
public attachChild(child: Workflow): void {
|
|
988
|
+
this.children.push(child);
|
|
989
|
+
this.node.children.push(child.node);
|
|
990
|
+
|
|
991
|
+
// Emit child attached event
|
|
992
|
+
this.emitEvent({
|
|
993
|
+
type: 'childAttached',
|
|
994
|
+
parentId: this.id,
|
|
995
|
+
child: child.node,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Emit an event to all root observers
|
|
1001
|
+
*/
|
|
1002
|
+
protected emitEvent(event: WorkflowEvent): void {
|
|
1003
|
+
this.node.events.push(event);
|
|
1004
|
+
|
|
1005
|
+
const observers = this.getRootObservers();
|
|
1006
|
+
for (const obs of observers) {
|
|
1007
|
+
try {
|
|
1008
|
+
obs.onEvent(event);
|
|
1009
|
+
|
|
1010
|
+
// Also notify tree changed for tree update events
|
|
1011
|
+
if (event.type === 'treeUpdated' || event.type === 'childAttached') {
|
|
1012
|
+
obs.onTreeChanged(this.getRoot().node);
|
|
1013
|
+
}
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
console.error('Observer onEvent error:', err);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Capture and emit a state snapshot
|
|
1022
|
+
*/
|
|
1023
|
+
public snapshotState(): void {
|
|
1024
|
+
const snapshot = getObservedState(this);
|
|
1025
|
+
this.node.stateSnapshot = snapshot;
|
|
1026
|
+
|
|
1027
|
+
// Notify observers
|
|
1028
|
+
const observers = this.getRootObservers();
|
|
1029
|
+
for (const obs of observers) {
|
|
1030
|
+
try {
|
|
1031
|
+
obs.onStateUpdated(this.node);
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
console.error('Observer onStateUpdated error:', err);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Emit snapshot event
|
|
1038
|
+
this.emitEvent({
|
|
1039
|
+
type: 'stateSnapshot',
|
|
1040
|
+
node: this.node,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Update workflow status and sync with node
|
|
1046
|
+
*/
|
|
1047
|
+
protected setStatus(status: WorkflowStatus): void {
|
|
1048
|
+
this.status = status;
|
|
1049
|
+
this.node.status = status;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Get the node representation of this workflow
|
|
1054
|
+
*/
|
|
1055
|
+
public getNode(): WorkflowNode {
|
|
1056
|
+
return this.node;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Abstract run method - must be implemented by subclasses
|
|
1061
|
+
* This is the main entry point for workflow execution
|
|
1062
|
+
*/
|
|
1063
|
+
public abstract run(...args: unknown[]): Promise<unknown>;
|
|
1064
|
+
}
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
**Output**: `src/core/workflow.ts`
|
|
1068
|
+
|
|
1069
|
+
**Validation**: TypeScript compiles without errors
|
|
1070
|
+
|
|
1071
|
+
---
|
|
1072
|
+
|
|
1073
|
+
### Task 7: @Step Decorator
|
|
1074
|
+
**Depends on**: Task 6
|
|
1075
|
+
|
|
1076
|
+
**Input**: Workflow base class
|
|
1077
|
+
|
|
1078
|
+
**Steps**:
|
|
1079
|
+
|
|
1080
|
+
1. Create `./src/decorators/step.ts`:
|
|
1081
|
+
```typescript
|
|
1082
|
+
import type { StepOptions, WorkflowError } from '../types/index.js';
|
|
1083
|
+
import { getObservedState } from './observed-state.js';
|
|
1084
|
+
|
|
1085
|
+
// Type for workflow-like objects
|
|
1086
|
+
interface WorkflowLike {
|
|
1087
|
+
id: string;
|
|
1088
|
+
node: {
|
|
1089
|
+
id: string;
|
|
1090
|
+
logs: unknown[];
|
|
1091
|
+
};
|
|
1092
|
+
logger: {
|
|
1093
|
+
info(message: string, data?: unknown): void;
|
|
1094
|
+
};
|
|
1095
|
+
emitEvent(event: unknown): void;
|
|
1096
|
+
snapshotState(): void;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* @Step decorator
|
|
1101
|
+
* Wraps a method to emit step events, handle errors, and optionally snapshot state
|
|
1102
|
+
*
|
|
1103
|
+
* @example
|
|
1104
|
+
* class MyWorkflow extends Workflow {
|
|
1105
|
+
* @Step({ snapshotState: true, trackTiming: true })
|
|
1106
|
+
* async processData() {
|
|
1107
|
+
* // ... step logic
|
|
1108
|
+
* }
|
|
1109
|
+
* }
|
|
1110
|
+
*/
|
|
1111
|
+
export function Step(opts: StepOptions = {}) {
|
|
1112
|
+
return function <This extends WorkflowLike, Args extends unknown[], Return>(
|
|
1113
|
+
originalMethod: (this: This, ...args: Args) => Promise<Return>,
|
|
1114
|
+
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Promise<Return>>
|
|
1115
|
+
) {
|
|
1116
|
+
const methodName = String(context.name);
|
|
1117
|
+
|
|
1118
|
+
// CRITICAL: Use regular function, not arrow function, to preserve 'this'
|
|
1119
|
+
async function stepWrapper(this: This, ...args: Args): Promise<Return> {
|
|
1120
|
+
const stepName = opts.name ?? methodName;
|
|
1121
|
+
const startTime = Date.now();
|
|
1122
|
+
|
|
1123
|
+
// Log start if requested
|
|
1124
|
+
if (opts.logStart) {
|
|
1125
|
+
this.logger.info(`STEP START: ${stepName}`);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Emit step start event
|
|
1129
|
+
this.emitEvent({
|
|
1130
|
+
type: 'stepStart',
|
|
1131
|
+
node: this.node,
|
|
1132
|
+
step: stepName,
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
try {
|
|
1136
|
+
// Execute the original method
|
|
1137
|
+
const result = await originalMethod.call(this, ...args);
|
|
1138
|
+
|
|
1139
|
+
// Snapshot state if requested
|
|
1140
|
+
if (opts.snapshotState) {
|
|
1141
|
+
this.snapshotState();
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Calculate duration and emit end event
|
|
1145
|
+
const duration = Date.now() - startTime;
|
|
1146
|
+
if (opts.trackTiming !== false) {
|
|
1147
|
+
this.emitEvent({
|
|
1148
|
+
type: 'stepEnd',
|
|
1149
|
+
node: this.node,
|
|
1150
|
+
step: stepName,
|
|
1151
|
+
duration,
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Log finish if requested
|
|
1156
|
+
if (opts.logFinish) {
|
|
1157
|
+
this.logger.info(`STEP END: ${stepName} (${duration}ms)`);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
return result;
|
|
1161
|
+
} catch (err: unknown) {
|
|
1162
|
+
// Create rich error with context
|
|
1163
|
+
const error = err as Error;
|
|
1164
|
+
const snap = getObservedState(this);
|
|
1165
|
+
|
|
1166
|
+
const workflowError: WorkflowError = {
|
|
1167
|
+
message: error?.message ?? 'Unknown error',
|
|
1168
|
+
original: err,
|
|
1169
|
+
workflowId: this.id,
|
|
1170
|
+
stack: error?.stack,
|
|
1171
|
+
state: snap,
|
|
1172
|
+
logs: [...this.node.logs] as any,
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
// Emit error event
|
|
1176
|
+
this.emitEvent({
|
|
1177
|
+
type: 'error',
|
|
1178
|
+
node: this.node,
|
|
1179
|
+
error: workflowError,
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
// Re-throw the enriched error
|
|
1183
|
+
throw workflowError;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
return stepWrapper;
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
**Output**: `src/decorators/step.ts`
|
|
1193
|
+
|
|
1194
|
+
**Validation**: TypeScript compiles without errors
|
|
1195
|
+
|
|
1196
|
+
---
|
|
1197
|
+
|
|
1198
|
+
### Task 8: @Task Decorator
|
|
1199
|
+
**Depends on**: Task 6
|
|
1200
|
+
|
|
1201
|
+
**Input**: Workflow base class
|
|
1202
|
+
|
|
1203
|
+
**Steps**:
|
|
1204
|
+
|
|
1205
|
+
1. Create `./src/decorators/task.ts`:
|
|
1206
|
+
```typescript
|
|
1207
|
+
import type { TaskOptions } from '../types/index.js';
|
|
1208
|
+
|
|
1209
|
+
// Type for workflow-like objects
|
|
1210
|
+
interface WorkflowLike {
|
|
1211
|
+
id: string;
|
|
1212
|
+
node: unknown;
|
|
1213
|
+
emitEvent(event: unknown): void;
|
|
1214
|
+
attachChild(child: WorkflowLike): void;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Minimal Workflow type for checking instanceof
|
|
1218
|
+
interface WorkflowClass {
|
|
1219
|
+
id: string;
|
|
1220
|
+
parent: WorkflowLike | null;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* @Task decorator
|
|
1225
|
+
* Wraps a method that returns child workflow(s), automatically attaching them
|
|
1226
|
+
*
|
|
1227
|
+
* @example
|
|
1228
|
+
* class ParentWorkflow extends Workflow {
|
|
1229
|
+
* @Task({ concurrent: true })
|
|
1230
|
+
* async createChildren(): Promise<ChildWorkflow[]> {
|
|
1231
|
+
* return [
|
|
1232
|
+
* new ChildWorkflow('child1', this),
|
|
1233
|
+
* new ChildWorkflow('child2', this),
|
|
1234
|
+
* ];
|
|
1235
|
+
* }
|
|
1236
|
+
* }
|
|
1237
|
+
*/
|
|
1238
|
+
export function Task(opts: TaskOptions = {}) {
|
|
1239
|
+
return function <This extends WorkflowLike, Args extends unknown[], Return>(
|
|
1240
|
+
originalMethod: (this: This, ...args: Args) => Promise<Return>,
|
|
1241
|
+
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Promise<Return>>
|
|
1242
|
+
) {
|
|
1243
|
+
const methodName = String(context.name);
|
|
1244
|
+
|
|
1245
|
+
// CRITICAL: Use regular function, not arrow function
|
|
1246
|
+
async function taskWrapper(this: This, ...args: Args): Promise<Return> {
|
|
1247
|
+
const taskName = opts.name ?? methodName;
|
|
1248
|
+
|
|
1249
|
+
// Emit task start event
|
|
1250
|
+
this.emitEvent({
|
|
1251
|
+
type: 'taskStart',
|
|
1252
|
+
node: this.node,
|
|
1253
|
+
task: taskName,
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
// Execute the original method
|
|
1257
|
+
const result = await originalMethod.call(this, ...args);
|
|
1258
|
+
|
|
1259
|
+
// Process returned workflows
|
|
1260
|
+
const workflows = Array.isArray(result) ? result : [result];
|
|
1261
|
+
|
|
1262
|
+
for (const workflow of workflows) {
|
|
1263
|
+
// Type guard to check if it's a workflow
|
|
1264
|
+
if (workflow && typeof workflow === 'object' && 'id' in workflow) {
|
|
1265
|
+
const wf = workflow as WorkflowClass;
|
|
1266
|
+
|
|
1267
|
+
// Only attach if not already attached
|
|
1268
|
+
if (!wf.parent) {
|
|
1269
|
+
wf.parent = this;
|
|
1270
|
+
this.attachChild(wf as unknown as WorkflowLike);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// If concurrent option is set and we have multiple workflows, run them in parallel
|
|
1276
|
+
if (opts.concurrent && Array.isArray(result)) {
|
|
1277
|
+
const runnable = workflows.filter(
|
|
1278
|
+
(w): w is WorkflowClass & { run(): Promise<unknown> } =>
|
|
1279
|
+
w && typeof w === 'object' && 'run' in w && typeof (w as any).run === 'function'
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
if (runnable.length > 0) {
|
|
1283
|
+
await Promise.all(runnable.map((w) => w.run()));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Emit task end event
|
|
1288
|
+
this.emitEvent({
|
|
1289
|
+
type: 'taskEnd',
|
|
1290
|
+
node: this.node,
|
|
1291
|
+
task: taskName,
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
return result;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
return taskWrapper;
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
**Output**: `src/decorators/task.ts`
|
|
1303
|
+
|
|
1304
|
+
**Validation**: TypeScript compiles without errors
|
|
1305
|
+
|
|
1306
|
+
---
|
|
1307
|
+
|
|
1308
|
+
### Task 9: Decorator Barrel Export
|
|
1309
|
+
**Depends on**: Tasks 5, 7, 8
|
|
1310
|
+
|
|
1311
|
+
**Input**: All decorator files
|
|
1312
|
+
|
|
1313
|
+
**Steps**:
|
|
1314
|
+
|
|
1315
|
+
1. Create `./src/decorators/index.ts`:
|
|
1316
|
+
```typescript
|
|
1317
|
+
export { ObservedState, getObservedState, isFieldObserved, getFieldMetadata } from './observed-state.js';
|
|
1318
|
+
export { Step } from './step.js';
|
|
1319
|
+
export { Task } from './task.js';
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
**Output**: `src/decorators/index.ts`
|
|
1323
|
+
|
|
1324
|
+
**Validation**: All exports resolve correctly
|
|
1325
|
+
|
|
1326
|
+
---
|
|
1327
|
+
|
|
1328
|
+
### Task 10: Core Barrel Export
|
|
1329
|
+
**Depends on**: Tasks 4, 6
|
|
1330
|
+
|
|
1331
|
+
**Input**: Core module files
|
|
1332
|
+
|
|
1333
|
+
**Steps**:
|
|
1334
|
+
|
|
1335
|
+
1. Create `./src/core/index.ts`:
|
|
1336
|
+
```typescript
|
|
1337
|
+
export { WorkflowLogger } from './logger.js';
|
|
1338
|
+
export { Workflow } from './workflow.js';
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
**Output**: `src/core/index.ts`
|
|
1342
|
+
|
|
1343
|
+
---
|
|
1344
|
+
|
|
1345
|
+
### Task 11: WorkflowTreeDebugger
|
|
1346
|
+
**Depends on**: Tasks 6, 3
|
|
1347
|
+
|
|
1348
|
+
**Input**: Workflow class and Observable utility
|
|
1349
|
+
|
|
1350
|
+
**Steps**:
|
|
1351
|
+
|
|
1352
|
+
1. Create `./src/debugger/tree-debugger.ts`:
|
|
1353
|
+
```typescript
|
|
1354
|
+
import type {
|
|
1355
|
+
WorkflowNode,
|
|
1356
|
+
WorkflowEvent,
|
|
1357
|
+
WorkflowObserver,
|
|
1358
|
+
LogEntry,
|
|
1359
|
+
} from '../types/index.js';
|
|
1360
|
+
import { Observable } from '../utils/observable.js';
|
|
1361
|
+
import type { Workflow } from '../core/workflow.js';
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Status symbols for tree visualization
|
|
1365
|
+
*/
|
|
1366
|
+
const STATUS_SYMBOLS: Record<string, string> = {
|
|
1367
|
+
idle: '○',
|
|
1368
|
+
running: '◐',
|
|
1369
|
+
completed: '✓',
|
|
1370
|
+
failed: '✗',
|
|
1371
|
+
cancelled: '⊘',
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* Tree debugger for real-time workflow visualization
|
|
1376
|
+
* Implements WorkflowObserver to receive all events
|
|
1377
|
+
*/
|
|
1378
|
+
export class WorkflowTreeDebugger implements WorkflowObserver {
|
|
1379
|
+
/** Root node of the workflow tree */
|
|
1380
|
+
private root: WorkflowNode;
|
|
1381
|
+
|
|
1382
|
+
/** Observable stream of workflow events */
|
|
1383
|
+
public readonly events: Observable<WorkflowEvent>;
|
|
1384
|
+
|
|
1385
|
+
/** Node lookup map for quick access */
|
|
1386
|
+
private nodeMap: Map<string, WorkflowNode> = new Map();
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Create a tree debugger attached to a workflow
|
|
1390
|
+
* @param workflow The root workflow to debug
|
|
1391
|
+
*/
|
|
1392
|
+
constructor(workflow: Workflow) {
|
|
1393
|
+
this.root = workflow.getNode();
|
|
1394
|
+
this.events = new Observable<WorkflowEvent>();
|
|
1395
|
+
|
|
1396
|
+
// Build initial node map
|
|
1397
|
+
this.buildNodeMap(this.root);
|
|
1398
|
+
|
|
1399
|
+
// Register as observer on the workflow
|
|
1400
|
+
workflow.addObserver(this);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Build node lookup map recursively
|
|
1405
|
+
*/
|
|
1406
|
+
private buildNodeMap(node: WorkflowNode): void {
|
|
1407
|
+
this.nodeMap.set(node.id, node);
|
|
1408
|
+
for (const child of node.children) {
|
|
1409
|
+
this.buildNodeMap(child);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// WorkflowObserver implementation
|
|
1414
|
+
|
|
1415
|
+
onLog(entry: LogEntry): void {
|
|
1416
|
+
// Events are forwarded through the event stream
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
onEvent(event: WorkflowEvent): void {
|
|
1420
|
+
// Rebuild node map on structural changes
|
|
1421
|
+
if (event.type === 'childAttached') {
|
|
1422
|
+
this.buildNodeMap(event.child);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Forward to event stream
|
|
1426
|
+
this.events.next(event);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
onStateUpdated(node: WorkflowNode): void {
|
|
1430
|
+
// State updates are available through the node
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
onTreeChanged(root: WorkflowNode): void {
|
|
1434
|
+
this.root = root;
|
|
1435
|
+
this.nodeMap.clear();
|
|
1436
|
+
this.buildNodeMap(root);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// Public API
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Get the current tree root
|
|
1443
|
+
*/
|
|
1444
|
+
getTree(): WorkflowNode {
|
|
1445
|
+
return this.root;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Get a node by ID
|
|
1450
|
+
*/
|
|
1451
|
+
getNode(id: string): WorkflowNode | undefined {
|
|
1452
|
+
return this.nodeMap.get(id);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Render tree as ASCII string
|
|
1457
|
+
* @param node Starting node (defaults to root)
|
|
1458
|
+
*/
|
|
1459
|
+
toTreeString(node?: WorkflowNode): string {
|
|
1460
|
+
return this.renderTree(node ?? this.root, '', true, true);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Recursive tree rendering
|
|
1465
|
+
*/
|
|
1466
|
+
private renderTree(
|
|
1467
|
+
node: WorkflowNode,
|
|
1468
|
+
prefix: string,
|
|
1469
|
+
isLast: boolean,
|
|
1470
|
+
isRoot: boolean
|
|
1471
|
+
): string {
|
|
1472
|
+
let result = '';
|
|
1473
|
+
|
|
1474
|
+
// Status symbol and color indicator
|
|
1475
|
+
const statusSymbol = STATUS_SYMBOLS[node.status] || '?';
|
|
1476
|
+
const nodeInfo = `${statusSymbol} ${node.name} [${node.status}]`;
|
|
1477
|
+
|
|
1478
|
+
if (isRoot) {
|
|
1479
|
+
result += nodeInfo + '\n';
|
|
1480
|
+
} else {
|
|
1481
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
1482
|
+
result += prefix + connector + nodeInfo + '\n';
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Render children
|
|
1486
|
+
const childCount = node.children.length;
|
|
1487
|
+
node.children.forEach((child, index) => {
|
|
1488
|
+
const isLastChild = index === childCount - 1;
|
|
1489
|
+
const childPrefix = isRoot ? '' : prefix + (isLast ? ' ' : '│ ');
|
|
1490
|
+
result += this.renderTree(child, childPrefix, isLastChild, false);
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
return result;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* Render logs as formatted string
|
|
1498
|
+
* @param node Starting node (defaults to root, includes descendants)
|
|
1499
|
+
*/
|
|
1500
|
+
toLogString(node?: WorkflowNode): string {
|
|
1501
|
+
const logs = this.collectLogs(node ?? this.root);
|
|
1502
|
+
|
|
1503
|
+
// Sort by timestamp
|
|
1504
|
+
logs.sort((a, b) => a.timestamp - b.timestamp);
|
|
1505
|
+
|
|
1506
|
+
return logs
|
|
1507
|
+
.map((log) => {
|
|
1508
|
+
const time = new Date(log.timestamp).toISOString();
|
|
1509
|
+
const level = log.level.toUpperCase().padEnd(5);
|
|
1510
|
+
const nodeRef = this.nodeMap.get(log.workflowId);
|
|
1511
|
+
const nodeName = nodeRef?.name ?? log.workflowId;
|
|
1512
|
+
return `[${time}] ${level} [${nodeName}] ${log.message}`;
|
|
1513
|
+
})
|
|
1514
|
+
.join('\n');
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* Collect all logs from a node and its descendants
|
|
1519
|
+
*/
|
|
1520
|
+
private collectLogs(node: WorkflowNode): LogEntry[] {
|
|
1521
|
+
const logs: LogEntry[] = [...node.logs];
|
|
1522
|
+
|
|
1523
|
+
for (const child of node.children) {
|
|
1524
|
+
logs.push(...this.collectLogs(child));
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
return logs;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* Get summary statistics for the tree
|
|
1532
|
+
*/
|
|
1533
|
+
getStats(): {
|
|
1534
|
+
totalNodes: number;
|
|
1535
|
+
byStatus: Record<string, number>;
|
|
1536
|
+
totalLogs: number;
|
|
1537
|
+
totalEvents: number;
|
|
1538
|
+
} {
|
|
1539
|
+
const stats = {
|
|
1540
|
+
totalNodes: 0,
|
|
1541
|
+
byStatus: {} as Record<string, number>,
|
|
1542
|
+
totalLogs: 0,
|
|
1543
|
+
totalEvents: 0,
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
this.collectStats(this.root, stats);
|
|
1547
|
+
return stats;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
private collectStats(
|
|
1551
|
+
node: WorkflowNode,
|
|
1552
|
+
stats: ReturnType<typeof this.getStats>
|
|
1553
|
+
): void {
|
|
1554
|
+
stats.totalNodes++;
|
|
1555
|
+
stats.byStatus[node.status] = (stats.byStatus[node.status] || 0) + 1;
|
|
1556
|
+
stats.totalLogs += node.logs.length;
|
|
1557
|
+
stats.totalEvents += node.events.length;
|
|
1558
|
+
|
|
1559
|
+
for (const child of node.children) {
|
|
1560
|
+
this.collectStats(child, stats);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
2. Create `./src/debugger/index.ts`:
|
|
1567
|
+
```typescript
|
|
1568
|
+
export { WorkflowTreeDebugger } from './tree-debugger.js';
|
|
1569
|
+
```
|
|
1570
|
+
|
|
1571
|
+
**Output**: `src/debugger/tree-debugger.ts` and `src/debugger/index.ts`
|
|
1572
|
+
|
|
1573
|
+
**Validation**: TypeScript compiles without errors
|
|
1574
|
+
|
|
1575
|
+
---
|
|
1576
|
+
|
|
1577
|
+
### Task 12: Example Workflows
|
|
1578
|
+
**Depends on**: Tasks 9, 10
|
|
1579
|
+
|
|
1580
|
+
**Input**: All core components
|
|
1581
|
+
|
|
1582
|
+
**Steps**:
|
|
1583
|
+
|
|
1584
|
+
1. Create `./src/examples/test-cycle-workflow.ts`:
|
|
1585
|
+
```typescript
|
|
1586
|
+
import { Workflow } from '../core/workflow.js';
|
|
1587
|
+
import { Step } from '../decorators/step.js';
|
|
1588
|
+
import { ObservedState } from '../decorators/observed-state.js';
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* Example child workflow demonstrating test cycle
|
|
1592
|
+
*/
|
|
1593
|
+
export class TestCycleWorkflow extends Workflow {
|
|
1594
|
+
@ObservedState()
|
|
1595
|
+
currentTest: string = '';
|
|
1596
|
+
|
|
1597
|
+
@ObservedState()
|
|
1598
|
+
testResult: 'pending' | 'passed' | 'failed' = 'pending';
|
|
1599
|
+
|
|
1600
|
+
@Step({ snapshotState: true, trackTiming: true, logStart: true })
|
|
1601
|
+
async generateTest(): Promise<string> {
|
|
1602
|
+
this.logger.info('Generating test case');
|
|
1603
|
+
this.currentTest = `test_${Date.now()}`;
|
|
1604
|
+
// Simulate test generation
|
|
1605
|
+
await this.delay(100);
|
|
1606
|
+
return this.currentTest;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
@Step({ trackTiming: true })
|
|
1610
|
+
async runTest(): Promise<boolean> {
|
|
1611
|
+
this.logger.info(`Running test: ${this.currentTest}`);
|
|
1612
|
+
// Simulate test execution
|
|
1613
|
+
await this.delay(200);
|
|
1614
|
+
|
|
1615
|
+
// Randomly pass or fail for demonstration
|
|
1616
|
+
const passed = Math.random() > 0.3;
|
|
1617
|
+
this.testResult = passed ? 'passed' : 'failed';
|
|
1618
|
+
|
|
1619
|
+
if (!passed) {
|
|
1620
|
+
throw new Error(`Test ${this.currentTest} failed`);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
return true;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
@Step({ snapshotState: true })
|
|
1627
|
+
async updateImplementation(): Promise<void> {
|
|
1628
|
+
this.logger.info('Updating implementation based on test results');
|
|
1629
|
+
await this.delay(150);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
async run(): Promise<void> {
|
|
1633
|
+
this.setStatus('running');
|
|
1634
|
+
|
|
1635
|
+
try {
|
|
1636
|
+
await this.generateTest();
|
|
1637
|
+
await this.runTest();
|
|
1638
|
+
await this.updateImplementation();
|
|
1639
|
+
this.setStatus('completed');
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
this.setStatus('failed');
|
|
1642
|
+
throw error;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
private delay(ms: number): Promise<void> {
|
|
1647
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
2. Create `./src/examples/tdd-orchestrator.ts`:
|
|
1653
|
+
```typescript
|
|
1654
|
+
import { Workflow } from '../core/workflow.js';
|
|
1655
|
+
import { Step } from '../decorators/step.js';
|
|
1656
|
+
import { Task } from '../decorators/task.js';
|
|
1657
|
+
import { ObservedState } from '../decorators/observed-state.js';
|
|
1658
|
+
import { TestCycleWorkflow } from './test-cycle-workflow.js';
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* Example parent workflow demonstrating TDD orchestration
|
|
1662
|
+
*/
|
|
1663
|
+
export class TDDOrchestrator extends Workflow {
|
|
1664
|
+
@ObservedState()
|
|
1665
|
+
cycleCount: number = 0;
|
|
1666
|
+
|
|
1667
|
+
@ObservedState()
|
|
1668
|
+
maxCycles: number = 3;
|
|
1669
|
+
|
|
1670
|
+
@ObservedState({ redact: true })
|
|
1671
|
+
apiKey: string = 'secret-key';
|
|
1672
|
+
|
|
1673
|
+
@Step({ logStart: true, logFinish: true })
|
|
1674
|
+
async setupEnvironment(): Promise<void> {
|
|
1675
|
+
this.logger.info('Setting up TDD environment');
|
|
1676
|
+
// Simulate environment setup
|
|
1677
|
+
await this.delay(50);
|
|
1678
|
+
this.logger.debug('Environment ready');
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
@Task()
|
|
1682
|
+
async runCycle(): Promise<TestCycleWorkflow> {
|
|
1683
|
+
this.cycleCount++;
|
|
1684
|
+
this.logger.info(`Starting cycle ${this.cycleCount}/${this.maxCycles}`);
|
|
1685
|
+
return new TestCycleWorkflow(`Cycle-${this.cycleCount}`, this);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
async run(): Promise<void> {
|
|
1689
|
+
this.setStatus('running');
|
|
1690
|
+
this.logger.info('TDD Orchestrator starting');
|
|
1691
|
+
|
|
1692
|
+
try {
|
|
1693
|
+
await this.setupEnvironment();
|
|
1694
|
+
|
|
1695
|
+
while (this.cycleCount < this.maxCycles) {
|
|
1696
|
+
try {
|
|
1697
|
+
const cycle = await this.runCycle();
|
|
1698
|
+
await cycle.run();
|
|
1699
|
+
this.logger.info(`Cycle ${this.cycleCount} completed successfully`);
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
this.logger.warn(`Cycle ${this.cycleCount} failed, continuing...`);
|
|
1702
|
+
// In real implementation, analyze error and potentially restart
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
this.setStatus('completed');
|
|
1707
|
+
this.logger.info('TDD Orchestrator completed all cycles');
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
this.setStatus('failed');
|
|
1710
|
+
this.logger.error('TDD Orchestrator failed', { error });
|
|
1711
|
+
throw error;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
private delay(ms: number): Promise<void> {
|
|
1716
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
```
|
|
1720
|
+
|
|
1721
|
+
3. Create `./src/examples/index.ts`:
|
|
1722
|
+
```typescript
|
|
1723
|
+
export { TestCycleWorkflow } from './test-cycle-workflow.js';
|
|
1724
|
+
export { TDDOrchestrator } from './tdd-orchestrator.js';
|
|
1725
|
+
```
|
|
1726
|
+
|
|
1727
|
+
**Output**: Example workflows in `src/examples/`
|
|
1728
|
+
|
|
1729
|
+
**Validation**: Examples can be instantiated and run
|
|
1730
|
+
|
|
1731
|
+
---
|
|
1732
|
+
|
|
1733
|
+
### Task 13: Main Entry Point
|
|
1734
|
+
**Depends on**: All previous tasks
|
|
1735
|
+
|
|
1736
|
+
**Input**: All module exports
|
|
1737
|
+
|
|
1738
|
+
**Steps**:
|
|
1739
|
+
|
|
1740
|
+
1. Create `./src/index.ts`:
|
|
1741
|
+
```typescript
|
|
1742
|
+
// Types
|
|
1743
|
+
export type {
|
|
1744
|
+
WorkflowStatus,
|
|
1745
|
+
WorkflowNode,
|
|
1746
|
+
LogLevel,
|
|
1747
|
+
LogEntry,
|
|
1748
|
+
SerializedWorkflowState,
|
|
1749
|
+
StateFieldMetadata,
|
|
1750
|
+
WorkflowError,
|
|
1751
|
+
WorkflowEvent,
|
|
1752
|
+
WorkflowObserver,
|
|
1753
|
+
StepOptions,
|
|
1754
|
+
TaskOptions,
|
|
1755
|
+
ErrorMergeStrategy,
|
|
1756
|
+
} from './types/index.js';
|
|
1757
|
+
|
|
1758
|
+
// Core classes
|
|
1759
|
+
export { Workflow } from './core/workflow.js';
|
|
1760
|
+
export { WorkflowLogger } from './core/logger.js';
|
|
1761
|
+
|
|
1762
|
+
// Decorators
|
|
1763
|
+
export { Step } from './decorators/step.js';
|
|
1764
|
+
export { Task } from './decorators/task.js';
|
|
1765
|
+
export { ObservedState, getObservedState } from './decorators/observed-state.js';
|
|
1766
|
+
|
|
1767
|
+
// Debugger
|
|
1768
|
+
export { WorkflowTreeDebugger } from './debugger/tree-debugger.js';
|
|
1769
|
+
|
|
1770
|
+
// Utilities
|
|
1771
|
+
export { Observable } from './utils/observable.js';
|
|
1772
|
+
export type { Subscription, Observer } from './utils/observable.js';
|
|
1773
|
+
export { generateId } from './utils/id.js';
|
|
1774
|
+
|
|
1775
|
+
// Examples (for reference)
|
|
1776
|
+
export { TestCycleWorkflow } from './examples/test-cycle-workflow.js';
|
|
1777
|
+
export { TDDOrchestrator } from './examples/tdd-orchestrator.js';
|
|
1778
|
+
```
|
|
1779
|
+
|
|
1780
|
+
**Output**: `src/index.ts`
|
|
1781
|
+
|
|
1782
|
+
**Validation**: `npm run build` completes successfully
|
|
1783
|
+
|
|
1784
|
+
---
|
|
1785
|
+
|
|
1786
|
+
### Task 14: Unit Tests
|
|
1787
|
+
**Depends on**: Task 13
|
|
1788
|
+
|
|
1789
|
+
**Input**: Complete implementation
|
|
1790
|
+
|
|
1791
|
+
**Steps**:
|
|
1792
|
+
|
|
1793
|
+
1. Create `./src/__tests__/unit/workflow.test.ts`:
|
|
1794
|
+
```typescript
|
|
1795
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
1796
|
+
import { Workflow, WorkflowObserver, WorkflowNode, LogEntry, WorkflowEvent } from '../../index.js';
|
|
1797
|
+
|
|
1798
|
+
class SimpleWorkflow extends Workflow {
|
|
1799
|
+
async run(): Promise<string> {
|
|
1800
|
+
this.setStatus('running');
|
|
1801
|
+
this.logger.info('Running simple workflow');
|
|
1802
|
+
this.setStatus('completed');
|
|
1803
|
+
return 'done';
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
describe('Workflow', () => {
|
|
1808
|
+
it('should create with unique id', () => {
|
|
1809
|
+
const wf1 = new SimpleWorkflow();
|
|
1810
|
+
const wf2 = new SimpleWorkflow();
|
|
1811
|
+
expect(wf1.id).not.toBe(wf2.id);
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
it('should use class name as default name', () => {
|
|
1815
|
+
const wf = new SimpleWorkflow();
|
|
1816
|
+
expect(wf.getNode().name).toBe('SimpleWorkflow');
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
it('should use custom name when provided', () => {
|
|
1820
|
+
const wf = new SimpleWorkflow('CustomName');
|
|
1821
|
+
expect(wf.getNode().name).toBe('CustomName');
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
it('should start with idle status', () => {
|
|
1825
|
+
const wf = new SimpleWorkflow();
|
|
1826
|
+
expect(wf.status).toBe('idle');
|
|
1827
|
+
expect(wf.getNode().status).toBe('idle');
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
it('should attach child to parent', () => {
|
|
1831
|
+
const parent = new SimpleWorkflow('Parent');
|
|
1832
|
+
const child = new SimpleWorkflow('Child', parent);
|
|
1833
|
+
|
|
1834
|
+
expect(child.parent).toBe(parent);
|
|
1835
|
+
expect(parent.children).toContain(child);
|
|
1836
|
+
expect(parent.getNode().children).toContain(child.getNode());
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
it('should emit logs to observers', () => {
|
|
1840
|
+
const wf = new SimpleWorkflow();
|
|
1841
|
+
const logs: LogEntry[] = [];
|
|
1842
|
+
|
|
1843
|
+
const observer: WorkflowObserver = {
|
|
1844
|
+
onLog: (entry) => logs.push(entry),
|
|
1845
|
+
onEvent: () => {},
|
|
1846
|
+
onStateUpdated: () => {},
|
|
1847
|
+
onTreeChanged: () => {},
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
wf.addObserver(observer);
|
|
1851
|
+
wf.run();
|
|
1852
|
+
|
|
1853
|
+
expect(logs.length).toBeGreaterThan(0);
|
|
1854
|
+
expect(logs[0].message).toBe('Running simple workflow');
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
it('should emit childAttached event', () => {
|
|
1858
|
+
const parent = new SimpleWorkflow('Parent');
|
|
1859
|
+
const events: WorkflowEvent[] = [];
|
|
1860
|
+
|
|
1861
|
+
const observer: WorkflowObserver = {
|
|
1862
|
+
onLog: () => {},
|
|
1863
|
+
onEvent: (event) => events.push(event),
|
|
1864
|
+
onStateUpdated: () => {},
|
|
1865
|
+
onTreeChanged: () => {},
|
|
1866
|
+
};
|
|
1867
|
+
|
|
1868
|
+
parent.addObserver(observer);
|
|
1869
|
+
const child = new SimpleWorkflow('Child', parent);
|
|
1870
|
+
|
|
1871
|
+
const attachEvent = events.find((e) => e.type === 'childAttached');
|
|
1872
|
+
expect(attachEvent).toBeDefined();
|
|
1873
|
+
expect(attachEvent?.type === 'childAttached' && attachEvent.parentId).toBe(parent.id);
|
|
1874
|
+
});
|
|
1875
|
+
});
|
|
1876
|
+
```
|
|
1877
|
+
|
|
1878
|
+
2. Create `./src/__tests__/unit/decorators.test.ts`:
|
|
1879
|
+
```typescript
|
|
1880
|
+
import { describe, it, expect } from 'vitest';
|
|
1881
|
+
import { Workflow, Step, Task, ObservedState, getObservedState, WorkflowEvent, WorkflowObserver } from '../../index.js';
|
|
1882
|
+
|
|
1883
|
+
describe('@Step decorator', () => {
|
|
1884
|
+
class StepTestWorkflow extends Workflow {
|
|
1885
|
+
stepCalled = false;
|
|
1886
|
+
|
|
1887
|
+
@Step({ trackTiming: true })
|
|
1888
|
+
async myStep(): Promise<string> {
|
|
1889
|
+
this.stepCalled = true;
|
|
1890
|
+
return 'step result';
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
async run(): Promise<void> {
|
|
1894
|
+
await this.myStep();
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
it('should execute the original method', async () => {
|
|
1899
|
+
const wf = new StepTestWorkflow();
|
|
1900
|
+
await wf.run();
|
|
1901
|
+
expect(wf.stepCalled).toBe(true);
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
it('should emit stepStart and stepEnd events', async () => {
|
|
1905
|
+
const wf = new StepTestWorkflow();
|
|
1906
|
+
const events: WorkflowEvent[] = [];
|
|
1907
|
+
|
|
1908
|
+
wf.addObserver({
|
|
1909
|
+
onLog: () => {},
|
|
1910
|
+
onEvent: (e) => events.push(e),
|
|
1911
|
+
onStateUpdated: () => {},
|
|
1912
|
+
onTreeChanged: () => {},
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
await wf.run();
|
|
1916
|
+
|
|
1917
|
+
const startEvent = events.find((e) => e.type === 'stepStart');
|
|
1918
|
+
const endEvent = events.find((e) => e.type === 'stepEnd');
|
|
1919
|
+
|
|
1920
|
+
expect(startEvent).toBeDefined();
|
|
1921
|
+
expect(endEvent).toBeDefined();
|
|
1922
|
+
if (endEvent?.type === 'stepEnd') {
|
|
1923
|
+
expect(endEvent.duration).toBeGreaterThanOrEqual(0);
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
it('should wrap errors in WorkflowError', async () => {
|
|
1928
|
+
class FailingWorkflow extends Workflow {
|
|
1929
|
+
@Step()
|
|
1930
|
+
async failingStep(): Promise<void> {
|
|
1931
|
+
throw new Error('Step failed');
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
async run(): Promise<void> {
|
|
1935
|
+
await this.failingStep();
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
const wf = new FailingWorkflow();
|
|
1940
|
+
|
|
1941
|
+
await expect(wf.run()).rejects.toMatchObject({
|
|
1942
|
+
message: 'Step failed',
|
|
1943
|
+
workflowId: wf.id,
|
|
1944
|
+
});
|
|
1945
|
+
});
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
describe('@ObservedState decorator', () => {
|
|
1949
|
+
class StateTestWorkflow extends Workflow {
|
|
1950
|
+
@ObservedState()
|
|
1951
|
+
publicField: string = 'public';
|
|
1952
|
+
|
|
1953
|
+
@ObservedState({ redact: true })
|
|
1954
|
+
secretField: string = 'secret';
|
|
1955
|
+
|
|
1956
|
+
@ObservedState({ hidden: true })
|
|
1957
|
+
hiddenField: string = 'hidden';
|
|
1958
|
+
|
|
1959
|
+
async run(): Promise<void> {}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
it('should include public fields in snapshot', () => {
|
|
1963
|
+
const wf = new StateTestWorkflow();
|
|
1964
|
+
const state = getObservedState(wf);
|
|
1965
|
+
expect(state.publicField).toBe('public');
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
it('should redact secret fields', () => {
|
|
1969
|
+
const wf = new StateTestWorkflow();
|
|
1970
|
+
const state = getObservedState(wf);
|
|
1971
|
+
expect(state.secretField).toBe('***');
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
it('should exclude hidden fields', () => {
|
|
1975
|
+
const wf = new StateTestWorkflow();
|
|
1976
|
+
const state = getObservedState(wf);
|
|
1977
|
+
expect('hiddenField' in state).toBe(false);
|
|
1978
|
+
});
|
|
1979
|
+
});
|
|
1980
|
+
```
|
|
1981
|
+
|
|
1982
|
+
3. Create `./src/__tests__/unit/tree-debugger.test.ts`:
|
|
1983
|
+
```typescript
|
|
1984
|
+
import { describe, it, expect } from 'vitest';
|
|
1985
|
+
import { Workflow, WorkflowTreeDebugger } from '../../index.js';
|
|
1986
|
+
|
|
1987
|
+
class DebugTestWorkflow extends Workflow {
|
|
1988
|
+
async run(): Promise<void> {
|
|
1989
|
+
this.setStatus('completed');
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
describe('WorkflowTreeDebugger', () => {
|
|
1994
|
+
it('should render tree string', () => {
|
|
1995
|
+
const wf = new DebugTestWorkflow('Root');
|
|
1996
|
+
const debugger_ = new WorkflowTreeDebugger(wf);
|
|
1997
|
+
|
|
1998
|
+
const tree = debugger_.toTreeString();
|
|
1999
|
+
expect(tree).toContain('Root');
|
|
2000
|
+
expect(tree).toContain('[idle]');
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
it('should show child nodes in tree', () => {
|
|
2004
|
+
const parent = new DebugTestWorkflow('Parent');
|
|
2005
|
+
const child1 = new DebugTestWorkflow('Child1', parent);
|
|
2006
|
+
const child2 = new DebugTestWorkflow('Child2', parent);
|
|
2007
|
+
|
|
2008
|
+
const debugger_ = new WorkflowTreeDebugger(parent);
|
|
2009
|
+
const tree = debugger_.toTreeString();
|
|
2010
|
+
|
|
2011
|
+
expect(tree).toContain('Parent');
|
|
2012
|
+
expect(tree).toContain('Child1');
|
|
2013
|
+
expect(tree).toContain('Child2');
|
|
2014
|
+
expect(tree).toContain('├──');
|
|
2015
|
+
expect(tree).toContain('└──');
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
it('should find node by ID', () => {
|
|
2019
|
+
const parent = new DebugTestWorkflow('Parent');
|
|
2020
|
+
const child = new DebugTestWorkflow('Child', parent);
|
|
2021
|
+
|
|
2022
|
+
const debugger_ = new WorkflowTreeDebugger(parent);
|
|
2023
|
+
|
|
2024
|
+
expect(debugger_.getNode(parent.id)).toBe(parent.getNode());
|
|
2025
|
+
expect(debugger_.getNode(child.id)).toBe(child.getNode());
|
|
2026
|
+
expect(debugger_.getNode('nonexistent')).toBeUndefined();
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
it('should collect logs from all nodes', async () => {
|
|
2030
|
+
const parent = new DebugTestWorkflow('Parent');
|
|
2031
|
+
const child = new DebugTestWorkflow('Child', parent);
|
|
2032
|
+
|
|
2033
|
+
const debugger_ = new WorkflowTreeDebugger(parent);
|
|
2034
|
+
|
|
2035
|
+
// Add some logs manually
|
|
2036
|
+
parent.getNode().logs.push({
|
|
2037
|
+
id: '1',
|
|
2038
|
+
workflowId: parent.id,
|
|
2039
|
+
timestamp: Date.now(),
|
|
2040
|
+
level: 'info',
|
|
2041
|
+
message: 'Parent log',
|
|
2042
|
+
});
|
|
2043
|
+
|
|
2044
|
+
child.getNode().logs.push({
|
|
2045
|
+
id: '2',
|
|
2046
|
+
workflowId: child.id,
|
|
2047
|
+
timestamp: Date.now(),
|
|
2048
|
+
level: 'info',
|
|
2049
|
+
message: 'Child log',
|
|
2050
|
+
});
|
|
2051
|
+
|
|
2052
|
+
const logString = debugger_.toLogString();
|
|
2053
|
+
expect(logString).toContain('Parent log');
|
|
2054
|
+
expect(logString).toContain('Child log');
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
it('should return stats', () => {
|
|
2058
|
+
const parent = new DebugTestWorkflow('Parent');
|
|
2059
|
+
new DebugTestWorkflow('Child1', parent);
|
|
2060
|
+
new DebugTestWorkflow('Child2', parent);
|
|
2061
|
+
|
|
2062
|
+
const debugger_ = new WorkflowTreeDebugger(parent);
|
|
2063
|
+
const stats = debugger_.getStats();
|
|
2064
|
+
|
|
2065
|
+
expect(stats.totalNodes).toBe(3);
|
|
2066
|
+
expect(stats.byStatus.idle).toBe(3);
|
|
2067
|
+
});
|
|
2068
|
+
});
|
|
2069
|
+
```
|
|
2070
|
+
|
|
2071
|
+
4. Create `./vitest.config.ts`:
|
|
2072
|
+
```typescript
|
|
2073
|
+
import { defineConfig } from 'vitest/config';
|
|
2074
|
+
|
|
2075
|
+
export default defineConfig({
|
|
2076
|
+
test: {
|
|
2077
|
+
include: ['src/__tests__/**/*.test.ts'],
|
|
2078
|
+
globals: true,
|
|
2079
|
+
},
|
|
2080
|
+
});
|
|
2081
|
+
```
|
|
2082
|
+
|
|
2083
|
+
**Output**: Test files and vitest config
|
|
2084
|
+
|
|
2085
|
+
**Validation**: `npm test` passes all tests
|
|
2086
|
+
|
|
2087
|
+
---
|
|
2088
|
+
|
|
2089
|
+
### Task 15: Integration Test
|
|
2090
|
+
**Depends on**: Task 14
|
|
2091
|
+
|
|
2092
|
+
**Input**: Complete implementation with unit tests
|
|
2093
|
+
|
|
2094
|
+
**Steps**:
|
|
2095
|
+
|
|
2096
|
+
1. Create `./src/__tests__/integration/tree-mirroring.test.ts`:
|
|
2097
|
+
```typescript
|
|
2098
|
+
import { describe, it, expect } from 'vitest';
|
|
2099
|
+
import {
|
|
2100
|
+
TDDOrchestrator,
|
|
2101
|
+
WorkflowTreeDebugger,
|
|
2102
|
+
WorkflowEvent,
|
|
2103
|
+
WorkflowObserver,
|
|
2104
|
+
} from '../../index.js';
|
|
2105
|
+
|
|
2106
|
+
describe('Tree Mirroring Integration', () => {
|
|
2107
|
+
it('should create 1:1 tree mirror of workflow execution', async () => {
|
|
2108
|
+
const orchestrator = new TDDOrchestrator('TDDOrchestrator');
|
|
2109
|
+
orchestrator['maxCycles'] = 1; // Limit to one cycle for test
|
|
2110
|
+
|
|
2111
|
+
const debugger_ = new WorkflowTreeDebugger(orchestrator);
|
|
2112
|
+
const events: WorkflowEvent[] = [];
|
|
2113
|
+
|
|
2114
|
+
debugger_.events.subscribe({
|
|
2115
|
+
next: (event) => events.push(event),
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
try {
|
|
2119
|
+
await orchestrator.run();
|
|
2120
|
+
} catch {
|
|
2121
|
+
// May fail due to random test failures, that's ok
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// Verify tree structure
|
|
2125
|
+
const tree = debugger_.getTree();
|
|
2126
|
+
expect(tree.name).toBe('TDDOrchestrator');
|
|
2127
|
+
|
|
2128
|
+
// Should have at least one child (the cycle)
|
|
2129
|
+
expect(tree.children.length).toBeGreaterThanOrEqual(1);
|
|
2130
|
+
|
|
2131
|
+
// Child should be named Cycle-N
|
|
2132
|
+
const cycleChild = tree.children.find((c) => c.name.startsWith('Cycle-'));
|
|
2133
|
+
expect(cycleChild).toBeDefined();
|
|
2134
|
+
|
|
2135
|
+
// Verify events were captured
|
|
2136
|
+
expect(events.some((e) => e.type === 'stepStart')).toBe(true);
|
|
2137
|
+
expect(events.some((e) => e.type === 'taskStart')).toBe(true);
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
it('should propagate events to root observer', async () => {
|
|
2141
|
+
const orchestrator = new TDDOrchestrator('Root');
|
|
2142
|
+
orchestrator['maxCycles'] = 1;
|
|
2143
|
+
|
|
2144
|
+
const allEvents: WorkflowEvent[] = [];
|
|
2145
|
+
const allLogs: any[] = [];
|
|
2146
|
+
|
|
2147
|
+
const observer: WorkflowObserver = {
|
|
2148
|
+
onLog: (entry) => allLogs.push(entry),
|
|
2149
|
+
onEvent: (event) => allEvents.push(event),
|
|
2150
|
+
onStateUpdated: () => {},
|
|
2151
|
+
onTreeChanged: () => {},
|
|
2152
|
+
};
|
|
2153
|
+
|
|
2154
|
+
orchestrator.addObserver(observer);
|
|
2155
|
+
|
|
2156
|
+
try {
|
|
2157
|
+
await orchestrator.run();
|
|
2158
|
+
} catch {
|
|
2159
|
+
// May fail
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// Events from child workflows should reach root
|
|
2163
|
+
expect(allLogs.length).toBeGreaterThan(0);
|
|
2164
|
+
expect(allEvents.length).toBeGreaterThan(0);
|
|
2165
|
+
|
|
2166
|
+
// Should have events from both parent and child
|
|
2167
|
+
const parentEvents = allEvents.filter(
|
|
2168
|
+
(e) => 'node' in e && e.node.name === 'Root'
|
|
2169
|
+
);
|
|
2170
|
+
const childEvents = allEvents.filter(
|
|
2171
|
+
(e) => 'node' in e && e.node.name.startsWith('Cycle-')
|
|
2172
|
+
);
|
|
2173
|
+
|
|
2174
|
+
expect(parentEvents.length).toBeGreaterThan(0);
|
|
2175
|
+
expect(childEvents.length).toBeGreaterThan(0);
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
it('should include state snapshot on error', async () => {
|
|
2179
|
+
const orchestrator = new TDDOrchestrator('ErrorTest');
|
|
2180
|
+
orchestrator['maxCycles'] = 1;
|
|
2181
|
+
|
|
2182
|
+
const errorEvents: WorkflowEvent[] = [];
|
|
2183
|
+
|
|
2184
|
+
orchestrator.addObserver({
|
|
2185
|
+
onLog: () => {},
|
|
2186
|
+
onEvent: (event) => {
|
|
2187
|
+
if (event.type === 'error') {
|
|
2188
|
+
errorEvents.push(event);
|
|
2189
|
+
}
|
|
2190
|
+
},
|
|
2191
|
+
onStateUpdated: () => {},
|
|
2192
|
+
onTreeChanged: () => {},
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
try {
|
|
2196
|
+
await orchestrator.run();
|
|
2197
|
+
} catch {
|
|
2198
|
+
// Expected
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// If there was an error, it should have state
|
|
2202
|
+
if (errorEvents.length > 0) {
|
|
2203
|
+
const errEvent = errorEvents[0];
|
|
2204
|
+
if (errEvent.type === 'error') {
|
|
2205
|
+
expect(errEvent.error.state).toBeDefined();
|
|
2206
|
+
expect(errEvent.error.logs).toBeDefined();
|
|
2207
|
+
expect(errEvent.error.workflowId).toBeDefined();
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
});
|
|
2211
|
+
});
|
|
2212
|
+
```
|
|
2213
|
+
|
|
2214
|
+
**Output**: Integration test file
|
|
2215
|
+
|
|
2216
|
+
**Validation**: `npm test` passes all integration tests
|
|
2217
|
+
|
|
2218
|
+
---
|
|
2219
|
+
|
|
2220
|
+
## 4. Implementation Details
|
|
2221
|
+
|
|
2222
|
+
### Code Patterns to Follow
|
|
2223
|
+
|
|
2224
|
+
**Decorator Pattern (Modern TC39 Stage 3)**:
|
|
2225
|
+
```typescript
|
|
2226
|
+
// Method decorator with proper this binding
|
|
2227
|
+
function MyDecorator(options: Options) {
|
|
2228
|
+
return function <This, Args extends unknown[], Return>(
|
|
2229
|
+
originalMethod: (this: This, ...args: Args) => Return,
|
|
2230
|
+
context: ClassMethodDecoratorContext<This>
|
|
2231
|
+
) {
|
|
2232
|
+
// CRITICAL: Regular function, NOT arrow function
|
|
2233
|
+
function wrapper(this: This, ...args: Args): Return {
|
|
2234
|
+
// Use originalMethod.call(this, ...args) to preserve context
|
|
2235
|
+
return originalMethod.call(this, ...args);
|
|
2236
|
+
}
|
|
2237
|
+
return wrapper;
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
```
|
|
2241
|
+
|
|
2242
|
+
**Observer Pattern**:
|
|
2243
|
+
```typescript
|
|
2244
|
+
// Always traverse to root for observers
|
|
2245
|
+
private getRootObservers(): WorkflowObserver[] {
|
|
2246
|
+
if (this.parent) {
|
|
2247
|
+
return this.parent.getRootObservers();
|
|
2248
|
+
}
|
|
2249
|
+
return this.observers;
|
|
2250
|
+
}
|
|
2251
|
+
```
|
|
2252
|
+
|
|
2253
|
+
### File Structure
|
|
2254
|
+
```
|
|
2255
|
+
src/
|
|
2256
|
+
├── types/
|
|
2257
|
+
│ ├── workflow.ts # WorkflowNode, WorkflowStatus
|
|
2258
|
+
│ ├── logging.ts # LogEntry, LogLevel
|
|
2259
|
+
│ ├── snapshot.ts # SerializedWorkflowState, StateFieldMetadata
|
|
2260
|
+
│ ├── error.ts # WorkflowError
|
|
2261
|
+
│ ├── events.ts # WorkflowEvent union
|
|
2262
|
+
│ ├── observer.ts # WorkflowObserver interface
|
|
2263
|
+
│ ├── decorators.ts # StepOptions, TaskOptions
|
|
2264
|
+
│ ├── error-strategy.ts # ErrorMergeStrategy
|
|
2265
|
+
│ └── index.ts # Barrel export
|
|
2266
|
+
├── core/
|
|
2267
|
+
│ ├── logger.ts # WorkflowLogger class
|
|
2268
|
+
│ ├── workflow.ts # Workflow abstract base class
|
|
2269
|
+
│ └── index.ts
|
|
2270
|
+
├── decorators/
|
|
2271
|
+
│ ├── observed-state.ts # @ObservedState decorator
|
|
2272
|
+
│ ├── step.ts # @Step decorator
|
|
2273
|
+
│ ├── task.ts # @Task decorator
|
|
2274
|
+
│ └── index.ts
|
|
2275
|
+
├── debugger/
|
|
2276
|
+
│ ├── tree-debugger.ts # WorkflowTreeDebugger class
|
|
2277
|
+
│ └── index.ts
|
|
2278
|
+
├── utils/
|
|
2279
|
+
│ ├── id.ts # generateId function
|
|
2280
|
+
│ ├── observable.ts # Observable class
|
|
2281
|
+
│ └── index.ts
|
|
2282
|
+
├── examples/
|
|
2283
|
+
│ ├── test-cycle-workflow.ts
|
|
2284
|
+
│ ├── tdd-orchestrator.ts
|
|
2285
|
+
│ └── index.ts
|
|
2286
|
+
├── __tests__/
|
|
2287
|
+
│ ├── unit/
|
|
2288
|
+
│ │ ├── workflow.test.ts
|
|
2289
|
+
│ │ ├── decorators.test.ts
|
|
2290
|
+
│ │ └── tree-debugger.test.ts
|
|
2291
|
+
│ └── integration/
|
|
2292
|
+
│ └── tree-mirroring.test.ts
|
|
2293
|
+
└── index.ts # Main entry point
|
|
2294
|
+
```
|
|
2295
|
+
|
|
2296
|
+
---
|
|
2297
|
+
|
|
2298
|
+
## 5. Testing Strategy
|
|
2299
|
+
|
|
2300
|
+
### Unit Tests
|
|
2301
|
+
```yaml
|
|
2302
|
+
test_files:
|
|
2303
|
+
- path: "src/__tests__/unit/workflow.test.ts"
|
|
2304
|
+
covers:
|
|
2305
|
+
- Workflow instantiation
|
|
2306
|
+
- Parent-child relationships
|
|
2307
|
+
- Status management
|
|
2308
|
+
- Observer registration
|
|
2309
|
+
- Event emission
|
|
2310
|
+
|
|
2311
|
+
- path: "src/__tests__/unit/decorators.test.ts"
|
|
2312
|
+
covers:
|
|
2313
|
+
- "@Step event emission"
|
|
2314
|
+
- "@Step error wrapping"
|
|
2315
|
+
- "@ObservedState snapshot inclusion"
|
|
2316
|
+
- "@ObservedState redaction"
|
|
2317
|
+
- "@ObservedState hidden fields"
|
|
2318
|
+
|
|
2319
|
+
- path: "src/__tests__/unit/tree-debugger.test.ts"
|
|
2320
|
+
covers:
|
|
2321
|
+
- Tree string rendering
|
|
2322
|
+
- Node lookup
|
|
2323
|
+
- Log collection
|
|
2324
|
+
- Statistics gathering
|
|
2325
|
+
|
|
2326
|
+
test_patterns:
|
|
2327
|
+
- "Use describe/it blocks from vitest"
|
|
2328
|
+
- "Test both success and failure paths"
|
|
2329
|
+
- "Verify event emission with mock observers"
|
|
2330
|
+
```
|
|
2331
|
+
|
|
2332
|
+
### Integration Tests
|
|
2333
|
+
```yaml
|
|
2334
|
+
scenarios:
|
|
2335
|
+
- name: "Tree Mirroring"
|
|
2336
|
+
validates: "1:1 tree mirror of workflow execution"
|
|
2337
|
+
|
|
2338
|
+
- name: "Event Propagation"
|
|
2339
|
+
validates: "Events from children reach root observers"
|
|
2340
|
+
|
|
2341
|
+
- name: "Error Context"
|
|
2342
|
+
validates: "Errors include state snapshots and logs"
|
|
2343
|
+
```
|
|
2344
|
+
|
|
2345
|
+
### Manual Validation
|
|
2346
|
+
```yaml
|
|
2347
|
+
steps:
|
|
2348
|
+
- action: "npm run build"
|
|
2349
|
+
expected: "Compiles without errors"
|
|
2350
|
+
|
|
2351
|
+
- action: "npm test"
|
|
2352
|
+
expected: "All tests pass"
|
|
2353
|
+
|
|
2354
|
+
- action: "Create test script that runs TDDOrchestrator"
|
|
2355
|
+
expected: "Console shows tree structure and logs"
|
|
2356
|
+
```
|
|
2357
|
+
|
|
2358
|
+
---
|
|
2359
|
+
|
|
2360
|
+
## 6. Final Validation Checklist
|
|
2361
|
+
|
|
2362
|
+
### Code Quality
|
|
2363
|
+
- [ ] All TypeScript compiles with `strict: true`
|
|
2364
|
+
- [ ] No linting warnings
|
|
2365
|
+
- [ ] Follows naming conventions (kebab-case files, PascalCase classes)
|
|
2366
|
+
- [ ] Proper error handling in all decorators
|
|
2367
|
+
|
|
2368
|
+
### Functionality
|
|
2369
|
+
- [ ] Workflow instances have unique IDs
|
|
2370
|
+
- [ ] Parent-child relationships work correctly
|
|
2371
|
+
- [ ] Events emit to root observers
|
|
2372
|
+
- [ ] @Step wraps errors in WorkflowError
|
|
2373
|
+
- [ ] @ObservedState respects hidden/redact options
|
|
2374
|
+
- [ ] @Task attaches child workflows
|
|
2375
|
+
- [ ] WorkflowTreeDebugger renders accurate tree
|
|
2376
|
+
|
|
2377
|
+
### Testing
|
|
2378
|
+
- [ ] Unit tests pass for all core components
|
|
2379
|
+
- [ ] Integration tests verify tree mirroring
|
|
2380
|
+
- [ ] Error scenarios are tested
|
|
2381
|
+
|
|
2382
|
+
### Documentation
|
|
2383
|
+
- [ ] All public APIs have JSDoc comments
|
|
2384
|
+
- [ ] Complex logic has inline comments
|
|
2385
|
+
- [ ] Example workflows demonstrate usage
|
|
2386
|
+
|
|
2387
|
+
---
|
|
2388
|
+
|
|
2389
|
+
## 7. "No Prior Knowledge" Test
|
|
2390
|
+
|
|
2391
|
+
**Validation**: If someone knew nothing about this codebase, would they have everything needed to implement this successfully using only this PRP?
|
|
2392
|
+
|
|
2393
|
+
- [x] All file paths are absolute and specific
|
|
2394
|
+
- [x] All patterns have concrete code examples
|
|
2395
|
+
- [x] All dependencies are explicitly listed (none required!)
|
|
2396
|
+
- [x] All validation steps are executable commands
|
|
2397
|
+
- [x] TypeScript configuration is complete
|
|
2398
|
+
- [x] Test patterns are specified
|
|
2399
|
+
- [x] Example workflows demonstrate all decorators
|
|
2400
|
+
|
|
2401
|
+
---
|
|
2402
|
+
|
|
2403
|
+
## Confidence Score: 9/10
|
|
2404
|
+
|
|
2405
|
+
**Rationale**:
|
|
2406
|
+
- Comprehensive research completed on decorators, observables, and tree visualization
|
|
2407
|
+
- PRD provides complete interfaces and class skeletons
|
|
2408
|
+
- All patterns are documented with working code examples
|
|
2409
|
+
- Zero runtime dependencies reduces complexity
|
|
2410
|
+
- Testing strategy is thorough
|
|
2411
|
+
|
|
2412
|
+
**Remaining uncertainties**:
|
|
2413
|
+
- Edge cases in concurrent @Task execution may need refinement
|
|
2414
|
+
- Real-world performance with deep workflow trees not validated
|
|
2415
|
+
- Additional error merge strategies could be added later
|
|
2416
|
+
|
|
2417
|
+
---
|
|
2418
|
+
|
|
2419
|
+
## Quick Start Commands
|
|
2420
|
+
|
|
2421
|
+
```bash
|
|
2422
|
+
# 1. Setup
|
|
2423
|
+
cd ./
|
|
2424
|
+
npm install
|
|
2425
|
+
|
|
2426
|
+
# 2. Build
|
|
2427
|
+
npm run build
|
|
2428
|
+
|
|
2429
|
+
# 3. Test
|
|
2430
|
+
npm test
|
|
2431
|
+
|
|
2432
|
+
# 4. Verify tree output (create this file to test)
|
|
2433
|
+
# Create src/demo.ts with:
|
|
2434
|
+
# import { TDDOrchestrator, WorkflowTreeDebugger } from './index.js';
|
|
2435
|
+
# const wf = new TDDOrchestrator();
|
|
2436
|
+
# const dbg = new WorkflowTreeDebugger(wf);
|
|
2437
|
+
# wf.run().then(() => console.log(dbg.toTreeString()));
|
|
2438
|
+
```
|