@zenti/sdk 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.
Files changed (47) hide show
  1. package/.gitattributes +6 -0
  2. package/README.md +211 -0
  3. package/dist/PersonaLayer.d.ts +28 -0
  4. package/dist/PersonaLayer.js +89 -0
  5. package/dist/cli/commands/diff.d.ts +2 -0
  6. package/dist/cli/commands/diff.js +129 -0
  7. package/dist/cli/commands/init.d.ts +4 -0
  8. package/dist/cli/commands/init.js +88 -0
  9. package/dist/cli/commands/test.d.ts +5 -0
  10. package/dist/cli/commands/test.js +121 -0
  11. package/dist/cli/commands/validate.d.ts +2 -0
  12. package/dist/cli/commands/validate.js +80 -0
  13. package/dist/cli/index.d.ts +3 -0
  14. package/dist/cli/index.js +42 -0
  15. package/dist/compiler/index.d.ts +5 -0
  16. package/dist/compiler/index.js +12 -0
  17. package/dist/compiler/toClaude.d.ts +15 -0
  18. package/dist/compiler/toClaude.js +62 -0
  19. package/dist/compiler/toGemini.d.ts +22 -0
  20. package/dist/compiler/toGemini.js +53 -0
  21. package/dist/compiler/toOpenAI.d.ts +17 -0
  22. package/dist/compiler/toOpenAI.js +51 -0
  23. package/dist/compiler/utils.d.ts +7 -0
  24. package/dist/compiler/utils.js +27 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.js +14 -0
  27. package/dist/parser/index.d.ts +34 -0
  28. package/dist/parser/index.js +160 -0
  29. package/examples/sofia.zenti +46 -0
  30. package/package.json +54 -0
  31. package/src/PersonaLayer.ts +63 -0
  32. package/src/cli/commands/diff.ts +112 -0
  33. package/src/cli/commands/init.ts +55 -0
  34. package/src/cli/commands/test.ts +105 -0
  35. package/src/cli/commands/validate.ts +52 -0
  36. package/src/cli/index.ts +47 -0
  37. package/src/compiler/index.ts +4 -0
  38. package/src/compiler/toClaude.ts +74 -0
  39. package/src/compiler/toGemini.ts +76 -0
  40. package/src/compiler/toOpenAI.ts +70 -0
  41. package/src/compiler/utils.ts +27 -0
  42. package/src/index.ts +14 -0
  43. package/src/parser/index.ts +164 -0
  44. package/tests/compiler.test.ts +218 -0
  45. package/tests/parser.test.ts +132 -0
  46. package/tsconfig.json +19 -0
  47. package/tsconfig.test.json +7 -0
@@ -0,0 +1,76 @@
1
+ import { ZentiPersona } from '../parser';
2
+ import { translateBigFive } from './utils';
3
+
4
+ export interface GeminiPart {
5
+ text: string;
6
+ }
7
+
8
+ export interface GeminiContent {
9
+ role: 'user' | 'model';
10
+ parts: GeminiPart[];
11
+ }
12
+
13
+ export interface GeminiOutput {
14
+ systemInstruction: { parts: GeminiPart[] };
15
+ contents: GeminiContent[];
16
+ }
17
+
18
+ /**
19
+ * Compiles a ZentiPersona into Gemini-compatible format.
20
+ *
21
+ * - `systemInstruction`: identity + guardrails (+ Big Five traits)
22
+ * - `contents`: on_brand examples as user/model history turns
23
+ */
24
+ export function toGemini(persona: ZentiPersona): GeminiOutput {
25
+ const systemLines: string[] = [];
26
+
27
+ // Identity
28
+ systemLines.push(`Eres ${persona.name}, ${persona.identity.role}.`);
29
+ systemLines.push(persona.identity.backstory);
30
+ systemLines.push(
31
+ `Estilo de comunicación: ${persona.identity.communication_style}.`
32
+ );
33
+ systemLines.push('');
34
+
35
+ // Core traits
36
+ const traits = translateBigFive(persona.core);
37
+ if (traits.length > 0) {
38
+ systemLines.push('Rasgos de personalidad:');
39
+ traits.forEach((trait) => systemLines.push(`- ${trait}`));
40
+ systemLines.push('');
41
+ }
42
+
43
+ // Guardrails
44
+ if (persona.guardrails.always.length > 0) {
45
+ systemLines.push('Siempre:');
46
+ persona.guardrails.always.forEach((rule) => systemLines.push(`- ${rule}`));
47
+ systemLines.push('');
48
+ }
49
+
50
+ if (persona.guardrails.never.length > 0) {
51
+ systemLines.push('Nunca:');
52
+ persona.guardrails.never.forEach((rule) => systemLines.push(`- ${rule}`));
53
+ systemLines.push('');
54
+ }
55
+
56
+ if (persona.guardrails.escalate_if.length > 0) {
57
+ systemLines.push('Escala si:');
58
+ persona.guardrails.escalate_if.forEach((condition) =>
59
+ systemLines.push(`- ${condition}`)
60
+ );
61
+ }
62
+
63
+ // Examples as conversation history
64
+ const contents: GeminiContent[] = [];
65
+ persona.examples.on_brand.forEach((ex) => {
66
+ contents.push({ role: 'user', parts: [{ text: ex.user }] });
67
+ contents.push({ role: 'model', parts: [{ text: ex.agent }] });
68
+ });
69
+
70
+ return {
71
+ systemInstruction: {
72
+ parts: [{ text: systemLines.join('\n').trim() }],
73
+ },
74
+ contents,
75
+ };
76
+ }
@@ -0,0 +1,70 @@
1
+ import { ZentiPersona } from '../parser';
2
+ import { translateBigFive } from './utils';
3
+
4
+ export interface OpenAIMessage {
5
+ role: 'user' | 'assistant';
6
+ content: string;
7
+ }
8
+
9
+ export interface OpenAIOutput {
10
+ system: string;
11
+ messages: OpenAIMessage[];
12
+ }
13
+
14
+ /**
15
+ * Compiles a ZentiPersona into OpenAI-compatible format.
16
+ *
17
+ * - `system`: identity + core in first person + guardrails
18
+ * - `messages`: on_brand examples as alternating user/assistant turns
19
+ */
20
+ export function toOpenAI(persona: ZentiPersona): OpenAIOutput {
21
+ const systemLines: string[] = [];
22
+
23
+ // Identity in first person
24
+ systemLines.push(`Eres ${persona.name}, ${persona.identity.role}.`);
25
+ systemLines.push(persona.identity.backstory);
26
+ systemLines.push(
27
+ `Tu estilo de comunicación es: ${persona.identity.communication_style}.`
28
+ );
29
+ systemLines.push('');
30
+
31
+ // Core traits in first person
32
+ const traits = translateBigFive(persona.core);
33
+ if (traits.length > 0) {
34
+ systemLines.push('En tu forma de ser eres:');
35
+ traits.forEach((trait) => systemLines.push(`- ${trait}`));
36
+ systemLines.push('');
37
+ }
38
+
39
+ // Guardrails
40
+ if (persona.guardrails.always.length > 0) {
41
+ systemLines.push('Siempre debes:');
42
+ persona.guardrails.always.forEach((rule) => systemLines.push(`- ${rule}`));
43
+ systemLines.push('');
44
+ }
45
+
46
+ if (persona.guardrails.never.length > 0) {
47
+ systemLines.push('Nunca debes:');
48
+ persona.guardrails.never.forEach((rule) => systemLines.push(`- ${rule}`));
49
+ systemLines.push('');
50
+ }
51
+
52
+ if (persona.guardrails.escalate_if.length > 0) {
53
+ systemLines.push('Escala si:');
54
+ persona.guardrails.escalate_if.forEach((condition) =>
55
+ systemLines.push(`- ${condition}`)
56
+ );
57
+ }
58
+
59
+ // Few-shot examples as user/assistant message pairs
60
+ const messages: OpenAIMessage[] = [];
61
+ persona.examples.on_brand.forEach((ex) => {
62
+ messages.push({ role: 'user', content: ex.user });
63
+ messages.push({ role: 'assistant', content: ex.agent });
64
+ });
65
+
66
+ return {
67
+ system: systemLines.join('\n').trim(),
68
+ messages,
69
+ };
70
+ }
@@ -0,0 +1,27 @@
1
+ import { ZentiPersona } from '../parser';
2
+
3
+ /**
4
+ * Translates Big Five personality scores into natural language descriptors.
5
+ * Only the conditions specified in the spec generate a trait string.
6
+ */
7
+ export function translateBigFive(core: ZentiPersona['core']): string[] {
8
+ const traits: string[] = [];
9
+
10
+ if (core.openness > 0.6) {
11
+ traits.push('creativo y curioso, abierto a explorar soluciones nuevas');
12
+ }
13
+ if (core.conscientiousness > 0.7) {
14
+ traits.push('metódico y confiable, siempre sigue los pasos correctos');
15
+ }
16
+ if (core.extraversion < 0.5) {
17
+ traits.push('reservado pero atento, responde con precisión sin ser efusivo');
18
+ }
19
+ if (core.agreeableness > 0.7) {
20
+ traits.push('empático y colaborativo, prioriza la satisfacción del usuario');
21
+ }
22
+ if (core.neuroticism < 0.3) {
23
+ traits.push('calmado bajo presión, mantiene tono estable ante usuarios frustrados');
24
+ }
25
+
26
+ return traits;
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { PersonaLayer, DeployTarget, DeployOutput } from './PersonaLayer';
2
+ export { parseZenti, ZentiPersona, ZentiExample, ZentiValidationError } from './parser';
3
+ export {
4
+ toClaude,
5
+ toOpenAI,
6
+ toGemini,
7
+ translateBigFive,
8
+ ClaudeOutput,
9
+ OpenAIOutput,
10
+ OpenAIMessage,
11
+ GeminiOutput,
12
+ GeminiContent,
13
+ GeminiPart,
14
+ } from './compiler';
@@ -0,0 +1,164 @@
1
+ import * as yaml from 'js-yaml';
2
+
3
+ export interface ZentiExample {
4
+ user: string;
5
+ agent: string;
6
+ }
7
+
8
+ export interface ZentiPersona {
9
+ version: string;
10
+ name: string;
11
+ core: {
12
+ openness: number;
13
+ conscientiousness: number;
14
+ extraversion: number;
15
+ agreeableness: number;
16
+ neuroticism: number;
17
+ };
18
+ identity: {
19
+ role: string;
20
+ backstory: string;
21
+ communication_style: string;
22
+ };
23
+ examples: {
24
+ on_brand: ZentiExample[];
25
+ off_brand: ZentiExample[];
26
+ };
27
+ guardrails: {
28
+ always: string[];
29
+ never: string[];
30
+ escalate_if: string[];
31
+ };
32
+ }
33
+
34
+ export class ZentiValidationError extends Error {
35
+ constructor(message: string) {
36
+ super(message);
37
+ this.name = 'ZentiValidationError';
38
+ }
39
+ }
40
+
41
+ function validateNumber(value: unknown, field: string): number {
42
+ if (typeof value !== 'number') {
43
+ throw new ZentiValidationError(
44
+ `${field} debe ser un número, se recibió ${typeof value}`
45
+ );
46
+ }
47
+ if (value < 0 || value > 1) {
48
+ throw new ZentiValidationError(
49
+ `${field} debe estar entre 0.0 y 1.0, se recibió ${value}`
50
+ );
51
+ }
52
+ return value;
53
+ }
54
+
55
+ function validateExamples(
56
+ arr: unknown,
57
+ field: string
58
+ ): ZentiExample[] {
59
+ if (!Array.isArray(arr)) {
60
+ throw new ZentiValidationError(`${field} debe ser un array`);
61
+ }
62
+ return arr.map((ex: unknown, i: number) => {
63
+ if (!ex || typeof ex !== 'object') {
64
+ throw new ZentiValidationError(`${field}[${i}] debe ser un objeto`);
65
+ }
66
+ const item = ex as Record<string, unknown>;
67
+ if (!item.user || !item.agent) {
68
+ throw new ZentiValidationError(
69
+ `${field}[${i}] debe tener los campos "user" y "agent"`
70
+ );
71
+ }
72
+ return { user: String(item.user), agent: String(item.agent) };
73
+ });
74
+ }
75
+
76
+ export function parseZenti(content: string): ZentiPersona {
77
+ let raw: unknown;
78
+ try {
79
+ raw = yaml.load(content);
80
+ } catch (e) {
81
+ throw new ZentiValidationError(`YAML inválido: ${(e as Error).message}`);
82
+ }
83
+
84
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
85
+ throw new ZentiValidationError('El archivo debe ser un objeto YAML válido');
86
+ }
87
+
88
+ const obj = raw as Record<string, unknown>;
89
+
90
+ // Top-level required fields
91
+ if (!obj.version) throw new ZentiValidationError('Falta el campo requerido: version');
92
+ if (!obj.name) throw new ZentiValidationError('Falta el campo requerido: name');
93
+ if (!obj.core) throw new ZentiValidationError('Falta la capa requerida: core');
94
+ if (!obj.identity) throw new ZentiValidationError('Falta la capa requerida: identity');
95
+ if (!obj.examples) throw new ZentiValidationError('Falta la capa requerida: examples');
96
+ if (!obj.guardrails) throw new ZentiValidationError('Falta la capa requerida: guardrails');
97
+
98
+ // Validate core (Big Five)
99
+ if (typeof obj.core !== 'object' || Array.isArray(obj.core)) {
100
+ throw new ZentiValidationError('core debe ser un objeto');
101
+ }
102
+ const core = obj.core as Record<string, unknown>;
103
+ const validatedCore = {
104
+ openness: validateNumber(core.openness, 'core.openness'),
105
+ conscientiousness: validateNumber(core.conscientiousness, 'core.conscientiousness'),
106
+ extraversion: validateNumber(core.extraversion, 'core.extraversion'),
107
+ agreeableness: validateNumber(core.agreeableness, 'core.agreeableness'),
108
+ neuroticism: validateNumber(core.neuroticism, 'core.neuroticism'),
109
+ };
110
+
111
+ // Validate identity
112
+ if (typeof obj.identity !== 'object' || Array.isArray(obj.identity)) {
113
+ throw new ZentiValidationError('identity debe ser un objeto');
114
+ }
115
+ const identity = obj.identity as Record<string, unknown>;
116
+ if (!identity.role) throw new ZentiValidationError('Falta el campo requerido: identity.role');
117
+ if (!identity.backstory) throw new ZentiValidationError('Falta el campo requerido: identity.backstory');
118
+ if (!identity.communication_style) {
119
+ throw new ZentiValidationError('Falta el campo requerido: identity.communication_style');
120
+ }
121
+
122
+ // Validate examples
123
+ if (typeof obj.examples !== 'object' || Array.isArray(obj.examples)) {
124
+ throw new ZentiValidationError('examples debe ser un objeto');
125
+ }
126
+ const examples = obj.examples as Record<string, unknown>;
127
+ const onBrand = validateExamples(examples.on_brand, 'examples.on_brand');
128
+ const offBrand = validateExamples(examples.off_brand, 'examples.off_brand');
129
+
130
+ // Validate guardrails
131
+ if (typeof obj.guardrails !== 'object' || Array.isArray(obj.guardrails)) {
132
+ throw new ZentiValidationError('guardrails debe ser un objeto');
133
+ }
134
+ const guardrails = obj.guardrails as Record<string, unknown>;
135
+ if (!Array.isArray(guardrails.always)) {
136
+ throw new ZentiValidationError('guardrails.always debe ser un array');
137
+ }
138
+ if (!Array.isArray(guardrails.never)) {
139
+ throw new ZentiValidationError('guardrails.never debe ser un array');
140
+ }
141
+ if (!Array.isArray(guardrails.escalate_if)) {
142
+ throw new ZentiValidationError('guardrails.escalate_if debe ser un array');
143
+ }
144
+
145
+ return {
146
+ version: String(obj.version),
147
+ name: String(obj.name),
148
+ core: validatedCore,
149
+ identity: {
150
+ role: String(identity.role),
151
+ backstory: String(identity.backstory),
152
+ communication_style: String(identity.communication_style),
153
+ },
154
+ examples: {
155
+ on_brand: onBrand,
156
+ off_brand: offBrand,
157
+ },
158
+ guardrails: {
159
+ always: (guardrails.always as unknown[]).map(String),
160
+ never: (guardrails.never as unknown[]).map(String),
161
+ escalate_if: (guardrails.escalate_if as unknown[]).map(String),
162
+ },
163
+ };
164
+ }
@@ -0,0 +1,218 @@
1
+ import { parseZenti } from '../src/parser';
2
+ import { toClaude } from '../src/compiler/toClaude';
3
+ import { toOpenAI } from '../src/compiler/toOpenAI';
4
+ import { toGemini } from '../src/compiler/toGemini';
5
+ import { translateBigFive } from '../src/compiler/utils';
6
+
7
+ const VALID_ZENTI = `
8
+ version: "1.0"
9
+ name: "Sofia"
10
+
11
+ core:
12
+ openness: 0.7
13
+ conscientiousness: 0.9
14
+ extraversion: 0.4
15
+ agreeableness: 0.8
16
+ neuroticism: 0.2
17
+
18
+ identity:
19
+ role: "Asistente de soporte de TechCorp"
20
+ backstory: "Experta técnica con 5 años de experiencia, paciente y directa"
21
+ communication_style: "profesional con calidez, respuestas concisas"
22
+
23
+ examples:
24
+ on_brand:
25
+ - user: "no funciona"
26
+ agent: "Entiendo tu frustración. Cuéntame qué error ves."
27
+ off_brand:
28
+ - user: "no funciona"
29
+ agent: "No sé, prueba reiniciando."
30
+
31
+ guardrails:
32
+ always:
33
+ - "Reconocer el problema antes de dar solución"
34
+ never:
35
+ - "Decir 'no puedo' sin ofrecer alternativa"
36
+ escalate_if:
37
+ - "El usuario menciona cancelación"
38
+ `;
39
+
40
+ // ── Claude Compiler ────────────────────────────────────────────────────────
41
+
42
+ describe('Compiler — Claude', () => {
43
+ const persona = parseZenti(VALID_ZENTI);
44
+ const { system } = toClaude(persona);
45
+
46
+ test('el output contiene la sección de guardrails', () => {
47
+ expect(system).toContain('REGLAS DE COMPORTAMIENTO');
48
+ });
49
+
50
+ test('el output contiene reglas always', () => {
51
+ expect(system).toContain('Reconocer el problema antes de dar solución');
52
+ });
53
+
54
+ test('el output contiene reglas never', () => {
55
+ expect(system).toContain("Decir 'no puedo' sin ofrecer alternativa");
56
+ });
57
+
58
+ test('el output contiene la condición de escalación', () => {
59
+ expect(system).toContain('El usuario menciona cancelación');
60
+ });
61
+
62
+ test('los guardrails aparecen antes que la identidad', () => {
63
+ const guardrailsPos = system.indexOf('REGLAS DE COMPORTAMIENTO');
64
+ const identityPos = system.indexOf('IDENTIDAD');
65
+ expect(guardrailsPos).toBeGreaterThanOrEqual(0);
66
+ expect(identityPos).toBeGreaterThanOrEqual(0);
67
+ expect(guardrailsPos).toBeLessThan(identityPos);
68
+ });
69
+
70
+ test('la identidad aparece antes que los ejemplos', () => {
71
+ const identityPos = system.indexOf('IDENTIDAD');
72
+ const examplesPos = system.indexOf('EJEMPLOS');
73
+ expect(identityPos).toBeLessThan(examplesPos);
74
+ });
75
+
76
+ test('el output contiene los ejemplos on_brand', () => {
77
+ expect(system).toContain('Entiendo tu frustración. Cuéntame qué error ves.');
78
+ });
79
+
80
+ test('el output contiene el nombre del agente', () => {
81
+ expect(system).toContain('Sofia');
82
+ });
83
+ });
84
+
85
+ // ── OpenAI Compiler ────────────────────────────────────────────────────────
86
+
87
+ describe('Compiler — OpenAI', () => {
88
+ const persona = parseZenti(VALID_ZENTI);
89
+ const result = toOpenAI(persona);
90
+
91
+ test('el output tiene el campo system', () => {
92
+ expect(result).toHaveProperty('system');
93
+ expect(typeof result.system).toBe('string');
94
+ expect(result.system.length).toBeGreaterThan(0);
95
+ });
96
+
97
+ test('el output tiene el campo messages como array', () => {
98
+ expect(result).toHaveProperty('messages');
99
+ expect(Array.isArray(result.messages)).toBe(true);
100
+ });
101
+
102
+ test('los messages tienen roles user/assistant alternados', () => {
103
+ const roles = result.messages.map((m) => m.role);
104
+ expect(roles[0]).toBe('user');
105
+ expect(roles[1]).toBe('assistant');
106
+ });
107
+
108
+ test('el system prompt menciona el nombre del agente en primera persona', () => {
109
+ expect(result.system).toContain('Sofia');
110
+ });
111
+
112
+ test('el system prompt incluye los guardrails', () => {
113
+ expect(result.system).toContain('Siempre debes');
114
+ expect(result.system).toContain('Nunca debes');
115
+ });
116
+
117
+ test('los messages reflejan los ejemplos on_brand', () => {
118
+ const userMsg = result.messages.find((m) => m.role === 'user');
119
+ expect(userMsg?.content).toBe('no funciona');
120
+ const assistantMsg = result.messages.find((m) => m.role === 'assistant');
121
+ expect(assistantMsg?.content).toContain('frustración');
122
+ });
123
+ });
124
+
125
+ // ── Gemini Compiler ────────────────────────────────────────────────────────
126
+
127
+ describe('Compiler — Gemini', () => {
128
+ const persona = parseZenti(VALID_ZENTI);
129
+ const result = toGemini(persona);
130
+
131
+ test('el output tiene systemInstruction', () => {
132
+ expect(result).toHaveProperty('systemInstruction');
133
+ expect(result.systemInstruction).toHaveProperty('parts');
134
+ expect(Array.isArray(result.systemInstruction.parts)).toBe(true);
135
+ expect(result.systemInstruction.parts[0]).toHaveProperty('text');
136
+ });
137
+
138
+ test('el output tiene contents como array', () => {
139
+ expect(result).toHaveProperty('contents');
140
+ expect(Array.isArray(result.contents)).toBe(true);
141
+ });
142
+
143
+ test('los contents tienen roles user/model', () => {
144
+ const roles = result.contents.map((c) => c.role);
145
+ expect(roles[0]).toBe('user');
146
+ expect(roles[1]).toBe('model');
147
+ });
148
+
149
+ test('systemInstruction incluye la identidad del agente', () => {
150
+ const text = result.systemInstruction.parts[0].text;
151
+ expect(text).toContain('Sofia');
152
+ expect(text).toContain('Asistente de soporte de TechCorp');
153
+ });
154
+
155
+ test('systemInstruction incluye los guardrails', () => {
156
+ const text = result.systemInstruction.parts[0].text;
157
+ expect(text).toContain('Siempre');
158
+ expect(text).toContain('Nunca');
159
+ });
160
+
161
+ test('los contents parts tienen la forma { text: string }', () => {
162
+ result.contents.forEach((content) => {
163
+ expect(Array.isArray(content.parts)).toBe(true);
164
+ content.parts.forEach((part) => {
165
+ expect(part).toHaveProperty('text');
166
+ expect(typeof part.text).toBe('string');
167
+ });
168
+ });
169
+ });
170
+ });
171
+
172
+ // ── Big Five Translation ───────────────────────────────────────────────────
173
+
174
+ describe('Compiler — translateBigFive', () => {
175
+ test('openness > 0.6 genera el trait correspondiente', () => {
176
+ const traits = translateBigFive({
177
+ openness: 0.7,
178
+ conscientiousness: 0.5,
179
+ extraversion: 0.5,
180
+ agreeableness: 0.5,
181
+ neuroticism: 0.5,
182
+ });
183
+ expect(traits.some((t) => t.includes('creativo'))).toBe(true);
184
+ });
185
+
186
+ test('extraversion < 0.5 genera el trait de reservado', () => {
187
+ const traits = translateBigFive({
188
+ openness: 0.5,
189
+ conscientiousness: 0.5,
190
+ extraversion: 0.4,
191
+ agreeableness: 0.5,
192
+ neuroticism: 0.5,
193
+ });
194
+ expect(traits.some((t) => t.includes('reservado'))).toBe(true);
195
+ });
196
+
197
+ test('valores sin umbral no generan ningún trait', () => {
198
+ const traits = translateBigFive({
199
+ openness: 0.5,
200
+ conscientiousness: 0.5,
201
+ extraversion: 0.5,
202
+ agreeableness: 0.5,
203
+ neuroticism: 0.5,
204
+ });
205
+ expect(traits).toHaveLength(0);
206
+ });
207
+
208
+ test('Sofia genera todos los traits esperados', () => {
209
+ const traits = translateBigFive({
210
+ openness: 0.7, // > 0.6 → creativo
211
+ conscientiousness: 0.9, // > 0.7 → metódico
212
+ extraversion: 0.4, // < 0.5 → reservado
213
+ agreeableness: 0.8, // > 0.7 → empático
214
+ neuroticism: 0.2, // < 0.3 → calmado
215
+ });
216
+ expect(traits).toHaveLength(5);
217
+ });
218
+ });