dopple-ai 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 @@
1
+ {"version":3,"sources":["../src/graph/query.ts"],"sourcesContent":["/**\n * Graph query utilities.\n *\n * Get neighborhoods, find patterns, compile persona-relevant context.\n */\n\nimport type { OceanVector, LLMProvider } from \"../types.js\";\nimport type { KnowledgeGraph, GraphNode, GraphEdge, GraphPattern } from \"./types.js\";\n\n/**\n * Get all nodes within N hops of a starting node.\n */\nexport function getNeighborhood(\n graph: KnowledgeGraph,\n nodeId: string,\n depth: number = 2\n): GraphNode[] {\n const visited = new Set<string>();\n const queue: Array<{ id: string; d: number }> = [{ id: nodeId, d: 0 }];\n\n while (queue.length > 0) {\n const { id, d } = queue.shift()!;\n if (visited.has(id) || d > depth) continue;\n visited.add(id);\n\n // Find connected nodes\n for (const edge of graph.edges) {\n if (edge.from === id && !visited.has(edge.to)) {\n queue.push({ id: edge.to, d: d + 1 });\n }\n if (edge.to === id && !visited.has(edge.from)) {\n queue.push({ id: edge.from, d: d + 1 });\n }\n }\n }\n\n return graph.nodes.filter((n) => visited.has(n.id));\n}\n\n/**\n * Find common patterns in the graph.\n * Looks for: frequent co-occurrences, hub nodes, churn patterns.\n */\nexport function findPatterns(graph: KnowledgeGraph): GraphPattern[] {\n const patterns: GraphPattern[] = [];\n\n // Find hub nodes (high degree)\n const degree: Record<string, number> = {};\n for (const edge of graph.edges) {\n degree[edge.from] = (degree[edge.from] ?? 0) + 1;\n degree[edge.to] = (degree[edge.to] ?? 0) + 1;\n }\n\n const hubs = Object.entries(degree)\n .filter(([, d]) => d >= 3)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5);\n\n for (const [nodeId, d] of hubs) {\n const node = graph.nodes.find((n) => n.id === nodeId);\n if (node) {\n patterns.push({\n description: `\"${node.label}\" is a hub node (${d} connections) — many users/entities relate to it`,\n nodes: [nodeId],\n edges: graph.edges.filter((e) => e.from === nodeId || e.to === nodeId).map((e) => e.id),\n frequency: d,\n significance: Math.min(1, d / 10),\n });\n }\n }\n\n // Find churn patterns (nodes connected to \"cancelled\" edges)\n const churnEdges = graph.edges.filter((e) => e.type === \"cancelled\" || e.type === \"churned_because\");\n if (churnEdges.length > 0) {\n const churnReasons = churnEdges\n .map((e) => {\n const target = graph.nodes.find((n) => n.id === e.to);\n return target?.label;\n })\n .filter(Boolean);\n\n if (churnReasons.length > 0) {\n patterns.push({\n description: `Churn pattern: ${churnReasons.slice(0, 3).join(\", \")}`,\n nodes: churnEdges.map((e) => e.to),\n edges: churnEdges.map((e) => e.id),\n frequency: churnEdges.length,\n significance: Math.min(1, churnEdges.length / 5),\n });\n }\n }\n\n // Find complaint clusters\n const complaintNodes = graph.nodes.filter((n) => n.type === \"complaint\");\n if (complaintNodes.length >= 2) {\n patterns.push({\n description: `${complaintNodes.length} complaints identified: ${complaintNodes.slice(0, 3).map((n) => `\"${n.label}\"`).join(\", \")}`,\n nodes: complaintNodes.map((n) => n.id),\n edges: [],\n frequency: complaintNodes.length,\n significance: Math.min(1, complaintNodes.length / 5),\n });\n }\n\n // Find feature usage patterns\n const eventNodes = graph.nodes.filter((n) => n.type === \"event\");\n const topEvents = eventNodes\n .sort((a, b) => (Number(b.properties.totalOccurrences) ?? 0) - (Number(a.properties.totalOccurrences) ?? 0))\n .slice(0, 5);\n\n if (topEvents.length >= 2) {\n patterns.push({\n description: `Top features: ${topEvents.map((n) => `${n.label} (${n.properties.totalOccurrences}x)`).join(\", \")}`,\n nodes: topEvents.map((n) => n.id),\n edges: [],\n frequency: topEvents.length,\n significance: 0.7,\n });\n }\n\n return patterns.sort((a, b) => b.significance - a.significance);\n}\n\n/**\n * Get persona-relevant context from the graph.\n *\n * Given a persona's OCEAN traits, find the most relevant graph neighborhood\n * and compile it into context strings the persona can reference.\n *\n * High openness personas → get innovation/feature nodes\n * High neuroticism personas → get complaint/churn nodes\n * High conscientiousness personas → get metric/reliability nodes\n */\nexport function getPersonaContext(\n graph: KnowledgeGraph,\n traits: OceanVector\n): string[] {\n const context: string[] = [];\n\n // Everyone gets the key metrics\n const metricNodes = graph.nodes.filter((n) => n.type === \"metric\");\n for (const m of metricNodes) {\n context.push(m.label);\n }\n\n // High openness → features, innovation, new things\n if (traits.openness > 0.6) {\n const featureNodes = graph.nodes.filter(\n (n) => n.type === \"feature\" || n.type === \"event\"\n );\n for (const f of featureNodes.slice(0, 5)) {\n const count = f.properties.totalOccurrences;\n context.push(\n count\n ? `Feature \"${f.label}\" is used ${count} times`\n : `Feature: ${f.label}`\n );\n }\n }\n\n // High neuroticism → complaints, churn, negative signals\n if (traits.neuroticism > 0.6) {\n const complaints = graph.nodes.filter((n) => n.type === \"complaint\");\n for (const c of complaints.slice(0, 5)) {\n context.push(`User complaint: \"${c.label}\"`);\n }\n\n const churnEdges = graph.edges.filter(\n (e) => e.type === \"cancelled\" || e.type === \"churned_because\"\n );\n if (churnEdges.length > 0) {\n context.push(`${churnEdges.length} users have cancelled`);\n }\n }\n\n // High conscientiousness → data, metrics, reliability\n if (traits.conscientiousness > 0.6) {\n const discovered = graph.edges.filter(\n (e) => e.properties.discovered === true\n );\n for (const d of discovered.slice(0, 3)) {\n const from = graph.nodes.find((n) => n.id === d.from);\n const to = graph.nodes.find((n) => n.id === d.to);\n if (from && to) {\n context.push(`Pattern: \"${from.label}\" ${d.type} \"${to.label}\"`);\n }\n }\n }\n\n // High agreeableness → social proof, what others do\n if (traits.agreeableness > 0.6) {\n const hubEdges = Object.entries(\n graph.edges.reduce(\n (acc, e) => {\n acc[e.to] = (acc[e.to] ?? 0) + 1;\n return acc;\n },\n {} as Record<string, number>\n )\n )\n .sort((a, b) => b[1] - a[1])\n .slice(0, 3);\n\n for (const [nodeId, count] of hubEdges) {\n const node = graph.nodes.find((n) => n.id === nodeId);\n if (node && count >= 2) {\n context.push(`${count} users interact with \"${node.label}\"`);\n }\n }\n }\n\n // High extraversion → popular features, social features\n if (traits.extraversion > 0.6) {\n const socialEvents = graph.nodes.filter(\n (n) =>\n n.type === \"event\" &&\n /share|invite|collab|team|social/i.test(n.label)\n );\n for (const s of socialEvents.slice(0, 3)) {\n context.push(`Social feature: \"${s.label}\" (${s.properties.totalOccurrences ?? 0} uses)`);\n }\n }\n\n return context;\n}\n\n/**\n * Human-readable graph summary.\n */\nexport function graphSummary(graph: KnowledgeGraph): string {\n const typeCounts: Record<string, number> = {};\n for (const node of graph.nodes) {\n typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1;\n }\n\n const edgeTypes: Record<string, number> = {};\n for (const edge of graph.edges) {\n edgeTypes[edge.type] = (edgeTypes[edge.type] ?? 0) + 1;\n }\n\n const lines: string[] = [\n `Knowledge graph: ${graph.metadata.nodeCount} nodes, ${graph.metadata.edgeCount} edges`,\n `Sources: ${graph.metadata.sources.join(\", \")}`,\n `Node types: ${Object.entries(typeCounts).map(([t, c]) => `${t}(${c})`).join(\", \")}`,\n `Edge types: ${Object.entries(edgeTypes).map(([t, c]) => `${t}(${c})`).join(\", \")}`,\n ];\n\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Natural language graph query\n// ---------------------------------------------------------------------------\n\nexport interface GraphQueryResult {\n question: string;\n answer: string;\n evidence: Array<{\n type: \"node\" | \"edge\" | \"pattern\";\n label: string;\n detail: string;\n relevance: number;\n }>;\n confidence: number;\n relatedNodes: string[];\n}\n\n/**\n * Ask the knowledge graph a natural language question.\n *\n * The LLM interprets the question, identifies relevant nodes/edges/patterns,\n * and synthesizes an answer grounded in the graph data.\n *\n * @example\n * ```ts\n * const result = await queryGraph(llm, graph, \"Why are users churning?\");\n * console.log(result.answer); // \"Users are primarily churning due to...\"\n * console.log(result.evidence); // specific nodes/edges that support the answer\n * ```\n */\nexport async function queryGraph(\n llm: LLMProvider,\n graph: KnowledgeGraph,\n question: string\n): Promise<GraphQueryResult> {\n // Serialize graph into a compact representation the LLM can reason over\n const graphContext = serializeGraphForLLM(graph);\n const patterns = findPatterns(graph);\n const patternText = patterns.length > 0\n ? `\\nDiscovered patterns:\\n${patterns.map((p) => `- ${p.description} (significance: ${p.significance.toFixed(2)})`).join(\"\\n\")}`\n : \"\";\n\n const result = await llm.generateJSON<{\n answer: string;\n evidence: Array<{\n type: \"node\" | \"edge\" | \"pattern\";\n label: string;\n detail: string;\n relevance: number;\n }>;\n confidence: number;\n relatedNodes: string[];\n }>(\n `You are a data analyst answering questions about a product's user base using a knowledge graph. The graph contains real user data — users, events, features, complaints, payments, and their relationships.\n\nGround every claim in specific graph data. If the graph doesn't contain enough information to answer confidently, say so and explain what data would help.`,\n `Knowledge Graph:\n${graphContext}\n${patternText}\n\nQuestion: \"${question}\"\n\nAnalyze the graph to answer this question. Return JSON:\n{\n \"answer\": \"<2-4 sentence answer grounded in the graph data>\",\n \"evidence\": [\n {\"type\": \"node|edge|pattern\", \"label\": \"<entity or relationship>\", \"detail\": \"<how this supports the answer>\", \"relevance\": <0-1>}\n ],\n \"confidence\": <0.0-1.0 based on how much graph data supports the answer>,\n \"relatedNodes\": [\"<labels of the most relevant nodes for follow-up questions>\"]\n}`\n );\n\n return {\n question,\n ...result,\n };\n}\n\n/**\n * Serialize the graph into a compact text representation for LLM context.\n * Keeps it under ~4K tokens by summarizing large graphs.\n */\nfunction serializeGraphForLLM(graph: KnowledgeGraph): string {\n const lines: string[] = [];\n lines.push(`${graph.metadata.nodeCount} nodes, ${graph.metadata.edgeCount} edges`);\n\n // Group nodes by type\n const byType: Record<string, GraphNode[]> = {};\n for (const node of graph.nodes) {\n if (!byType[node.type]) byType[node.type] = [];\n byType[node.type].push(node);\n }\n\n for (const [type, nodes] of Object.entries(byType)) {\n const limit = type === \"user\" ? 5 : 15; // Cap user nodes to avoid bloat\n const shown = nodes.slice(0, limit);\n const remaining = nodes.length - shown.length;\n\n lines.push(`\\n[${type.toUpperCase()}] (${nodes.length} total)`);\n for (const n of shown) {\n const props = Object.entries(n.properties)\n .filter(([k]) => !k.startsWith(\"_\") && k !== \"fullText\")\n .slice(0, 3)\n .map(([k, v]) => `${k}=${JSON.stringify(v)}`)\n .join(\", \");\n lines.push(` - ${n.label}${props ? ` (${props})` : \"\"}`);\n }\n if (remaining > 0) {\n lines.push(` ... and ${remaining} more`);\n }\n }\n\n // Show edges grouped by type\n const edgesByType: Record<string, Array<{ from: string; to: string; weight: number }>> = {};\n for (const edge of graph.edges) {\n if (!edgesByType[edge.type]) edgesByType[edge.type] = [];\n const from = graph.nodes.find((n) => n.id === edge.from);\n const to = graph.nodes.find((n) => n.id === edge.to);\n if (from && to) {\n edgesByType[edge.type].push({ from: from.label, to: to.label, weight: edge.weight });\n }\n }\n\n lines.push(\"\\n[RELATIONSHIPS]\");\n for (const [type, edges] of Object.entries(edgesByType)) {\n const shown = edges.slice(0, 8);\n lines.push(` ${type} (${edges.length} total):`);\n for (const e of shown) {\n lines.push(` \"${e.from}\" → \"${e.to}\" (weight: ${e.weight.toFixed(1)})`);\n }\n if (edges.length > shown.length) {\n lines.push(` ... and ${edges.length - shown.length} more`);\n }\n }\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/graph/query.ts
4
+ function getNeighborhood(graph, nodeId, depth = 2) {
5
+ const visited = /* @__PURE__ */ new Set();
6
+ const queue = [{ id: nodeId, d: 0 }];
7
+ while (queue.length > 0) {
8
+ const { id, d } = queue.shift();
9
+ if (visited.has(id) || d > depth) continue;
10
+ visited.add(id);
11
+ for (const edge of graph.edges) {
12
+ if (edge.from === id && !visited.has(edge.to)) {
13
+ queue.push({ id: edge.to, d: d + 1 });
14
+ }
15
+ if (edge.to === id && !visited.has(edge.from)) {
16
+ queue.push({ id: edge.from, d: d + 1 });
17
+ }
18
+ }
19
+ }
20
+ return graph.nodes.filter((n) => visited.has(n.id));
21
+ }
22
+ function findPatterns(graph) {
23
+ const patterns = [];
24
+ const degree = {};
25
+ for (const edge of graph.edges) {
26
+ degree[edge.from] = (degree[edge.from] ?? 0) + 1;
27
+ degree[edge.to] = (degree[edge.to] ?? 0) + 1;
28
+ }
29
+ const hubs = Object.entries(degree).filter(([, d]) => d >= 3).sort((a, b) => b[1] - a[1]).slice(0, 5);
30
+ for (const [nodeId, d] of hubs) {
31
+ const node = graph.nodes.find((n) => n.id === nodeId);
32
+ if (node) {
33
+ patterns.push({
34
+ description: `"${node.label}" is a hub node (${d} connections) \u2014 many users/entities relate to it`,
35
+ nodes: [nodeId],
36
+ edges: graph.edges.filter((e) => e.from === nodeId || e.to === nodeId).map((e) => e.id),
37
+ frequency: d,
38
+ significance: Math.min(1, d / 10)
39
+ });
40
+ }
41
+ }
42
+ const churnEdges = graph.edges.filter((e) => e.type === "cancelled" || e.type === "churned_because");
43
+ if (churnEdges.length > 0) {
44
+ const churnReasons = churnEdges.map((e) => {
45
+ const target = graph.nodes.find((n) => n.id === e.to);
46
+ return target?.label;
47
+ }).filter(Boolean);
48
+ if (churnReasons.length > 0) {
49
+ patterns.push({
50
+ description: `Churn pattern: ${churnReasons.slice(0, 3).join(", ")}`,
51
+ nodes: churnEdges.map((e) => e.to),
52
+ edges: churnEdges.map((e) => e.id),
53
+ frequency: churnEdges.length,
54
+ significance: Math.min(1, churnEdges.length / 5)
55
+ });
56
+ }
57
+ }
58
+ const complaintNodes = graph.nodes.filter((n) => n.type === "complaint");
59
+ if (complaintNodes.length >= 2) {
60
+ patterns.push({
61
+ description: `${complaintNodes.length} complaints identified: ${complaintNodes.slice(0, 3).map((n) => `"${n.label}"`).join(", ")}`,
62
+ nodes: complaintNodes.map((n) => n.id),
63
+ edges: [],
64
+ frequency: complaintNodes.length,
65
+ significance: Math.min(1, complaintNodes.length / 5)
66
+ });
67
+ }
68
+ const eventNodes = graph.nodes.filter((n) => n.type === "event");
69
+ const topEvents = eventNodes.sort((a, b) => (Number(b.properties.totalOccurrences) ?? 0) - (Number(a.properties.totalOccurrences) ?? 0)).slice(0, 5);
70
+ if (topEvents.length >= 2) {
71
+ patterns.push({
72
+ description: `Top features: ${topEvents.map((n) => `${n.label} (${n.properties.totalOccurrences}x)`).join(", ")}`,
73
+ nodes: topEvents.map((n) => n.id),
74
+ edges: [],
75
+ frequency: topEvents.length,
76
+ significance: 0.7
77
+ });
78
+ }
79
+ return patterns.sort((a, b) => b.significance - a.significance);
80
+ }
81
+ function getPersonaContext(graph, traits) {
82
+ const context = [];
83
+ const metricNodes = graph.nodes.filter((n) => n.type === "metric");
84
+ for (const m of metricNodes) {
85
+ context.push(m.label);
86
+ }
87
+ if (traits.openness > 0.6) {
88
+ const featureNodes = graph.nodes.filter(
89
+ (n) => n.type === "feature" || n.type === "event"
90
+ );
91
+ for (const f of featureNodes.slice(0, 5)) {
92
+ const count = f.properties.totalOccurrences;
93
+ context.push(
94
+ count ? `Feature "${f.label}" is used ${count} times` : `Feature: ${f.label}`
95
+ );
96
+ }
97
+ }
98
+ if (traits.neuroticism > 0.6) {
99
+ const complaints = graph.nodes.filter((n) => n.type === "complaint");
100
+ for (const c of complaints.slice(0, 5)) {
101
+ context.push(`User complaint: "${c.label}"`);
102
+ }
103
+ const churnEdges = graph.edges.filter(
104
+ (e) => e.type === "cancelled" || e.type === "churned_because"
105
+ );
106
+ if (churnEdges.length > 0) {
107
+ context.push(`${churnEdges.length} users have cancelled`);
108
+ }
109
+ }
110
+ if (traits.conscientiousness > 0.6) {
111
+ const discovered = graph.edges.filter(
112
+ (e) => e.properties.discovered === true
113
+ );
114
+ for (const d of discovered.slice(0, 3)) {
115
+ const from = graph.nodes.find((n) => n.id === d.from);
116
+ const to = graph.nodes.find((n) => n.id === d.to);
117
+ if (from && to) {
118
+ context.push(`Pattern: "${from.label}" ${d.type} "${to.label}"`);
119
+ }
120
+ }
121
+ }
122
+ if (traits.agreeableness > 0.6) {
123
+ const hubEdges = Object.entries(
124
+ graph.edges.reduce(
125
+ (acc, e) => {
126
+ acc[e.to] = (acc[e.to] ?? 0) + 1;
127
+ return acc;
128
+ },
129
+ {}
130
+ )
131
+ ).sort((a, b) => b[1] - a[1]).slice(0, 3);
132
+ for (const [nodeId, count] of hubEdges) {
133
+ const node = graph.nodes.find((n) => n.id === nodeId);
134
+ if (node && count >= 2) {
135
+ context.push(`${count} users interact with "${node.label}"`);
136
+ }
137
+ }
138
+ }
139
+ if (traits.extraversion > 0.6) {
140
+ const socialEvents = graph.nodes.filter(
141
+ (n) => n.type === "event" && /share|invite|collab|team|social/i.test(n.label)
142
+ );
143
+ for (const s of socialEvents.slice(0, 3)) {
144
+ context.push(`Social feature: "${s.label}" (${s.properties.totalOccurrences ?? 0} uses)`);
145
+ }
146
+ }
147
+ return context;
148
+ }
149
+ function graphSummary(graph) {
150
+ const typeCounts = {};
151
+ for (const node of graph.nodes) {
152
+ typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1;
153
+ }
154
+ const edgeTypes = {};
155
+ for (const edge of graph.edges) {
156
+ edgeTypes[edge.type] = (edgeTypes[edge.type] ?? 0) + 1;
157
+ }
158
+ const lines = [
159
+ `Knowledge graph: ${graph.metadata.nodeCount} nodes, ${graph.metadata.edgeCount} edges`,
160
+ `Sources: ${graph.metadata.sources.join(", ")}`,
161
+ `Node types: ${Object.entries(typeCounts).map(([t, c]) => `${t}(${c})`).join(", ")}`,
162
+ `Edge types: ${Object.entries(edgeTypes).map(([t, c]) => `${t}(${c})`).join(", ")}`
163
+ ];
164
+ return lines.join("\n");
165
+ }
166
+ async function queryGraph(llm, graph, question) {
167
+ const graphContext = serializeGraphForLLM(graph);
168
+ const patterns = findPatterns(graph);
169
+ const patternText = patterns.length > 0 ? `
170
+ Discovered patterns:
171
+ ${patterns.map((p) => `- ${p.description} (significance: ${p.significance.toFixed(2)})`).join("\n")}` : "";
172
+ const result = await llm.generateJSON(
173
+ `You are a data analyst answering questions about a product's user base using a knowledge graph. The graph contains real user data \u2014 users, events, features, complaints, payments, and their relationships.
174
+
175
+ Ground every claim in specific graph data. If the graph doesn't contain enough information to answer confidently, say so and explain what data would help.`,
176
+ `Knowledge Graph:
177
+ ${graphContext}
178
+ ${patternText}
179
+
180
+ Question: "${question}"
181
+
182
+ Analyze the graph to answer this question. Return JSON:
183
+ {
184
+ "answer": "<2-4 sentence answer grounded in the graph data>",
185
+ "evidence": [
186
+ {"type": "node|edge|pattern", "label": "<entity or relationship>", "detail": "<how this supports the answer>", "relevance": <0-1>}
187
+ ],
188
+ "confidence": <0.0-1.0 based on how much graph data supports the answer>,
189
+ "relatedNodes": ["<labels of the most relevant nodes for follow-up questions>"]
190
+ }`
191
+ );
192
+ return {
193
+ question,
194
+ ...result
195
+ };
196
+ }
197
+ function serializeGraphForLLM(graph) {
198
+ const lines = [];
199
+ lines.push(`${graph.metadata.nodeCount} nodes, ${graph.metadata.edgeCount} edges`);
200
+ const byType = {};
201
+ for (const node of graph.nodes) {
202
+ if (!byType[node.type]) byType[node.type] = [];
203
+ byType[node.type].push(node);
204
+ }
205
+ for (const [type, nodes] of Object.entries(byType)) {
206
+ const limit = type === "user" ? 5 : 15;
207
+ const shown = nodes.slice(0, limit);
208
+ const remaining = nodes.length - shown.length;
209
+ lines.push(`
210
+ [${type.toUpperCase()}] (${nodes.length} total)`);
211
+ for (const n of shown) {
212
+ const props = Object.entries(n.properties).filter(([k]) => !k.startsWith("_") && k !== "fullText").slice(0, 3).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
213
+ lines.push(` - ${n.label}${props ? ` (${props})` : ""}`);
214
+ }
215
+ if (remaining > 0) {
216
+ lines.push(` ... and ${remaining} more`);
217
+ }
218
+ }
219
+ const edgesByType = {};
220
+ for (const edge of graph.edges) {
221
+ if (!edgesByType[edge.type]) edgesByType[edge.type] = [];
222
+ const from = graph.nodes.find((n) => n.id === edge.from);
223
+ const to = graph.nodes.find((n) => n.id === edge.to);
224
+ if (from && to) {
225
+ edgesByType[edge.type].push({ from: from.label, to: to.label, weight: edge.weight });
226
+ }
227
+ }
228
+ lines.push("\n[RELATIONSHIPS]");
229
+ for (const [type, edges] of Object.entries(edgesByType)) {
230
+ const shown = edges.slice(0, 8);
231
+ lines.push(` ${type} (${edges.length} total):`);
232
+ for (const e of shown) {
233
+ lines.push(` "${e.from}" \u2192 "${e.to}" (weight: ${e.weight.toFixed(1)})`);
234
+ }
235
+ if (edges.length > shown.length) {
236
+ lines.push(` ... and ${edges.length - shown.length} more`);
237
+ }
238
+ }
239
+ return lines.join("\n");
240
+ }
241
+
242
+ export {
243
+ getNeighborhood,
244
+ findPatterns,
245
+ getPersonaContext,
246
+ graphSummary,
247
+ queryGraph
248
+ };
249
+ //# sourceMappingURL=chunk-PGZVVIL6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/graph/query.ts"],"sourcesContent":["/**\n * Graph query utilities.\n *\n * Get neighborhoods, find patterns, compile persona-relevant context.\n */\n\nimport type { OceanVector, LLMProvider } from \"../types.js\";\nimport type { KnowledgeGraph, GraphNode, GraphEdge, GraphPattern } from \"./types.js\";\n\n/**\n * Get all nodes within N hops of a starting node.\n */\nexport function getNeighborhood(\n graph: KnowledgeGraph,\n nodeId: string,\n depth: number = 2\n): GraphNode[] {\n const visited = new Set<string>();\n const queue: Array<{ id: string; d: number }> = [{ id: nodeId, d: 0 }];\n\n while (queue.length > 0) {\n const { id, d } = queue.shift()!;\n if (visited.has(id) || d > depth) continue;\n visited.add(id);\n\n // Find connected nodes\n for (const edge of graph.edges) {\n if (edge.from === id && !visited.has(edge.to)) {\n queue.push({ id: edge.to, d: d + 1 });\n }\n if (edge.to === id && !visited.has(edge.from)) {\n queue.push({ id: edge.from, d: d + 1 });\n }\n }\n }\n\n return graph.nodes.filter((n) => visited.has(n.id));\n}\n\n/**\n * Find common patterns in the graph.\n * Looks for: frequent co-occurrences, hub nodes, churn patterns.\n */\nexport function findPatterns(graph: KnowledgeGraph): GraphPattern[] {\n const patterns: GraphPattern[] = [];\n\n // Find hub nodes (high degree)\n const degree: Record<string, number> = {};\n for (const edge of graph.edges) {\n degree[edge.from] = (degree[edge.from] ?? 0) + 1;\n degree[edge.to] = (degree[edge.to] ?? 0) + 1;\n }\n\n const hubs = Object.entries(degree)\n .filter(([, d]) => d >= 3)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5);\n\n for (const [nodeId, d] of hubs) {\n const node = graph.nodes.find((n) => n.id === nodeId);\n if (node) {\n patterns.push({\n description: `\"${node.label}\" is a hub node (${d} connections) — many users/entities relate to it`,\n nodes: [nodeId],\n edges: graph.edges.filter((e) => e.from === nodeId || e.to === nodeId).map((e) => e.id),\n frequency: d,\n significance: Math.min(1, d / 10),\n });\n }\n }\n\n // Find churn patterns (nodes connected to \"cancelled\" edges)\n const churnEdges = graph.edges.filter((e) => e.type === \"cancelled\" || e.type === \"churned_because\");\n if (churnEdges.length > 0) {\n const churnReasons = churnEdges\n .map((e) => {\n const target = graph.nodes.find((n) => n.id === e.to);\n return target?.label;\n })\n .filter(Boolean);\n\n if (churnReasons.length > 0) {\n patterns.push({\n description: `Churn pattern: ${churnReasons.slice(0, 3).join(\", \")}`,\n nodes: churnEdges.map((e) => e.to),\n edges: churnEdges.map((e) => e.id),\n frequency: churnEdges.length,\n significance: Math.min(1, churnEdges.length / 5),\n });\n }\n }\n\n // Find complaint clusters\n const complaintNodes = graph.nodes.filter((n) => n.type === \"complaint\");\n if (complaintNodes.length >= 2) {\n patterns.push({\n description: `${complaintNodes.length} complaints identified: ${complaintNodes.slice(0, 3).map((n) => `\"${n.label}\"`).join(\", \")}`,\n nodes: complaintNodes.map((n) => n.id),\n edges: [],\n frequency: complaintNodes.length,\n significance: Math.min(1, complaintNodes.length / 5),\n });\n }\n\n // Find feature usage patterns\n const eventNodes = graph.nodes.filter((n) => n.type === \"event\");\n const topEvents = eventNodes\n .sort((a, b) => (Number(b.properties.totalOccurrences) ?? 0) - (Number(a.properties.totalOccurrences) ?? 0))\n .slice(0, 5);\n\n if (topEvents.length >= 2) {\n patterns.push({\n description: `Top features: ${topEvents.map((n) => `${n.label} (${n.properties.totalOccurrences}x)`).join(\", \")}`,\n nodes: topEvents.map((n) => n.id),\n edges: [],\n frequency: topEvents.length,\n significance: 0.7,\n });\n }\n\n return patterns.sort((a, b) => b.significance - a.significance);\n}\n\n/**\n * Get persona-relevant context from the graph.\n *\n * Given a persona's OCEAN traits, find the most relevant graph neighborhood\n * and compile it into context strings the persona can reference.\n *\n * High openness personas → get innovation/feature nodes\n * High neuroticism personas → get complaint/churn nodes\n * High conscientiousness personas → get metric/reliability nodes\n */\nexport function getPersonaContext(\n graph: KnowledgeGraph,\n traits: OceanVector\n): string[] {\n const context: string[] = [];\n\n // Everyone gets the key metrics\n const metricNodes = graph.nodes.filter((n) => n.type === \"metric\");\n for (const m of metricNodes) {\n context.push(m.label);\n }\n\n // High openness → features, innovation, new things\n if (traits.openness > 0.6) {\n const featureNodes = graph.nodes.filter(\n (n) => n.type === \"feature\" || n.type === \"event\"\n );\n for (const f of featureNodes.slice(0, 5)) {\n const count = f.properties.totalOccurrences;\n context.push(\n count\n ? `Feature \"${f.label}\" is used ${count} times`\n : `Feature: ${f.label}`\n );\n }\n }\n\n // High neuroticism → complaints, churn, negative signals\n if (traits.neuroticism > 0.6) {\n const complaints = graph.nodes.filter((n) => n.type === \"complaint\");\n for (const c of complaints.slice(0, 5)) {\n context.push(`User complaint: \"${c.label}\"`);\n }\n\n const churnEdges = graph.edges.filter(\n (e) => e.type === \"cancelled\" || e.type === \"churned_because\"\n );\n if (churnEdges.length > 0) {\n context.push(`${churnEdges.length} users have cancelled`);\n }\n }\n\n // High conscientiousness → data, metrics, reliability\n if (traits.conscientiousness > 0.6) {\n const discovered = graph.edges.filter(\n (e) => e.properties.discovered === true\n );\n for (const d of discovered.slice(0, 3)) {\n const from = graph.nodes.find((n) => n.id === d.from);\n const to = graph.nodes.find((n) => n.id === d.to);\n if (from && to) {\n context.push(`Pattern: \"${from.label}\" ${d.type} \"${to.label}\"`);\n }\n }\n }\n\n // High agreeableness → social proof, what others do\n if (traits.agreeableness > 0.6) {\n const hubEdges = Object.entries(\n graph.edges.reduce(\n (acc, e) => {\n acc[e.to] = (acc[e.to] ?? 0) + 1;\n return acc;\n },\n {} as Record<string, number>\n )\n )\n .sort((a, b) => b[1] - a[1])\n .slice(0, 3);\n\n for (const [nodeId, count] of hubEdges) {\n const node = graph.nodes.find((n) => n.id === nodeId);\n if (node && count >= 2) {\n context.push(`${count} users interact with \"${node.label}\"`);\n }\n }\n }\n\n // High extraversion → popular features, social features\n if (traits.extraversion > 0.6) {\n const socialEvents = graph.nodes.filter(\n (n) =>\n n.type === \"event\" &&\n /share|invite|collab|team|social/i.test(n.label)\n );\n for (const s of socialEvents.slice(0, 3)) {\n context.push(`Social feature: \"${s.label}\" (${s.properties.totalOccurrences ?? 0} uses)`);\n }\n }\n\n return context;\n}\n\n/**\n * Human-readable graph summary.\n */\nexport function graphSummary(graph: KnowledgeGraph): string {\n const typeCounts: Record<string, number> = {};\n for (const node of graph.nodes) {\n typeCounts[node.type] = (typeCounts[node.type] ?? 0) + 1;\n }\n\n const edgeTypes: Record<string, number> = {};\n for (const edge of graph.edges) {\n edgeTypes[edge.type] = (edgeTypes[edge.type] ?? 0) + 1;\n }\n\n const lines: string[] = [\n `Knowledge graph: ${graph.metadata.nodeCount} nodes, ${graph.metadata.edgeCount} edges`,\n `Sources: ${graph.metadata.sources.join(\", \")}`,\n `Node types: ${Object.entries(typeCounts).map(([t, c]) => `${t}(${c})`).join(\", \")}`,\n `Edge types: ${Object.entries(edgeTypes).map(([t, c]) => `${t}(${c})`).join(\", \")}`,\n ];\n\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Natural language graph query\n// ---------------------------------------------------------------------------\n\nexport interface GraphQueryResult {\n question: string;\n answer: string;\n evidence: Array<{\n type: \"node\" | \"edge\" | \"pattern\";\n label: string;\n detail: string;\n relevance: number;\n }>;\n confidence: number;\n relatedNodes: string[];\n}\n\n/**\n * Ask the knowledge graph a natural language question.\n *\n * The LLM interprets the question, identifies relevant nodes/edges/patterns,\n * and synthesizes an answer grounded in the graph data.\n *\n * @example\n * ```ts\n * const result = await queryGraph(llm, graph, \"Why are users churning?\");\n * console.log(result.answer); // \"Users are primarily churning due to...\"\n * console.log(result.evidence); // specific nodes/edges that support the answer\n * ```\n */\nexport async function queryGraph(\n llm: LLMProvider,\n graph: KnowledgeGraph,\n question: string\n): Promise<GraphQueryResult> {\n // Serialize graph into a compact representation the LLM can reason over\n const graphContext = serializeGraphForLLM(graph);\n const patterns = findPatterns(graph);\n const patternText = patterns.length > 0\n ? `\\nDiscovered patterns:\\n${patterns.map((p) => `- ${p.description} (significance: ${p.significance.toFixed(2)})`).join(\"\\n\")}`\n : \"\";\n\n const result = await llm.generateJSON<{\n answer: string;\n evidence: Array<{\n type: \"node\" | \"edge\" | \"pattern\";\n label: string;\n detail: string;\n relevance: number;\n }>;\n confidence: number;\n relatedNodes: string[];\n }>(\n `You are a data analyst answering questions about a product's user base using a knowledge graph. The graph contains real user data — users, events, features, complaints, payments, and their relationships.\n\nGround every claim in specific graph data. If the graph doesn't contain enough information to answer confidently, say so and explain what data would help.`,\n `Knowledge Graph:\n${graphContext}\n${patternText}\n\nQuestion: \"${question}\"\n\nAnalyze the graph to answer this question. Return JSON:\n{\n \"answer\": \"<2-4 sentence answer grounded in the graph data>\",\n \"evidence\": [\n {\"type\": \"node|edge|pattern\", \"label\": \"<entity or relationship>\", \"detail\": \"<how this supports the answer>\", \"relevance\": <0-1>}\n ],\n \"confidence\": <0.0-1.0 based on how much graph data supports the answer>,\n \"relatedNodes\": [\"<labels of the most relevant nodes for follow-up questions>\"]\n}`\n );\n\n return {\n question,\n ...result,\n };\n}\n\n/**\n * Serialize the graph into a compact text representation for LLM context.\n * Keeps it under ~4K tokens by summarizing large graphs.\n */\nfunction serializeGraphForLLM(graph: KnowledgeGraph): string {\n const lines: string[] = [];\n lines.push(`${graph.metadata.nodeCount} nodes, ${graph.metadata.edgeCount} edges`);\n\n // Group nodes by type\n const byType: Record<string, GraphNode[]> = {};\n for (const node of graph.nodes) {\n if (!byType[node.type]) byType[node.type] = [];\n byType[node.type].push(node);\n }\n\n for (const [type, nodes] of Object.entries(byType)) {\n const limit = type === \"user\" ? 5 : 15; // Cap user nodes to avoid bloat\n const shown = nodes.slice(0, limit);\n const remaining = nodes.length - shown.length;\n\n lines.push(`\\n[${type.toUpperCase()}] (${nodes.length} total)`);\n for (const n of shown) {\n const props = Object.entries(n.properties)\n .filter(([k]) => !k.startsWith(\"_\") && k !== \"fullText\")\n .slice(0, 3)\n .map(([k, v]) => `${k}=${JSON.stringify(v)}`)\n .join(\", \");\n lines.push(` - ${n.label}${props ? ` (${props})` : \"\"}`);\n }\n if (remaining > 0) {\n lines.push(` ... and ${remaining} more`);\n }\n }\n\n // Show edges grouped by type\n const edgesByType: Record<string, Array<{ from: string; to: string; weight: number }>> = {};\n for (const edge of graph.edges) {\n if (!edgesByType[edge.type]) edgesByType[edge.type] = [];\n const from = graph.nodes.find((n) => n.id === edge.from);\n const to = graph.nodes.find((n) => n.id === edge.to);\n if (from && to) {\n edgesByType[edge.type].push({ from: from.label, to: to.label, weight: edge.weight });\n }\n }\n\n lines.push(\"\\n[RELATIONSHIPS]\");\n for (const [type, edges] of Object.entries(edgesByType)) {\n const shown = edges.slice(0, 8);\n lines.push(` ${type} (${edges.length} total):`);\n for (const e of shown) {\n lines.push(` \"${e.from}\" → \"${e.to}\" (weight: ${e.weight.toFixed(1)})`);\n }\n if (edges.length > shown.length) {\n lines.push(` ... and ${edges.length - shown.length} more`);\n }\n }\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/traits/ocean.ts
4
+ var TRAIT_PROFILES = {
5
+ openness: {
6
+ trait: "openness",
7
+ facets: [
8
+ "imagination",
9
+ "artistic interests",
10
+ "emotionality",
11
+ "adventurousness",
12
+ "intellect",
13
+ "liberalism"
14
+ ],
15
+ highBehaviors: [
16
+ "Tries new products and features early",
17
+ "Values innovation and uniqueness over reliability",
18
+ "Open to unconventional solutions",
19
+ "Seeks variety \u2014 switches brands to explore",
20
+ "Drawn to aesthetics and design quality",
21
+ "Reads broadly, considers multiple perspectives before deciding"
22
+ ],
23
+ lowBehaviors: [
24
+ "Prefers familiar, proven products",
25
+ "Skeptical of novelty and trends",
26
+ "Sticks with what works \u2014 resists change",
27
+ "Values practical function over design",
28
+ "Makes decisions based on past experience, not curiosity"
29
+ ],
30
+ correlations: {
31
+ noveltySeekingScore: 0.41,
32
+ brandLoyaltyScore: -0.18,
33
+ priceSensitivityScore: -0.12,
34
+ socialInfluenceScore: 0.08,
35
+ complaintLikelihoodScore: 0.15,
36
+ decisionDeliberationScore: 0.22,
37
+ advocacyScore: 0.25,
38
+ riskToleranceScore: 0.38
39
+ }
40
+ },
41
+ conscientiousness: {
42
+ trait: "conscientiousness",
43
+ facets: [
44
+ "self-efficacy",
45
+ "orderliness",
46
+ "dutifulness",
47
+ "achievement-striving",
48
+ "self-discipline",
49
+ "cautiousness"
50
+ ],
51
+ highBehaviors: [
52
+ "Researches thoroughly before purchasing",
53
+ "Reads reviews, compares options methodically",
54
+ "Loyal to brands that consistently deliver",
55
+ "Plans purchases \u2014 rarely impulse buys",
56
+ "Values reliability and quality over novelty",
57
+ "Keeps subscriptions organized \u2014 cancels unused ones"
58
+ ],
59
+ lowBehaviors: [
60
+ "Impulse buyer \u2014 decides quickly on feel",
61
+ "Doesn't read the fine print",
62
+ "Forgets to cancel subscriptions",
63
+ "Less likely to leave reviews",
64
+ "Switches tools often without fully evaluating"
65
+ ],
66
+ correlations: {
67
+ noveltySeekingScore: -0.15,
68
+ brandLoyaltyScore: 0.34,
69
+ priceSensitivityScore: 0.19,
70
+ socialInfluenceScore: -0.08,
71
+ complaintLikelihoodScore: 0.12,
72
+ decisionDeliberationScore: 0.42,
73
+ advocacyScore: 0.18,
74
+ riskToleranceScore: -0.28
75
+ }
76
+ },
77
+ extraversion: {
78
+ trait: "extraversion",
79
+ facets: [
80
+ "friendliness",
81
+ "gregariousness",
82
+ "assertiveness",
83
+ "activity level",
84
+ "excitement-seeking",
85
+ "cheerfulness"
86
+ ],
87
+ highBehaviors: [
88
+ "Heavily influenced by social proof and peer behavior",
89
+ "Shares product experiences publicly \u2014 writes reviews, posts on social",
90
+ "Prefers products with community or social features",
91
+ "Talks about purchases with friends \u2014 word of mouth driver",
92
+ "Makes faster decisions \u2014 trusts gut and social signals",
93
+ "Values status and visibility in product choices"
94
+ ],
95
+ lowBehaviors: [
96
+ "Decides independently \u2014 not swayed by trends or peers",
97
+ "Researches alone, quietly",
98
+ "Rarely shares opinions publicly",
99
+ "Prefers solo-use products over social/collaborative tools",
100
+ "Values privacy in product interactions"
101
+ ],
102
+ correlations: {
103
+ noveltySeekingScore: 0.22,
104
+ brandLoyaltyScore: 0.05,
105
+ priceSensitivityScore: -0.1,
106
+ socialInfluenceScore: 0.38,
107
+ complaintLikelihoodScore: 0.2,
108
+ decisionDeliberationScore: -0.18,
109
+ advocacyScore: 0.42,
110
+ riskToleranceScore: 0.25
111
+ }
112
+ },
113
+ agreeableness: {
114
+ trait: "agreeableness",
115
+ facets: [
116
+ "trust",
117
+ "morality",
118
+ "altruism",
119
+ "cooperation",
120
+ "modesty",
121
+ "sympathy"
122
+ ],
123
+ highBehaviors: [
124
+ "Trusts brand promises and recommendations easily",
125
+ "Avoids confrontation \u2014 unlikely to complain publicly",
126
+ "Influenced by friend and family recommendations",
127
+ "Loyal even when slightly dissatisfied \u2014 gives second chances",
128
+ "Values ethical brands and social responsibility",
129
+ "Churns silently \u2014 switches without telling you why"
130
+ ],
131
+ lowBehaviors: [
132
+ "Skeptical of marketing claims",
133
+ "Will complain loudly if unsatisfied",
134
+ "Negotiates prices, seeks discounts aggressively",
135
+ "Makes decisions based on personal benefit, not brand relationship",
136
+ "Less influenced by others' opinions"
137
+ ],
138
+ correlations: {
139
+ noveltySeekingScore: -0.05,
140
+ brandLoyaltyScore: 0.28,
141
+ priceSensitivityScore: -0.08,
142
+ socialInfluenceScore: 0.3,
143
+ complaintLikelihoodScore: -0.35,
144
+ decisionDeliberationScore: 0.05,
145
+ advocacyScore: 0.2,
146
+ riskToleranceScore: -0.12
147
+ }
148
+ },
149
+ neuroticism: {
150
+ trait: "neuroticism",
151
+ facets: [
152
+ "anxiety",
153
+ "anger",
154
+ "depression",
155
+ "self-consciousness",
156
+ "immoderation",
157
+ "vulnerability"
158
+ ],
159
+ highBehaviors: [
160
+ "Anxious about purchase decisions \u2014 needs reassurance",
161
+ "Risk-averse \u2014 avoids unknown brands or untested products",
162
+ "Reads negative reviews disproportionately",
163
+ "Sensitive to price increases \u2014 feels loss acutely",
164
+ "May impulse-buy to cope with stress, then regret it",
165
+ "Seeks guarantees, refund policies, social proof before committing"
166
+ ],
167
+ lowBehaviors: [
168
+ "Confident in decisions \u2014 low buyer's remorse",
169
+ "Comfortable with risk and uncertainty",
170
+ "Less affected by negative reviews",
171
+ "Pragmatic about price changes \u2014 evaluates rationally",
172
+ "Stable preferences \u2014 less emotional in purchasing"
173
+ ],
174
+ correlations: {
175
+ noveltySeekingScore: -0.1,
176
+ brandLoyaltyScore: -0.05,
177
+ priceSensitivityScore: 0.28,
178
+ socialInfluenceScore: 0.18,
179
+ complaintLikelihoodScore: 0.22,
180
+ decisionDeliberationScore: -0.15,
181
+ advocacyScore: -0.12,
182
+ riskToleranceScore: -0.35
183
+ }
184
+ }
185
+ };
186
+ function randomOceanVector(constraints) {
187
+ const base = {
188
+ openness: Math.random(),
189
+ conscientiousness: Math.random(),
190
+ extraversion: Math.random(),
191
+ agreeableness: Math.random(),
192
+ neuroticism: Math.random()
193
+ };
194
+ return { ...base, ...constraints };
195
+ }
196
+ function traitDistance(a, b) {
197
+ const traits = [
198
+ "openness",
199
+ "conscientiousness",
200
+ "extraversion",
201
+ "agreeableness",
202
+ "neuroticism"
203
+ ];
204
+ let sum = 0;
205
+ for (const t of traits) {
206
+ sum += (a[t] - b[t]) ** 2;
207
+ }
208
+ return Math.sqrt(sum);
209
+ }
210
+ function averageOceanVector(vectors) {
211
+ const n = vectors.length;
212
+ if (n === 0) {
213
+ return {
214
+ openness: 0.5,
215
+ conscientiousness: 0.5,
216
+ extraversion: 0.5,
217
+ agreeableness: 0.5,
218
+ neuroticism: 0.5
219
+ };
220
+ }
221
+ const sum = {
222
+ openness: 0,
223
+ conscientiousness: 0,
224
+ extraversion: 0,
225
+ agreeableness: 0,
226
+ neuroticism: 0
227
+ };
228
+ for (const v of vectors) {
229
+ for (const t of Object.keys(sum)) {
230
+ sum[t] += v[t];
231
+ }
232
+ }
233
+ for (const t of Object.keys(sum)) {
234
+ sum[t] /= n;
235
+ }
236
+ return sum;
237
+ }
238
+ function clampVector(v) {
239
+ const clamped = { ...v };
240
+ for (const t of Object.keys(clamped)) {
241
+ clamped[t] = Math.max(0, Math.min(1, clamped[t]));
242
+ }
243
+ return clamped;
244
+ }
245
+ function compileBehavioralProfile(traits) {
246
+ const traitNames = [
247
+ "openness",
248
+ "conscientiousness",
249
+ "extraversion",
250
+ "agreeableness",
251
+ "neuroticism"
252
+ ];
253
+ const behaviorKeys = [
254
+ "noveltySeekingScore",
255
+ "brandLoyaltyScore",
256
+ "priceSensitivityScore",
257
+ "socialInfluenceScore",
258
+ "complaintLikelihoodScore",
259
+ "decisionDeliberationScore",
260
+ "advocacyScore",
261
+ "riskToleranceScore"
262
+ ];
263
+ const scores = {};
264
+ for (const bKey of behaviorKeys) {
265
+ let score = 0.5;
266
+ for (const tName of traitNames) {
267
+ const correlation = TRAIT_PROFILES[tName].correlations[bKey];
268
+ score += (traits[tName] - 0.5) * correlation;
269
+ }
270
+ scores[bKey] = Math.max(0, Math.min(1, score));
271
+ }
272
+ const rules = [];
273
+ for (const tName of traitNames) {
274
+ const profile = TRAIT_PROFILES[tName];
275
+ const value = traits[tName];
276
+ const behaviors = value >= 0.5 ? profile.highBehaviors : profile.lowBehaviors;
277
+ const intensity = Math.abs(value - 0.5) * 2;
278
+ const count = intensity > 0.6 ? 3 : intensity > 0.3 ? 2 : 1;
279
+ rules.push(...behaviors.slice(0, count));
280
+ }
281
+ return {
282
+ noveltySeekingScore: scores.noveltySeekingScore,
283
+ brandLoyaltyScore: scores.brandLoyaltyScore,
284
+ priceSensitivityScore: scores.priceSensitivityScore,
285
+ socialInfluenceScore: scores.socialInfluenceScore,
286
+ complaintLikelihoodScore: scores.complaintLikelihoodScore,
287
+ decisionDeliberationScore: scores.decisionDeliberationScore,
288
+ advocacyScore: scores.advocacyScore,
289
+ riskToleranceScore: scores.riskToleranceScore,
290
+ rules
291
+ };
292
+ }
293
+
294
+ export {
295
+ TRAIT_PROFILES,
296
+ randomOceanVector,
297
+ traitDistance,
298
+ averageOceanVector,
299
+ clampVector,
300
+ compileBehavioralProfile
301
+ };
302
+ //# sourceMappingURL=chunk-QR5GEK27.js.map