@machinespirits/eval 0.2.0 → 0.3.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/README.md +91 -9
- package/config/eval-settings.yaml +3 -3
- package/config/paper-manifest.json +486 -0
- package/config/providers.yaml +9 -6
- package/config/tutor-agents.yaml +2261 -0
- package/content/README.md +23 -0
- package/content/courses/479/course.md +53 -0
- package/content/courses/479/lecture-1.md +361 -0
- package/content/courses/479/lecture-2.md +360 -0
- package/content/courses/479/lecture-3.md +655 -0
- package/content/courses/479/lecture-4.md +530 -0
- package/content/courses/479/lecture-5.md +326 -0
- package/content/courses/479/lecture-6.md +346 -0
- package/content/courses/479/lecture-7.md +326 -0
- package/content/courses/479/lecture-8.md +273 -0
- package/content/courses/479/roadmap-slides.md +656 -0
- package/content/manifest.yaml +8 -0
- package/docs/research/build.sh +44 -20
- package/docs/research/figures/figure10.png +0 -0
- package/docs/research/figures/figure11.png +0 -0
- package/docs/research/figures/figure3.png +0 -0
- package/docs/research/figures/figure4.png +0 -0
- package/docs/research/figures/figure5.png +0 -0
- package/docs/research/figures/figure6.png +0 -0
- package/docs/research/figures/figure7.png +0 -0
- package/docs/research/figures/figure8.png +0 -0
- package/docs/research/figures/figure9.png +0 -0
- package/docs/research/header.tex +23 -2
- package/docs/research/paper-full.md +941 -285
- package/docs/research/paper-short.md +216 -585
- package/docs/research/references.bib +132 -0
- package/docs/research/slides-header.tex +188 -0
- package/docs/research/slides-pptx.md +363 -0
- package/docs/research/slides.md +531 -0
- package/docs/research/style-reference-pptx.py +199 -0
- package/package.json +6 -5
- package/scripts/analyze-eval-results.js +69 -17
- package/scripts/analyze-mechanism-traces.js +763 -0
- package/scripts/analyze-modulation-learning.js +498 -0
- package/scripts/analyze-prosthesis.js +144 -0
- package/scripts/analyze-run.js +264 -79
- package/scripts/assess-transcripts.js +853 -0
- package/scripts/browse-transcripts.js +854 -0
- package/scripts/check-parse-failures.js +73 -0
- package/scripts/code-dialectical-modulation.js +1320 -0
- package/scripts/download-data.sh +55 -0
- package/scripts/eval-cli.js +106 -18
- package/scripts/generate-paper-figures.js +663 -0
- package/scripts/generate-paper-figures.py +577 -76
- package/scripts/generate-paper-tables.js +299 -0
- package/scripts/qualitative-analysis-ai.js +3 -3
- package/scripts/render-sequence-diagram.js +694 -0
- package/scripts/test-latency.js +210 -0
- package/scripts/test-rate-limit.js +95 -0
- package/scripts/test-token-budget.js +332 -0
- package/scripts/validate-paper-manifest.js +670 -0
- package/services/__tests__/evalConfigLoader.test.js +2 -2
- package/services/__tests__/learnerRubricEvaluator.test.js +361 -0
- package/services/__tests__/learnerTutorInteractionEngine.test.js +326 -0
- package/services/evaluationRunner.js +975 -98
- package/services/evaluationStore.js +12 -4
- package/services/learnerTutorInteractionEngine.js +27 -2
- package/services/mockProvider.js +133 -0
- package/services/promptRewriter.js +1471 -5
- package/services/rubricEvaluator.js +55 -2
- package/services/transcriptFormatter.js +675 -0
- package/docs/EVALUATION-VARIABLES.md +0 -589
- package/docs/REPLICATION-PLAN.md +0 -577
- package/scripts/analyze-run.mjs +0 -282
- package/scripts/compare-runs.js +0 -44
- package/scripts/compare-suggestions.js +0 -80
- package/scripts/dig-into-run.js +0 -158
- package/scripts/show-failed-suggestions.js +0 -64
- /package/scripts/{check-run.mjs → check-run.js} +0 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mechanism Process Trace Analysis
|
|
5
|
+
*
|
|
6
|
+
* Extracts process measures from dialogue traces to identify which mechanisms
|
|
7
|
+
* produce the most behavioral change in tutor output. Works on existing data
|
|
8
|
+
* without requiring new LLM calls.
|
|
9
|
+
*
|
|
10
|
+
* Process measures:
|
|
11
|
+
* 1. Revision magnitude — how much does superego feedback change ego output?
|
|
12
|
+
* 2. Self-reflection specificity — does the reflection reference actual learner behavior?
|
|
13
|
+
* 3. Cross-turn adaptation — how different is the tutor's message between turns?
|
|
14
|
+
* 4. Profile richness — detail and evolution of other-ego profiles
|
|
15
|
+
* 5. Intersubjective coordination — agreement vs pushback between ego and superego
|
|
16
|
+
* 6. Between-run variance — does the mechanism produce different outputs across runs?
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* node scripts/analyze-mechanism-traces.js <runId> [options]
|
|
20
|
+
*
|
|
21
|
+
* Options:
|
|
22
|
+
* --output <path> Output file path (default: exports/mechanism-traces-<runId>.md)
|
|
23
|
+
* --json Also output JSON data
|
|
24
|
+
* --verbose Print per-dialogue details
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import Database from 'better-sqlite3';
|
|
28
|
+
import fs from 'fs';
|
|
29
|
+
import path from 'path';
|
|
30
|
+
import { fileURLToPath } from 'url';
|
|
31
|
+
|
|
32
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const LOGS_DIR = path.resolve(__dirname, '..', 'logs', 'tutor-dialogues');
|
|
34
|
+
const DB_PATH = path.resolve(__dirname, '..', 'data', 'evaluations.db');
|
|
35
|
+
|
|
36
|
+
// ── Text Similarity ─────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function tokenize(text) {
|
|
39
|
+
if (!text) return [];
|
|
40
|
+
return text.toLowerCase().replace(/[^\w\s]/g, ' ').split(/\s+/).filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function jaccardSimilarity(a, b) {
|
|
44
|
+
const setA = new Set(tokenize(a));
|
|
45
|
+
const setB = new Set(tokenize(b));
|
|
46
|
+
if (setA.size === 0 && setB.size === 0) return 1;
|
|
47
|
+
const intersection = new Set([...setA].filter(x => setB.has(x)));
|
|
48
|
+
const union = new Set([...setA, ...setB]);
|
|
49
|
+
return intersection.size / union.size;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cosineSimilarity(a, b) {
|
|
53
|
+
const tokensA = tokenize(a);
|
|
54
|
+
const tokensB = tokenize(b);
|
|
55
|
+
if (tokensA.length === 0 && tokensB.length === 0) return 1;
|
|
56
|
+
if (tokensA.length === 0 || tokensB.length === 0) return 0;
|
|
57
|
+
|
|
58
|
+
const freqA = {};
|
|
59
|
+
const freqB = {};
|
|
60
|
+
tokensA.forEach(t => { freqA[t] = (freqA[t] || 0) + 1; });
|
|
61
|
+
tokensB.forEach(t => { freqB[t] = (freqB[t] || 0) + 1; });
|
|
62
|
+
|
|
63
|
+
const allTokens = new Set([...Object.keys(freqA), ...Object.keys(freqB)]);
|
|
64
|
+
let dot = 0, magA = 0, magB = 0;
|
|
65
|
+
for (const t of allTokens) {
|
|
66
|
+
const va = freqA[t] || 0;
|
|
67
|
+
const vb = freqB[t] || 0;
|
|
68
|
+
dot += va * vb;
|
|
69
|
+
magA += va * va;
|
|
70
|
+
magB += vb * vb;
|
|
71
|
+
}
|
|
72
|
+
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function editDistance(a, b) {
|
|
76
|
+
// Normalized Levenshtein on words (not chars) for efficiency
|
|
77
|
+
const wa = tokenize(a);
|
|
78
|
+
const wb = tokenize(b);
|
|
79
|
+
if (wa.length === 0 && wb.length === 0) return 0;
|
|
80
|
+
if (wa.length === 0 || wb.length === 0) return 1;
|
|
81
|
+
|
|
82
|
+
// Use only first 200 words to avoid O(n^2) blowup
|
|
83
|
+
const sa = wa.slice(0, 200);
|
|
84
|
+
const sb = wb.slice(0, 200);
|
|
85
|
+
|
|
86
|
+
const m = sa.length;
|
|
87
|
+
const n = sb.length;
|
|
88
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
89
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
90
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
91
|
+
for (let i = 1; i <= m; i++) {
|
|
92
|
+
for (let j = 1; j <= n; j++) {
|
|
93
|
+
dp[i][j] = sa[i - 1] === sb[j - 1]
|
|
94
|
+
? dp[i - 1][j - 1]
|
|
95
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return dp[m][n] / Math.max(m, n);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Mechanism Detection ─────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function detectMechanism(profileName) {
|
|
104
|
+
if (!profileName) return 'unknown';
|
|
105
|
+
const name = profileName.toLowerCase();
|
|
106
|
+
if (name.includes('_combined')) return 'combined';
|
|
107
|
+
if (name.includes('_quantitative')) return 'quantitative';
|
|
108
|
+
if (name.includes('_erosion')) return 'erosion';
|
|
109
|
+
if (name.includes('_intersubjective')) return 'intersubjective';
|
|
110
|
+
if (name.includes('_profile_bidirectional_full')) return 'profile_bidir_full';
|
|
111
|
+
if (name.includes('_profile_bidirectional_strategy')) return 'profile_bidir_strategy';
|
|
112
|
+
if (name.includes('_profile_bidirectional')) return 'profile_bidirectional';
|
|
113
|
+
if (name.includes('_profile_tutor')) return 'profile_tutor';
|
|
114
|
+
if (name.includes('_advocate')) return 'advocate';
|
|
115
|
+
if (name.includes('_adversary')) return 'adversary';
|
|
116
|
+
if (name.includes('_selfreflect')) return 'self_reflection';
|
|
117
|
+
if (name.includes('_suspicious') && name.includes('_superego')) return 'self_reflection';
|
|
118
|
+
if (name.includes('_suspicious')) return 'suspicious';
|
|
119
|
+
return 'unknown';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isRecognition(profileName) {
|
|
123
|
+
return profileName?.includes('recog') || false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Measure 1: Ego Revision Magnitude ───────────────────────────────────
|
|
127
|
+
|
|
128
|
+
function measureRevisionMagnitude(trace) {
|
|
129
|
+
const results = [];
|
|
130
|
+
// Group by round pairs: generate at round N, revise at round N+1
|
|
131
|
+
const generates = trace.filter(e => e.agent === 'ego' && e.action === 'generate');
|
|
132
|
+
const revisions = trace.filter(e => e.agent === 'ego' && e.action === 'revise');
|
|
133
|
+
|
|
134
|
+
for (const gen of generates) {
|
|
135
|
+
const genMsg = gen.suggestions?.[0]?.message || '';
|
|
136
|
+
// Find the revision that follows this generation (same or next round)
|
|
137
|
+
const rev = revisions.find(r => r.round >= gen.round);
|
|
138
|
+
if (!rev) continue;
|
|
139
|
+
const revMsg = rev.suggestions?.[0]?.message || '';
|
|
140
|
+
if (!genMsg || !revMsg) continue;
|
|
141
|
+
|
|
142
|
+
results.push({
|
|
143
|
+
round: gen.round,
|
|
144
|
+
jaccard: 1 - jaccardSimilarity(genMsg, revMsg),
|
|
145
|
+
cosine: 1 - cosineSimilarity(genMsg, revMsg),
|
|
146
|
+
editDist: editDistance(genMsg, revMsg),
|
|
147
|
+
genLength: tokenize(genMsg).length,
|
|
148
|
+
revLength: tokenize(revMsg).length,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Measure 2: Self-Reflection Specificity ──────────────────────────────
|
|
155
|
+
|
|
156
|
+
const LEARNER_SPECIFIC_PATTERNS = [
|
|
157
|
+
/the learner/gi, /they said/gi, /they asked/gi, /their (?:question|response|point|insight|critique)/gi,
|
|
158
|
+
/the student/gi, /learner's/gi, /their (?:argument|claim|objection)/gi,
|
|
159
|
+
/you (?:said|asked|raised|pointed|mentioned|noted)/gi,
|
|
160
|
+
/your (?:question|response|point|insight|critique|argument|claim)/gi,
|
|
161
|
+
/specific(?:ally)/gi, /in this case/gi, /this particular/gi,
|
|
162
|
+
/when they/gi, /after they/gi, /because they/gi,
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const GENERIC_PEDAGOGY_PATTERNS = [
|
|
166
|
+
/in general/gi, /typically/gi, /usually/gi, /always/gi,
|
|
167
|
+
/best practice/gi, /should (?:always|generally|typically)/gi,
|
|
168
|
+
/it's important to/gi, /one should/gi, /a good (?:tutor|teacher)/gi,
|
|
169
|
+
/pedagogical(?:ly)?/gi, /scaffolding/gi, /next time/gi,
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
function measureReflectionSpecificity(trace) {
|
|
173
|
+
const reflections = trace.filter(e =>
|
|
174
|
+
e.agent === 'ego_self_reflection' || e.agent === 'superego_self_reflection'
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return reflections.map(r => {
|
|
178
|
+
const text = r.detail || '';
|
|
179
|
+
const words = tokenize(text).length;
|
|
180
|
+
|
|
181
|
+
let specificCount = 0;
|
|
182
|
+
for (const p of LEARNER_SPECIFIC_PATTERNS) {
|
|
183
|
+
const matches = text.match(p);
|
|
184
|
+
specificCount += matches ? matches.length : 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let genericCount = 0;
|
|
188
|
+
for (const p of GENERIC_PEDAGOGY_PATTERNS) {
|
|
189
|
+
const matches = text.match(p);
|
|
190
|
+
genericCount += matches ? matches.length : 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const total = specificCount + genericCount;
|
|
194
|
+
return {
|
|
195
|
+
agent: r.agent,
|
|
196
|
+
turnIndex: r.turnIndex ?? -1,
|
|
197
|
+
wordCount: words,
|
|
198
|
+
specificCount,
|
|
199
|
+
genericCount,
|
|
200
|
+
specificityRatio: total > 0 ? specificCount / total : 0,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Measure 3: Cross-Turn Adaptation ────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
function measureCrossTurnAdaptation(trace) {
|
|
208
|
+
// Find final suggestions per turn (last ego revise or generate before final_output/turn_action)
|
|
209
|
+
const finalOutputs = [];
|
|
210
|
+
let currentTurnSuggestions = null;
|
|
211
|
+
let currentTurnIndex = -1;
|
|
212
|
+
|
|
213
|
+
for (const entry of trace) {
|
|
214
|
+
if (entry.agent === 'ego' && (entry.action === 'generate' || entry.action === 'revise')) {
|
|
215
|
+
const msg = entry.suggestions?.[0]?.message;
|
|
216
|
+
if (msg) {
|
|
217
|
+
// Track turn index from nearby entries
|
|
218
|
+
const turnIdx = entry.turnIndex ?? currentTurnIndex;
|
|
219
|
+
currentTurnSuggestions = { message: msg, turnIndex: turnIdx, round: entry.round };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (entry.action === 'final_output' || entry.action === 'turn_action') {
|
|
223
|
+
if (currentTurnSuggestions) {
|
|
224
|
+
finalOutputs.push(currentTurnSuggestions);
|
|
225
|
+
currentTurnSuggestions = null;
|
|
226
|
+
}
|
|
227
|
+
if (entry.action === 'turn_action') {
|
|
228
|
+
currentTurnIndex++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Catch last turn if no final_output marker
|
|
233
|
+
if (currentTurnSuggestions) {
|
|
234
|
+
finalOutputs.push(currentTurnSuggestions);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const adaptations = [];
|
|
238
|
+
for (let i = 1; i < finalOutputs.length; i++) {
|
|
239
|
+
const prev = finalOutputs[i - 1].message;
|
|
240
|
+
const curr = finalOutputs[i].message;
|
|
241
|
+
adaptations.push({
|
|
242
|
+
fromTurn: i - 1,
|
|
243
|
+
toTurn: i,
|
|
244
|
+
jaccard: 1 - jaccardSimilarity(prev, curr),
|
|
245
|
+
cosine: 1 - cosineSimilarity(prev, curr),
|
|
246
|
+
editDist: editDistance(prev, curr),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return adaptations;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Measure 4: Profile Richness & Evolution ─────────────────────────────
|
|
253
|
+
|
|
254
|
+
function measureProfileRichness(trace) {
|
|
255
|
+
const profiles = trace.filter(e =>
|
|
256
|
+
e.agent === 'tutor_other_ego' || e.agent === 'learner_other_ego'
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (profiles.length === 0) return { profiles: [], evolution: [] };
|
|
260
|
+
|
|
261
|
+
const profileData = profiles.map(p => {
|
|
262
|
+
const text = p.detail || '';
|
|
263
|
+
const words = tokenize(text).length;
|
|
264
|
+
// Count dimensions mentioned (numbered sections or bold headers)
|
|
265
|
+
const dimensions = (text.match(/\*\*\d+\./g) || []).length;
|
|
266
|
+
// Check for prediction
|
|
267
|
+
const hasPrediction = /prediction/i.test(text);
|
|
268
|
+
// Check for confidence
|
|
269
|
+
const hasConfidence = /confidence/i.test(text);
|
|
270
|
+
// Check for [REVISED] markers
|
|
271
|
+
const revisedCount = (text.match(/\[REVISED\]/g) || []).length;
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
agent: p.agent,
|
|
275
|
+
turnIndex: p.turnIndex ?? -1,
|
|
276
|
+
wordCount: words,
|
|
277
|
+
dimensions,
|
|
278
|
+
hasPrediction,
|
|
279
|
+
hasConfidence,
|
|
280
|
+
revisedCount,
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Measure evolution between consecutive profiles of same agent
|
|
285
|
+
const evolution = [];
|
|
286
|
+
const byAgent = {};
|
|
287
|
+
for (const p of profiles) {
|
|
288
|
+
if (!byAgent[p.agent]) byAgent[p.agent] = [];
|
|
289
|
+
byAgent[p.agent].push(p);
|
|
290
|
+
}
|
|
291
|
+
for (const [agent, agentProfiles] of Object.entries(byAgent)) {
|
|
292
|
+
agentProfiles.sort((a, b) => (a.turnIndex ?? 0) - (b.turnIndex ?? 0));
|
|
293
|
+
for (let i = 1; i < agentProfiles.length; i++) {
|
|
294
|
+
const prev = agentProfiles[i - 1].detail || '';
|
|
295
|
+
const curr = agentProfiles[i].detail || '';
|
|
296
|
+
evolution.push({
|
|
297
|
+
agent,
|
|
298
|
+
fromTurn: agentProfiles[i - 1].turnIndex ?? i - 1,
|
|
299
|
+
toTurn: agentProfiles[i].turnIndex ?? i,
|
|
300
|
+
editDist: editDistance(prev, curr),
|
|
301
|
+
cosine: 1 - cosineSimilarity(prev, curr),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return { profiles: profileData, evolution };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Measure 5: Intersubjective Coordination ─────────────────────────────
|
|
310
|
+
|
|
311
|
+
const AGREEMENT_MARKERS = [
|
|
312
|
+
/i agree/gi, /you're right/gi, /fair point/gi, /well said/gi,
|
|
313
|
+
/exactly/gi, /that's true/gi, /good catch/gi, /you're correct/gi,
|
|
314
|
+
/i'm relieved/gi, /helpful/gi,
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
const DISAGREEMENT_MARKERS = [
|
|
318
|
+
/i(?:'d| would) push back/gi, /i disagree/gi, /where i(?:'d| would) challenge/gi,
|
|
319
|
+
/but i think/gi, /i(?:'d| would) argue/gi, /that's not quite/gi,
|
|
320
|
+
/i(?:'m| am) not sure/gi, /overcorrect/gi, /too (?:strict|harsh|rigid)/gi,
|
|
321
|
+
/missed the point/gi, /doesn't capture/gi, /oversimplif/gi,
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
function measureIntersubjectiveCoordination(trace) {
|
|
325
|
+
const responses = trace.filter(e => e.agent === 'ego_intersubjective');
|
|
326
|
+
|
|
327
|
+
return responses.map(r => {
|
|
328
|
+
const text = r.detail || '';
|
|
329
|
+
const words = tokenize(text).length;
|
|
330
|
+
|
|
331
|
+
let agreeCount = 0;
|
|
332
|
+
for (const p of AGREEMENT_MARKERS) {
|
|
333
|
+
const matches = text.match(p);
|
|
334
|
+
agreeCount += matches ? matches.length : 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let disagreeCount = 0;
|
|
338
|
+
for (const p of DISAGREEMENT_MARKERS) {
|
|
339
|
+
const matches = text.match(p);
|
|
340
|
+
disagreeCount += matches ? matches.length : 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const total = agreeCount + disagreeCount;
|
|
344
|
+
return {
|
|
345
|
+
turnIndex: r.turnIndex ?? -1,
|
|
346
|
+
wordCount: words,
|
|
347
|
+
agreeCount,
|
|
348
|
+
disagreeCount,
|
|
349
|
+
disagreementRatio: total > 0 ? disagreeCount / total : 0,
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ── Measure 6: Behavioral Parameter Evolution ───────────────────────────
|
|
355
|
+
|
|
356
|
+
function measureBehavioralEvolution(trace) {
|
|
357
|
+
const overrides = trace.filter(e => e.agent === 'behavioral_overrides');
|
|
358
|
+
|
|
359
|
+
const parsed = overrides.map(o => {
|
|
360
|
+
try {
|
|
361
|
+
const params = JSON.parse(o.detail);
|
|
362
|
+
return {
|
|
363
|
+
turnIndex: o.turnIndex ?? -1,
|
|
364
|
+
rejectionThreshold: params.rejection_threshold,
|
|
365
|
+
maxRejections: params.max_rejections,
|
|
366
|
+
priorityCriteria: params.priority_criteria || [],
|
|
367
|
+
deprioritizedCriteria: params.deprioritized_criteria || [],
|
|
368
|
+
};
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}).filter(Boolean);
|
|
373
|
+
|
|
374
|
+
const evolution = [];
|
|
375
|
+
for (let i = 1; i < parsed.length; i++) {
|
|
376
|
+
const prev = parsed[i - 1];
|
|
377
|
+
const curr = parsed[i];
|
|
378
|
+
const prioChange = new Set([
|
|
379
|
+
...curr.priorityCriteria.filter(c => !prev.priorityCriteria.includes(c)),
|
|
380
|
+
...prev.priorityCriteria.filter(c => !curr.priorityCriteria.includes(c)),
|
|
381
|
+
]).size;
|
|
382
|
+
|
|
383
|
+
evolution.push({
|
|
384
|
+
fromTurn: prev.turnIndex,
|
|
385
|
+
toTurn: curr.turnIndex,
|
|
386
|
+
thresholdDelta: curr.rejectionThreshold - prev.rejectionThreshold,
|
|
387
|
+
priorityCriteriaChanged: prioChange,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { params: parsed, evolution };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ── Data Loading ────────────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
function loadDialogueTrace(dialogueId) {
|
|
397
|
+
if (!dialogueId) return null;
|
|
398
|
+
const files = fs.readdirSync(LOGS_DIR).filter(f => f.includes(dialogueId));
|
|
399
|
+
if (files.length === 0) return null;
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const data = JSON.parse(fs.readFileSync(path.join(LOGS_DIR, files[0]), 'utf-8'));
|
|
403
|
+
return data.dialogueTrace || [];
|
|
404
|
+
} catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ── Aggregation ─────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
function avg(arr) {
|
|
412
|
+
if (arr.length === 0) return 0;
|
|
413
|
+
return arr.reduce((s, v) => s + v, 0) / arr.length;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function sd(arr) {
|
|
417
|
+
if (arr.length < 2) return 0;
|
|
418
|
+
const mean = avg(arr);
|
|
419
|
+
return Math.sqrt(arr.reduce((s, v) => s + (v - mean) ** 2, 0) / (arr.length - 1));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function aggregateMeasures(allMeasures) {
|
|
423
|
+
const groups = {};
|
|
424
|
+
for (const m of allMeasures) {
|
|
425
|
+
const key = `${m.mechanism}|${m.condition}`;
|
|
426
|
+
if (!groups[key]) {
|
|
427
|
+
groups[key] = { mechanism: m.mechanism, condition: m.condition, measures: [] };
|
|
428
|
+
}
|
|
429
|
+
groups[key].measures.push(m);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const summary = [];
|
|
433
|
+
for (const [, group] of Object.entries(groups)) {
|
|
434
|
+
const measures = group.measures;
|
|
435
|
+
const n = measures.length;
|
|
436
|
+
|
|
437
|
+
// Revision magnitude
|
|
438
|
+
const revisions = measures.flatMap(m => m.revisionMagnitude);
|
|
439
|
+
const revEditDists = revisions.map(r => r.editDist);
|
|
440
|
+
|
|
441
|
+
// Self-reflection specificity
|
|
442
|
+
const reflections = measures.flatMap(m => m.reflectionSpecificity);
|
|
443
|
+
const egoReflections = reflections.filter(r => r.agent === 'ego_self_reflection');
|
|
444
|
+
const superegoReflections = reflections.filter(r => r.agent === 'superego_self_reflection');
|
|
445
|
+
|
|
446
|
+
// Cross-turn adaptation
|
|
447
|
+
const adaptations = measures.flatMap(m => m.crossTurnAdaptation);
|
|
448
|
+
const adaptEditDists = adaptations.map(a => a.editDist);
|
|
449
|
+
|
|
450
|
+
// Profile richness
|
|
451
|
+
const profiles = measures.flatMap(m => m.profileRichness.profiles);
|
|
452
|
+
const profileEvolutions = measures.flatMap(m => m.profileRichness.evolution);
|
|
453
|
+
|
|
454
|
+
// Intersubjective
|
|
455
|
+
const intersubjective = measures.flatMap(m => m.intersubjectiveCoordination);
|
|
456
|
+
|
|
457
|
+
// Behavioral
|
|
458
|
+
const behavioralEvolutions = measures.flatMap(m => m.behavioralEvolution.evolution);
|
|
459
|
+
|
|
460
|
+
// Between-run variance of final output
|
|
461
|
+
const finalMessages = measures.map(m => {
|
|
462
|
+
// Get last ego suggestion message
|
|
463
|
+
const revs = m.revisionMagnitude;
|
|
464
|
+
return revs.length > 0 ? revs[revs.length - 1] : null;
|
|
465
|
+
}).filter(Boolean);
|
|
466
|
+
|
|
467
|
+
// Pairwise cosine distances between runs
|
|
468
|
+
const pairwiseDists = [];
|
|
469
|
+
for (let i = 0; i < measures.length; i++) {
|
|
470
|
+
for (let j = i + 1; j < measures.length; j++) {
|
|
471
|
+
const mi = measures[i].finalMessage;
|
|
472
|
+
const mj = measures[j].finalMessage;
|
|
473
|
+
if (mi && mj) {
|
|
474
|
+
pairwiseDists.push(1 - cosineSimilarity(mi, mj));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
summary.push({
|
|
480
|
+
mechanism: group.mechanism,
|
|
481
|
+
condition: group.condition,
|
|
482
|
+
n,
|
|
483
|
+
revision: {
|
|
484
|
+
count: revisions.length,
|
|
485
|
+
avgEditDist: avg(revEditDists),
|
|
486
|
+
sdEditDist: sd(revEditDists),
|
|
487
|
+
},
|
|
488
|
+
egoReflection: {
|
|
489
|
+
count: egoReflections.length,
|
|
490
|
+
avgSpecificity: avg(egoReflections.map(r => r.specificityRatio)),
|
|
491
|
+
avgWordCount: avg(egoReflections.map(r => r.wordCount)),
|
|
492
|
+
},
|
|
493
|
+
superegoReflection: {
|
|
494
|
+
count: superegoReflections.length,
|
|
495
|
+
avgSpecificity: avg(superegoReflections.map(r => r.specificityRatio)),
|
|
496
|
+
avgWordCount: avg(superegoReflections.map(r => r.wordCount)),
|
|
497
|
+
},
|
|
498
|
+
adaptation: {
|
|
499
|
+
count: adaptations.length,
|
|
500
|
+
avgEditDist: avg(adaptEditDists),
|
|
501
|
+
sdEditDist: sd(adaptEditDists),
|
|
502
|
+
},
|
|
503
|
+
profiles: {
|
|
504
|
+
count: profiles.length,
|
|
505
|
+
avgWordCount: avg(profiles.map(p => p.wordCount)),
|
|
506
|
+
avgDimensions: avg(profiles.map(p => p.dimensions)),
|
|
507
|
+
evolutionCount: profileEvolutions.length,
|
|
508
|
+
avgEvolutionDist: avg(profileEvolutions.map(e => e.editDist)),
|
|
509
|
+
},
|
|
510
|
+
intersubjective: {
|
|
511
|
+
count: intersubjective.length,
|
|
512
|
+
avgDisagreement: avg(intersubjective.map(i => i.disagreementRatio)),
|
|
513
|
+
avgWordCount: avg(intersubjective.map(i => i.wordCount)),
|
|
514
|
+
},
|
|
515
|
+
behavioral: {
|
|
516
|
+
evolutionCount: behavioralEvolutions.length,
|
|
517
|
+
avgThresholdDelta: avg(behavioralEvolutions.map(e => e.thresholdDelta)),
|
|
518
|
+
avgCriteriaChanged: avg(behavioralEvolutions.map(e => e.priorityCriteriaChanged)),
|
|
519
|
+
},
|
|
520
|
+
betweenRunVariance: {
|
|
521
|
+
pairCount: pairwiseDists.length,
|
|
522
|
+
avgPairwiseDist: avg(pairwiseDists),
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return summary.sort((a, b) => a.mechanism.localeCompare(b.mechanism) || a.condition.localeCompare(b.condition));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ── Report Generation ───────────────────────────────────────────────────
|
|
531
|
+
|
|
532
|
+
function generateReport(runId, summary, allMeasures) {
|
|
533
|
+
const lines = [];
|
|
534
|
+
lines.push(`# Mechanism Process Trace Analysis: ${runId}`);
|
|
535
|
+
lines.push(`\nGenerated: ${new Date().toISOString()}`);
|
|
536
|
+
lines.push(`\nTotal dialogues analyzed: ${allMeasures.length}`);
|
|
537
|
+
|
|
538
|
+
// Overview table
|
|
539
|
+
lines.push('\n## Summary by Mechanism × Condition\n');
|
|
540
|
+
lines.push('| Mechanism | Cond | N | Rev Δ | Ego Spec | SE Spec | Adapt Δ | Profile WC | Intersub Disagree | Run Var |');
|
|
541
|
+
lines.push('|-----------|------|---|-------|----------|---------|---------|------------|-------------------|---------|');
|
|
542
|
+
|
|
543
|
+
for (const s of summary) {
|
|
544
|
+
lines.push([
|
|
545
|
+
'',
|
|
546
|
+
s.mechanism,
|
|
547
|
+
s.condition,
|
|
548
|
+
s.n,
|
|
549
|
+
s.revision.count > 0 ? s.revision.avgEditDist.toFixed(3) : '—',
|
|
550
|
+
s.egoReflection.count > 0 ? s.egoReflection.avgSpecificity.toFixed(2) : '—',
|
|
551
|
+
s.superegoReflection.count > 0 ? s.superegoReflection.avgSpecificity.toFixed(2) : '—',
|
|
552
|
+
s.adaptation.count > 0 ? s.adaptation.avgEditDist.toFixed(3) : '—',
|
|
553
|
+
s.profiles.count > 0 ? s.profiles.avgWordCount.toFixed(0) : '—',
|
|
554
|
+
s.intersubjective.count > 0 ? s.intersubjective.avgDisagreement.toFixed(2) : '—',
|
|
555
|
+
s.betweenRunVariance.pairCount > 0 ? s.betweenRunVariance.avgPairwiseDist.toFixed(3) : '—',
|
|
556
|
+
'',
|
|
557
|
+
].join(' | '));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Column legend
|
|
561
|
+
lines.push('\n**Column key:**');
|
|
562
|
+
lines.push('- **Rev Δ**: Avg normalized edit distance between ego generate → revise (0=identical, 1=completely different)');
|
|
563
|
+
lines.push('- **Ego Spec**: Ego self-reflection specificity ratio (learner-specific / total references)');
|
|
564
|
+
lines.push('- **SE Spec**: Superego self-reflection specificity ratio');
|
|
565
|
+
lines.push('- **Adapt Δ**: Avg cross-turn adaptation (edit distance between consecutive turn outputs)');
|
|
566
|
+
lines.push('- **Profile WC**: Avg word count of other-ego profiles');
|
|
567
|
+
lines.push('- **Intersub Disagree**: Avg disagreement ratio in intersubjective responses');
|
|
568
|
+
lines.push('- **Run Var**: Avg pairwise cosine distance between runs (higher = more between-run variance)');
|
|
569
|
+
|
|
570
|
+
// Detailed sections
|
|
571
|
+
lines.push('\n## Revision Magnitude by Mechanism\n');
|
|
572
|
+
lines.push('How much does superego feedback actually change the ego\'s output?\n');
|
|
573
|
+
for (const s of summary) {
|
|
574
|
+
if (s.revision.count === 0) continue;
|
|
575
|
+
lines.push(`- **${s.mechanism}** (${s.condition}): edit_dist=${s.revision.avgEditDist.toFixed(3)} ±${s.revision.sdEditDist.toFixed(3)} (N=${s.revision.count})`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
lines.push('\n## Self-Reflection Specificity\n');
|
|
579
|
+
lines.push('Does the reflection reference actual learner behavior (specific) or generic pedagogy?\n');
|
|
580
|
+
for (const s of summary) {
|
|
581
|
+
if (s.egoReflection.count === 0) continue;
|
|
582
|
+
lines.push(`- **${s.mechanism}** (${s.condition}): ego_specificity=${s.egoReflection.avgSpecificity.toFixed(2)}, ` +
|
|
583
|
+
`superego_specificity=${s.superegoReflection.avgSpecificity.toFixed(2)}, ` +
|
|
584
|
+
`ego_words=${s.egoReflection.avgWordCount.toFixed(0)}, superego_words=${s.superegoReflection.avgWordCount.toFixed(0)}`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
lines.push('\n## Cross-Turn Adaptation\n');
|
|
588
|
+
lines.push('How much does the tutor\'s output change between turns?\n');
|
|
589
|
+
for (const s of summary) {
|
|
590
|
+
if (s.adaptation.count === 0) continue;
|
|
591
|
+
lines.push(`- **${s.mechanism}** (${s.condition}): edit_dist=${s.adaptation.avgEditDist.toFixed(3)} ±${s.adaptation.sdEditDist.toFixed(3)} (N=${s.adaptation.count})`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
lines.push('\n## Profile Richness & Evolution\n');
|
|
595
|
+
lines.push('How detailed are other-ego profiles and how much do they change?\n');
|
|
596
|
+
for (const s of summary) {
|
|
597
|
+
if (s.profiles.count === 0) continue;
|
|
598
|
+
lines.push(`- **${s.mechanism}** (${s.condition}): avg_words=${s.profiles.avgWordCount.toFixed(0)}, ` +
|
|
599
|
+
`avg_dimensions=${s.profiles.avgDimensions.toFixed(1)}, ` +
|
|
600
|
+
`evolution_dist=${s.profiles.avgEvolutionDist.toFixed(3)} (N_profiles=${s.profiles.count}, N_evolutions=${s.profiles.evolutionCount})`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
lines.push('\n## Intersubjective Coordination\n');
|
|
604
|
+
lines.push('Does the ego agree with or push back against the superego?\n');
|
|
605
|
+
for (const s of summary) {
|
|
606
|
+
if (s.intersubjective.count === 0) continue;
|
|
607
|
+
lines.push(`- **${s.mechanism}** (${s.condition}): disagreement=${s.intersubjective.avgDisagreement.toFixed(2)}, ` +
|
|
608
|
+
`avg_words=${s.intersubjective.avgWordCount.toFixed(0)} (N=${s.intersubjective.count})`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
lines.push('\n## Between-Run Variance\n');
|
|
612
|
+
lines.push('Does the mechanism produce different outputs across runs of the same cell?\n');
|
|
613
|
+
for (const s of summary) {
|
|
614
|
+
if (s.betweenRunVariance.pairCount === 0) continue;
|
|
615
|
+
lines.push(`- **${s.mechanism}** (${s.condition}): avg_pairwise_cosine_dist=${s.betweenRunVariance.avgPairwiseDist.toFixed(3)} (${s.betweenRunVariance.pairCount} pairs)`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
lines.push('\n## Interpretation Guide\n');
|
|
619
|
+
lines.push('A mechanism that matters should show:');
|
|
620
|
+
lines.push('1. **High revision magnitude** — superego feedback actually changes output');
|
|
621
|
+
lines.push('2. **High specificity** — reflections reference THIS learner, not generic pedagogy');
|
|
622
|
+
lines.push('3. **High cross-turn adaptation** — tutor genuinely changes approach between turns');
|
|
623
|
+
lines.push('4. **Profile evolution** — profiles update with new information each turn');
|
|
624
|
+
lines.push('5. **Productive disagreement** — ego pushes back on superego, not just complying');
|
|
625
|
+
lines.push('6. **High between-run variance** — mechanism is context-sensitive, not formulaic');
|
|
626
|
+
lines.push('\nA mechanism that doesn\'t matter shows low/uniform values across all measures.');
|
|
627
|
+
|
|
628
|
+
return lines.join('\n');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// ── Main ────────────────────────────────────────────────────────────────
|
|
632
|
+
|
|
633
|
+
function main() {
|
|
634
|
+
const args = process.argv.slice(2);
|
|
635
|
+
const runId = args.find(a => !a.startsWith('--'));
|
|
636
|
+
if (!runId) {
|
|
637
|
+
console.error('Usage: node scripts/analyze-mechanism-traces.js <runId> [--output <path>] [--json] [--verbose]');
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const outputPath = args.includes('--output')
|
|
642
|
+
? args[args.indexOf('--output') + 1]
|
|
643
|
+
: `exports/mechanism-traces-${runId}.md`;
|
|
644
|
+
const outputJson = args.includes('--json');
|
|
645
|
+
const verbose = args.includes('--verbose');
|
|
646
|
+
|
|
647
|
+
const db = new Database(DB_PATH, { readonly: true });
|
|
648
|
+
|
|
649
|
+
// Load multi-turn results with dialogue IDs
|
|
650
|
+
const rows = db.prepare(`
|
|
651
|
+
SELECT id, scenario_id, profile_name, overall_score, dialogue_id, dialogue_rounds
|
|
652
|
+
FROM evaluation_results
|
|
653
|
+
WHERE run_id = ? AND success = 1 AND dialogue_id IS NOT NULL
|
|
654
|
+
ORDER BY profile_name, scenario_id
|
|
655
|
+
`).all(runId);
|
|
656
|
+
|
|
657
|
+
console.log(`Found ${rows.length} multi-turn dialogues for ${runId}`);
|
|
658
|
+
|
|
659
|
+
if (rows.length === 0) {
|
|
660
|
+
console.error('No multi-turn dialogues found. This script requires multi-turn data with dialogue traces.');
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const allMeasures = [];
|
|
665
|
+
let loaded = 0;
|
|
666
|
+
let skipped = 0;
|
|
667
|
+
|
|
668
|
+
for (const row of rows) {
|
|
669
|
+
const trace = loadDialogueTrace(row.dialogue_id);
|
|
670
|
+
if (!trace || trace.length === 0) {
|
|
671
|
+
skipped++;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
loaded++;
|
|
675
|
+
|
|
676
|
+
const mechanism = detectMechanism(row.profile_name);
|
|
677
|
+
const condition = isRecognition(row.profile_name) ? 'recog' : 'base';
|
|
678
|
+
|
|
679
|
+
const revisionMagnitude = measureRevisionMagnitude(trace);
|
|
680
|
+
const reflectionSpecificity = measureReflectionSpecificity(trace);
|
|
681
|
+
const crossTurnAdaptation = measureCrossTurnAdaptation(trace);
|
|
682
|
+
const profileRichness = measureProfileRichness(trace);
|
|
683
|
+
const intersubjectiveCoordination = measureIntersubjectiveCoordination(trace);
|
|
684
|
+
const behavioralEvolution = measureBehavioralEvolution(trace);
|
|
685
|
+
|
|
686
|
+
// Get final message for between-run variance
|
|
687
|
+
const lastRevision = [...trace].reverse().find(e =>
|
|
688
|
+
e.agent === 'ego' && (e.action === 'revise' || e.action === 'generate')
|
|
689
|
+
);
|
|
690
|
+
const finalMessage = lastRevision?.suggestions?.[0]?.message || '';
|
|
691
|
+
|
|
692
|
+
const measures = {
|
|
693
|
+
dialogueId: row.dialogue_id,
|
|
694
|
+
profileName: row.profile_name,
|
|
695
|
+
scenarioId: row.scenario_id,
|
|
696
|
+
score: row.overall_score,
|
|
697
|
+
mechanism,
|
|
698
|
+
condition,
|
|
699
|
+
revisionMagnitude,
|
|
700
|
+
reflectionSpecificity,
|
|
701
|
+
crossTurnAdaptation,
|
|
702
|
+
profileRichness,
|
|
703
|
+
intersubjectiveCoordination,
|
|
704
|
+
behavioralEvolution,
|
|
705
|
+
finalMessage,
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
allMeasures.push(measures);
|
|
709
|
+
|
|
710
|
+
if (verbose) {
|
|
711
|
+
console.log(` ${row.profile_name} | ${row.scenario_id} | ` +
|
|
712
|
+
`rev=${revisionMagnitude.length} refl=${reflectionSpecificity.length} ` +
|
|
713
|
+
`adapt=${crossTurnAdaptation.length} prof=${profileRichness.profiles.length} ` +
|
|
714
|
+
`inter=${intersubjectiveCoordination.length}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
console.log(`Loaded: ${loaded}, Skipped (no trace): ${skipped}`);
|
|
719
|
+
|
|
720
|
+
// Aggregate
|
|
721
|
+
const summary = aggregateMeasures(allMeasures);
|
|
722
|
+
|
|
723
|
+
// Generate report
|
|
724
|
+
const report = generateReport(runId, summary, allMeasures);
|
|
725
|
+
|
|
726
|
+
// Ensure output directory exists
|
|
727
|
+
const outputDir = path.dirname(outputPath);
|
|
728
|
+
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
729
|
+
|
|
730
|
+
fs.writeFileSync(outputPath, report);
|
|
731
|
+
console.log(`\nReport written to: ${outputPath}`);
|
|
732
|
+
|
|
733
|
+
if (outputJson) {
|
|
734
|
+
const jsonPath = outputPath.replace(/\.md$/, '.json');
|
|
735
|
+
// Remove finalMessage from JSON output (too large)
|
|
736
|
+
const cleanMeasures = allMeasures.map(m => {
|
|
737
|
+
const { finalMessage, ...rest } = m;
|
|
738
|
+
return rest;
|
|
739
|
+
});
|
|
740
|
+
fs.writeFileSync(jsonPath, JSON.stringify({ runId, summary, measures: cleanMeasures }, null, 2));
|
|
741
|
+
console.log(`JSON written to: ${jsonPath}`);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Print quick summary to console
|
|
745
|
+
console.log('\n── Quick Summary ──────────────────────────────────────');
|
|
746
|
+
console.log(`${'Mechanism'.padEnd(25)} ${'Cond'.padEnd(6)} ${'N'.padEnd(4)} ${'RevΔ'.padEnd(7)} ${'EgoSpec'.padEnd(8)} ${'AdaptΔ'.padEnd(7)} ${'RunVar'.padEnd(7)}`);
|
|
747
|
+
console.log('─'.repeat(70));
|
|
748
|
+
for (const s of summary) {
|
|
749
|
+
console.log([
|
|
750
|
+
s.mechanism.padEnd(25),
|
|
751
|
+
s.condition.padEnd(6),
|
|
752
|
+
String(s.n).padEnd(4),
|
|
753
|
+
s.revision.count > 0 ? s.revision.avgEditDist.toFixed(3).padEnd(7) : '—'.padEnd(7),
|
|
754
|
+
s.egoReflection.count > 0 ? s.egoReflection.avgSpecificity.toFixed(2).padEnd(8) : '—'.padEnd(8),
|
|
755
|
+
s.adaptation.count > 0 ? s.adaptation.avgEditDist.toFixed(3).padEnd(7) : '—'.padEnd(7),
|
|
756
|
+
s.betweenRunVariance.pairCount > 0 ? s.betweenRunVariance.avgPairwiseDist.toFixed(3) : '—',
|
|
757
|
+
].join(' '));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
db.close();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
main();
|