phaedo-mcp 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.
@@ -0,0 +1,599 @@
1
+ // ─── Phaedo Context Block ─────────────────────────────────────────────────────
2
+ //
3
+ // The single source of truth for the injected <phaedo_user_profile> block.
4
+ // Lifted out of content.js (Fork A, validation-harness Phase 1) so the exact
5
+ // same renderer is shared by:
6
+ // - content.js — builds the block and injects it into AI sessions
7
+ // - harness.js — shows the user "what the AI sees" (must be byte-identical)
8
+ //
9
+ // Classic script (no ES modules — loaded via manifest content_scripts before
10
+ // content.js, and via <script src> on harness.html). Exposes one namespace,
11
+ // `globalThis.PhaedoContext`, matching the `globalThis.Phaedo` / `window.phaedo*`
12
+ // convention used elsewhere. These functions are pure (no DOM/storage/window):
13
+ // `renderContextBlock(vaultData)` returns the exact injected string.
14
+
15
+ (function () {
16
+
17
+ // ─── Signal labels (human-readable, injected into context block) ─────────────
18
+
19
+ const SIGNAL_LABELS = {
20
+ // Surface
21
+ too_long: 'responses have been too long — prefers concise',
22
+ too_short: 'responses have been too short — prefers more detail',
23
+ avoid_bullets: 'prefers prose over bullet points',
24
+ prefer_bullets: 'prefers bullet-point formatting',
25
+ no_headers: 'prefers no section headers',
26
+ use_headers: 'prefers section headers for organization',
27
+ use_table: 'prefers tabular format when comparing',
28
+ use_code_block: 'prefers code wrapped in code blocks',
29
+ prefers_casual: 'prefers casual, conversational tone',
30
+ prefers_formal: 'prefers formal, professional tone',
31
+ more_direct: 'prefers direct, bottom-line-first communication',
32
+ no_preamble: 'prefers no intro or preamble — dive straight in',
33
+ no_examples: 'prefers no illustrative examples',
34
+ wants_examples: 'prefers concrete examples to illustrate concepts',
35
+ plainer_language: 'prefers plain language over jargon',
36
+ // Behavioral
37
+ fewer_questions: 'prefers AI to act without asking for confirmation',
38
+ wants_confirmation: 'prefers AI to check before acting',
39
+ show_reasoning: 'prefers AI to show its reasoning and approach',
40
+ skip_reasoning: 'prefers just the answer without explanation',
41
+ fewer_options: 'prefers a single recommendation over multiple options',
42
+ wants_options: 'prefers to see options and choose',
43
+ wants_proactive: 'prefers proactive suggestions beyond the question asked',
44
+ stay_scoped: 'prefers AI to answer exactly what was asked, nothing more',
45
+ iterate_more: 'prefers iterative refinement over single-shot answers',
46
+ // Domain
47
+ overexplaining: 'AI over-explained — user knows more than assumed',
48
+ too_technical: 'explanations were too technical for the context',
49
+ wants_depth: 'wants deeper technical detail and implementation specifics',
50
+ wants_sources: 'prefers cited sources and references',
51
+ conceptual_only: 'wants conceptual understanding, not implementation',
52
+ wants_implementation: 'wants concrete code and implementation steps',
53
+ too_broad: 'responses have been too broad — prefers focused scope',
54
+ // Temporal
55
+ urgent: 'frequently time-constrained — prefers brevity',
56
+ no_rush: 'prefers thoroughness over speed',
57
+ exploring_phase: 'often in exploratory mode — prefers high-level',
58
+ build_mode: 'often in build mode — prefers concrete and specific',
59
+ big_picture: 'prefers to start with strategy before details',
60
+ detail_mode: 'prefers to get into granular details',
61
+ // Collaboration
62
+ non_technical_audience: 'often writing for non-technical audiences',
63
+ technical_audience: 'often writing for technical audiences',
64
+ match_voice: 'wants AI to match their personal voice and style',
65
+ external_audience: 'often producing external or client-facing content',
66
+ too_jargony_for_audience: 'AI used jargon that was too complex for their audience',
67
+ // Decision
68
+ wants_decisiveness: 'prefers direct recommendations over hedged options',
69
+ wants_options_decision: 'prefers to see choices rather than a single recommendation',
70
+ wants_nuance: 'prefers responses that surface tradeoffs and risks',
71
+ wants_bold_call: 'prefers bold, definitive recommendations',
72
+ wants_conservative: 'prefers conservative, low-risk recommendations',
73
+ needs_certainty: 'prefers definitive answers over probabilistic ones',
74
+ rough_estimate_ok: 'comfortable with approximate answers and estimates',
75
+ // Creative
76
+ wants_creative: 'prefers unexpected, creative approaches',
77
+ wants_elegance: 'prefers elegant, simple solutions over complex ones',
78
+ wants_conventional: 'prefers conventional, straightforward approaches',
79
+ wants_originality: 'explicitly wants non-obvious angles',
80
+ // Meta
81
+ skip_meta: 'prefers AI not to narrate its own process',
82
+ show_meta: 'prefers AI to think out loud and show its approach',
83
+ wants_transparency: 'prefers AI to surface assumptions and reasoning',
84
+ less_second_guessing: 'prefers confident, committed responses',
85
+ };
86
+
87
+ // ─── Synthesis: derive cognitive dimensions from accumulated data ─────────────
88
+
89
+ function synthesizeCognitiveProfile(lp, sm) {
90
+ if (!lp || (lp.messageCount || 0) < 8) return null;
91
+
92
+ const n = lp.messageCount;
93
+ const profile = {};
94
+
95
+ // Communication stance: directive vs collaborative
96
+ // Pennebaker: high imperative + low collaborative = directive
97
+ if (n >= 10) {
98
+ const directiveScore = (lp.imperativeRate || 0) - (lp.collaborativeRate || 0) * 2;
99
+ if (directiveScore > 0.12)
100
+ profile.communicationStance = 'Directive — frames requests as clear tasks, prefers the AI to execute without dialogue';
101
+ else if (directiveScore < -0.04)
102
+ profile.communicationStance = 'Collaborative — thinks through problems in dialogue, values the exchange not just the output';
103
+ else
104
+ profile.communicationStance = 'Adaptive — shifts between directing and collaborating depending on task type';
105
+ }
106
+
107
+ // Processing style: analytical vs exploratory (Kahneman System 2 vs System 1 proxy)
108
+ // High causal reasoning + high certainty = analytical; high hedging + high questions = exploratory
109
+ if (n >= 12) {
110
+ const analyticalScore = (lp.causalRate || 0) * 3 + (lp.certaintyRate || 0);
111
+ const exploratoryScore = (lp.hedgeRate || 0) * 2 + Math.min((lp.avgQuestionCount || 0) / 4, 0.3);
112
+ if (analyticalScore > exploratoryScore * 1.4)
113
+ profile.processingStyle = 'Analytical — reasons causally, seeks mechanism and explanation before accepting output';
114
+ else if (exploratoryScore > analyticalScore * 1.4)
115
+ profile.processingStyle = 'Exploratory — thinks in questions, comfortable with open threads, iterates toward clarity';
116
+ else
117
+ profile.processingStyle = 'Pragmatic — shifts between analytical rigor and intuitive exploration based on stakes';
118
+ }
119
+
120
+ // Abstraction level: principles vs implementation (Construal Level Theory)
121
+ // Dominant question type: why = abstract, how = concrete, what-if = strategic
122
+ const qt = lp.questionTypes || {};
123
+ const qtTotal = (qt.what || 0) + (qt.how || 0) + (qt.why || 0) + (qt.whatIf || 0);
124
+ if (qtTotal >= 6) {
125
+ const whyRatio = (qt.why || 0) / qtTotal;
126
+ const howRatio = (qt.how || 0) / qtTotal;
127
+ const whatIfRatio = (qt.whatIf || 0) / qtTotal;
128
+ if (whyRatio > 0.35)
129
+ profile.abstractionLevel = 'Principles-first — asks why before how; needs conceptual understanding before implementation';
130
+ else if (howRatio > 0.45)
131
+ profile.abstractionLevel = 'Implementation-first — focused on mechanics and execution; concepts are a means to an end';
132
+ else if (whatIfRatio > 0.25)
133
+ profile.abstractionLevel = 'Strategic — thinks in scenarios and possibilities; naturally operates at the system level';
134
+ else
135
+ profile.abstractionLevel = 'Fluid — moves comfortably between conceptual and implementation levels';
136
+ }
137
+
138
+ // Depth preference: from message length
139
+ // Short messages (< 18 words avg) = compressed communicator; long (> 55) = context-provider
140
+ if (n >= 10) {
141
+ const wc = lp.avgWordCount || 0;
142
+ if (wc > 55)
143
+ profile.depthPreference = 'Context-provider — writes long, richly contextualised messages; processes detailed responses well';
144
+ else if (wc < 18)
145
+ profile.depthPreference = 'Compressed — communicates in terse, high-density messages; prefers responses that match';
146
+ else
147
+ profile.depthPreference = 'Calibrated — message length tracks task complexity; adapts naturally';
148
+ }
149
+
150
+ // Self-reference ratio → perspective frame (Pennebaker I-word research)
151
+ // High self-ref = personal/individual framing; low = systems/external framing
152
+ if (n >= 15) {
153
+ const sr = lp.selfReferenceRatio ?? 0.5;
154
+ if (sr > 0.72)
155
+ profile.perspectiveFrame = 'Personal frame — thinks and communicates in first-person terms; I-centric problem framing';
156
+ else if (sr < 0.35)
157
+ profile.perspectiveFrame = 'Systems frame — thinks in external terms and structures; we/it-centric problem framing';
158
+ else
159
+ profile.perspectiveFrame = 'Mixed frame — shifts between personal ownership and systemic thinking';
160
+ }
161
+
162
+ // Certainty tolerance: hedge rate vs number density (precision orientation)
163
+ if (n >= 15) {
164
+ const hedge = lp.hedgeRate || 0;
165
+ const number = lp.numberDensity || 0;
166
+ if (hedge < 0.04 && number > 0.025)
167
+ profile.certaintyTolerance = 'Precision-oriented — low hedging, quantitative framing; prefers definitive answers over probabilistic ones';
168
+ else if (hedge > 0.14)
169
+ profile.certaintyTolerance = 'Ambiguity-tolerant — comfortable with estimates and nuance; suspicious of false certainty';
170
+ else
171
+ profile.certaintyTolerance = 'Context-dependent — precise when stakes are high, approximate when speed matters';
172
+ }
173
+
174
+ // Vocabulary sophistication: avg word length proxy for expertise signal
175
+ // (Mairesse et al.: longer avg word = higher Openness, often correlates with expertise)
176
+ if (n >= 12) {
177
+ const awl = lp.avgWordLength || 0;
178
+ if (awl > 5.4)
179
+ profile.vocabularySophistication = 'High — uses long, precise, domain-specific vocabulary; calibrate explanations accordingly';
180
+ else if (awl < 4.2)
181
+ profile.vocabularySophistication = 'Accessible — favours common, direct words; prefers plain explanations';
182
+ else
183
+ profile.vocabularySophistication = 'Mixed — vocabulary varies with context and domain';
184
+ }
185
+
186
+ // Working pattern: from session metrics (only if enough sessions)
187
+ if (sm && (sm.sessionCount || 0) >= 3) {
188
+ const avgMsgs = sm.avgMessagesPerSession || 0;
189
+ const avgDur = sm.avgSessionDuration || 0;
190
+ const corrRate = sm.avgCorrectionRate || 0;
191
+ if (avgMsgs > 10 || avgDur > 720000)
192
+ profile.workingPattern = 'Deep-collaborator — long sessions, high message density; works problems through with the AI';
193
+ else if (avgMsgs < 4 && avgDur < 180000)
194
+ profile.workingPattern = 'Quick-lookup — short, targeted sessions; extracts specific information efficiently';
195
+ else if (corrRate > 0.2)
196
+ profile.workingPattern = 'Iterative-refiner — moderate sessions, frequent corrections; reaches quality through revision';
197
+ else
198
+ profile.workingPattern = 'Task-driven — sessions organized around discrete outcomes; moves on when done';
199
+
200
+ // Satisfaction signal: high positive rate = fingerprint is working
201
+ const posRate = sm.avgPositiveRate || 0;
202
+ if (posRate > 0.15)
203
+ profile.satisfactionSignal = 'Frequently confirms when output lands correctly — positive feedback rate is high';
204
+ else if ((sm.avgCorrectionRate || 0) > 0.25)
205
+ profile.satisfactionSignal = 'Frequent corrections — fingerprint should be weighted heavily to close the gap';
206
+ }
207
+
208
+ // Dominant question type as cognitive orientation
209
+ if (qtTotal >= 8) {
210
+ const dominant = Object.entries(qt).sort((a, b) => b[1] - a[1])[0];
211
+ const orientations = {
212
+ what: 'Knowledge-seeker — asks what-questions; builds mental models through facts and definitions',
213
+ how: 'Process-thinker — asks how-questions; learns through mechanisms and procedural understanding',
214
+ why: 'Causal-reasoner — asks why-questions; needs the reasoning chain before accepting conclusions',
215
+ whatIf: 'Scenario-planner — asks what-if questions; thinks in contingencies and strategic possibilities',
216
+ };
217
+ if (orientations[dominant[0]])
218
+ profile.questioningOrientation = orientations[dominant[0]];
219
+ }
220
+
221
+ return Object.keys(profile).length > 0 ? profile : null;
222
+ }
223
+
224
+ // Turn accumulated mental-model markers into a named-framework inventory.
225
+ // Returns one descriptive string for the context block, or null when there is
226
+ // not yet enough signal. Gated on message volume: a single message's markers
227
+ // are coarse — the reliability is in aggregation across the user's history.
228
+ function synthesizeMentalModels(lp) {
229
+ const markers = lp && lp.mentalModelMarkers;
230
+ const n = (lp && lp.messageCount) || 0;
231
+ if (!markers || n < 20) return null;
232
+
233
+ const FRAMEWORK_LABELS = {
234
+ first_principles: 'first-principles reasoning',
235
+ systems_thinking: 'systems thinking',
236
+ theory_of_constraints: 'constraint and bottleneck framing',
237
+ second_order_effects: 'second-order thinking',
238
+ tradeoff_reasoning: 'explicit trade-off analysis',
239
+ analogical_reasoning: 'reasoning by analogy',
240
+ decomposition: 'problem decomposition',
241
+ scenario_planning: 'scenario and contingency planning',
242
+ };
243
+
244
+ // Rank frameworks by occurrence; require a floor of 3 hits so one stray
245
+ // match does not surface a framework. Top 3 become the inventory.
246
+ const ranked = Object.entries(markers)
247
+ .filter(([fw, count]) => FRAMEWORK_LABELS[fw] && count >= 3)
248
+ .sort((a, b) => b[1] - a[1])
249
+ .slice(0, 3);
250
+
251
+ if (!ranked.length) return null;
252
+
253
+ const phrased = ranked.map(([fw, count]) => {
254
+ const rate = count / n;
255
+ const strength = rate >= 0.10 ? 'strongly' : rate >= 0.04 ? 'consistently' : 'occasionally';
256
+ return `${FRAMEWORK_LABELS[fw]} (${strength})`;
257
+ });
258
+
259
+ return `Reaches for: ${phrased.join(', ')} — frame analysis and recommendations through these lenses.`;
260
+ }
261
+
262
+ // ─── Profile synthesis helpers ────────────────────────────────────────────────
263
+
264
+ function scoreAnswerQuality(text) {
265
+ const t = (text || '').trim();
266
+ if (!t) return 'empty';
267
+ const words = (t.match(/\b[a-z']+\b/gi) || []).length;
268
+ if (words < 5) return 'sparse';
269
+ const hasPersonal = /\bI (\w+ed|was|had|found|learned|realized|struggled|built|chose|spent|tried)\b/i.test(t);
270
+ const hasSpecific = /\b\d+\b|\bfor example\b|\bhowever\b|\bbut\b|\bI think\b|\bI believe\b/i.test(t);
271
+ const hasInsight = /\b(learned that|in hindsight|looking back|I now|what changed|I realized)\b/i.test(t);
272
+ const signals = (hasPersonal ? 1 : 0) + (hasSpecific ? 1 : 0) + (hasInsight ? 1 : 0);
273
+ if (words >= 40 && signals >= 2) return 'rich';
274
+ if (words >= 15 && signals >= 1) return 'moderate';
275
+ return 'sparse';
276
+ }
277
+
278
+ function detectTechnicalProfile(fingerprint) {
279
+ const allText = Object.values(fingerprint.layers || {})
280
+ .flatMap(l => l.responses || [])
281
+ .map(r => r.answer || '')
282
+ .join(' ');
283
+ const TECH_RE = /\b(javascript|typescript|python|react|node\.?js|sql|postgres|mysql|mongodb|docker|kubernetes|aws|gcp|azure|git|graphql|rest|api|golang|rust|swift|kotlin|java|ruby|rails|django|vue|angular|next\.?js|terraform|webpack|vite|linux|bash|html|css|frontend|backend|fullstack|devops|microservices|serverless|machine learning|llm|algorithm|data structure)\b/gi;
284
+ const matches = allText.match(TECH_RE) || [];
285
+ const unique = new Set(matches.map(m => m.toLowerCase())).size;
286
+ const techs = [...new Set(matches.map(m => m.toLowerCase()))].slice(0, 10);
287
+ return { level: unique >= 5 ? 'power' : unique >= 2 ? 'technical' : 'general', techs };
288
+ }
289
+
290
+ function extractValueSignals(fingerprint) {
291
+ let hasValues = false, valueCount = 0, futureOriented = false, growthMinded = false;
292
+ for (const layer of Object.values(fingerprint.layers || {})) {
293
+ for (const { answer } of layer.responses || []) {
294
+ const t = (answer || '').trim();
295
+ if (!t) continue;
296
+ if (/\b(what matters (most )?(to me|for me)|I believe in|at my core|my (core )?(principle|value|belief)|I care (deeply|strongly )?(about|for)|what I (value|stand for)|fundamentally I|it('s| is) important to me|I('m| am) committed to)\b/i.test(t)) {
297
+ hasValues = true; valueCount++;
298
+ }
299
+ if (/\bI (want|hope|plan|aim|aspire) to\b|\bmy (goal|aim|ambition) is\b|\bI('m| am) working toward\b|\bI see myself\b/i.test(t))
300
+ futureOriented = true;
301
+ if (/\bI (struggled|failed|was wrong|made a mistake)\b|\b(in hindsight|looking back|the hardest part for me|where I went wrong)\b/i.test(t))
302
+ growthMinded = true;
303
+ }
304
+ }
305
+ return { hasValues, valueCount, futureOriented, growthMinded };
306
+ }
307
+
308
+ function buildLayerConfidence(fingerprint) {
309
+ return Object.values(fingerprint.layers || {}).map(layer => {
310
+ // A layer may be signals-only (extraction-built, no questionnaire responses).
311
+ const responses = Array.isArray(layer.responses) ? layer.responses : [];
312
+ const total = responses.length;
313
+ const answered = responses.filter(r => r && typeof r.answer === 'string' && r.answer.trim());
314
+ const qualities = answered.map(r => scoreAnswerQuality(r.answer));
315
+ const rich = qualities.filter(q => q === 'rich').length;
316
+ const moderate = qualities.filter(q => q === 'moderate').length;
317
+ let quality;
318
+ if (answered.length === 0) quality = 'empty';
319
+ else if (rich >= answered.length * 0.5) quality = 'rich';
320
+ else if ((rich + moderate) >= answered.length * 0.4) quality = 'moderate';
321
+ else quality = 'sparse';
322
+ return { label: layer.label, answered: answered.length, total, quality };
323
+ });
324
+ }
325
+
326
+ function synthesizeResponseInstructions(behavioralSignals, cognitiveProfile) {
327
+ const flat = {};
328
+ for (const layer of Object.values(behavioralSignals))
329
+ for (const [k, v] of Object.entries(layer)) flat[k] = (flat[k] || 0) + v;
330
+
331
+ const inst = { format: [], tone: [], depth: [], autonomy: [], decisions: [], pace: [] };
332
+
333
+ // Format
334
+ if (flat.too_long >= 2) inst.format.push(`Concise — corrected for length ${flat.too_long}×; keep tight`);
335
+ if (flat.too_short >= 2) inst.format.push(`Expansive — asked for more detail ${flat.too_short}×`);
336
+ if (flat.prefer_bullets) inst.format.push('Bullet points preferred for multi-part answers');
337
+ if (flat.avoid_bullets) inst.format.push('Prose preferred — user pushes back on bullets');
338
+ if (flat.use_headers) inst.format.push('Section headers preferred for long-form responses');
339
+ if (flat.no_headers) inst.format.push('No section headers');
340
+ if (flat.use_table) inst.format.push('Tables preferred for comparisons');
341
+ if (flat.use_code_block) inst.format.push('Always wrap code in code blocks');
342
+ if (flat.more_direct) inst.format.push(`Bottom-line-first — corrected for roundaboutness ${flat.more_direct}×`);
343
+ if (flat.no_preamble) inst.format.push('No intro or preamble — dive straight in');
344
+ if (!inst.format.length) {
345
+ if (cognitiveProfile?.depthPreference?.startsWith('Compressed'))
346
+ inst.format.push('Compressed communicator — keep responses tight and high-density');
347
+ else if (cognitiveProfile?.depthPreference?.startsWith('Context-provider'))
348
+ inst.format.push('Context-provider — can handle thorough, detailed responses');
349
+ }
350
+
351
+ // Tone
352
+ if (flat.prefers_casual) inst.tone.push('Casual, conversational register');
353
+ if (flat.prefers_formal) inst.tone.push('Formal, professional register');
354
+ if (flat.plainer_language) inst.tone.push('Plain language — minimize jargon');
355
+ if (flat.wants_examples) inst.tone.push(`Include concrete examples (explicitly requested ${flat.wants_examples}×)`);
356
+ if (flat.no_examples) inst.tone.push('Skip illustrative examples');
357
+ if (flat.match_voice) inst.tone.push('Match this user\'s voice and style when writing on their behalf');
358
+ if (flat.non_technical_audience) inst.tone.push('Often writing for non-technical audiences — adjust register accordingly');
359
+ if (flat.technical_audience) inst.tone.push('Often writing for technical audiences');
360
+
361
+ // Depth
362
+ if (flat.skip_reasoning) inst.depth.push(`Lead with the answer — reasoning has been skipped ${flat.skip_reasoning}×`);
363
+ if (flat.show_reasoning) inst.depth.push('Show your reasoning — user follows and values the logic chain');
364
+ if (flat.wants_depth) inst.depth.push(`Technical depth requested ${flat.wants_depth}× — go deep`);
365
+ if (flat.overexplaining) inst.depth.push('Skip fundamentals — user already knows them');
366
+ if (flat.too_technical) inst.depth.push('Simplify — was too technical for the context');
367
+ if (flat.conceptual_only) inst.depth.push('Stay conceptual — skip implementation detail');
368
+ if (flat.wants_implementation) inst.depth.push('Provide concrete code and implementation steps');
369
+ if (flat.wants_sources) inst.depth.push('Cite sources and references');
370
+
371
+ // Autonomy
372
+ if (flat.fewer_questions) inst.autonomy.push(`Act without checking — flagged over-confirmation ${flat.fewer_questions}×`);
373
+ if (flat.wants_confirmation) inst.autonomy.push('Confirm before acting — user prefers to verify assumptions');
374
+ if (flat.stay_scoped) inst.autonomy.push(`Answer exactly what was asked, nothing more (corrected ${flat.stay_scoped}×)`);
375
+ if (flat.wants_proactive) inst.autonomy.push('Proactively surface related considerations');
376
+ if (flat.iterate_more) inst.autonomy.push('Iterative mode — expect refinement, not single-shot completion');
377
+ if (flat.fewer_options) inst.autonomy.push('Single recommendation — user dislikes option overload');
378
+ if (flat.wants_options) inst.autonomy.push('Present options — user wants to choose');
379
+ if (flat.skip_meta) inst.autonomy.push("Don't narrate your process — just do it");
380
+ if (flat.show_meta) inst.autonomy.push('Think out loud — show your approach before executing');
381
+ if (flat.wants_transparency) inst.autonomy.push('Surface assumptions and rationale explicitly');
382
+
383
+ // Decisions
384
+ if (flat.wants_decisiveness || flat.wants_bold_call || flat.less_second_guessing)
385
+ inst.decisions.push('Be definitive — user has pushed back on hedging; commit to recommendations');
386
+ if (flat.wants_nuance) inst.decisions.push('Surface tradeoffs and risks — user wants the full picture');
387
+ if (flat.wants_conservative) inst.decisions.push('Conservative options preferred');
388
+ if (flat.needs_certainty) inst.decisions.push('Definitive answers over probabilistic ones');
389
+ if (flat.rough_estimate_ok) inst.decisions.push('Approximations are acceptable');
390
+ if (flat.wants_creative) inst.decisions.push('Unexpected, creative approaches welcome');
391
+ if (flat.wants_elegance) inst.decisions.push('Elegant, simple solutions over complex ones');
392
+ if (flat.wants_conventional) inst.decisions.push('Conventional, proven approaches preferred');
393
+
394
+ // Pace
395
+ if (flat.urgent >= 2) inst.pace.push(`Often time-constrained (flagged ${flat.urgent}×) — lead with essentials`);
396
+ if (flat.no_rush) inst.pace.push('Thoroughness over speed — complete answers preferred');
397
+ if (flat.big_picture) inst.pace.push('Strategy before detail — start at the macro level');
398
+ if (flat.detail_mode) inst.pace.push('Detail mode — user wants granular, specific output');
399
+ if (flat.too_broad) inst.pace.push(`Too broad — narrow scope and focus (corrected ${flat.too_broad}×)`);
400
+
401
+ return inst;
402
+ }
403
+
404
+ // ─── Context block assembly ───────────────────────────────────────────────────
405
+
406
+ function buildContextBlock(fingerprint, behavioralSignals = {}, linguisticProfile = {}, cognitiveProfile = null, sessionMetrics = {}, mentalModels = null) {
407
+ const lines = ['<phaedo_user_profile>'];
408
+
409
+ // ── 1. Response instructions (derived from observed behavioral corrections) ──
410
+ const inst = synthesizeResponseInstructions(behavioralSignals, cognitiveProfile);
411
+ const hasInst = Object.values(inst).some(a => a.length > 0);
412
+ if (hasInst) {
413
+ lines.push('\n## How to respond to this user');
414
+ if (inst.format.length) { lines.push('Format:'); inst.format.forEach(i => lines.push(` - ${i}`)); }
415
+ if (inst.tone.length) { lines.push('Tone:'); inst.tone.forEach(i => lines.push(` - ${i}`)); }
416
+ if (inst.depth.length) { lines.push('Depth:'); inst.depth.forEach(i => lines.push(` - ${i}`)); }
417
+ if (inst.autonomy.length) { lines.push('Autonomy:'); inst.autonomy.forEach(i => lines.push(` - ${i}`)); }
418
+ if (inst.decisions.length) { lines.push('Decisions:'); inst.decisions.forEach(i => lines.push(` - ${i}`)); }
419
+ if (inst.pace.length) { lines.push('Pace/scope:'); inst.pace.forEach(i => lines.push(` - ${i}`)); }
420
+ }
421
+
422
+ // ── 2. Cognitive model (inferred from linguistic patterns) ────────────────
423
+ if ((cognitiveProfile && Object.keys(cognitiveProfile).length > 0) || mentalModels) {
424
+ lines.push('\n## How this user thinks');
425
+ if (cognitiveProfile) for (const desc of Object.values(cognitiveProfile)) lines.push(`- ${desc}`);
426
+ if (mentalModels) lines.push(`- ${mentalModels}`);
427
+ const dist = linguisticProfile.platformDistribution;
428
+ if (dist) {
429
+ const sorted = Object.entries(dist).sort((a, b) => b[1] - a[1]);
430
+ const total = sorted.reduce((s, [, n]) => s + n, 0);
431
+ if (sorted.length && total > 0) {
432
+ const [top, count] = sorted[0];
433
+ lines.push(`- Primarily uses ${top} (${Math.round((count / total) * 100)}% of observed sessions)`);
434
+ }
435
+ }
436
+ }
437
+
438
+ // ── 3. Technical profile (detected from interview answers) ────────────────
439
+ const tech = detectTechnicalProfile(fingerprint);
440
+ if (tech.level !== 'general') {
441
+ lines.push('\n## Technical profile');
442
+ lines.push(tech.level === 'power'
443
+ ? '- Power user — skip basics, assume domain knowledge, provide code-level specificity'
444
+ : '- Technical — comfortable with technical content; some domain knowledge assumed');
445
+ if (tech.techs.length > 0)
446
+ lines.push(`- Technologies mentioned: ${tech.techs.join(', ')}`);
447
+ }
448
+
449
+ // ── 4. Values and motivations (extracted from interview answers) ──────────
450
+ const values = extractValueSignals(fingerprint);
451
+ if (values.hasValues || values.futureOriented || values.growthMinded) {
452
+ lines.push('\n## Values and motivations');
453
+ if (values.hasValues)
454
+ lines.push(`- Expresses held principles in their answers (${values.valueCount} instance${values.valueCount !== 1 ? 's' : ''}) — engage with what matters to them`);
455
+ if (values.futureOriented)
456
+ lines.push('- Future-oriented — references goals and where they are headed; connect responses to their direction');
457
+ if (values.growthMinded)
458
+ lines.push('- Growth-minded — acknowledges failure and reflects honestly; does not need false positivity');
459
+ }
460
+
461
+ // ── 5. Interview answers (supporting evidence) ────────────────────────────
462
+ const layerConf = buildLayerConfidence(fingerprint);
463
+ if (layerConf.some(l => l.answered > 0)) {
464
+ lines.push('\n## Interview answers');
465
+ for (const [layerId, layer] of Object.entries(fingerprint.layers)) {
466
+ // A layer may be extraction-only (added by Loop 2 reconcile) with no
467
+ // questionnaire responses — guard before filtering.
468
+ const answered = (layer.responses || []).filter(r => r.answer.trim());
469
+ if (!answered.length) continue;
470
+ lines.push(`\n### ${layer.label}`);
471
+ for (const { label, question, answer } of answered)
472
+ lines.push(`${label || question}: ${answer.trim()}`);
473
+ const layerSigs = behavioralSignals[layerId];
474
+ if (layerSigs) {
475
+ const observed = Object.entries(layerSigs)
476
+ .map(([sig, count]) => { const lbl = SIGNAL_LABELS[sig]; return lbl ? (count > 1 ? `${lbl} (×${count})` : lbl) : null; })
477
+ .filter(Boolean);
478
+ if (observed.length) lines.push(`Observed: ${observed.join('; ')}`);
479
+ }
480
+ const quals = answered.map(r => scoreAnswerQuality(r.answer));
481
+ const richN = quals.filter(q => q === 'rich').length;
482
+ const modN = quals.filter(q => q === 'moderate').length;
483
+ const layerQ = richN >= answered.length * 0.5 ? 'high'
484
+ : (richN + modN) >= answered.length * 0.4 ? 'moderate'
485
+ : 'sparse';
486
+ lines.push(`Confidence: ${layerQ}`);
487
+ }
488
+ }
489
+
490
+ // ── 5b. Learned from observed conversations (Loop 2 extraction) ───────────
491
+ // Each layer's `summary` is rendered by the extraction projection/reconcile
492
+ // step (anti-preference precedence already applied). Inject it as-is — no
493
+ // module import needed, the text is precomputed and stored on the layer.
494
+ const learnedLines = [];
495
+ for (const [layerId, layer] of Object.entries(fingerprint.layers)) {
496
+ if (layer && typeof layer.summary === 'string' && layer.summary.trim()) {
497
+ learnedLines.push(`\n### ${layer.label || layerId.replace(/_/g, ' ')}`);
498
+ learnedLines.push(layer.summary.trim());
499
+ }
500
+ }
501
+ if (learnedLines.length) {
502
+ lines.push('\n## Learned from your conversations');
503
+ lines.push(...learnedLines);
504
+ }
505
+
506
+ // ── 6. Confidence summary ─────────────────────────────────────────────────
507
+ lines.push('\n## Fingerprint confidence');
508
+ const msgCount = linguisticProfile.messageCount || 0;
509
+ const sessCount = sessionMetrics.sessionCount || 0;
510
+ const totalAns = layerConf.reduce((s, l) => s + l.answered, 0);
511
+ const totalQ = layerConf.reduce((s, l) => s + l.total, 0);
512
+ const richCount = layerConf.filter(l => l.quality === 'rich').length;
513
+ const modCount = layerConf.filter(l => l.quality === 'moderate').length;
514
+
515
+ lines.push(`- Interview: ${totalAns}/${totalQ} answered · ${richCount} rich layer${richCount !== 1 ? 's' : ''}, ${modCount} moderate`);
516
+ if (sessCount > 0 || msgCount > 0)
517
+ lines.push(`- Behavioral: ${sessCount} session${sessCount !== 1 ? 's' : ''} · ${msgCount.toLocaleString()} messages observed`);
518
+
519
+ const confidence =
520
+ msgCount > 300 && richCount >= 3 ? 'high' :
521
+ msgCount > 80 || richCount >= 2 ? 'medium' :
522
+ totalAns > 0 || msgCount > 0 ? 'building' : 'minimal';
523
+
524
+ const guidance = {
525
+ high: 'weight behavioral profile heavily; interview confirms and anchors it',
526
+ medium: 'blend interview and behavioral signals equally',
527
+ building: 'rely primarily on interview answers; behavioral data still accumulating',
528
+ minimal: 'minimal data — treat all signals as preliminary',
529
+ }[confidence];
530
+
531
+ lines.push(`- Overall: ${confidence} — ${guidance}`);
532
+ lines.push('\n</phaedo_user_profile>');
533
+ return lines.join('\n');
534
+ }
535
+
536
+ // ─── Glue: vault payload → injected string ────────────────────────────────────
537
+ // The canonical derivation (mirrors the old content.js rebuildContext): read the
538
+ // four vault keys, synthesize the derived cognitive profile + mental models, and
539
+ // render the block. Returns '' when there is no fingerprint yet. Pure — the
540
+ // Gemini documentElement side-effect stays in content.js.
541
+ // ── Input contract ────────────────────────────────────────────────────────────
542
+ // The vault-data object renderContextBlock consumes. A producer feeding a
543
+ // malformed shape used to silently yield an empty/degraded block; validate it so
544
+ // the failure is loud (or throwable) instead of silent.
545
+ function isPlainObject(x) { return x !== null && typeof x === 'object' && !Array.isArray(x); }
546
+ function ctxWarn(msg) { try { if (typeof console !== 'undefined' && console.warn) console.warn('[PhaedoContext] ' + msg); } catch (_) {} }
547
+
548
+ // Returns { ok, empty, errors }. `empty` (no fingerprint) is a LEGITIMATE
549
+ // nothing-to-render case (e.g. a fresh user), not a violation. `ok:false` means a
550
+ // real contract breach (wrong types) — the caller should treat that as a bug.
551
+ function validateVaultData(vd) {
552
+ if (!isPlainObject(vd)) return { ok: false, empty: false, errors: ['vault data must be a plain object'] };
553
+ const errors = [];
554
+ const fp = vd.phaedo_fingerprint;
555
+ const empty = fp === undefined || fp === null;
556
+ if (!empty) {
557
+ if (!isPlainObject(fp)) errors.push('phaedo_fingerprint must be an object');
558
+ else {
559
+ if (!isPlainObject(fp.layers)) errors.push('phaedo_fingerprint.layers must be an object');
560
+ else for (const [id, L] of Object.entries(fp.layers)) if (!isPlainObject(L)) errors.push(`layer "${id}" must be an object`);
561
+ if (fp.persona_strength !== undefined && (typeof fp.persona_strength !== 'number' || fp.persona_strength < 0 || fp.persona_strength > 1))
562
+ errors.push('persona_strength must be a number in [0,1]');
563
+ if (fp.fingerprint_id !== undefined && fp.fingerprint_id !== null && typeof fp.fingerprint_id !== 'string')
564
+ errors.push('fingerprint_id must be a string');
565
+ }
566
+ }
567
+ for (const k of ['phaedo_behavioral_signals', 'phaedo_linguistic_profile', 'phaedo_session_metrics'])
568
+ if (vd[k] !== undefined && vd[k] !== null && !isPlainObject(vd[k])) errors.push(`${k} must be an object`);
569
+ return { ok: errors.length === 0, empty, errors };
570
+ }
571
+
572
+ function renderContextBlock(vd, opts) {
573
+ const v = validateVaultData(vd);
574
+ if (!v.ok) {
575
+ const msg = 'renderContextBlock got invalid vault data: ' + v.errors.join('; ');
576
+ if (opts && opts.strict) throw new Error('[PhaedoContext] ' + msg);
577
+ ctxWarn(msg); // loud, not silent — a malformed producer is a bug
578
+ return ''; // …but still degrade gracefully (don't crash the page)
579
+ }
580
+ const fingerprint = vd.phaedo_fingerprint;
581
+ if (!fingerprint) return ''; // v.empty — legitimately nothing to render
582
+ const behavioralSignals = vd.phaedo_behavioral_signals || {};
583
+ const linguisticProfile = vd.phaedo_linguistic_profile || {};
584
+ const sessionMetrics = vd.phaedo_session_metrics || {};
585
+ const cognitiveProfile = synthesizeCognitiveProfile(linguisticProfile, sessionMetrics);
586
+ const mentalModels = synthesizeMentalModels(linguisticProfile);
587
+ return buildContextBlock(fingerprint, behavioralSignals, linguisticProfile, cognitiveProfile, sessionMetrics, mentalModels);
588
+ }
589
+
590
+ globalThis.PhaedoContext = {
591
+ buildContextBlock,
592
+ synthesizeCognitiveProfile,
593
+ synthesizeMentalModels,
594
+ renderContextBlock,
595
+ validateVaultData,
596
+ SIGNAL_LABELS,
597
+ };
598
+
599
+ })();