@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.
Files changed (93) hide show
  1. package/.secretsignore.example +20 -0
  2. package/CHANGELOG.md +360 -0
  3. package/CONTRIBUTING.md +63 -0
  4. package/DEPLOYMENT.md +80 -0
  5. package/LICENSE +22 -0
  6. package/README.md +142 -0
  7. package/SECURITY.md +108 -0
  8. package/api/health.js +34 -0
  9. package/api/validate.js +252 -0
  10. package/index.d.ts +1221 -0
  11. package/package.json +112 -0
  12. package/public/index.html +149 -0
  13. package/src/batch-optimizer.mjs +451 -0
  14. package/src/bias-detector.mjs +370 -0
  15. package/src/bias-mitigation.mjs +233 -0
  16. package/src/cache.mjs +433 -0
  17. package/src/config.mjs +268 -0
  18. package/src/constants.mjs +80 -0
  19. package/src/context-compressor.mjs +350 -0
  20. package/src/convenience.mjs +617 -0
  21. package/src/cost-tracker.mjs +257 -0
  22. package/src/cross-modal-consistency.mjs +170 -0
  23. package/src/data-extractor.mjs +232 -0
  24. package/src/dynamic-few-shot.mjs +140 -0
  25. package/src/dynamic-prompts.mjs +361 -0
  26. package/src/ensemble/index.mjs +53 -0
  27. package/src/ensemble-judge.mjs +366 -0
  28. package/src/error-handler.mjs +67 -0
  29. package/src/errors.mjs +167 -0
  30. package/src/experience-propagation.mjs +128 -0
  31. package/src/experience-tracer.mjs +487 -0
  32. package/src/explanation-manager.mjs +299 -0
  33. package/src/feedback-aggregator.mjs +248 -0
  34. package/src/game-goal-prompts.mjs +478 -0
  35. package/src/game-player.mjs +548 -0
  36. package/src/hallucination-detector.mjs +155 -0
  37. package/src/helpers/playwright.mjs +80 -0
  38. package/src/human-validation-manager.mjs +516 -0
  39. package/src/index.mjs +364 -0
  40. package/src/judge.mjs +929 -0
  41. package/src/latency-aware-batch-optimizer.mjs +192 -0
  42. package/src/load-env.mjs +159 -0
  43. package/src/logger.mjs +55 -0
  44. package/src/metrics.mjs +187 -0
  45. package/src/model-tier-selector.mjs +221 -0
  46. package/src/multi-modal/index.mjs +36 -0
  47. package/src/multi-modal-fusion.mjs +190 -0
  48. package/src/multi-modal.mjs +524 -0
  49. package/src/natural-language-specs.mjs +1071 -0
  50. package/src/pair-comparison.mjs +277 -0
  51. package/src/persona/index.mjs +42 -0
  52. package/src/persona-enhanced.mjs +200 -0
  53. package/src/persona-experience.mjs +572 -0
  54. package/src/position-counterbalance.mjs +140 -0
  55. package/src/prompt-composer.mjs +375 -0
  56. package/src/render-change-detector.mjs +583 -0
  57. package/src/research-enhanced-validation.mjs +436 -0
  58. package/src/retry.mjs +152 -0
  59. package/src/rubrics.mjs +231 -0
  60. package/src/score-tracker.mjs +277 -0
  61. package/src/smart-validator.mjs +447 -0
  62. package/src/spec-config.mjs +106 -0
  63. package/src/spec-templates.mjs +347 -0
  64. package/src/specs/index.mjs +38 -0
  65. package/src/temporal/index.mjs +102 -0
  66. package/src/temporal-adaptive.mjs +163 -0
  67. package/src/temporal-batch-optimizer.mjs +222 -0
  68. package/src/temporal-constants.mjs +69 -0
  69. package/src/temporal-context.mjs +49 -0
  70. package/src/temporal-decision-manager.mjs +271 -0
  71. package/src/temporal-decision.mjs +669 -0
  72. package/src/temporal-errors.mjs +58 -0
  73. package/src/temporal-note-pruner.mjs +173 -0
  74. package/src/temporal-preprocessor.mjs +543 -0
  75. package/src/temporal-prompt-formatter.mjs +219 -0
  76. package/src/temporal-validation.mjs +159 -0
  77. package/src/temporal.mjs +415 -0
  78. package/src/type-guards.mjs +311 -0
  79. package/src/uncertainty-reducer.mjs +470 -0
  80. package/src/utils/index.mjs +175 -0
  81. package/src/validation-framework.mjs +321 -0
  82. package/src/validation-result-normalizer.mjs +64 -0
  83. package/src/validation.mjs +243 -0
  84. package/src/validators/accessibility-programmatic.mjs +345 -0
  85. package/src/validators/accessibility-validator.mjs +223 -0
  86. package/src/validators/batch-validator.mjs +143 -0
  87. package/src/validators/hybrid-validator.mjs +268 -0
  88. package/src/validators/index.mjs +34 -0
  89. package/src/validators/prompt-builder.mjs +218 -0
  90. package/src/validators/rubric.mjs +85 -0
  91. package/src/validators/state-programmatic.mjs +260 -0
  92. package/src/validators/state-validator.mjs +291 -0
  93. package/vercel.json +27 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Temporal Batch Optimizer
3
+ *
4
+ * Adaptive batching that considers temporal dependencies and human perception.
5
+ *
6
+ * Research context (loosely related):
7
+ * - Efficient Sequential Decision Making (arXiv:2406.12125)
8
+ * * Paper focuses on online model selection (NOT implemented here)
9
+ * * We use temporal dependency concepts inspired by sequential decision aspects
10
+ * * We do NOT implement the paper's core online model selection algorithm
11
+ * - Serving LLM Reasoning Efficiently (arXiv:2505.13326)
12
+ * * Paper discusses "short and right" thinking management (NOT implemented here)
13
+ * * We do adaptive batching with temporal awareness (loosely related concept)
14
+ * * We do NOT implement the paper's specific adaptive batching strategy
15
+ * - Human Time Perception (NN/g, PMC)
16
+ * * 0.1s threshold for direct manipulation (used in time scales)
17
+ * * Attention affects temporal perception (used in weighting)
18
+ *
19
+ * IMPORTANT: This implements temporal-aware batching with dependencies, but does NOT
20
+ * implement the core algorithms from the cited papers. The citations are for loosely
21
+ * related concepts, not direct implementations.
22
+ */
23
+
24
+ import { BatchOptimizer } from './batch-optimizer.mjs';
25
+
26
+ /**
27
+ * Temporal Batch Optimizer
28
+ * Extends BatchOptimizer with temporal awareness
29
+ */
30
+ export class TemporalBatchOptimizer extends BatchOptimizer {
31
+ constructor(options = {}) {
32
+ super(options);
33
+ this.temporalDependencies = new Map(); // Track dependencies
34
+ this.sequentialContext = options.sequentialContext || null;
35
+ this.adaptiveBatching = options.adaptiveBatching !== false;
36
+ }
37
+
38
+ /**
39
+ * Add request with temporal dependencies
40
+ */
41
+ async addTemporalRequest(imagePath, prompt, context, dependencies = []) {
42
+ // Track dependencies
43
+ this.temporalDependencies.set(imagePath, {
44
+ dependencies,
45
+ timestamp: Date.now(),
46
+ priority: this.calculatePriority(dependencies, context)
47
+ });
48
+
49
+ return this._queueRequest(imagePath, prompt, context);
50
+ }
51
+
52
+ /**
53
+ * Calculate priority based on temporal dependencies
54
+ */
55
+ calculatePriority(dependencies, context) {
56
+ // Higher priority for:
57
+ // - No dependencies (can run in parallel)
58
+ // - Earlier timestamps (sequential order)
59
+ // - Critical evaluations (high importance)
60
+
61
+ let priority = 0;
62
+
63
+ // No dependencies = can run immediately
64
+ if (dependencies.length === 0) {
65
+ priority += 100;
66
+ } else {
67
+ // Dependencies reduce priority (must wait)
68
+ priority -= dependencies.length * 10;
69
+ }
70
+
71
+ // Earlier timestamps get higher priority (decay over time)
72
+ if (context.timestamp) {
73
+ const age = Date.now() - context.timestamp;
74
+ // Earlier requests (older timestamp = larger age) should have higher priority
75
+ // But we want to decay this over time, so very old requests get lower priority
76
+ // For requests within reasonable time window, earlier = higher priority
77
+ if (age < 60000) { // Within 1 minute
78
+ priority += Math.max(0, 30 - age / 1000); // Earlier = higher, but decays
79
+ }
80
+ }
81
+
82
+ // Critical evaluations get higher priority
83
+ if (context.critical || context.testType === 'critical') {
84
+ priority += 50;
85
+ }
86
+
87
+ return priority;
88
+ }
89
+
90
+ /**
91
+ * Process queue with temporal awareness
92
+ */
93
+ async _processQueue() {
94
+ if (this.processing || this.queue.length === 0) {
95
+ return;
96
+ }
97
+
98
+ this.processing = true;
99
+
100
+ try {
101
+ // Sort by priority and dependencies
102
+ const sortedQueue = this.sortByTemporalDependencies([...this.queue]);
103
+
104
+ // Process in batches, respecting dependencies
105
+ while (sortedQueue.length > 0 && this.activeRequests < this.maxConcurrency) {
106
+ const batch = this.selectTemporalBatch(sortedQueue);
107
+
108
+ // Remove from queue
109
+ batch.forEach(item => {
110
+ const index = this.queue.findIndex(q => q.imagePath === item.imagePath);
111
+ if (index >= 0) this.queue.splice(index, 1);
112
+ });
113
+
114
+ // Process batch
115
+ const promises = batch.map(async ({ imagePath, prompt, context, validateFn, resolve, reject }) => {
116
+ try {
117
+ // Check cache
118
+ if (this.cache) {
119
+ const cacheKey = this._getCacheKey(imagePath, prompt, context);
120
+ if (this.cache.has(cacheKey)) {
121
+ resolve(this.cache.get(cacheKey));
122
+ return;
123
+ }
124
+ }
125
+
126
+ // Add sequential context if available
127
+ if (this.sequentialContext) {
128
+ context = {
129
+ ...context,
130
+ sequentialContext: this.sequentialContext.getContext()
131
+ };
132
+ }
133
+
134
+ const result = await this._processRequest(imagePath, prompt, context, validateFn);
135
+
136
+ // Update sequential context
137
+ if (this.sequentialContext && result.score !== null) {
138
+ this.sequentialContext.addDecision({
139
+ score: result.score,
140
+ issues: result.issues || [],
141
+ assessment: result.assessment,
142
+ reasoning: result.reasoning
143
+ });
144
+ }
145
+
146
+ resolve(result);
147
+ } catch (error) {
148
+ reject(error);
149
+ }
150
+ });
151
+
152
+ await Promise.allSettled(promises);
153
+ }
154
+ } finally {
155
+ this.processing = false;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Sort queue by temporal dependencies
161
+ */
162
+ sortByTemporalDependencies(queue) {
163
+ return queue.sort((a, b) => {
164
+ const depsA = this.temporalDependencies.get(a.imagePath);
165
+ const depsB = this.temporalDependencies.get(b.imagePath);
166
+
167
+ // No dependencies come first
168
+ if (!depsA && depsB) return -1;
169
+ if (depsA && !depsB) return 1;
170
+ if (!depsA && !depsB) return 0;
171
+
172
+ // Compare priorities
173
+ return depsB.priority - depsA.priority;
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Select batch considering temporal dependencies
179
+ */
180
+ selectTemporalBatch(sortedQueue) {
181
+ if (!this.adaptiveBatching) {
182
+ // Fixed batch size
183
+ return sortedQueue.splice(0, this.batchSize);
184
+ }
185
+
186
+ // Adaptive batch selection
187
+ const batch = [];
188
+ const processed = new Set();
189
+
190
+ for (const item of sortedQueue) {
191
+ if (batch.length >= this.batchSize) break;
192
+ if (processed.has(item.imagePath)) continue;
193
+
194
+ const deps = this.temporalDependencies.get(item.imagePath);
195
+
196
+ // Check if dependencies are satisfied
197
+ if (!deps || deps.dependencies.every(dep => processed.has(dep))) {
198
+ batch.push(item);
199
+ processed.add(item.imagePath);
200
+ }
201
+ }
202
+
203
+ return batch;
204
+ }
205
+
206
+ /**
207
+ * Get temporal statistics
208
+ */
209
+ getTemporalStats() {
210
+ return {
211
+ ...this.getCacheStats(),
212
+ dependencies: this.temporalDependencies.size,
213
+ sequentialContext: this.sequentialContext
214
+ ? {
215
+ historyLength: this.sequentialContext.history.length,
216
+ patterns: this.sequentialContext.identifyPatterns()
217
+ }
218
+ : null
219
+ };
220
+ }
221
+ }
222
+
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Temporal Constants
3
+ * Centralized definitions for all temporal decision-making constants
4
+ *
5
+ * Single source of truth for all magic numbers and time scales
6
+ */
7
+
8
+ // Time Scales (based on research: NN/g, PMC, Lindgaard)
9
+ export const TIME_SCALES = {
10
+ INSTANT: 100, // 0.1s - perceived instant, direct manipulation threshold (NN/g)
11
+ VISUAL_DECISION: 50, // 50ms - visual appeal decision (Lindgaard research)
12
+ QUICK: 1000, // 1s - noticeable delay (NN/g)
13
+ NORMAL: 3000, // 3s - normal interaction
14
+ EXTENDED: 10000, // 10s - extended focus (NN/g)
15
+ LONG: 60000 // 60s - deep evaluation
16
+ };
17
+
18
+ // Multi-Scale Windows
19
+ export const MULTI_SCALE_WINDOWS = {
20
+ immediate: TIME_SCALES.INSTANT,
21
+ short: TIME_SCALES.QUICK,
22
+ medium: TIME_SCALES.EXTENDED,
23
+ long: TIME_SCALES.LONG
24
+ };
25
+
26
+ // Reading Speeds (words per minute)
27
+ export const READING_SPEEDS = {
28
+ SCANNING: 300, // Fast scanning
29
+ NORMAL: 250, // Average reading
30
+ DEEP: 200 // Deep reading
31
+ };
32
+
33
+ // Attention Multipliers
34
+ export const ATTENTION_MULTIPLIERS = {
35
+ focused: 0.8, // Faster when focused (reduced cognitive load)
36
+ normal: 1.0,
37
+ distracted: 1.5 // Slower when distracted (increased cognitive load)
38
+ };
39
+
40
+ // Complexity Multipliers
41
+ export const COMPLEXITY_MULTIPLIERS = {
42
+ simple: 0.7, // Simple actions are faster
43
+ normal: 1.0,
44
+ complex: 1.5 // Complex actions take longer
45
+ };
46
+
47
+ // Confidence Thresholds
48
+ export const CONFIDENCE_THRESHOLDS = {
49
+ HIGH_VARIANCE: 1.0, // Variance < 1.0 = high confidence
50
+ MEDIUM_VARIANCE: 2.0, // Variance < 2.0 = medium confidence
51
+ LOW_VARIANCE: 2.0 // Variance >= 2.0 = low confidence
52
+ };
53
+
54
+ // Time Bounds
55
+ export const TIME_BOUNDS = {
56
+ MIN_PERCEPTION: 100, // Minimum perception time (0.1s)
57
+ MIN_READING_SHORT: 1000, // Minimum reading time for short content
58
+ MIN_READING_LONG: 2000, // Minimum reading time for long content
59
+ MAX_READING_SHORT: 15000, // Maximum reading time for short content
60
+ MAX_READING_LONG: 30000 // Maximum reading time for long content
61
+ };
62
+
63
+ // Content Length Thresholds
64
+ export const CONTENT_THRESHOLDS = {
65
+ SHORT: 100, // Short content (< 100 chars)
66
+ MEDIUM: 1000, // Medium content (< 1000 chars)
67
+ LONG: 1000 // Long content (>= 1000 chars)
68
+ };
69
+
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Temporal Context Utilities
3
+ * Standardized context creation and merging for temporal components
4
+ */
5
+
6
+ /**
7
+ * Create standardized temporal context
8
+ */
9
+ export function createTemporalContext(options = {}) {
10
+ return {
11
+ sequentialContext: options.sequentialContext || null,
12
+ viewport: options.viewport || null,
13
+ testType: options.testType || null,
14
+ enableBiasMitigation: options.enableBiasMitigation !== false,
15
+ attentionLevel: options.attentionLevel || 'normal',
16
+ actionComplexity: options.actionComplexity || 'normal',
17
+ persona: options.persona || null,
18
+ contentLength: options.contentLength || 0,
19
+ ...options
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Merge temporal contexts
25
+ */
26
+ export function mergeTemporalContext(base, additional) {
27
+ return {
28
+ ...base,
29
+ ...additional,
30
+ sequentialContext: additional.sequentialContext || base.sequentialContext,
31
+ // Preserve base values if additional doesn't override
32
+ attentionLevel: additional.attentionLevel || base.attentionLevel || 'normal',
33
+ actionComplexity: additional.actionComplexity || base.actionComplexity || 'normal'
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Extract temporal context from options
39
+ */
40
+ export function extractTemporalContext(options) {
41
+ return {
42
+ sequentialContext: options.sequentialContext,
43
+ attentionLevel: options.attentionLevel,
44
+ actionComplexity: options.actionComplexity,
45
+ persona: options.persona,
46
+ contentLength: options.contentLength
47
+ };
48
+ }
49
+
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Temporal Decision Manager
3
+ *
4
+ * Implements decision logic for WHEN to prompt based on temporal context.
5
+ *
6
+ * Research: arXiv:2406.12125 - "Efficient Sequential Decision Making"
7
+ * - Paper's core finding: Don't prompt on every state change, prompt when decision is needed
8
+ * - Online model selection achieves 6x gains with 1.5% LLM calls
9
+ *
10
+ * This module implements the decision logic (when to prompt) that was missing from
11
+ * our temporal aggregation system. It complements temporal-decision.mjs which handles
12
+ * HOW to aggregate temporal context, while this handles WHEN to use it.
13
+ */
14
+
15
+ import { aggregateTemporalNotes } from './temporal.mjs';
16
+ import { aggregateMultiScale } from './temporal-decision.mjs';
17
+
18
+ /**
19
+ * Temporal Decision Manager
20
+ *
21
+ * Decides when context is sufficient to prompt, when to wait, and when to prompt immediately.
22
+ */
23
+ export class TemporalDecisionManager {
24
+ constructor(options = {}) {
25
+ this.minNotesForPrompt = options.minNotesForPrompt || 3; // Minimum notes before prompting
26
+ this.coherenceThreshold = options.coherenceThreshold || 0.5; // Minimum coherence to prompt
27
+ this.urgencyThreshold = options.urgencyThreshold || 0.3; // Coherence drop triggers urgency
28
+ this.maxWaitTime = options.maxWaitTime || 10000; // Max time to wait (10s)
29
+ this.stateChangeThreshold = options.stateChangeThreshold || 0.2; // Significant state change
30
+ }
31
+
32
+ /**
33
+ * Decide if we should prompt now or wait for more context
34
+ *
35
+ * @param {Object} currentState - Current state
36
+ * @param {Object} previousState - Previous state (if any)
37
+ * @param {import('./index.mjs').TemporalNote[]} temporalNotes - Temporal notes so far
38
+ * @param {Object} context - Additional context
39
+ * @returns {{shouldPrompt: boolean, reason: string, urgency: 'low'|'medium'|'high'}}
40
+ */
41
+ shouldPrompt(currentState, previousState, temporalNotes, context = {}) {
42
+ // 1. Check if we have minimum notes
43
+ if (temporalNotes.length < this.minNotesForPrompt) {
44
+ return {
45
+ shouldPrompt: false,
46
+ reason: `Insufficient notes (${temporalNotes.length} < ${this.minNotesForPrompt})`,
47
+ urgency: 'low'
48
+ };
49
+ }
50
+
51
+ // 2. Calculate temporal coherence
52
+ const aggregated = aggregateTemporalNotes(temporalNotes);
53
+ const coherence = aggregated.coherence || 0;
54
+
55
+ // 3. Check for significant state change
56
+ const stateChange = this.calculateStateChange(currentState, previousState);
57
+
58
+ // 4. Check for user action
59
+ const hasUserAction = this.hasRecentUserAction(temporalNotes, context);
60
+
61
+ // 5. Check for decision point
62
+ const isDecisionPoint = this.isDecisionPoint(currentState, context);
63
+
64
+ // 6. Check for coherence drop (urgency signal)
65
+ const coherenceDrop = this.detectCoherenceDrop(temporalNotes, aggregated);
66
+
67
+ // Decision logic (from research: prompt when decision needed, not on every change)
68
+ if (isDecisionPoint) {
69
+ return {
70
+ shouldPrompt: true,
71
+ reason: 'Decision point reached',
72
+ urgency: 'high'
73
+ };
74
+ }
75
+
76
+ if (coherenceDrop) {
77
+ return {
78
+ shouldPrompt: true,
79
+ reason: 'Coherence drop detected (quality issue)',
80
+ urgency: 'high'
81
+ };
82
+ }
83
+
84
+ if (hasUserAction && stateChange > this.stateChangeThreshold) {
85
+ return {
86
+ shouldPrompt: true,
87
+ reason: 'User action with significant state change',
88
+ urgency: 'medium'
89
+ };
90
+ }
91
+
92
+ if (coherence >= this.coherenceThreshold && stateChange > this.stateChangeThreshold) {
93
+ return {
94
+ shouldPrompt: true,
95
+ reason: 'Stable context with significant state change',
96
+ urgency: 'medium'
97
+ };
98
+ }
99
+
100
+ // Wait for more context
101
+ return {
102
+ shouldPrompt: false,
103
+ reason: `Context not sufficient (coherence: ${coherence.toFixed(2)}, stateChange: ${stateChange.toFixed(2)})`,
104
+ urgency: 'low'
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Calculate state change magnitude
110
+ */
111
+ calculateStateChange(currentState, previousState) {
112
+ if (!previousState) return 1.0; // First state = maximum change
113
+
114
+ // Simple state change calculation (can be enhanced)
115
+ let change = 0.0;
116
+ let comparisons = 0;
117
+
118
+ // Compare scores if available
119
+ if (currentState.score !== undefined && previousState.score !== undefined) {
120
+ const scoreChange = Math.abs(currentState.score - previousState.score) / 10; // Normalize to 0-1
121
+ change += scoreChange;
122
+ comparisons++;
123
+ }
124
+
125
+ // Compare issues if available
126
+ if (currentState.issues && previousState.issues) {
127
+ const currentIssues = new Set(currentState.issues);
128
+ const previousIssues = new Set(previousState.issues);
129
+ const added = [...currentIssues].filter(i => !previousIssues.has(i)).length;
130
+ const removed = [...previousIssues].filter(i => !currentIssues.has(i)).length;
131
+ const issueChange = (added + removed) / Math.max(currentIssues.size + previousIssues.size, 1);
132
+ change += issueChange;
133
+ comparisons++;
134
+ }
135
+
136
+ // Compare game state if available
137
+ if (currentState.gameState && previousState.gameState) {
138
+ const gameStateChange = this.calculateGameStateChange(currentState.gameState, previousState.gameState);
139
+ change += gameStateChange;
140
+ comparisons++;
141
+ }
142
+
143
+ return comparisons > 0 ? change / comparisons : 0.0;
144
+ }
145
+
146
+ /**
147
+ * Calculate game state change
148
+ */
149
+ calculateGameStateChange(current, previous) {
150
+ // Simple comparison (can be enhanced)
151
+ const currentKeys = Object.keys(current || {});
152
+ const previousKeys = Object.keys(previous || {});
153
+
154
+ const added = currentKeys.filter(k => !previousKeys.includes(k)).length;
155
+ const removed = previousKeys.filter(k => !currentKeys.includes(k)).length;
156
+ const changed = currentKeys.filter(k =>
157
+ previousKeys.includes(k) &&
158
+ JSON.stringify(current[k]) !== JSON.stringify(previous[k])
159
+ ).length;
160
+
161
+ const totalKeys = new Set([...currentKeys, ...previousKeys]).size;
162
+ return totalKeys > 0 ? (added + removed + changed) / totalKeys : 0.0;
163
+ }
164
+
165
+ /**
166
+ * Check for recent user action
167
+ */
168
+ hasRecentUserAction(temporalNotes, context) {
169
+ if (context.recentAction) return true;
170
+
171
+ // Check last few notes for interactions
172
+ const recentNotes = temporalNotes.slice(-3);
173
+ return recentNotes.some(note =>
174
+ note.step?.includes('interaction') ||
175
+ note.step?.includes('click') ||
176
+ note.step?.includes('action') ||
177
+ note.observation?.includes('user') ||
178
+ note.observation?.includes('clicked')
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Check if this is a decision point
184
+ */
185
+ isDecisionPoint(currentState, context) {
186
+ // Decision points based on context
187
+ if (context.stage === 'decision' || context.stage === 'evaluation') return true;
188
+ if (context.testType === 'critical' || context.critical) return true;
189
+ if (context.goal && context.goalCompleted) return true;
190
+
191
+ return false;
192
+ }
193
+
194
+ /**
195
+ * Detect coherence drop (quality issue signal)
196
+ */
197
+ detectCoherenceDrop(temporalNotes, currentAggregated) {
198
+ if (temporalNotes.length < 4) return false; // Need history to detect drop
199
+
200
+ // Get previous coherence (from notes without last one)
201
+ const previousNotes = temporalNotes.slice(0, -1);
202
+ const previousAggregated = aggregateTemporalNotes(previousNotes);
203
+ const previousCoherence = previousAggregated.coherence || 1.0;
204
+ const currentCoherence = currentAggregated.coherence || 1.0;
205
+
206
+ // Drop detected if coherence decreased significantly
207
+ const drop = previousCoherence - currentCoherence;
208
+ return drop > this.urgencyThreshold;
209
+ }
210
+
211
+ /**
212
+ * Calculate prompt urgency
213
+ */
214
+ calculatePromptUrgency(temporalContext, decision) {
215
+ if (decision.urgency === 'high') return 1.0;
216
+ if (decision.urgency === 'medium') return 0.6;
217
+
218
+ // Low urgency, but check if we should wait
219
+ const coherence = temporalContext.coherence || 0;
220
+ const timeSinceLastPrompt = temporalContext.timeSinceLastPrompt || 0;
221
+
222
+ // Increase urgency if coherence is good and it's been a while
223
+ if (coherence > 0.7 && timeSinceLastPrompt > 5000) {
224
+ return 0.4; // Medium-low
225
+ }
226
+
227
+ return 0.2; // Low
228
+ }
229
+
230
+ /**
231
+ * Select optimal timing for requests
232
+ */
233
+ selectOptimalTiming(requests, temporalContext) {
234
+ const decisions = requests.map(req => ({
235
+ request: req,
236
+ decision: this.shouldPrompt(
237
+ req.currentState,
238
+ req.previousState,
239
+ req.temporalNotes || [],
240
+ req.context || {}
241
+ )
242
+ }));
243
+
244
+ // Separate by urgency
245
+ const urgent = decisions.filter(d => d.decision.urgency === 'high');
246
+ const medium = decisions.filter(d => d.decision.urgency === 'medium');
247
+ const low = decisions.filter(d => d.decision.urgency === 'low');
248
+
249
+ // Urgent: prompt immediately
250
+ // Medium: batch if context stable, otherwise prompt
251
+ // Low: wait or batch
252
+
253
+ const stable = temporalContext.coherence > 0.7;
254
+ const shouldBatch = stable && medium.length + low.length > 1;
255
+
256
+ return {
257
+ promptNow: urgent.map(d => d.request),
258
+ batch: shouldBatch ? [...medium, ...low].map(d => d.request) : medium.map(d => d.request),
259
+ wait: shouldBatch ? [] : low.map(d => d.request),
260
+ decisions
261
+ };
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Create a temporal decision manager with default options
267
+ */
268
+ export function createTemporalDecisionManager(options = {}) {
269
+ return new TemporalDecisionManager(options);
270
+ }
271
+