@zibby/core 0.1.0
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/LICENSE +21 -0
- package/README.md +147 -0
- package/package.json +94 -0
- package/src/agents/base.js +361 -0
- package/src/constants.js +47 -0
- package/src/enrichment/base.js +49 -0
- package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
- package/src/enrichment/enrichers/dom-enricher.js +171 -0
- package/src/enrichment/enrichers/page-state-enricher.js +129 -0
- package/src/enrichment/enrichers/position-enricher.js +67 -0
- package/src/enrichment/index.js +96 -0
- package/src/enrichment/mcp-integration.js +149 -0
- package/src/enrichment/mcp-ref-enricher.js +78 -0
- package/src/enrichment/pipeline.js +192 -0
- package/src/enrichment/trace-text-enricher.js +115 -0
- package/src/framework/AGENTS.md +98 -0
- package/src/framework/agents/base.js +72 -0
- package/src/framework/agents/claude-strategy.js +278 -0
- package/src/framework/agents/cursor-strategy.js +459 -0
- package/src/framework/agents/index.js +105 -0
- package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
- package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
- package/src/framework/code-generator.js +301 -0
- package/src/framework/constants.js +33 -0
- package/src/framework/context-loader.js +101 -0
- package/src/framework/function-bridge.js +78 -0
- package/src/framework/function-skill-registry.js +20 -0
- package/src/framework/graph-compiler.js +342 -0
- package/src/framework/graph.js +610 -0
- package/src/framework/index.js +28 -0
- package/src/framework/node-registry.js +163 -0
- package/src/framework/node.js +259 -0
- package/src/framework/output-parser.js +71 -0
- package/src/framework/skill-registry.js +55 -0
- package/src/framework/state-utils.js +52 -0
- package/src/framework/state.js +67 -0
- package/src/framework/tool-resolver.js +65 -0
- package/src/index.js +342 -0
- package/src/runtime/generation/base.js +46 -0
- package/src/runtime/generation/index.js +70 -0
- package/src/runtime/generation/mcp-ref-strategy.js +197 -0
- package/src/runtime/generation/stable-id-strategy.js +170 -0
- package/src/runtime/stable-id-runtime.js +248 -0
- package/src/runtime/verification/base.js +44 -0
- package/src/runtime/verification/index.js +67 -0
- package/src/runtime/verification/playwright-json-strategy.js +119 -0
- package/src/runtime/zibby-runtime.js +299 -0
- package/src/sync/index.js +2 -0
- package/src/sync/uploader.js +29 -0
- package/src/tools/run-playwright-test.js +158 -0
- package/src/utils/adf-converter.js +68 -0
- package/src/utils/ast-utils.js +37 -0
- package/src/utils/ci-setup.js +124 -0
- package/src/utils/cursor-utils.js +71 -0
- package/src/utils/logger.js +144 -0
- package/src/utils/mcp-config-writer.js +115 -0
- package/src/utils/node-schema-parser.js +522 -0
- package/src/utils/post-process-events.js +55 -0
- package/src/utils/result-handler.js +102 -0
- package/src/utils/ripple-effect.js +84 -0
- package/src/utils/selector-generator.js +239 -0
- package/src/utils/streaming-parser.js +387 -0
- package/src/utils/test-post-processor.js +211 -0
- package/src/utils/timeline.js +217 -0
- package/src/utils/trace-parser.js +325 -0
- package/src/utils/video-organizer.js +91 -0
- package/templates/browser-test-automation/README.md +114 -0
- package/templates/browser-test-automation/graph.js +54 -0
- package/templates/browser-test-automation/nodes/execute-live.js +250 -0
- package/templates/browser-test-automation/nodes/generate-script.js +77 -0
- package/templates/browser-test-automation/nodes/index.js +3 -0
- package/templates/browser-test-automation/nodes/preflight.js +59 -0
- package/templates/browser-test-automation/nodes/utils.js +154 -0
- package/templates/browser-test-automation/result-handler.js +286 -0
- package/templates/code-analysis/graph.js +72 -0
- package/templates/code-analysis/index.js +18 -0
- package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
- package/templates/code-analysis/nodes/create-pr-node.js +175 -0
- package/templates/code-analysis/nodes/finalize-node.js +118 -0
- package/templates/code-analysis/nodes/generate-code-node.js +425 -0
- package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
- package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
- package/templates/code-analysis/nodes/setup-node.js +142 -0
- package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
- package/templates/code-analysis/prompts/generate-code.md +33 -0
- package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
- package/templates/code-analysis/state.js +40 -0
- package/templates/code-implementation/graph.js +35 -0
- package/templates/code-implementation/index.js +7 -0
- package/templates/code-implementation/state.js +14 -0
- package/templates/global-setup.js +56 -0
- package/templates/index.js +94 -0
- package/templates/register-nodes.js +24 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Recorder Integration with Enrichment Pipeline
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities to enrich MCP-recorded events
|
|
5
|
+
* after test execution (when we have access to trace data)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createDefaultPipeline, createMinimalPipeline } from './index.js';
|
|
9
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Enrich recorded events.json file with data from Playwright trace
|
|
14
|
+
*
|
|
15
|
+
* @param {string} sessionPath - Path to session directory
|
|
16
|
+
* @param {Object} config - Enrichment configuration
|
|
17
|
+
* @returns {Promise<Object>} - { enrichedCount, skippedCount, errors }
|
|
18
|
+
*/
|
|
19
|
+
export async function enrichRecordedEvents(sessionPath, _config = {}) {
|
|
20
|
+
const eventsPath = join(sessionPath, 'events.json');
|
|
21
|
+
const _tracePath = join(sessionPath, 'trace.zip');
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Read events
|
|
25
|
+
const events = JSON.parse(readFileSync(eventsPath, 'utf-8'));
|
|
26
|
+
|
|
27
|
+
// For now, we add placeholder enrichment metadata
|
|
28
|
+
// Full enrichment requires re-playing with Playwright access
|
|
29
|
+
const enriched = events.map(event => ({
|
|
30
|
+
...event,
|
|
31
|
+
_enrichmentNote: 'Full enrichment requires live Playwright access. Use EnrichmentPipeline during test execution.'
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
// Save enriched events
|
|
35
|
+
const backupPath = `${eventsPath }.backup`;
|
|
36
|
+
writeFileSync(backupPath, JSON.stringify(events, null, 2));
|
|
37
|
+
writeFileSync(eventsPath, JSON.stringify(enriched, null, 2));
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
enrichedCount: enriched.length,
|
|
41
|
+
skippedCount: 0,
|
|
42
|
+
errors: []
|
|
43
|
+
};
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('[EnrichmentIntegration] Failed to enrich events:', error.message);
|
|
46
|
+
return {
|
|
47
|
+
enrichedCount: 0,
|
|
48
|
+
skippedCount: 0,
|
|
49
|
+
errors: [error.message]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Example: How to use enrichment pipeline during live test execution
|
|
56
|
+
*
|
|
57
|
+
* This is a reference implementation for integrating enrichment
|
|
58
|
+
* into your test execution flow where you have Playwright access.
|
|
59
|
+
*/
|
|
60
|
+
export class LiveEnrichmentRecorder {
|
|
61
|
+
constructor(config = {}) {
|
|
62
|
+
// Create enrichment pipeline
|
|
63
|
+
this.pipeline = config.minimal
|
|
64
|
+
? createMinimalPipeline(config)
|
|
65
|
+
: createDefaultPipeline(config);
|
|
66
|
+
|
|
67
|
+
this.events = [];
|
|
68
|
+
this.config = config;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Record and enrich an event during test execution
|
|
73
|
+
*
|
|
74
|
+
* @param {string} type - Event type (click, fill, etc)
|
|
75
|
+
* @param {Object} data - Event data
|
|
76
|
+
* @param {Object} context - { page, element, ref }
|
|
77
|
+
* @returns {Promise<Object>} - Enriched event
|
|
78
|
+
*/
|
|
79
|
+
async recordEvent(type, data, context) {
|
|
80
|
+
// Create base event (MCP format)
|
|
81
|
+
const baseEvent = {
|
|
82
|
+
id: this.events.length,
|
|
83
|
+
type,
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
data
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Enrich with pipeline
|
|
89
|
+
const enriched = await this.pipeline.enrich(baseEvent, {
|
|
90
|
+
...context,
|
|
91
|
+
event: baseEvent
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.events.push(enriched);
|
|
95
|
+
return enriched;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Save enriched events to file
|
|
100
|
+
* @param {string} filePath
|
|
101
|
+
*/
|
|
102
|
+
saveEvents(filePath) {
|
|
103
|
+
writeFileSync(filePath, JSON.stringify(this.events, null, 2));
|
|
104
|
+
console.log(`[LiveEnrichment] Saved ${this.events.length} enriched events to ${filePath}`);
|
|
105
|
+
|
|
106
|
+
// Log statistics
|
|
107
|
+
this.pipeline.logStatus();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get pipeline statistics
|
|
112
|
+
*/
|
|
113
|
+
getStats() {
|
|
114
|
+
return this.pipeline.getStats();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Example integration with Cursor Agent workflow
|
|
120
|
+
*
|
|
121
|
+
* Usage in execute_live node:
|
|
122
|
+
*
|
|
123
|
+
* ```javascript
|
|
124
|
+
* import { LiveEnrichmentRecorder } from '@zibby/core';
|
|
125
|
+
*
|
|
126
|
+
* const recorder = new LiveEnrichmentRecorder({ minimal: true });
|
|
127
|
+
*
|
|
128
|
+
* // During test execution (when you have page/element access):
|
|
129
|
+
* const page = await browser.newPage();
|
|
130
|
+
* const element = await page.locator('button');
|
|
131
|
+
*
|
|
132
|
+
* // Record enriched event
|
|
133
|
+
* await recorder.recordEvent('click', {
|
|
134
|
+
* tool: 'browser_click',
|
|
135
|
+
* params: { element: 'Submit button', ref: 'k123' }
|
|
136
|
+
* }, {
|
|
137
|
+
* page,
|
|
138
|
+
* element
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* // Save at end
|
|
142
|
+
* recorder.saveEvents('enriched-events.json');
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
export default {
|
|
147
|
+
enrichRecordedEvents,
|
|
148
|
+
LiveEnrichmentRecorder
|
|
149
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { EventEnricher } from './base.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCPRefEnricher - Captures ACTUAL element text from trace DOM snapshots
|
|
5
|
+
*
|
|
6
|
+
* Problem: AI might say "Login button" but actual button text is "登录" (Chinese)
|
|
7
|
+
* Solution: Extract the REAL text from trace DOM snapshot at action time
|
|
8
|
+
*
|
|
9
|
+
* Priority order:
|
|
10
|
+
* 1. Extract actual text/label from trace DOM snapshot (handles Chinese/any language)
|
|
11
|
+
* 2. Fall back to MCP element description (AI's description)
|
|
12
|
+
*
|
|
13
|
+
* This ensures Chinese, Arabic, Japanese, etc. apps work perfectly!
|
|
14
|
+
*/
|
|
15
|
+
export class MCPRefEnricher extends EventEnricher {
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
super(config);
|
|
18
|
+
this.priority = 200; // Highest priority
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getName() {
|
|
22
|
+
return 'MCPRef';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getPriority() {
|
|
26
|
+
return this.priority;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async enrich(event, context) {
|
|
30
|
+
const ref = event.data?.params?.ref;
|
|
31
|
+
const mcpElement = event.data?.params?.element; // What AI said
|
|
32
|
+
|
|
33
|
+
if (!ref && !mcpElement) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Try to extract ACTUAL element properties from the DOM (if we have element access)
|
|
38
|
+
let actualText = null;
|
|
39
|
+
let actualRole = null;
|
|
40
|
+
let actualLabel = null;
|
|
41
|
+
|
|
42
|
+
if (context?.element) {
|
|
43
|
+
try {
|
|
44
|
+
// Extract the REAL text that user sees (not what AI said)
|
|
45
|
+
const elementData = await context.element.evaluate(el => {
|
|
46
|
+
return {
|
|
47
|
+
text: el.textContent?.trim() || '',
|
|
48
|
+
innerText: el.innerText?.trim() || '',
|
|
49
|
+
value: el.value || '',
|
|
50
|
+
label: el.getAttribute('aria-label') || el.getAttribute('label') || '',
|
|
51
|
+
role: el.getAttribute('role') || el.tagName.toLowerCase(),
|
|
52
|
+
placeholder: el.getAttribute('placeholder') || '',
|
|
53
|
+
title: el.getAttribute('title') || ''
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Use the most specific text we can find
|
|
58
|
+
actualText = elementData.text || elementData.innerText || elementData.value || elementData.placeholder;
|
|
59
|
+
actualRole = elementData.role;
|
|
60
|
+
actualLabel = elementData.label || elementData.title;
|
|
61
|
+
|
|
62
|
+
console.log(`[MCPRefEnricher] ✅ Captured actual text: "${actualText}" (AI said: "${mcpElement}")`);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.log(`[MCPRefEnricher] ⚠️ Could not extract actual text: ${e.message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
mcpRef: ref,
|
|
70
|
+
mcpElement, // What AI said (might be translated)
|
|
71
|
+
actualText, // ✅ REAL text from DOM (Chinese/any language)
|
|
72
|
+
actualRole, // ✅ REAL role (button, link, textbox)
|
|
73
|
+
actualLabel, // ✅ REAL aria-label
|
|
74
|
+
// Use actual text if available, otherwise fall back to AI's description
|
|
75
|
+
recordedSelector: actualText || mcpElement
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enrichment Pipeline Manager
|
|
3
|
+
* Orchestrates multiple enrichers to add data to events
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class EnrichmentPipeline {
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.enrichers = [];
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.stats = {
|
|
11
|
+
totalEvents: 0,
|
|
12
|
+
enrichedEvents: 0,
|
|
13
|
+
skippedEvents: 0,
|
|
14
|
+
errors: {}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register an enricher
|
|
20
|
+
* @param {EventEnricher} enricher
|
|
21
|
+
*/
|
|
22
|
+
register(enricher) {
|
|
23
|
+
this.enrichers.push(enricher);
|
|
24
|
+
// Sort by priority (highest first)
|
|
25
|
+
this.enrichers.sort((a, b) => b.getPriority() - a.getPriority());
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Unregister an enricher by name
|
|
31
|
+
* @param {string} name
|
|
32
|
+
*/
|
|
33
|
+
unregister(name) {
|
|
34
|
+
this.enrichers = this.enrichers.filter(e => e.getName() !== name);
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get enricher by name
|
|
40
|
+
* @param {string} name
|
|
41
|
+
*/
|
|
42
|
+
get(name) {
|
|
43
|
+
return this.enrichers.find(e => e.getName() === name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Enable/disable specific enricher
|
|
48
|
+
* @param {string} name
|
|
49
|
+
* @param {boolean} enabled
|
|
50
|
+
*/
|
|
51
|
+
setEnabled(name, enabled) {
|
|
52
|
+
const enricher = this.get(name);
|
|
53
|
+
if (enricher) {
|
|
54
|
+
enricher.enabled = enabled;
|
|
55
|
+
}
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Enrich an event through the pipeline
|
|
61
|
+
* @param {Object} event - MCP event
|
|
62
|
+
* @param {Object} context - { page, element, ref, session }
|
|
63
|
+
* @returns {Promise<Object>} - Enriched event
|
|
64
|
+
*/
|
|
65
|
+
async enrich(event, context) {
|
|
66
|
+
this.stats.totalEvents++;
|
|
67
|
+
|
|
68
|
+
// Start with original event
|
|
69
|
+
const enriched = { ...event };
|
|
70
|
+
|
|
71
|
+
// Track which enrichers ran
|
|
72
|
+
const enrichersRun = [];
|
|
73
|
+
const enrichersSkipped = [];
|
|
74
|
+
const enrichersFailed = [];
|
|
75
|
+
|
|
76
|
+
// Run each enricher in priority order
|
|
77
|
+
for (const enricher of this.enrichers) {
|
|
78
|
+
try {
|
|
79
|
+
// Check if enricher can run
|
|
80
|
+
if (!enricher.canEnrich(context)) {
|
|
81
|
+
enrichersSkipped.push(enricher.getName());
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Run enricher
|
|
86
|
+
const startTime = Date.now();
|
|
87
|
+
const data = await enricher.enrich(event, context);
|
|
88
|
+
const duration = Date.now() - startTime;
|
|
89
|
+
|
|
90
|
+
if (data) {
|
|
91
|
+
// Merge enriched data
|
|
92
|
+
Object.assign(enriched, data);
|
|
93
|
+
enrichersRun.push({ name: enricher.getName(), duration });
|
|
94
|
+
} else {
|
|
95
|
+
enrichersSkipped.push(enricher.getName());
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.warn(`[EnrichmentPipeline] ${enricher.getName()} failed:`, error.message);
|
|
99
|
+
enrichersFailed.push(enricher.getName());
|
|
100
|
+
|
|
101
|
+
// Track errors
|
|
102
|
+
this.stats.errors[enricher.getName()] = (this.stats.errors[enricher.getName()] || 0) + 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Add enrichment metadata
|
|
107
|
+
enriched._enrichment = {
|
|
108
|
+
version: '1.0',
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
enrichers: {
|
|
111
|
+
run: enrichersRun,
|
|
112
|
+
skipped: enrichersSkipped,
|
|
113
|
+
failed: enrichersFailed
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Update stats
|
|
118
|
+
if (enrichersRun.length > 0) {
|
|
119
|
+
this.stats.enrichedEvents++;
|
|
120
|
+
} else {
|
|
121
|
+
this.stats.skippedEvents++;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return enriched;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Enrich multiple events in batch
|
|
129
|
+
* @param {Array} events
|
|
130
|
+
* @param {Object} context
|
|
131
|
+
* @returns {Promise<Array>}
|
|
132
|
+
*/
|
|
133
|
+
async enrichBatch(events, context) {
|
|
134
|
+
const results = [];
|
|
135
|
+
|
|
136
|
+
for (const event of events) {
|
|
137
|
+
const enriched = await this.enrich(event, context);
|
|
138
|
+
results.push(enriched);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return results;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get pipeline statistics
|
|
146
|
+
*/
|
|
147
|
+
getStats() {
|
|
148
|
+
return {
|
|
149
|
+
...this.stats,
|
|
150
|
+
enrichers: this.enrichers.map(e => ({
|
|
151
|
+
name: e.getName(),
|
|
152
|
+
enabled: e.enabled,
|
|
153
|
+
priority: e.getPriority(),
|
|
154
|
+
errors: this.stats.errors[e.getName()] || 0
|
|
155
|
+
}))
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Reset statistics
|
|
161
|
+
*/
|
|
162
|
+
resetStats() {
|
|
163
|
+
this.stats = {
|
|
164
|
+
totalEvents: 0,
|
|
165
|
+
enrichedEvents: 0,
|
|
166
|
+
skippedEvents: 0,
|
|
167
|
+
errors: {}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Log pipeline status
|
|
173
|
+
*/
|
|
174
|
+
logStatus() {
|
|
175
|
+
console.log('\n📊 Enrichment Pipeline Status:');
|
|
176
|
+
console.log(` Total events: ${this.stats.totalEvents}`);
|
|
177
|
+
console.log(` Enriched: ${this.stats.enrichedEvents}`);
|
|
178
|
+
console.log(` Skipped: ${this.stats.skippedEvents}`);
|
|
179
|
+
console.log(`\n Registered enrichers (${this.enrichers.length}):`);
|
|
180
|
+
|
|
181
|
+
for (const enricher of this.enrichers) {
|
|
182
|
+
const status = enricher.enabled ? '✓' : '✗';
|
|
183
|
+
const errors = this.stats.errors[enricher.getName()] || 0;
|
|
184
|
+
const errorStr = errors > 0 ? ` (${errors} errors)` : '';
|
|
185
|
+
console.log(` ${status} ${enricher.getName()} (priority: ${enricher.getPriority()})${errorStr}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default EnrichmentPipeline;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { EventEnricher } from './base.js';
|
|
2
|
+
import { TraceParser } from '../utils/trace-parser.js';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* TraceTextEnricher - Extracts ACTUAL element text from Playwright trace
|
|
8
|
+
*
|
|
9
|
+
* Problem: AI might say "Login button" but actual DOM has "登录" (Chinese)
|
|
10
|
+
* Solution: Parse trace.zip to find the actual element text at action time
|
|
11
|
+
*
|
|
12
|
+
* This runs AFTER recording as a post-processing step.
|
|
13
|
+
*/
|
|
14
|
+
export class TraceTextEnricher extends EventEnricher {
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
super(config);
|
|
17
|
+
this.priority = 190; // Just below MCPRefEnricher
|
|
18
|
+
this.traceData = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getName() {
|
|
22
|
+
return 'TraceText';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getPriority() {
|
|
26
|
+
return this.priority;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load trace data once for all events
|
|
31
|
+
*/
|
|
32
|
+
async loadTrace(sessionPath) {
|
|
33
|
+
if (this.traceData) return;
|
|
34
|
+
|
|
35
|
+
const _tracePath = join(sessionPath, 'traces');
|
|
36
|
+
const traceZipPath = join(sessionPath, 'trace.zip');
|
|
37
|
+
|
|
38
|
+
if (existsSync(traceZipPath)) {
|
|
39
|
+
try {
|
|
40
|
+
this.traceData = await TraceParser.parseTraceZip(traceZipPath);
|
|
41
|
+
console.log(`[TraceTextEnricher] ✅ Loaded trace with ${this.traceData.length} actions`);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.log(`[TraceTextEnricher] ⚠️ Failed to parse trace: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async enrich(event, context) {
|
|
49
|
+
const ref = event.data?.params?.ref;
|
|
50
|
+
const eventId = event.id;
|
|
51
|
+
if (ref === undefined && eventId === undefined) return null;
|
|
52
|
+
|
|
53
|
+
// Load trace on first event
|
|
54
|
+
if (!this.traceData && context.sessionPath) {
|
|
55
|
+
await this.loadTrace(context.sessionPath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!this.traceData) return null;
|
|
59
|
+
|
|
60
|
+
// Match by sequence order: event.id maps to traceData[id]
|
|
61
|
+
// This is more reliable than matching by ref (which trace doesn't have)
|
|
62
|
+
const traceAction = this.traceData[eventId];
|
|
63
|
+
if (!traceAction) {
|
|
64
|
+
console.log(`[TraceTextEnricher] ⚠️ No trace action for event ${eventId}`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// NEW: Use the actualText extracted from accessibility tree
|
|
69
|
+
const actualText = traceAction.actualText || this._extractTextFromSelector(traceAction.selector);
|
|
70
|
+
const actualRole = traceAction.actualRole;
|
|
71
|
+
const actualAriaLabel = traceAction.actualAriaLabel;
|
|
72
|
+
|
|
73
|
+
if (actualText || actualRole || actualAriaLabel) {
|
|
74
|
+
console.log(`[TraceTextEnricher] ✅ Event ${eventId}: text="${actualText}", role="${actualRole}", label="${actualAriaLabel}"`);
|
|
75
|
+
return {
|
|
76
|
+
traceActualText: actualText,
|
|
77
|
+
traceActualRole: actualRole,
|
|
78
|
+
traceActualAriaLabel: actualAriaLabel,
|
|
79
|
+
traceSelector: traceAction.selector,
|
|
80
|
+
traceStrategies: traceAction.strategies // Include all strategies
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract actual text from Playwright's internal selector format
|
|
89
|
+
* Examples:
|
|
90
|
+
* - internal:label="登录" -> "登录"
|
|
91
|
+
* - getByText('Submit') -> "Submit"
|
|
92
|
+
* - getByRole('button', { name: '确定' }) -> "确定"
|
|
93
|
+
*/
|
|
94
|
+
_extractTextFromSelector(selector) {
|
|
95
|
+
if (!selector) return null;
|
|
96
|
+
|
|
97
|
+
// Try internal:label format
|
|
98
|
+
const labelMatch = selector.match(/internal:label="([^"]+)"/);
|
|
99
|
+
if (labelMatch) return labelMatch[1];
|
|
100
|
+
|
|
101
|
+
// Try internal:text format
|
|
102
|
+
const textMatch = selector.match(/internal:text="([^"]+)"/);
|
|
103
|
+
if (textMatch) return textMatch[1];
|
|
104
|
+
|
|
105
|
+
// Try getByText format
|
|
106
|
+
const getByTextMatch = selector.match(/getByText\(['"]([^'"]+)['"]\)/);
|
|
107
|
+
if (getByTextMatch) return getByTextMatch[1];
|
|
108
|
+
|
|
109
|
+
// Try getByRole with name format
|
|
110
|
+
const getByRoleMatch = selector.match(/name:\s*['"]([^'"]+)['"]/);
|
|
111
|
+
if (getByRoleMatch) return getByRoleMatch[1];
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Workflow Execution Architecture
|
|
2
|
+
|
|
3
|
+
## Single Source of Truth: Template Node Files
|
|
4
|
+
|
|
5
|
+
Workflows are **compiled from source code**, not loaded from JSON files.
|
|
6
|
+
|
|
7
|
+
The template node definitions in `packages/core/templates/` are the single source of truth.
|
|
8
|
+
There is no static JSON workflow file — any such file (e.g. `default-analysis-workflow.json`) is stale and unused.
|
|
9
|
+
|
|
10
|
+
## How It Works
|
|
11
|
+
|
|
12
|
+
### 1. Graph Builders Compile Templates into Workflows
|
|
13
|
+
|
|
14
|
+
Each workflow type has a graph builder that imports node definitions and wires them together:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
packages/core/templates/code-analysis/graph.js → buildAnalysisGraph()
|
|
18
|
+
packages/core/templates/code-implementation/graph.js → buildImplementationGraph()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
These builders import the actual node files:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
packages/core/templates/code-analysis/nodes/
|
|
25
|
+
├── setup-node.js
|
|
26
|
+
├── analyze-ticket-node.js
|
|
27
|
+
├── generate-code-node.js
|
|
28
|
+
├── generate-test-cases-node.js
|
|
29
|
+
└── finalize-node.js
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Each node file exports `{ name, outputSchema, execute }`. The `execute` function is the actual runtime code.
|
|
33
|
+
|
|
34
|
+
### 2. Backend Resolves Workflow at Execution Time
|
|
35
|
+
|
|
36
|
+
When a user triggers an analysis (`POST /projects/:projectId/tickets/:ticketKey/analyze`), the backend resolves the workflow in this order:
|
|
37
|
+
|
|
38
|
+
1. **WORKFLOWS_TABLE** (DynamoDB) — user-customized workflow saved via the UI workflow editor
|
|
39
|
+
2. **project.analysisGraphConfig** (PROJECTS_TABLE) — legacy per-project config
|
|
40
|
+
3. **null** — no saved workflow; the CLI will use the compiled default
|
|
41
|
+
|
|
42
|
+
The resolved config (or null) is uploaded to S3 as `context.json` and passed to the ECS container via `CONTEXT_PRESIGNED_URL`.
|
|
43
|
+
|
|
44
|
+
### 3. ECS Container Compiles and Runs the Graph
|
|
45
|
+
|
|
46
|
+
The CLI (`packages/cli/src/commands/analyze-graph.js`) picks the workflow source:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
if (options.workflow) → local file (--workflow flag, dev only)
|
|
50
|
+
else if (execCtx.graphConfig) → custom saved workflow from DynamoDB (serialized JSON)
|
|
51
|
+
else → getDefaultGraph('analysis') → buildAnalysisGraph() from templates
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When using the default path, `buildAnalysisGraph()` imports the node files directly — the `execute` functions run as compiled JavaScript, not as eval'd strings.
|
|
55
|
+
|
|
56
|
+
When using a saved custom workflow, `compileGraph()` deserializes the JSON and wraps `executeCode` strings via `new Function()`.
|
|
57
|
+
|
|
58
|
+
### 4. Key Implication: Saved Workflows Can Go Stale
|
|
59
|
+
|
|
60
|
+
If a user saves a workflow via the UI editor, the `executeCode` is serialized as a string snapshot into DynamoDB. Future code fixes to the template node files **do not** propagate to saved workflows.
|
|
61
|
+
|
|
62
|
+
**To pick up template fixes**: reset the workflow in the UI settings (Project → Settings → Workflow → Reset to Default). This deletes the DynamoDB record and forces the CLI to recompile from templates.
|
|
63
|
+
|
|
64
|
+
## State Access Pattern
|
|
65
|
+
|
|
66
|
+
The graph stores each node's output under `state[nodeName]`:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
// In graph.js after node execution:
|
|
70
|
+
state.update({ [currentNode]: result.output });
|
|
71
|
+
|
|
72
|
+
// So to access previous node results:
|
|
73
|
+
state.analyze_ticket?.analysis // NOT state.analysis
|
|
74
|
+
state.generate_code?.codeImplementation // NOT state.codeImplementation
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## invokeAgent Return Shape
|
|
78
|
+
|
|
79
|
+
When `invokeAgent` is called with a `schema` option, it returns `{ raw, structured }` (not the structured data directly):
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
const result = await invokeAgent(prompt, { state, model, schema: SomeSchema });
|
|
83
|
+
const output = result?.structured || result; // unwrap
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Without a schema, it returns a plain string.
|
|
87
|
+
|
|
88
|
+
## File-Based Structured Output (Cursor Strategy)
|
|
89
|
+
|
|
90
|
+
For Cursor agent, structured output uses a file-based approach:
|
|
91
|
+
|
|
92
|
+
1. Framework generates a unique file path: `.zibby/tmp/zibby-result-<timestamp>.json`
|
|
93
|
+
2. Instructions appended to prompt telling agent to write JSON to that file
|
|
94
|
+
3. After agent completes, framework reads the file and validates with Zod
|
|
95
|
+
4. If validation fails and `strictMode` is enabled, falls back to OpenAI proxy for schema enforcement
|
|
96
|
+
5. Result files are kept (not deleted) for debugging
|
|
97
|
+
|
|
98
|
+
The Claude strategy uses its SDK's native structured output — completely separate path.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for AI agent strategies.
|
|
3
|
+
* All provider implementations (Cursor, Claude, etc.) must extend this class.
|
|
4
|
+
*
|
|
5
|
+
* @abstract
|
|
6
|
+
*/
|
|
7
|
+
export class AgentStrategy {
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} name - Provider identifier (e.g. 'cursor', 'claude')
|
|
10
|
+
* @param {string} description - Human-readable description
|
|
11
|
+
* @param {number} [priority=0] - Selection priority (higher = preferred)
|
|
12
|
+
*/
|
|
13
|
+
constructor(name, description, priority = 0) {
|
|
14
|
+
this.name = name;
|
|
15
|
+
this.description = description;
|
|
16
|
+
this.priority = priority;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Execute a prompt against this agent.
|
|
21
|
+
*
|
|
22
|
+
* @abstract
|
|
23
|
+
* @param {string} prompt - The prompt text
|
|
24
|
+
* @param {AgentInvokeOptions} options
|
|
25
|
+
* @returns {Promise<string | AgentStructuredResult>}
|
|
26
|
+
* - Without schema: returns raw output string
|
|
27
|
+
* - With schema: returns { raw: string, structured: object }
|
|
28
|
+
* - On failure: throws Error
|
|
29
|
+
*
|
|
30
|
+
* @typedef {Object} AgentInvokeOptions
|
|
31
|
+
* @property {string} [model] - Model name or 'auto'
|
|
32
|
+
* @property {string} [workspace] - Working directory
|
|
33
|
+
* @property {object} [schema] - Zod schema for structured output
|
|
34
|
+
* @property {Array} [tools] - MCP tools available to the agent
|
|
35
|
+
* @property {Array} [images] - Image attachments (provider-specific)
|
|
36
|
+
* @property {string} [sessionPath] - Session artifact directory
|
|
37
|
+
* @property {number} [timeout] - Execution timeout in ms
|
|
38
|
+
* @property {object} [config] - Full workflow config
|
|
39
|
+
*
|
|
40
|
+
* @typedef {Object} AgentStructuredResult
|
|
41
|
+
* @property {string} raw - Raw agent output
|
|
42
|
+
* @property {object} structured - Parsed and validated output
|
|
43
|
+
*/
|
|
44
|
+
async invoke(prompt, _options = {}) {
|
|
45
|
+
throw new Error('AgentStrategy.invoke() must be implemented by subclass');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if this agent strategy is available in the current environment.
|
|
50
|
+
*
|
|
51
|
+
* @abstract
|
|
52
|
+
* @param {object} [context] - Environment context (unused by most providers)
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
canHandle(_context) {
|
|
56
|
+
throw new Error('AgentStrategy.canHandle() must be implemented by subclass');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getName() {
|
|
60
|
+
return this.name;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getDescription() {
|
|
64
|
+
return this.description;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getPriority() {
|
|
68
|
+
return this.priority;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default AgentStrategy;
|