@arclabs561/ai-visual-test 0.5.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/.secretsignore.example +20 -0
- package/CHANGELOG.md +360 -0
- package/CONTRIBUTING.md +63 -0
- package/DEPLOYMENT.md +80 -0
- package/LICENSE +22 -0
- package/README.md +142 -0
- package/SECURITY.md +108 -0
- package/api/health.js +34 -0
- package/api/validate.js +252 -0
- package/index.d.ts +1221 -0
- package/package.json +112 -0
- package/public/index.html +149 -0
- package/src/batch-optimizer.mjs +451 -0
- package/src/bias-detector.mjs +370 -0
- package/src/bias-mitigation.mjs +233 -0
- package/src/cache.mjs +433 -0
- package/src/config.mjs +268 -0
- package/src/constants.mjs +80 -0
- package/src/context-compressor.mjs +350 -0
- package/src/convenience.mjs +617 -0
- package/src/cost-tracker.mjs +257 -0
- package/src/cross-modal-consistency.mjs +170 -0
- package/src/data-extractor.mjs +232 -0
- package/src/dynamic-few-shot.mjs +140 -0
- package/src/dynamic-prompts.mjs +361 -0
- package/src/ensemble/index.mjs +53 -0
- package/src/ensemble-judge.mjs +366 -0
- package/src/error-handler.mjs +67 -0
- package/src/errors.mjs +167 -0
- package/src/experience-propagation.mjs +128 -0
- package/src/experience-tracer.mjs +487 -0
- package/src/explanation-manager.mjs +299 -0
- package/src/feedback-aggregator.mjs +248 -0
- package/src/game-goal-prompts.mjs +478 -0
- package/src/game-player.mjs +548 -0
- package/src/hallucination-detector.mjs +155 -0
- package/src/helpers/playwright.mjs +80 -0
- package/src/human-validation-manager.mjs +516 -0
- package/src/index.mjs +364 -0
- package/src/judge.mjs +929 -0
- package/src/latency-aware-batch-optimizer.mjs +192 -0
- package/src/load-env.mjs +159 -0
- package/src/logger.mjs +55 -0
- package/src/metrics.mjs +187 -0
- package/src/model-tier-selector.mjs +221 -0
- package/src/multi-modal/index.mjs +36 -0
- package/src/multi-modal-fusion.mjs +190 -0
- package/src/multi-modal.mjs +524 -0
- package/src/natural-language-specs.mjs +1071 -0
- package/src/pair-comparison.mjs +277 -0
- package/src/persona/index.mjs +42 -0
- package/src/persona-enhanced.mjs +200 -0
- package/src/persona-experience.mjs +572 -0
- package/src/position-counterbalance.mjs +140 -0
- package/src/prompt-composer.mjs +375 -0
- package/src/render-change-detector.mjs +583 -0
- package/src/research-enhanced-validation.mjs +436 -0
- package/src/retry.mjs +152 -0
- package/src/rubrics.mjs +231 -0
- package/src/score-tracker.mjs +277 -0
- package/src/smart-validator.mjs +447 -0
- package/src/spec-config.mjs +106 -0
- package/src/spec-templates.mjs +347 -0
- package/src/specs/index.mjs +38 -0
- package/src/temporal/index.mjs +102 -0
- package/src/temporal-adaptive.mjs +163 -0
- package/src/temporal-batch-optimizer.mjs +222 -0
- package/src/temporal-constants.mjs +69 -0
- package/src/temporal-context.mjs +49 -0
- package/src/temporal-decision-manager.mjs +271 -0
- package/src/temporal-decision.mjs +669 -0
- package/src/temporal-errors.mjs +58 -0
- package/src/temporal-note-pruner.mjs +173 -0
- package/src/temporal-preprocessor.mjs +543 -0
- package/src/temporal-prompt-formatter.mjs +219 -0
- package/src/temporal-validation.mjs +159 -0
- package/src/temporal.mjs +415 -0
- package/src/type-guards.mjs +311 -0
- package/src/uncertainty-reducer.mjs +470 -0
- package/src/utils/index.mjs +175 -0
- package/src/validation-framework.mjs +321 -0
- package/src/validation-result-normalizer.mjs +64 -0
- package/src/validation.mjs +243 -0
- package/src/validators/accessibility-programmatic.mjs +345 -0
- package/src/validators/accessibility-validator.mjs +223 -0
- package/src/validators/batch-validator.mjs +143 -0
- package/src/validators/hybrid-validator.mjs +268 -0
- package/src/validators/index.mjs +34 -0
- package/src/validators/prompt-builder.mjs +218 -0
- package/src/validators/rubric.mjs +85 -0
- package/src/validators/state-programmatic.mjs +260 -0
- package/src/validators/state-validator.mjs +291 -0
- package/vercel.json +27 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pair Comparison Evaluation
|
|
3
|
+
*
|
|
4
|
+
* Implements pairwise comparison evaluation method.
|
|
5
|
+
* Research shows Pair Comparison is more reliable than absolute scoring
|
|
6
|
+
* (MLLM-as-a-Judge, arXiv:2402.04788).
|
|
7
|
+
*
|
|
8
|
+
* Instead of scoring each screenshot independently, compares pairs
|
|
9
|
+
* to determine which is better, then derives relative scores.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { VLLMJudge } from './judge.mjs';
|
|
13
|
+
import { detectBias, detectPositionBias } from './bias-detector.mjs';
|
|
14
|
+
import { composeComparisonPrompt } from './prompt-composer.mjs';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compare two screenshots and determine which is better
|
|
18
|
+
*
|
|
19
|
+
* @param {string} imagePath1 - Path to first screenshot
|
|
20
|
+
* @param {string} imagePath2 - Path to second screenshot
|
|
21
|
+
* @param {string} prompt - Evaluation prompt describing what to compare
|
|
22
|
+
* @param {import('./index.mjs').ValidationContext} [context={}] - Validation context
|
|
23
|
+
* @returns {Promise<import('./index.mjs').PairComparisonResult>} Comparison result
|
|
24
|
+
*/
|
|
25
|
+
export async function comparePair(imagePath1, imagePath2, prompt, context = {}) {
|
|
26
|
+
const judge = new VLLMJudge(context);
|
|
27
|
+
|
|
28
|
+
if (!judge.enabled) {
|
|
29
|
+
return {
|
|
30
|
+
enabled: false,
|
|
31
|
+
winner: null,
|
|
32
|
+
confidence: null,
|
|
33
|
+
reasoning: 'VLLM validation is disabled',
|
|
34
|
+
comparison: null
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const comparisonPrompt = buildComparisonPrompt(prompt, context);
|
|
39
|
+
|
|
40
|
+
// Randomize order to reduce position bias
|
|
41
|
+
const [first, second, order] = Math.random() > 0.5
|
|
42
|
+
? [imagePath1, imagePath2, 'original']
|
|
43
|
+
: [imagePath2, imagePath1, 'reversed'];
|
|
44
|
+
|
|
45
|
+
const fullPrompt = `${comparisonPrompt}
|
|
46
|
+
|
|
47
|
+
You are comparing two screenshots. Screenshot A is shown first, then Screenshot B.
|
|
48
|
+
|
|
49
|
+
SCREENSHOT A:
|
|
50
|
+
[First screenshot will be provided]
|
|
51
|
+
|
|
52
|
+
SCREENSHOT B:
|
|
53
|
+
[Second screenshot will be provided]
|
|
54
|
+
|
|
55
|
+
Compare them and determine which is better based on the evaluation criteria. Return JSON:
|
|
56
|
+
{
|
|
57
|
+
"winner": "A" | "B" | "tie",
|
|
58
|
+
"confidence": 0.0-1.0,
|
|
59
|
+
"reasoning": "explanation of comparison",
|
|
60
|
+
"differences": ["key difference 1", "key difference 2"],
|
|
61
|
+
"scores": {
|
|
62
|
+
"A": 0-10,
|
|
63
|
+
"B": 0-10
|
|
64
|
+
}
|
|
65
|
+
}`;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// TRUE MULTI-IMAGE COMPARISON: Send both images in single API call
|
|
69
|
+
// This is the research-optimal approach (MLLM-as-a-Judge, arXiv:2402.04788)
|
|
70
|
+
const comparisonResult = await judge.judgeScreenshot([first, second], comparisonPrompt, {
|
|
71
|
+
...context,
|
|
72
|
+
comparisonContext: { position: 'both', total: 2 }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!comparisonResult.enabled || comparisonResult.error) {
|
|
76
|
+
return {
|
|
77
|
+
enabled: false,
|
|
78
|
+
winner: null,
|
|
79
|
+
confidence: null,
|
|
80
|
+
reasoning: comparisonResult.error || 'Comparison failed',
|
|
81
|
+
comparison: null,
|
|
82
|
+
error: comparisonResult.error || 'API disabled'
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Parse comparison result - expect JSON with winner, scores, reasoning
|
|
87
|
+
const judgment = comparisonResult.judgment || '';
|
|
88
|
+
let parsedComparison = null;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const jsonMatch = judgment.match(/\{[\s\S]*\}/);
|
|
92
|
+
if (jsonMatch) {
|
|
93
|
+
parsedComparison = JSON.parse(jsonMatch[0]);
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
// Fall through to score-based comparison
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If we got structured comparison, use it
|
|
100
|
+
if (parsedComparison && parsedComparison.winner) {
|
|
101
|
+
const winner = parsedComparison.winner.toLowerCase();
|
|
102
|
+
const scoreA = parsedComparison.scores?.A ?? parsedComparison.scores?.['A'] ?? null;
|
|
103
|
+
const scoreB = parsedComparison.scores?.B ?? parsedComparison.scores?.['B'] ?? null;
|
|
104
|
+
|
|
105
|
+
// Map winner back to original order
|
|
106
|
+
const mappedWinner = order === 'reversed'
|
|
107
|
+
? (winner === 'a' ? 'B' : winner === 'b' ? 'A' : 'tie')
|
|
108
|
+
: (winner === 'a' ? 'A' : winner === 'b' ? 'B' : 'tie');
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
enabled: true,
|
|
112
|
+
winner: mappedWinner,
|
|
113
|
+
confidence: parsedComparison.confidence ?? 0.5,
|
|
114
|
+
reasoning: parsedComparison.reasoning || comparisonResult.reasoning || 'Direct comparison completed',
|
|
115
|
+
differences: parsedComparison.differences || [],
|
|
116
|
+
comparison: {
|
|
117
|
+
score1: scoreA ?? (mappedWinner === 'A' ? 8 : mappedWinner === 'B' ? 6 : 7),
|
|
118
|
+
score2: scoreB ?? (mappedWinner === 'B' ? 8 : mappedWinner === 'A' ? 6 : 7),
|
|
119
|
+
difference: scoreA && scoreB ? Math.abs(scoreA - scoreB) : null,
|
|
120
|
+
order: order === 'reversed' ? 'reversed' : 'original',
|
|
121
|
+
method: 'multi-image'
|
|
122
|
+
},
|
|
123
|
+
biasDetection: {
|
|
124
|
+
positionBias: false, // Multi-image eliminates position bias
|
|
125
|
+
adjusted: false
|
|
126
|
+
},
|
|
127
|
+
metadata: {
|
|
128
|
+
provider: comparisonResult.provider,
|
|
129
|
+
cached: comparisonResult.cached || false,
|
|
130
|
+
responseTime: comparisonResult.responseTime || 0,
|
|
131
|
+
estimatedCost: comparisonResult.estimatedCost,
|
|
132
|
+
logprobs: comparisonResult.logprobs || null // Include if available
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fallback: If structured parse failed, treat as tie (multi-image should return structured JSON)
|
|
138
|
+
// This should rarely happen if prompt is clear
|
|
139
|
+
return {
|
|
140
|
+
enabled: true,
|
|
141
|
+
winner: 'tie',
|
|
142
|
+
confidence: 0.5,
|
|
143
|
+
reasoning: comparisonResult.reasoning || 'Comparison completed but could not parse structured result. Treating as tie.',
|
|
144
|
+
comparison: {
|
|
145
|
+
score1: comparisonResult.score ?? 7,
|
|
146
|
+
score2: comparisonResult.score ?? 7,
|
|
147
|
+
difference: 0,
|
|
148
|
+
order: order === 'reversed' ? 'reversed' : 'original',
|
|
149
|
+
method: 'multi-image-fallback'
|
|
150
|
+
},
|
|
151
|
+
biasDetection: {
|
|
152
|
+
positionBias: false,
|
|
153
|
+
adjusted: false
|
|
154
|
+
},
|
|
155
|
+
metadata: {
|
|
156
|
+
provider: comparisonResult.provider,
|
|
157
|
+
cached: comparisonResult.cached || false,
|
|
158
|
+
responseTime: comparisonResult.responseTime || 0,
|
|
159
|
+
estimatedCost: comparisonResult.estimatedCost,
|
|
160
|
+
logprobs: comparisonResult.logprobs || null,
|
|
161
|
+
warning: 'Structured comparison parse failed - using fallback'
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return {
|
|
166
|
+
enabled: false,
|
|
167
|
+
winner: null,
|
|
168
|
+
confidence: null,
|
|
169
|
+
reasoning: `Comparison failed: ${error.message}`,
|
|
170
|
+
comparison: null,
|
|
171
|
+
error: error.message
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Build comparison prompt from base prompt
|
|
178
|
+
*
|
|
179
|
+
* Now uses unified prompt composition system for research-backed consistency.
|
|
180
|
+
*/
|
|
181
|
+
function buildComparisonPrompt(basePrompt, context) {
|
|
182
|
+
try {
|
|
183
|
+
return composeComparisonPrompt(basePrompt, context, {
|
|
184
|
+
includeRubric: context.includeRubric !== false // Default true (research-backed)
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// Fallback to basic comparison prompt
|
|
188
|
+
return `Compare the two screenshots based on the following criteria:
|
|
189
|
+
|
|
190
|
+
${basePrompt}
|
|
191
|
+
|
|
192
|
+
Focus on:
|
|
193
|
+
- Which screenshot better meets the criteria?
|
|
194
|
+
- What are the key differences?
|
|
195
|
+
- Which has fewer issues?
|
|
196
|
+
- Which provides better user experience?
|
|
197
|
+
|
|
198
|
+
Be specific about what makes one better than the other.`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Rank multiple screenshots using pairwise comparisons
|
|
204
|
+
* Uses tournament-style ranking
|
|
205
|
+
*
|
|
206
|
+
* @param {Array<string>} imagePaths - Array of screenshot paths
|
|
207
|
+
* @param {string} prompt - Evaluation prompt
|
|
208
|
+
* @param {import('./index.mjs').ValidationContext} [context={}] - Validation context
|
|
209
|
+
* @returns {Promise<import('./index.mjs').BatchRankingResult>} Ranking result
|
|
210
|
+
*/
|
|
211
|
+
export async function rankBatch(imagePaths, prompt, context = {}) {
|
|
212
|
+
if (imagePaths.length < 2) {
|
|
213
|
+
return {
|
|
214
|
+
enabled: false,
|
|
215
|
+
rankings: [],
|
|
216
|
+
error: 'Need at least 2 screenshots for ranking'
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// For efficiency, compare each pair
|
|
221
|
+
// In practice, you might use a tournament bracket or sampling
|
|
222
|
+
const comparisons = [];
|
|
223
|
+
const scores = new Map();
|
|
224
|
+
|
|
225
|
+
// Compare all pairs
|
|
226
|
+
for (let i = 0; i < imagePaths.length; i++) {
|
|
227
|
+
for (let j = i + 1; j < imagePaths.length; j++) {
|
|
228
|
+
const comparison = await comparePair(
|
|
229
|
+
imagePaths[i],
|
|
230
|
+
imagePaths[j],
|
|
231
|
+
prompt,
|
|
232
|
+
context
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (comparison.enabled && comparison.winner !== 'tie') {
|
|
236
|
+
comparisons.push({
|
|
237
|
+
image1: i,
|
|
238
|
+
image2: j,
|
|
239
|
+
winner: comparison.winner === 'A' ? i : j,
|
|
240
|
+
confidence: comparison.confidence
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Update scores based on wins
|
|
244
|
+
const winnerIdx = comparison.winner === 'A' ? i : j;
|
|
245
|
+
const loserIdx = comparison.winner === 'A' ? j : i;
|
|
246
|
+
|
|
247
|
+
scores.set(winnerIdx, (scores.get(winnerIdx) || 0) + comparison.confidence);
|
|
248
|
+
scores.set(loserIdx, (scores.get(loserIdx) || 0) + (1 - comparison.confidence));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Rank by scores
|
|
254
|
+
const rankings = Array.from(scores.entries())
|
|
255
|
+
.map(([idx, score]) => ({
|
|
256
|
+
index: idx,
|
|
257
|
+
path: imagePaths[idx],
|
|
258
|
+
score,
|
|
259
|
+
wins: comparisons.filter(c => c.winner === idx).length
|
|
260
|
+
}))
|
|
261
|
+
.sort((a, b) => b.score - a.score)
|
|
262
|
+
.map((r, rank) => ({
|
|
263
|
+
...r,
|
|
264
|
+
rank: rank + 1
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
enabled: true,
|
|
269
|
+
rankings,
|
|
270
|
+
comparisons: comparisons.length,
|
|
271
|
+
metadata: {
|
|
272
|
+
totalScreenshots: imagePaths.length,
|
|
273
|
+
totalComparisons: comparisons.length
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persona Sub-Module
|
|
3
|
+
*
|
|
4
|
+
* Persona-based experience testing and evaluation.
|
|
5
|
+
*
|
|
6
|
+
* Import from 'ai-visual-test/persona'
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Core persona experience
|
|
10
|
+
export {
|
|
11
|
+
experiencePageAsPersona,
|
|
12
|
+
experiencePageWithPersonas
|
|
13
|
+
} from '../persona-experience.mjs';
|
|
14
|
+
|
|
15
|
+
// Enhanced persona
|
|
16
|
+
export {
|
|
17
|
+
createEnhancedPersona,
|
|
18
|
+
experiencePageWithEnhancedPersona,
|
|
19
|
+
calculatePersonaConsistency,
|
|
20
|
+
calculatePersonaDiversity
|
|
21
|
+
} from '../persona-enhanced.mjs';
|
|
22
|
+
|
|
23
|
+
// Experience tracing
|
|
24
|
+
export {
|
|
25
|
+
ExperienceTrace,
|
|
26
|
+
ExperienceTracerManager,
|
|
27
|
+
getTracerManager
|
|
28
|
+
} from '../experience-tracer.mjs';
|
|
29
|
+
|
|
30
|
+
// Experience propagation
|
|
31
|
+
export {
|
|
32
|
+
ExperiencePropagationTracker,
|
|
33
|
+
getPropagationTracker,
|
|
34
|
+
trackPropagation
|
|
35
|
+
} from '../experience-propagation.mjs';
|
|
36
|
+
|
|
37
|
+
// Explanation manager
|
|
38
|
+
export {
|
|
39
|
+
ExplanationManager,
|
|
40
|
+
getExplanationManager
|
|
41
|
+
} from '../explanation-manager.mjs';
|
|
42
|
+
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Persona Structure
|
|
3
|
+
*
|
|
4
|
+
* Adds rich context to personas based on research findings:
|
|
5
|
+
* - Workflows, frustrations, usage patterns
|
|
6
|
+
* - Temporal evolution tracking
|
|
7
|
+
* - Consistency metrics
|
|
8
|
+
*
|
|
9
|
+
* Research:
|
|
10
|
+
* - "Can LLM be a Personalized Judge?" - Persona-based LLM judging with uncertainty estimation
|
|
11
|
+
* - "The Prompt Makes the Person(a)" - Systematic evaluation of sociodemographic persona prompting
|
|
12
|
+
* - "Persona-judge: Personalized Alignment of Large Language Models" - Personalized alignment
|
|
13
|
+
* - "PERSONA: Evaluating Pluralistic Alignment in LLMs" - Pluralistic alignment with personas
|
|
14
|
+
*
|
|
15
|
+
* Note: Research shows direct persona-based judging has low reliability, but uncertainty
|
|
16
|
+
* estimation improves performance to >80% agreement on high-certainty samples. LLMs struggle
|
|
17
|
+
* to authentically simulate marginalized groups. Multi-agent debate can amplify bias.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { experiencePageAsPersona } from './persona-experience.mjs';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Enhanced persona structure with rich context
|
|
24
|
+
*
|
|
25
|
+
* @typedef {Object} EnhancedPersona
|
|
26
|
+
* @property {string} name - Persona name
|
|
27
|
+
* @property {string} device - Device type
|
|
28
|
+
* @property {string[]} goals - Primary goals
|
|
29
|
+
* @property {string[]} concerns - Primary concerns
|
|
30
|
+
* @property {Object} workflows - Documented workflows and use cases
|
|
31
|
+
* @property {string[]} frustrations - Specific frustrations
|
|
32
|
+
* @property {Object} usagePatterns - Historical usage patterns
|
|
33
|
+
* @property {Object} temporalEvolution - Temporal usage evolution
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create enhanced persona with rich context
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} basePersona - Base persona (name, device, goals, concerns)
|
|
40
|
+
* @param {{
|
|
41
|
+
* workflows?: Object;
|
|
42
|
+
* frustrations?: string[];
|
|
43
|
+
* usagePatterns?: Object;
|
|
44
|
+
* temporalEvolution?: Object;
|
|
45
|
+
* }} [context={}] - Rich context
|
|
46
|
+
* @returns {EnhancedPersona} Enhanced persona
|
|
47
|
+
*/
|
|
48
|
+
export function createEnhancedPersona(basePersona, context = {}) {
|
|
49
|
+
return {
|
|
50
|
+
...basePersona,
|
|
51
|
+
workflows: context.workflows || {
|
|
52
|
+
primary: [],
|
|
53
|
+
secondary: [],
|
|
54
|
+
edgeCases: []
|
|
55
|
+
},
|
|
56
|
+
frustrations: context.frustrations || [],
|
|
57
|
+
usagePatterns: context.usagePatterns || {
|
|
58
|
+
frequency: 'unknown',
|
|
59
|
+
duration: 'unknown',
|
|
60
|
+
peakTimes: []
|
|
61
|
+
},
|
|
62
|
+
temporalEvolution: context.temporalEvolution || {
|
|
63
|
+
firstUse: null,
|
|
64
|
+
lastUse: null,
|
|
65
|
+
usageTrend: 'stable'
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Calculate consistency metrics for persona observations
|
|
72
|
+
*
|
|
73
|
+
* @param {Array} observations - Array of observations from persona
|
|
74
|
+
* @returns {Object} Consistency metrics
|
|
75
|
+
*/
|
|
76
|
+
export function calculatePersonaConsistency(observations) {
|
|
77
|
+
if (observations.length < 2) {
|
|
78
|
+
return {
|
|
79
|
+
promptToLine: 1.0,
|
|
80
|
+
lineToLine: 1.0,
|
|
81
|
+
overall: 1.0
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Extract keywords from each observation
|
|
86
|
+
const keywordSets = observations.map(obs => {
|
|
87
|
+
const text = typeof obs === 'string' ? obs : obs.observation || '';
|
|
88
|
+
const words = text.toLowerCase()
|
|
89
|
+
.split(/\s+/)
|
|
90
|
+
.filter(w => w.length > 3)
|
|
91
|
+
.filter(w => !['the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can'].includes(w));
|
|
92
|
+
return new Set(words);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Calculate prompt-to-line consistency (first vs all others)
|
|
96
|
+
const firstKeywords = keywordSets[0];
|
|
97
|
+
let promptToLineMatches = 0;
|
|
98
|
+
for (let i = 1; i < keywordSets.length; i++) {
|
|
99
|
+
const intersection = new Set([...firstKeywords].filter(x => keywordSets[i].has(x)));
|
|
100
|
+
const union = new Set([...firstKeywords, ...keywordSets[i]]);
|
|
101
|
+
const similarity = union.size > 0 ? intersection.size / union.size : 0;
|
|
102
|
+
promptToLineMatches += similarity;
|
|
103
|
+
}
|
|
104
|
+
const promptToLine = promptToLineMatches / Math.max(1, keywordSets.length - 1);
|
|
105
|
+
|
|
106
|
+
// Calculate line-to-line consistency (adjacent observations)
|
|
107
|
+
let lineToLineMatches = 0;
|
|
108
|
+
for (let i = 1; i < keywordSets.length; i++) {
|
|
109
|
+
const prev = keywordSets[i - 1];
|
|
110
|
+
const curr = keywordSets[i];
|
|
111
|
+
const intersection = new Set([...prev].filter(x => curr.has(x)));
|
|
112
|
+
const union = new Set([...prev, ...curr]);
|
|
113
|
+
const similarity = union.size > 0 ? intersection.size / union.size : 0;
|
|
114
|
+
lineToLineMatches += similarity;
|
|
115
|
+
}
|
|
116
|
+
const lineToLine = lineToLineMatches / Math.max(1, keywordSets.length - 1);
|
|
117
|
+
|
|
118
|
+
// Overall consistency (weighted average)
|
|
119
|
+
const overall = (promptToLine * 0.4 + lineToLine * 0.6);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
promptToLine,
|
|
123
|
+
lineToLine,
|
|
124
|
+
overall,
|
|
125
|
+
observationCount: observations.length
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Experience page with enhanced persona
|
|
131
|
+
*
|
|
132
|
+
* @param {any} page - Playwright page object
|
|
133
|
+
* @param {EnhancedPersona} persona - Enhanced persona
|
|
134
|
+
* @param {Object} options - Experience options
|
|
135
|
+
* @returns {Promise<Object>} Experience result with consistency metrics
|
|
136
|
+
*/
|
|
137
|
+
export async function experiencePageWithEnhancedPersona(page, persona, options = {}) {
|
|
138
|
+
// Use base experience function
|
|
139
|
+
const experience = await experiencePageAsPersona(page, persona, options);
|
|
140
|
+
|
|
141
|
+
// Extract observations
|
|
142
|
+
const observations = experience.notes.map(n => n.observation || '');
|
|
143
|
+
|
|
144
|
+
// Calculate consistency metrics
|
|
145
|
+
const consistency = calculatePersonaConsistency(observations);
|
|
146
|
+
|
|
147
|
+
// Add persona context to experience
|
|
148
|
+
return {
|
|
149
|
+
...experience,
|
|
150
|
+
persona: {
|
|
151
|
+
...persona,
|
|
152
|
+
workflows: persona.workflows,
|
|
153
|
+
frustrations: persona.frustrations,
|
|
154
|
+
usagePatterns: persona.usagePatterns
|
|
155
|
+
},
|
|
156
|
+
consistency,
|
|
157
|
+
observations
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Compare persona observations for diversity
|
|
163
|
+
*
|
|
164
|
+
* @param {Array} personaExperiences - Array of persona experience results
|
|
165
|
+
* @returns {Object} Diversity metrics
|
|
166
|
+
*/
|
|
167
|
+
export function calculatePersonaDiversity(personaExperiences) {
|
|
168
|
+
if (personaExperiences.length < 2) {
|
|
169
|
+
return {
|
|
170
|
+
diversityRatio: 0,
|
|
171
|
+
uniqueKeywords: 0,
|
|
172
|
+
totalKeywords: 0
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Extract all keywords from all personas
|
|
177
|
+
const allKeywords = personaExperiences.flatMap(exp => {
|
|
178
|
+
const observations = exp.observations || exp.notes?.map(n => n.observation || '') || [];
|
|
179
|
+
return observations.flatMap(obs => {
|
|
180
|
+
const words = obs.toLowerCase()
|
|
181
|
+
.split(/\s+/)
|
|
182
|
+
.filter(w => w.length > 3)
|
|
183
|
+
.filter(w => !['the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can'].includes(w));
|
|
184
|
+
return words;
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const uniqueKeywords = new Set(allKeywords);
|
|
189
|
+
const diversityRatio = uniqueKeywords.size / Math.max(1, allKeywords.length);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
diversityRatio,
|
|
193
|
+
uniqueKeywords: uniqueKeywords.size,
|
|
194
|
+
totalKeywords: allKeywords.length,
|
|
195
|
+
personaCount: personaExperiences.length
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|