create-lexy 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2488 -61
- package/package.json +13 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFileSync as
|
|
3
|
-
`)};import
|
|
4
|
-
`);if(process.stdout.isTTY&&process.stdin.isTTY){let
|
|
5
|
-
${
|
|
6
|
-
\xBFContinuar?`});if(
|
|
7
|
-
`)+
|
|
8
|
-
Usa --overwrite para sobrescribirlos.`)),process.exitCode=1;return}}for(let[
|
|
2
|
+
import{readFileSync as To}from"fs";import tt from"path";import{fileURLToPath as Mo}from"url";import{Command as Lo}from"commander";import zo from"picocolors";import{spawn as lt}from"child_process";import W from"fs/promises";import Z from"path";import{confirm as dt,isCancel as pt,spinner as je}from"@clack/prompts";import S from"picocolors";import te from"path";var ot=(e,t)=>t.architecture!=="feature"?e:e.replace(/from ["']@\/lib\/utils\/cn["']/g,'from "@/shared/lib/utils/cn"').replace(/from ["']@\/components\/base\//g,'from "@/shared/components/base/'),nt=[["components/base/",e=>e.paths.components],["views/",e=>e.paths.views],["hooks/",e=>e.paths.hooks],["lib/",e=>e.paths.lib]],C=(e,t)=>e.files.filter(o=>typeof o.content=="string").map(o=>{let r=nt.find(([i])=>o.path.startsWith(i));return{relPath:r?te.posix.join(r[1](t),o.path.slice(r[0].length)):te.posix.join(t.paths.components,te.posix.basename(o.path)),content:ot(o.content,t)}});import xe from"fs/promises";import at from"path";var be=e=>at.join(e,".lexy"),L=async e=>{let t;try{t=await xe.readFile(be(e),"utf-8")}catch{throw new Error("No se encontr\xF3 el archivo .lexy. \xBFEst\xE1s en la ra\xEDz de un proyecto Lexy? (cr\xE9alo con `npx create-lexy create`)")}let o=JSON.parse(t);if(!o.paths?.components)throw new Error("El .lexy no tiene paths.components \u2014 archivo corrupto o de una versi\xF3n incompatible.");return o.installed??={},o},Ie=async(e,t)=>{await xe.writeFile(be(e),JSON.stringify(t,null,2)+`
|
|
3
|
+
`)};import N from"fs/promises";import rt from"os";import j from"path";import{fileURLToPath as it}from"url";var Ee="@lexydesign/registry",F=`https://cdn.jsdelivr.net/npm/${Ee}`,st=8e3,J=j.resolve(j.dirname(it(import.meta.url)),"../assets"),ct=j.join(rt.homedir(),".cache","create-lexy","registry"),B=async e=>{let t=await fetch(e,{signal:AbortSignal.timeout(st)});if(!t.ok){let o=new Error(`HTTP ${t.status} al pedir ${e}`);throw o.status=t.status,o}return await t.json()},ke=async e=>{try{return JSON.parse(await N.readFile(j.join(J,"r",e),"utf-8"))}catch{return null}},oe=async()=>{try{return(await N.readFile(j.join(J,"registry-version"),"utf-8")).trim()}catch{return"bundle"}},_=null,G=async()=>{if(_)return _;try{_=(await B(`https://registry.npmjs.org/${Ee}/latest`)).version}catch{try{_=(await B(`${F}@latest/package.json`)).version}catch{_=await oe()}}return _},x=async(e,t)=>{let o=e.replace(/^@lexy\//,""),r=t??await G(),a=j.join(ct,r,`${o}.json`);try{return{item:JSON.parse(await N.readFile(a,"utf-8")),version:r,source:"cache"}}catch{}try{let i=await B(`${F}@${r}/r/${o}.json`);return await N.mkdir(j.dirname(a),{recursive:!0}),await N.writeFile(a,JSON.stringify(i)),{item:i,version:r,source:"cdn"}}catch(i){if(i.status===404)throw new Error(`El componente "${o}" no existe en el registry (versi\xF3n ${r}). Mira el cat\xE1logo con \`create-lexy view --list\`.`);let n=await ke(`${o}.json`);if(n)return{item:n,version:await oe(),source:"bundle"};throw new Error(`No se pudo alcanzar el registry (\xBFsin red?) y "${o}" tampoco est\xE1 en el bundle local del CLI.`)}},we=async()=>{try{let e=await G();return{index:await B(`${F}@${e}/r/registry.json`),version:e,source:"cdn"}}catch{let e=await ke("registry.json");if(e)return{index:e,version:await oe(),source:"bundle"};throw new Error("No se pudo alcanzar el registry ni leer el bundle local del CLI.")}},Pe=j.join(J,"theme/lexy-theme.css"),Ce=j.join(J,"fonts");var mt=(e,t)=>new Promise((o,r)=>{lt("pnpm",["add",...e],{cwd:t,stdio:"ignore",shell:!0}).on("close",i=>i===0?o():r(new Error(`pnpm add fall\xF3 con c\xF3digo ${i}`)))}),ut=async e=>{try{return await W.readFile(e,"utf-8")}catch{return null}},Re=async(e,t)=>{let o=process.cwd(),r=await L(o),a=je();a.start("Resolviendo componentes en el registry...");let i=new Map,n=async m=>{let h=m.toLowerCase().replace(/^@lexy\//,"");if(i.has(h))return;let s=await x(h);i.set(h,s);for(let c of s.item.registryDependencies??[])await n(c)};try{for(let m of e)await n(m)}catch(m){throw a.stop(S.red("No se pudo resolver el pedido.")),m}let p=new Map;for(let[m,{item:h}]of i){let s=[];for(let c of C(h,r)){let g=await ut(Z.join(o,c.relPath));s.push({...c,status:g===null?"new":g===c.content?"same":"differs"})}p.set(m,s)}let l=Array.from(p.values()).flat().filter(m=>m.status==="differs");if(a.stop(`Resueltos: ${Array.from(i.keys()).join(", ")}`),l.length>0&&!t.overwrite){let m=l.map(h=>` ${h.relPath}`).join(`
|
|
4
|
+
`);if(process.stdout.isTTY&&process.stdin.isTTY){let h=await dt({message:`Estos archivos existen con cambios locales y ser\xE1n sobrescritos:
|
|
5
|
+
${m}
|
|
6
|
+
\xBFContinuar?`});if(pt(h)||h!==!0){console.log(S.yellow("Operaci\xF3n cancelada \u2014 nada fue modificado.")),process.exitCode=1;return}}else{console.error(S.red(`Estos archivos existen con cambios locales:
|
|
7
|
+
`)+m+S.dim(`
|
|
8
|
+
Usa --overwrite para sobrescribirlos.`)),process.exitCode=1;return}}for(let[m,h]of p){let{item:s,version:c,source:g}=i.get(m);for(let v of h){if(v.status==="same")continue;let O=Z.join(o,v.relPath);await W.mkdir(Z.dirname(O),{recursive:!0}),await W.writeFile(O,v.content)}r.installed[s.name]=c;let I=e.map(v=>v.toLowerCase()).includes(m)?"":S.dim(" (dependencia)"),q=g==="bundle"?S.yellow(" [bundle local \u2014 sin red]"):"";console.log(S.green(`${S.bold(s.title??s.name)} instalado`)+I+S.cyan(` \u2192 ${r.paths.components}/`)+q);for(let v of h){let O=v.status==="same"?S.dim(" (sin cambios)"):"";console.log(S.dim(` ${v.relPath}${O}`))}}await Ie(o,r);let y=new Set;for(let{item:m}of i.values())for(let h of m.dependencies??[])y.add(h);let u=JSON.parse(await W.readFile(Z.join(o,"package.json"),"utf-8")),d={...u.dependencies,...u.devDependencies},P=m=>{let h=m.lastIndexOf("@");return h>0?m.slice(0,h):m},V=Array.from(y).filter(m=>!d[P(m)]);if(V.length>0)if(t.install===!1)console.log(S.yellow(`Dependencias pendientes (inst\xE1lalas t\xFA): ${V.join(", ")}`));else{let m=je();m.start(`Instalando dependencias: ${V.join(", ")}...`),await mt(V,o),m.stop("Dependencias instaladas.")}};import{spawn as ho}from"child_process";import{note as go,outro as vo,spinner as So}from"@clack/prompts";import E from"picocolors";import{cancel as ft,intro as yt,isCancel as ht,select as Me,text as gt}from"@clack/prompts";import Le from"picocolors";var ne=[{value:"screen",label:"Una pantalla o flujo corto",hint:"landing, formulario, demo \u2014 sin navegaci\xF3n entre secciones",architecture:"layer"},{value:"app",label:"Una app con secciones y navegaci\xF3n",hint:"dashboard, listados, detalle \u2014 varias vistas con router",architecture:"feature"}],Te=e=>ne.find(t=>t.value===e).architecture;var vt=`
|
|
9
9
|
###### ######
|
|
10
10
|
##::+#### ####+::##
|
|
11
11
|
#-..:+################+:..:#
|
|
@@ -20,9 +20,2399 @@ Usa --overwrite para sobrescribirlos.`)),process.exitCode=1;return}}for(let[a,l]
|
|
|
20
20
|
...........:====:...........
|
|
21
21
|
......................
|
|
22
22
|
..............
|
|
23
|
-
`,
|
|
24
|
-
${
|
|
25
|
-
|
|
23
|
+
`,ae=e=>{ht(e)&&(ft("Operaci\xF3n cancelada por el usuario."),process.exit(0))},ze=async e=>{console.clear(),yt(`${Le.cyan(vt)}
|
|
24
|
+
${Le.bold(" Bienvenido al generador de proyectos Lexy")}`);let t=e;if(!t){let a=await gt({message:"\xBFCu\xE1l es el nombre de tu proyecto?",placeholder:"lexy-app",validate:i=>{if(!i||i.trim()==="")return"El nombre es obligatorio.";if(!/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(i))return"El nombre debe estar en min\xFAsculas, sin espacios ni caracteres especiales."}});ae(a),t=a}let o=await Me({message:"\xBFPara qui\xE9n es este proyecto?",options:[{label:"Clientes de Lexy",value:"cliente",hint:"Producto de cara al cliente final"},{label:"Equipo interno / CRM",value:"crm",hint:"Herramienta densa para el equipo Lexy"},{label:"Ambos o a\xFAn no lo s\xE9",value:"mixto",hint:"Se decide por pantalla m\xE1s adelante"}],initialValue:"mixto"});ae(o);let r=await Me({message:"\xBFC\xF3mo es lo que vas a construir?",options:ne.map(a=>({label:a.label,value:a.value,hint:a.hint})),initialValue:"app"});return ae(r),{projectName:t,architecture:Te(r),world:o}};import R from"fs/promises";import T from"path";import Ae from"fs/promises";import Oe from"path";var re="src/prototype/data-contract/prototype-data-contract.ts",St="src/prototype/data-contract/data-contract-schema.ts",xt="scripts/check-data-contract.ts",bt=String.raw`import { z } from "zod";
|
|
25
|
+
|
|
26
|
+
const camelCaseIdSchema = z
|
|
27
|
+
.string()
|
|
28
|
+
.regex(/^[a-z][a-zA-Z0-9]*$/, "Debe usar camelCase y comenzar con una letra minúscula.");
|
|
29
|
+
|
|
30
|
+
const nonEmptyTextSchema = z.string().trim().min(1, "No puede quedar vacío.");
|
|
31
|
+
const experienceLabelsSchema = z
|
|
32
|
+
.array(nonEmptyTextSchema)
|
|
33
|
+
.min(1, "Debe indicar al menos un uso dentro de la experiencia.");
|
|
34
|
+
|
|
35
|
+
export const dataOriginSchema = z.enum([
|
|
36
|
+
"lexyConfirmed",
|
|
37
|
+
"productAssumption",
|
|
38
|
+
"generatedByUsability",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export const technicalValidationStatusSchema = z.enum(["validated", "pendingTi", "rejected"]);
|
|
42
|
+
|
|
43
|
+
export const dataClassificationSchema = z.enum(["public", "internal", "sensitive", "restricted"]);
|
|
44
|
+
|
|
45
|
+
export const technicalValidationSchema = z
|
|
46
|
+
.object({
|
|
47
|
+
status: technicalValidationStatusSchema,
|
|
48
|
+
note: nonEmptyTextSchema.optional(),
|
|
49
|
+
validatedBy: nonEmptyTextSchema.optional(),
|
|
50
|
+
validatedAt: z.string().datetime({ offset: true }).optional(),
|
|
51
|
+
evidence: nonEmptyTextSchema.optional(),
|
|
52
|
+
})
|
|
53
|
+
.strict()
|
|
54
|
+
.superRefine((validation, context) => {
|
|
55
|
+
if (validation.status === "pendingTi" && !validation.note) {
|
|
56
|
+
context.addIssue({
|
|
57
|
+
code: "custom",
|
|
58
|
+
path: ["note"],
|
|
59
|
+
message: "Un pendiente de TI debe explicar qué necesita validarse.",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (validation.status === "validated") {
|
|
64
|
+
if (!validation.validatedBy) {
|
|
65
|
+
context.addIssue({
|
|
66
|
+
code: "custom",
|
|
67
|
+
path: ["validatedBy"],
|
|
68
|
+
message: "Un dato validado debe indicar quién lo validó.",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (!validation.validatedAt) {
|
|
72
|
+
context.addIssue({
|
|
73
|
+
code: "custom",
|
|
74
|
+
path: ["validatedAt"],
|
|
75
|
+
message: "Un dato validado debe indicar cuándo fue validado.",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const backendReferenceSchema = z
|
|
82
|
+
.string()
|
|
83
|
+
.regex(
|
|
84
|
+
/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/,
|
|
85
|
+
"Las referencias de Lexy deben usar snake_case, por ejemplo cliente.rut_cliente.",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export const sourceReferenceSchema = z
|
|
89
|
+
.object({
|
|
90
|
+
kind: z.enum(["lexyEntity", "lexyProjection", "api", "derived", "unknown"]),
|
|
91
|
+
reference: nonEmptyTextSchema.optional(),
|
|
92
|
+
})
|
|
93
|
+
.strict()
|
|
94
|
+
.superRefine((source, context) => {
|
|
95
|
+
if ((source.kind === "lexyEntity" || source.kind === "lexyProjection") && !source.reference) {
|
|
96
|
+
context.addIssue({
|
|
97
|
+
code: "custom",
|
|
98
|
+
path: ["reference"],
|
|
99
|
+
message: "La fuente Lexy debe indicar su referencia en snake_case.",
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (source.reference && (source.kind === "lexyEntity" || source.kind === "lexyProjection")) {
|
|
105
|
+
const result = backendReferenceSchema.safeParse(source.reference);
|
|
106
|
+
if (!result.success) {
|
|
107
|
+
context.addIssue({
|
|
108
|
+
code: "custom",
|
|
109
|
+
path: ["reference"],
|
|
110
|
+
message: result.error.issues[0]?.message ?? "Referencia backend inválida.",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const usageSchema = z
|
|
117
|
+
.object({
|
|
118
|
+
visible: z.boolean(),
|
|
119
|
+
editable: z.boolean(),
|
|
120
|
+
calculated: z.boolean(),
|
|
121
|
+
technical: z.boolean(),
|
|
122
|
+
filterable: z.boolean().optional(),
|
|
123
|
+
sortable: z.boolean().optional(),
|
|
124
|
+
})
|
|
125
|
+
.strict();
|
|
126
|
+
|
|
127
|
+
const metadataSchema = z
|
|
128
|
+
.object({
|
|
129
|
+
origin: dataOriginSchema,
|
|
130
|
+
source: sourceReferenceSchema,
|
|
131
|
+
dataClassification: dataClassificationSchema,
|
|
132
|
+
technicalValidation: technicalValidationSchema,
|
|
133
|
+
})
|
|
134
|
+
.strict();
|
|
135
|
+
|
|
136
|
+
const validateUsabilityEvidence = (
|
|
137
|
+
value: {
|
|
138
|
+
origin: z.infer<typeof dataOriginSchema>;
|
|
139
|
+
technicalValidation: z.infer<typeof technicalValidationSchema>;
|
|
140
|
+
},
|
|
141
|
+
context: z.RefinementCtx,
|
|
142
|
+
) => {
|
|
143
|
+
if (
|
|
144
|
+
value.origin === "generatedByUsability" &&
|
|
145
|
+
value.technicalValidation.status === "validated" &&
|
|
146
|
+
!value.technicalValidation.evidence
|
|
147
|
+
) {
|
|
148
|
+
context.addIssue({
|
|
149
|
+
code: "custom",
|
|
150
|
+
path: ["technicalValidation", "evidence"],
|
|
151
|
+
message: "Un dato generado por usabilidad solo puede validarse con evidencia explícita.",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const fieldSpecSchema = z
|
|
157
|
+
.object({
|
|
158
|
+
id: camelCaseIdSchema,
|
|
159
|
+
productDescription: nonEmptyTextSchema,
|
|
160
|
+
dataType: z.enum(["string", "number", "boolean", "date", "datetime", "enum", "identifier"]),
|
|
161
|
+
required: z.boolean(),
|
|
162
|
+
usage: usageSchema,
|
|
163
|
+
usedIn: experienceLabelsSchema,
|
|
164
|
+
enumValues: z.array(nonEmptyTextSchema).min(1).optional(),
|
|
165
|
+
derivation: nonEmptyTextSchema.optional(),
|
|
166
|
+
})
|
|
167
|
+
.extend(metadataSchema.shape)
|
|
168
|
+
.strict()
|
|
169
|
+
.superRefine((field, context) => {
|
|
170
|
+
validateUsabilityEvidence(field, context);
|
|
171
|
+
|
|
172
|
+
if (field.dataType === "enum" && !field.enumValues) {
|
|
173
|
+
context.addIssue({
|
|
174
|
+
code: "custom",
|
|
175
|
+
path: ["enumValues"],
|
|
176
|
+
message: "Un campo enum debe declarar al menos un valor.",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (field.dataType !== "enum" && field.enumValues) {
|
|
180
|
+
context.addIssue({
|
|
181
|
+
code: "custom",
|
|
182
|
+
path: ["enumValues"],
|
|
183
|
+
message: "Solo los campos enum pueden declarar enumValues.",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if ((field.usage.calculated || field.source.kind === "derived") && !field.derivation) {
|
|
187
|
+
context.addIssue({
|
|
188
|
+
code: "custom",
|
|
189
|
+
path: ["derivation"],
|
|
190
|
+
message: "Un dato calculado debe explicar su regla de derivación.",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export const entityStateSpecSchema = z
|
|
196
|
+
.object({
|
|
197
|
+
id: camelCaseIdSchema,
|
|
198
|
+
productDescription: nonEmptyTextSchema,
|
|
199
|
+
activationCondition: nonEmptyTextSchema,
|
|
200
|
+
visualImpact: nonEmptyTextSchema,
|
|
201
|
+
usedIn: experienceLabelsSchema,
|
|
202
|
+
})
|
|
203
|
+
.extend(metadataSchema.shape)
|
|
204
|
+
.strict()
|
|
205
|
+
.superRefine(validateUsabilityEvidence);
|
|
206
|
+
|
|
207
|
+
export const entitySpecSchema = z
|
|
208
|
+
.object({
|
|
209
|
+
id: camelCaseIdSchema,
|
|
210
|
+
productDescription: nonEmptyTextSchema,
|
|
211
|
+
roleInExperience: nonEmptyTextSchema,
|
|
212
|
+
usedIn: experienceLabelsSchema,
|
|
213
|
+
fields: z.record(camelCaseIdSchema, fieldSpecSchema),
|
|
214
|
+
states: z.array(entityStateSpecSchema).optional(),
|
|
215
|
+
})
|
|
216
|
+
.extend(metadataSchema.shape)
|
|
217
|
+
.strict()
|
|
218
|
+
.superRefine(validateUsabilityEvidence);
|
|
219
|
+
|
|
220
|
+
export const relationSpecSchema = z
|
|
221
|
+
.object({
|
|
222
|
+
id: camelCaseIdSchema,
|
|
223
|
+
fromEntity: camelCaseIdSchema,
|
|
224
|
+
toEntity: camelCaseIdSchema,
|
|
225
|
+
cardinality: z.enum(["oneToOne", "oneToMany", "manyToMany"]),
|
|
226
|
+
productDescription: nonEmptyTextSchema,
|
|
227
|
+
requiredBy: experienceLabelsSchema,
|
|
228
|
+
resolution: z.enum(["embedded", "queried", "derived", "unknown"]),
|
|
229
|
+
derivation: nonEmptyTextSchema.optional(),
|
|
230
|
+
})
|
|
231
|
+
.extend(metadataSchema.shape)
|
|
232
|
+
.strict()
|
|
233
|
+
.superRefine((relation, context) => {
|
|
234
|
+
validateUsabilityEvidence(relation, context);
|
|
235
|
+
if (
|
|
236
|
+
(relation.resolution === "derived" || relation.source.kind === "derived") &&
|
|
237
|
+
!relation.derivation
|
|
238
|
+
) {
|
|
239
|
+
context.addIssue({
|
|
240
|
+
code: "custom",
|
|
241
|
+
path: ["derivation"],
|
|
242
|
+
message: "Una relación derivada debe explicar su regla de derivación.",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
export const projectionFieldSpecSchema = z
|
|
248
|
+
.object({
|
|
249
|
+
id: camelCaseIdSchema,
|
|
250
|
+
source: z
|
|
251
|
+
.string()
|
|
252
|
+
.regex(
|
|
253
|
+
/^[a-z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*$/,
|
|
254
|
+
"Debe referenciar un campo como entidad.campo usando camelCase.",
|
|
255
|
+
),
|
|
256
|
+
required: z.boolean(),
|
|
257
|
+
})
|
|
258
|
+
.strict();
|
|
259
|
+
|
|
260
|
+
export const projectionSpecSchema = z
|
|
261
|
+
.object({
|
|
262
|
+
id: camelCaseIdSchema,
|
|
263
|
+
productDescription: nonEmptyTextSchema,
|
|
264
|
+
sourceEntities: experienceLabelsSchema,
|
|
265
|
+
consumedBy: experienceLabelsSchema,
|
|
266
|
+
fields: z.array(projectionFieldSpecSchema),
|
|
267
|
+
derivation: nonEmptyTextSchema.optional(),
|
|
268
|
+
})
|
|
269
|
+
.extend(metadataSchema.shape)
|
|
270
|
+
.strict()
|
|
271
|
+
.superRefine((projection, context) => {
|
|
272
|
+
validateUsabilityEvidence(projection, context);
|
|
273
|
+
if (projection.source.kind === "derived" && !projection.derivation) {
|
|
274
|
+
context.addIssue({
|
|
275
|
+
code: "custom",
|
|
276
|
+
path: ["derivation"],
|
|
277
|
+
message: "Una proyección derivada debe explicar su regla de derivación.",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const projectSchema = z
|
|
283
|
+
.object({
|
|
284
|
+
name: nonEmptyTextSchema,
|
|
285
|
+
description: z.string().trim(),
|
|
286
|
+
})
|
|
287
|
+
.strict();
|
|
288
|
+
|
|
289
|
+
export const prototypeDataContractSchema = z
|
|
290
|
+
.object({
|
|
291
|
+
contractVersion: z.literal("1"),
|
|
292
|
+
project: projectSchema,
|
|
293
|
+
entities: z.record(camelCaseIdSchema, entitySpecSchema),
|
|
294
|
+
relations: z.array(relationSpecSchema),
|
|
295
|
+
projections: z.record(camelCaseIdSchema, projectionSpecSchema),
|
|
296
|
+
})
|
|
297
|
+
.strict()
|
|
298
|
+
.superRefine((contract, context) => {
|
|
299
|
+
for (const [entityKey, entity] of Object.entries(contract.entities)) {
|
|
300
|
+
if (entityKey !== entity.id) {
|
|
301
|
+
context.addIssue({
|
|
302
|
+
code: "custom",
|
|
303
|
+
path: ["entities", entityKey, "id"],
|
|
304
|
+
message: 'El id "' + entity.id + '" debe coincidir con la key "' + entityKey + '".',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const [fieldKey, field] of Object.entries(entity.fields)) {
|
|
309
|
+
if (fieldKey !== field.id) {
|
|
310
|
+
context.addIssue({
|
|
311
|
+
code: "custom",
|
|
312
|
+
path: ["entities", entityKey, "fields", fieldKey, "id"],
|
|
313
|
+
message: 'El id "' + field.id + '" debe coincidir con la key "' + fieldKey + '".',
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const stateIds = new Set<string>();
|
|
319
|
+
for (const [stateIndex, state] of (entity.states ?? []).entries()) {
|
|
320
|
+
if (stateIds.has(state.id)) {
|
|
321
|
+
context.addIssue({
|
|
322
|
+
code: "custom",
|
|
323
|
+
path: ["entities", entityKey, "states", stateIndex, "id"],
|
|
324
|
+
message: 'El estado "' + state.id + '" está declarado más de una vez.',
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
stateIds.add(state.id);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const relationIds = new Set<string>();
|
|
332
|
+
for (const [relationIndex, relation] of contract.relations.entries()) {
|
|
333
|
+
if (relationIds.has(relation.id)) {
|
|
334
|
+
context.addIssue({
|
|
335
|
+
code: "custom",
|
|
336
|
+
path: ["relations", relationIndex, "id"],
|
|
337
|
+
message: 'La relación "' + relation.id + '" está declarada más de una vez.',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
relationIds.add(relation.id);
|
|
341
|
+
|
|
342
|
+
if (!contract.entities[relation.fromEntity]) {
|
|
343
|
+
context.addIssue({
|
|
344
|
+
code: "custom",
|
|
345
|
+
path: ["relations", relationIndex, "fromEntity"],
|
|
346
|
+
message: 'La entidad "' + relation.fromEntity + '" no existe.',
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (!contract.entities[relation.toEntity]) {
|
|
350
|
+
context.addIssue({
|
|
351
|
+
code: "custom",
|
|
352
|
+
path: ["relations", relationIndex, "toEntity"],
|
|
353
|
+
message: 'La entidad "' + relation.toEntity + '" no existe.',
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const [projectionKey, projection] of Object.entries(contract.projections)) {
|
|
359
|
+
if (projectionKey !== projection.id) {
|
|
360
|
+
context.addIssue({
|
|
361
|
+
code: "custom",
|
|
362
|
+
path: ["projections", projectionKey, "id"],
|
|
363
|
+
message:
|
|
364
|
+
'El id "' + projection.id + '" debe coincidir con la key "' + projectionKey + '".',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const [sourceIndex, entityId] of projection.sourceEntities.entries()) {
|
|
369
|
+
if (!contract.entities[entityId]) {
|
|
370
|
+
context.addIssue({
|
|
371
|
+
code: "custom",
|
|
372
|
+
path: ["projections", projectionKey, "sourceEntities", sourceIndex],
|
|
373
|
+
message: 'La entidad "' + entityId + '" no existe.',
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const projectionFieldIds = new Set<string>();
|
|
379
|
+
for (const [fieldIndex, field] of projection.fields.entries()) {
|
|
380
|
+
if (projectionFieldIds.has(field.id)) {
|
|
381
|
+
context.addIssue({
|
|
382
|
+
code: "custom",
|
|
383
|
+
path: ["projections", projectionKey, "fields", fieldIndex, "id"],
|
|
384
|
+
message: 'El campo proyectado "' + field.id + '" está declarado más de una vez.',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
projectionFieldIds.add(field.id);
|
|
388
|
+
|
|
389
|
+
const [entityId, fieldId] = field.source.split(".");
|
|
390
|
+
const entity = contract.entities[entityId];
|
|
391
|
+
if (!entity) {
|
|
392
|
+
context.addIssue({
|
|
393
|
+
code: "custom",
|
|
394
|
+
path: ["projections", projectionKey, "fields", fieldIndex, "source"],
|
|
395
|
+
message: 'La referencia "' + field.source + '" apunta a una entidad que no existe.',
|
|
396
|
+
});
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (!entity.fields[fieldId]) {
|
|
400
|
+
context.addIssue({
|
|
401
|
+
code: "custom",
|
|
402
|
+
path: ["projections", projectionKey, "fields", fieldIndex, "source"],
|
|
403
|
+
message: 'La referencia "' + field.source + '" apunta a un campo que no existe.',
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (!projection.sourceEntities.includes(entityId)) {
|
|
407
|
+
context.addIssue({
|
|
408
|
+
code: "custom",
|
|
409
|
+
path: ["projections", projectionKey, "fields", fieldIndex, "source"],
|
|
410
|
+
message: 'La entidad "' + entityId + '" debe declararse también en sourceEntities.',
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
export type DataOrigin = z.infer<typeof dataOriginSchema>;
|
|
418
|
+
export type TechnicalValidationStatus = z.infer<typeof technicalValidationStatusSchema>;
|
|
419
|
+
export type TechnicalValidation = z.infer<typeof technicalValidationSchema>;
|
|
420
|
+
export type SourceReference = z.infer<typeof sourceReferenceSchema>;
|
|
421
|
+
export type FieldSpec = z.infer<typeof fieldSpecSchema>;
|
|
422
|
+
export type EntityStateSpec = z.infer<typeof entityStateSpecSchema>;
|
|
423
|
+
export type EntitySpec = z.infer<typeof entitySpecSchema>;
|
|
424
|
+
export type RelationSpec = z.infer<typeof relationSpecSchema>;
|
|
425
|
+
export type ProjectionSpec = z.infer<typeof projectionSpecSchema>;
|
|
426
|
+
export type PrototypeDataContract = z.infer<typeof prototypeDataContractSchema>;
|
|
427
|
+
|
|
428
|
+
export function definePrototypeDataContract(
|
|
429
|
+
contract: PrototypeDataContract,
|
|
430
|
+
): PrototypeDataContract {
|
|
431
|
+
return prototypeDataContractSchema.parse(contract);
|
|
432
|
+
}
|
|
433
|
+
`,It=String.raw`import { ZodError } from "zod";
|
|
434
|
+
|
|
435
|
+
import { prototypeDataContractSchema } from "../src/prototype/data-contract/data-contract-schema";
|
|
436
|
+
|
|
437
|
+
const formatPath = (path: PropertyKey[]) =>
|
|
438
|
+
path.length === 0 ? "contrato" : path.map(String).join(".");
|
|
439
|
+
|
|
440
|
+
const printIssues = (error: ZodError) => {
|
|
441
|
+
console.error("✗ Contrato de datos inválido");
|
|
442
|
+
for (const issue of error.issues) {
|
|
443
|
+
console.error(" " + formatPath(issue.path));
|
|
444
|
+
console.error(" " + issue.message);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const countPendingValidations = (value: unknown): number => {
|
|
449
|
+
if (Array.isArray(value)) {
|
|
450
|
+
return value.reduce((total, item) => total + countPendingValidations(item), 0);
|
|
451
|
+
}
|
|
452
|
+
if (!value || typeof value !== "object") return 0;
|
|
453
|
+
|
|
454
|
+
const record = value as Record<string, unknown>;
|
|
455
|
+
const ownPending =
|
|
456
|
+
record.technicalValidation &&
|
|
457
|
+
typeof record.technicalValidation === "object" &&
|
|
458
|
+
(record.technicalValidation as Record<string, unknown>).status === "pendingTi"
|
|
459
|
+
? 1
|
|
460
|
+
: 0;
|
|
461
|
+
|
|
462
|
+
return Object.values(record).reduce(
|
|
463
|
+
(total, item) => total + countPendingValidations(item),
|
|
464
|
+
ownPending,
|
|
465
|
+
);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
const module = await import("../src/prototype/data-contract/prototype-data-contract");
|
|
470
|
+
const result = prototypeDataContractSchema.safeParse(module.prototypeDataContract);
|
|
471
|
+
|
|
472
|
+
if (!result.success) {
|
|
473
|
+
printIssues(result.error);
|
|
474
|
+
process.exitCode = 1;
|
|
475
|
+
} else {
|
|
476
|
+
const contract = result.data;
|
|
477
|
+
console.log("✓ Contrato de datos válido");
|
|
478
|
+
console.log(" " + Object.keys(contract.entities).length + " entidades");
|
|
479
|
+
console.log(" " + contract.relations.length + " relaciones");
|
|
480
|
+
console.log(" " + Object.keys(contract.projections).length + " proyecciones");
|
|
481
|
+
console.log(
|
|
482
|
+
" " + countPendingValidations(contract) + " elementos pendientes de validación con TI",
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (error instanceof ZodError) {
|
|
487
|
+
printIssues(error);
|
|
488
|
+
} else {
|
|
489
|
+
console.error("✗ No se pudo cargar el contrato de datos");
|
|
490
|
+
console.error(" " + (error instanceof Error ? error.message : String(error)));
|
|
491
|
+
}
|
|
492
|
+
process.exitCode = 1;
|
|
493
|
+
}
|
|
494
|
+
`,Et=e=>`import { definePrototypeDataContract } from "./data-contract-schema";
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Fuente de verdad de los datos usados por el prototipo.
|
|
498
|
+
*
|
|
499
|
+
* El designer describe el producto en lenguaje natural; el agente mantiene este
|
|
500
|
+
* contrato y ejecuta \`pnpm check:data-contract\` antes de implementar la UI.
|
|
501
|
+
*/
|
|
502
|
+
export const prototypeDataContract = definePrototypeDataContract({
|
|
503
|
+
contractVersion: "1",
|
|
504
|
+
project: {
|
|
505
|
+
name: ${JSON.stringify(e)},
|
|
506
|
+
description: "",
|
|
507
|
+
},
|
|
508
|
+
entities: {},
|
|
509
|
+
relations: [],
|
|
510
|
+
projections: {},
|
|
511
|
+
});
|
|
512
|
+
`,De=async(e,t)=>{let o=async(r,a)=>{let i=Oe.join(e,r);await Ae.mkdir(Oe.dirname(i),{recursive:!0}),await Ae.writeFile(i,a)};await o(St,bt),await o(re,Et(t)),await o(xt,It)};import $e from"fs/promises";import Ve from"path";var ie="src/prototype/event-engine/prototype-event-engine-contract.ts",kt="src/prototype/event-engine/event-engine-contract-schema.ts",wt="src/prototype/event-engine/payload-schema.ts",se="src/prototype/event-engine/event-engine.ts",Pt="scripts/check-event-engine.ts",Ct=String.raw`import { z } from "zod";
|
|
513
|
+
|
|
514
|
+
const camelCaseIdSchema = z
|
|
515
|
+
.string()
|
|
516
|
+
.regex(/^[a-z][a-zA-Z0-9]*$/, "Debe usar camelCase y comenzar con una letra minúscula.");
|
|
517
|
+
|
|
518
|
+
const nonEmptyTextSchema = z.string().trim().min(1, "No puede quedar vacío.");
|
|
519
|
+
|
|
520
|
+
const technicalEventIdSchema = z
|
|
521
|
+
.string()
|
|
522
|
+
.regex(
|
|
523
|
+
/^[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9]+-[A-Za-z0-9]+_V[0-9]+$/,
|
|
524
|
+
"Debe usar el formato eventista Dominio_Subdominio-NombreEvento_V1.",
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const dataReferenceSchema = z
|
|
528
|
+
.string()
|
|
529
|
+
.regex(
|
|
530
|
+
/^[a-z][a-zA-Z0-9]*(\.[a-z][a-zA-Z0-9]*)?$/,
|
|
531
|
+
"Debe referenciar una entidad o campo como entidad o entidad.campo usando camelCase.",
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
const triggerSchema = z
|
|
535
|
+
.object({
|
|
536
|
+
id: camelCaseIdSchema.optional(),
|
|
537
|
+
label: nonEmptyTextSchema,
|
|
538
|
+
source: z.enum(["screen", "userAction", "system", "external", "unknown"]),
|
|
539
|
+
description: nonEmptyTextSchema.optional(),
|
|
540
|
+
})
|
|
541
|
+
.strict();
|
|
542
|
+
|
|
543
|
+
const schemaReferenceSchema = z
|
|
544
|
+
.object({
|
|
545
|
+
schemaId: camelCaseIdSchema,
|
|
546
|
+
})
|
|
547
|
+
.strict();
|
|
548
|
+
|
|
549
|
+
export const dataLoadSpecSchema = z
|
|
550
|
+
.object({
|
|
551
|
+
id: camelCaseIdSchema,
|
|
552
|
+
name: nonEmptyTextSchema,
|
|
553
|
+
productDescription: nonEmptyTextSchema,
|
|
554
|
+
trigger: triggerSchema,
|
|
555
|
+
paramsSchema: schemaReferenceSchema.optional(),
|
|
556
|
+
responseSchema: schemaReferenceSchema.optional(),
|
|
557
|
+
reads: z
|
|
558
|
+
.object({
|
|
559
|
+
entities: z.array(camelCaseIdSchema).optional(),
|
|
560
|
+
projections: z.array(camelCaseIdSchema).optional(),
|
|
561
|
+
fields: z.array(dataReferenceSchema).optional(),
|
|
562
|
+
})
|
|
563
|
+
.strict()
|
|
564
|
+
.optional(),
|
|
565
|
+
visibleResult: nonEmptyTextSchema,
|
|
566
|
+
pendingTi: nonEmptyTextSchema.optional(),
|
|
567
|
+
})
|
|
568
|
+
.strict();
|
|
569
|
+
|
|
570
|
+
export const publishedEventSpecSchema = z
|
|
571
|
+
.object({
|
|
572
|
+
id: camelCaseIdSchema,
|
|
573
|
+
name: nonEmptyTextSchema,
|
|
574
|
+
technicalId: technicalEventIdSchema,
|
|
575
|
+
productDescription: nonEmptyTextSchema,
|
|
576
|
+
trigger: triggerSchema,
|
|
577
|
+
payloadSchema: schemaReferenceSchema.optional(),
|
|
578
|
+
receiptSchema: schemaReferenceSchema.optional(),
|
|
579
|
+
writes: z
|
|
580
|
+
.object({
|
|
581
|
+
entities: z.array(camelCaseIdSchema).optional(),
|
|
582
|
+
fields: z.array(dataReferenceSchema).optional(),
|
|
583
|
+
})
|
|
584
|
+
.strict()
|
|
585
|
+
.optional(),
|
|
586
|
+
visibleResult: nonEmptyTextSchema,
|
|
587
|
+
pendingTi: nonEmptyTextSchema.optional(),
|
|
588
|
+
})
|
|
589
|
+
.strict();
|
|
590
|
+
|
|
591
|
+
export const prototypeEventEngineContractSchema = z
|
|
592
|
+
.object({
|
|
593
|
+
contractVersion: z.literal("1"),
|
|
594
|
+
dataLoads: z.record(camelCaseIdSchema, dataLoadSpecSchema),
|
|
595
|
+
events: z.record(camelCaseIdSchema, publishedEventSpecSchema),
|
|
596
|
+
})
|
|
597
|
+
.strict()
|
|
598
|
+
.superRefine((contract, context) => {
|
|
599
|
+
for (const [loadKey, load] of Object.entries(contract.dataLoads)) {
|
|
600
|
+
if (loadKey !== load.id) {
|
|
601
|
+
context.addIssue({
|
|
602
|
+
code: "custom",
|
|
603
|
+
path: ["dataLoads", loadKey, "id"],
|
|
604
|
+
message: 'El id "' + load.id + '" debe coincidir con la key "' + loadKey + '".',
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const technicalIds = new Set<string>();
|
|
610
|
+
for (const [eventKey, event] of Object.entries(contract.events)) {
|
|
611
|
+
if (eventKey !== event.id) {
|
|
612
|
+
context.addIssue({
|
|
613
|
+
code: "custom",
|
|
614
|
+
path: ["events", eventKey, "id"],
|
|
615
|
+
message: 'El id "' + event.id + '" debe coincidir con la key "' + eventKey + '".',
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (technicalIds.has(event.technicalId)) {
|
|
620
|
+
context.addIssue({
|
|
621
|
+
code: "custom",
|
|
622
|
+
path: ["events", eventKey, "technicalId"],
|
|
623
|
+
message: 'El ID técnico "' + event.technicalId + '" está declarado más de una vez.',
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
technicalIds.add(event.technicalId);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
export type PrototypeEventEngineContract = z.infer<typeof prototypeEventEngineContractSchema>;
|
|
631
|
+
export type DataLoadSpec = z.infer<typeof dataLoadSpecSchema>;
|
|
632
|
+
export type PublishedEventSpec = z.infer<typeof publishedEventSpecSchema>;
|
|
633
|
+
|
|
634
|
+
export type EventEngineValidationIssue = {
|
|
635
|
+
path: Array<string | number>;
|
|
636
|
+
message: string;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
type DataContractLike = {
|
|
640
|
+
entities: Record<string, { fields: Record<string, unknown> }>;
|
|
641
|
+
projections: Record<string, unknown>;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
type PayloadSchemasLike = Record<string, unknown>;
|
|
645
|
+
|
|
646
|
+
const hasOwn = (record: Record<string, unknown>, key: string) =>
|
|
647
|
+
Object.prototype.hasOwnProperty.call(record, key);
|
|
648
|
+
|
|
649
|
+
const validateDataReference = (
|
|
650
|
+
reference: string,
|
|
651
|
+
dataContract: DataContractLike,
|
|
652
|
+
): string | null => {
|
|
653
|
+
const [entityId, fieldId] = reference.split(".");
|
|
654
|
+
const entity = dataContract.entities[entityId];
|
|
655
|
+
if (!entity) return 'La entidad "' + entityId + '" no existe en el contrato de datos.';
|
|
656
|
+
if (fieldId && !hasOwn(entity.fields, fieldId)) {
|
|
657
|
+
return 'El campo "' + reference + '" no existe en el contrato de datos.';
|
|
658
|
+
}
|
|
659
|
+
return null;
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
export const validateEventEngineReferences = (input: {
|
|
663
|
+
eventEngineContract: PrototypeEventEngineContract;
|
|
664
|
+
dataContract: DataContractLike;
|
|
665
|
+
payloadSchemas: PayloadSchemasLike;
|
|
666
|
+
}): EventEngineValidationIssue[] => {
|
|
667
|
+
const issues: EventEngineValidationIssue[] = [];
|
|
668
|
+
const { eventEngineContract, dataContract, payloadSchemas } = input;
|
|
669
|
+
|
|
670
|
+
for (const [loadId, load] of Object.entries(eventEngineContract.dataLoads)) {
|
|
671
|
+
for (const [schemaKey, schemaRef] of [
|
|
672
|
+
["paramsSchema", load.paramsSchema],
|
|
673
|
+
["responseSchema", load.responseSchema],
|
|
674
|
+
] as const) {
|
|
675
|
+
if (schemaRef && !hasOwn(payloadSchemas, schemaRef.schemaId)) {
|
|
676
|
+
issues.push({
|
|
677
|
+
path: ["dataLoads", loadId, schemaKey, "schemaId"],
|
|
678
|
+
message: 'El schema "' + schemaRef.schemaId + '" no existe en payload-schema.ts.',
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
for (const [index, entityId] of (load.reads?.entities ?? []).entries()) {
|
|
684
|
+
if (!dataContract.entities[entityId]) {
|
|
685
|
+
issues.push({
|
|
686
|
+
path: ["dataLoads", loadId, "reads", "entities", index],
|
|
687
|
+
message: 'La entidad "' + entityId + '" no existe en el contrato de datos.',
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
for (const [index, projectionId] of (load.reads?.projections ?? []).entries()) {
|
|
692
|
+
if (!dataContract.projections[projectionId]) {
|
|
693
|
+
issues.push({
|
|
694
|
+
path: ["dataLoads", loadId, "reads", "projections", index],
|
|
695
|
+
message: 'La proyección "' + projectionId + '" no existe en el contrato de datos.',
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
for (const [index, reference] of (load.reads?.fields ?? []).entries()) {
|
|
700
|
+
const message = validateDataReference(reference, dataContract);
|
|
701
|
+
if (message) {
|
|
702
|
+
issues.push({
|
|
703
|
+
path: ["dataLoads", loadId, "reads", "fields", index],
|
|
704
|
+
message,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
for (const [eventId, event] of Object.entries(eventEngineContract.events)) {
|
|
711
|
+
for (const [schemaKey, schemaRef] of [
|
|
712
|
+
["payloadSchema", event.payloadSchema],
|
|
713
|
+
["receiptSchema", event.receiptSchema],
|
|
714
|
+
] as const) {
|
|
715
|
+
if (schemaRef && !hasOwn(payloadSchemas, schemaRef.schemaId)) {
|
|
716
|
+
issues.push({
|
|
717
|
+
path: ["events", eventId, schemaKey, "schemaId"],
|
|
718
|
+
message: 'El schema "' + schemaRef.schemaId + '" no existe en payload-schema.ts.',
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
for (const [index, entityId] of (event.writes?.entities ?? []).entries()) {
|
|
724
|
+
if (!dataContract.entities[entityId]) {
|
|
725
|
+
issues.push({
|
|
726
|
+
path: ["events", eventId, "writes", "entities", index],
|
|
727
|
+
message: 'La entidad "' + entityId + '" no existe en el contrato de datos.',
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
for (const [index, reference] of (event.writes?.fields ?? []).entries()) {
|
|
732
|
+
const message = validateDataReference(reference, dataContract);
|
|
733
|
+
if (message) {
|
|
734
|
+
issues.push({
|
|
735
|
+
path: ["events", eventId, "writes", "fields", index],
|
|
736
|
+
message,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return issues;
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
export function definePrototypeEventEngineContract(
|
|
746
|
+
contract: PrototypeEventEngineContract,
|
|
747
|
+
): PrototypeEventEngineContract {
|
|
748
|
+
return prototypeEventEngineContractSchema.parse(contract);
|
|
749
|
+
}
|
|
750
|
+
`,jt=String.raw`import { z } from "zod";
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Schemas runtime para params, payloads, responses y receipts del motor.
|
|
754
|
+
*
|
|
755
|
+
* Los contratos de Spec 2 referencian estas keys mediante schemaId. El agente
|
|
756
|
+
* agrega schemas específicos cuando declara cargas o eventos concretos.
|
|
757
|
+
*/
|
|
758
|
+
export const unknownPayloadSchema = z.unknown();
|
|
759
|
+
export const emptyObjectSchema = z.object({}).strict();
|
|
760
|
+
export const unknownResponseSchema = z.unknown();
|
|
761
|
+
export const absentReceiptSchema = z.undefined().optional();
|
|
762
|
+
|
|
763
|
+
export const prototypePayloadSchemas = {
|
|
764
|
+
unknown: unknownPayloadSchema,
|
|
765
|
+
emptyObject: emptyObjectSchema,
|
|
766
|
+
unknownResponse: unknownResponseSchema,
|
|
767
|
+
absentReceipt: absentReceiptSchema,
|
|
768
|
+
} satisfies Record<string, z.ZodType>;
|
|
769
|
+
|
|
770
|
+
export type PrototypePayloadSchemaId = keyof typeof prototypePayloadSchemas;
|
|
771
|
+
`,Rt=String.raw`import { z } from "zod";
|
|
772
|
+
|
|
773
|
+
import { prototypeEventEngineContract } from "./prototype-event-engine-contract";
|
|
774
|
+
import { prototypePayloadSchemas } from "./payload-schema";
|
|
775
|
+
import type { PrototypeEventEngineContract } from "./event-engine-contract-schema";
|
|
776
|
+
|
|
777
|
+
export type DataLoadInput = {
|
|
778
|
+
loadId: string;
|
|
779
|
+
triggerId?: string;
|
|
780
|
+
params?: unknown;
|
|
781
|
+
signal?: AbortSignal;
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
export type PublishEventInput = {
|
|
785
|
+
eventId: string;
|
|
786
|
+
triggerId?: string;
|
|
787
|
+
payload?: unknown;
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
export type EventPublicationEnvelope = {
|
|
791
|
+
technicalId: string;
|
|
792
|
+
occurredAt: string;
|
|
793
|
+
publicationId: string;
|
|
794
|
+
payload: unknown;
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
export type PublishedEventResult = {
|
|
798
|
+
status: "published";
|
|
799
|
+
label: "Evento publicado";
|
|
800
|
+
eventId: string;
|
|
801
|
+
technicalId: string;
|
|
802
|
+
occurredAt: string;
|
|
803
|
+
publicationId: string;
|
|
804
|
+
receipt?: unknown;
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
export type PrototypeBackendExecutor = {
|
|
808
|
+
loadData(input: {
|
|
809
|
+
loadId: string;
|
|
810
|
+
triggerId?: string;
|
|
811
|
+
params: unknown;
|
|
812
|
+
signal?: AbortSignal;
|
|
813
|
+
}): Promise<unknown>;
|
|
814
|
+
publishEvent(input: {
|
|
815
|
+
eventId: string;
|
|
816
|
+
triggerId?: string;
|
|
817
|
+
envelope: EventPublicationEnvelope;
|
|
818
|
+
}): Promise<{ receipt?: unknown } | void>;
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
type PrototypeEventEngineOptions = {
|
|
822
|
+
contract?: PrototypeEventEngineContract;
|
|
823
|
+
payloadSchemas?: Record<string, z.ZodType>;
|
|
824
|
+
executor: PrototypeBackendExecutor;
|
|
825
|
+
clock?: () => Date;
|
|
826
|
+
publicationId?: () => string;
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
const createDefaultPublicationId = () =>
|
|
830
|
+
"pub_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 10);
|
|
831
|
+
|
|
832
|
+
const parseWithSchema = (schema: z.ZodType | undefined, value: unknown, label: string) => {
|
|
833
|
+
if (!schema) return value;
|
|
834
|
+
const result = schema.safeParse(value);
|
|
835
|
+
if (!result.success) {
|
|
836
|
+
const message = result.error.issues
|
|
837
|
+
.map((issue) => (issue.path.length > 0 ? issue.path.join(".") + ": " : "") + issue.message)
|
|
838
|
+
.join("; ");
|
|
839
|
+
throw new Error(label + " inválido: " + message);
|
|
840
|
+
}
|
|
841
|
+
return result.data;
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
export const createPrototypeEventEngine = (options: PrototypeEventEngineOptions) => {
|
|
845
|
+
const contract = options.contract ?? prototypeEventEngineContract;
|
|
846
|
+
const payloadSchemas: Record<string, z.ZodType> =
|
|
847
|
+
options.payloadSchemas ?? prototypePayloadSchemas;
|
|
848
|
+
const clock = options.clock ?? (() => new Date());
|
|
849
|
+
const publicationId = options.publicationId ?? createDefaultPublicationId;
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
async loadData(input: DataLoadInput): Promise<unknown> {
|
|
853
|
+
const load = contract.dataLoads[input.loadId];
|
|
854
|
+
if (!load) throw new Error('La carga de datos "' + input.loadId + '" no está declarada.');
|
|
855
|
+
|
|
856
|
+
const params = parseWithSchema(
|
|
857
|
+
load.paramsSchema ? payloadSchemas[load.paramsSchema.schemaId] : undefined,
|
|
858
|
+
input.params,
|
|
859
|
+
'Params de la carga "' + input.loadId + '"',
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
const response = await options.executor.loadData({
|
|
863
|
+
loadId: input.loadId,
|
|
864
|
+
triggerId: input.triggerId,
|
|
865
|
+
params,
|
|
866
|
+
signal: input.signal,
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
return parseWithSchema(
|
|
870
|
+
load.responseSchema ? payloadSchemas[load.responseSchema.schemaId] : undefined,
|
|
871
|
+
response,
|
|
872
|
+
'Response de la carga "' + input.loadId + '"',
|
|
873
|
+
);
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
async publishEvent(input: PublishEventInput): Promise<PublishedEventResult> {
|
|
877
|
+
const event = contract.events[input.eventId];
|
|
878
|
+
if (!event) throw new Error('El evento publicado "' + input.eventId + '" no está declarado.');
|
|
879
|
+
|
|
880
|
+
const payload = parseWithSchema(
|
|
881
|
+
event.payloadSchema ? payloadSchemas[event.payloadSchema.schemaId] : undefined,
|
|
882
|
+
input.payload,
|
|
883
|
+
'Payload del evento "' + input.eventId + '"',
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
const envelope: EventPublicationEnvelope = {
|
|
887
|
+
technicalId: event.technicalId,
|
|
888
|
+
occurredAt: clock().toISOString(),
|
|
889
|
+
publicationId: publicationId(),
|
|
890
|
+
payload,
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
const executorResult = await options.executor.publishEvent({
|
|
894
|
+
eventId: input.eventId,
|
|
895
|
+
triggerId: input.triggerId,
|
|
896
|
+
envelope,
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
const receipt = parseWithSchema(
|
|
900
|
+
event.receiptSchema ? payloadSchemas[event.receiptSchema.schemaId] : undefined,
|
|
901
|
+
executorResult?.receipt,
|
|
902
|
+
'Receipt del evento "' + input.eventId + '"',
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
status: "published",
|
|
907
|
+
label: "Evento publicado",
|
|
908
|
+
eventId: input.eventId,
|
|
909
|
+
technicalId: event.technicalId,
|
|
910
|
+
occurredAt: envelope.occurredAt,
|
|
911
|
+
publicationId: envelope.publicationId,
|
|
912
|
+
...(receipt === undefined ? {} : { receipt }),
|
|
913
|
+
};
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
};
|
|
917
|
+
`,Tt=String.raw`import { ZodError } from "zod";
|
|
918
|
+
|
|
919
|
+
import { prototypeDataContractSchema } from "../src/prototype/data-contract/data-contract-schema";
|
|
920
|
+
import {
|
|
921
|
+
prototypeEventEngineContractSchema,
|
|
922
|
+
validateEventEngineReferences,
|
|
923
|
+
type EventEngineValidationIssue,
|
|
924
|
+
} from "../src/prototype/event-engine/event-engine-contract-schema";
|
|
925
|
+
import { prototypePayloadSchemas } from "../src/prototype/event-engine/payload-schema";
|
|
926
|
+
|
|
927
|
+
const formatPath = (path: Array<string | number>) =>
|
|
928
|
+
path.length === 0 ? "eventEngine" : path.map(String).join(".");
|
|
929
|
+
|
|
930
|
+
const printIssues = (issues: EventEngineValidationIssue[]) => {
|
|
931
|
+
console.error("✗ Motor de eventos inválido");
|
|
932
|
+
for (const issue of issues) {
|
|
933
|
+
console.error(" " + formatPath(issue.path));
|
|
934
|
+
console.error(" " + issue.message);
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
try {
|
|
939
|
+
const dataContractModule = await import("../src/prototype/data-contract/prototype-data-contract");
|
|
940
|
+
const eventEngineModule =
|
|
941
|
+
await import("../src/prototype/event-engine/prototype-event-engine-contract");
|
|
942
|
+
|
|
943
|
+
const dataContractResult = prototypeDataContractSchema.safeParse(
|
|
944
|
+
dataContractModule.prototypeDataContract,
|
|
945
|
+
);
|
|
946
|
+
const eventEngineResult = prototypeEventEngineContractSchema.safeParse(
|
|
947
|
+
eventEngineModule.prototypeEventEngineContract,
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
const issues: EventEngineValidationIssue[] = [];
|
|
951
|
+
if (!dataContractResult.success) {
|
|
952
|
+
for (const issue of dataContractResult.error.issues) {
|
|
953
|
+
issues.push({
|
|
954
|
+
path: ["dataContract", ...issue.path.map(String)],
|
|
955
|
+
message: issue.message,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (!eventEngineResult.success) {
|
|
960
|
+
for (const issue of eventEngineResult.error.issues) {
|
|
961
|
+
issues.push({
|
|
962
|
+
path: issue.path.map(String),
|
|
963
|
+
message: issue.message,
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (dataContractResult.success && eventEngineResult.success) {
|
|
969
|
+
issues.push(
|
|
970
|
+
...validateEventEngineReferences({
|
|
971
|
+
eventEngineContract: eventEngineResult.data,
|
|
972
|
+
dataContract: dataContractResult.data,
|
|
973
|
+
payloadSchemas: prototypePayloadSchemas,
|
|
974
|
+
}),
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (issues.length > 0) {
|
|
979
|
+
printIssues(issues);
|
|
980
|
+
process.exitCode = 1;
|
|
981
|
+
} else {
|
|
982
|
+
const contract = eventEngineResult.data!;
|
|
983
|
+
console.log("✓ Motor de eventos válido");
|
|
984
|
+
console.log(" " + Object.keys(contract.dataLoads).length + " cargas de datos");
|
|
985
|
+
console.log(" " + Object.keys(contract.events).length + " eventos publicados");
|
|
986
|
+
}
|
|
987
|
+
} catch (error) {
|
|
988
|
+
if (error instanceof ZodError) {
|
|
989
|
+
printIssues(
|
|
990
|
+
error.issues.map((issue) => ({ path: issue.path.map(String), message: issue.message })),
|
|
991
|
+
);
|
|
992
|
+
} else {
|
|
993
|
+
console.error("✗ No se pudo cargar el motor de eventos");
|
|
994
|
+
console.error(" " + (error instanceof Error ? error.message : String(error)));
|
|
995
|
+
}
|
|
996
|
+
process.exitCode = 1;
|
|
997
|
+
}
|
|
998
|
+
`,Mt=()=>`import { definePrototypeEventEngineContract } from "./event-engine-contract-schema";
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Cat\xE1logo de comunicaciones externas del frontend.
|
|
1002
|
+
*
|
|
1003
|
+
* - dataLoads: lecturas remotas para alimentar la UI.
|
|
1004
|
+
* - events: escrituras publicadas como hechos Lexy.
|
|
1005
|
+
*
|
|
1006
|
+
* Las interacciones locales no se declaran ac\xE1.
|
|
1007
|
+
*/
|
|
1008
|
+
export const prototypeEventEngineContract = definePrototypeEventEngineContract({
|
|
1009
|
+
contractVersion: "1",
|
|
1010
|
+
dataLoads: {},
|
|
1011
|
+
events: {},
|
|
1012
|
+
});
|
|
1013
|
+
`,_e=async e=>{let t=async(o,r)=>{let a=Ve.join(e,o);await $e.mkdir(Ve.dirname(a),{recursive:!0}),await $e.writeFile(a,r)};await t(kt,Ct),await t(ie,Mt()),await t(wt,jt),await t(se,Rt),await t(Pt,Tt)};import Fe from"fs/promises";import Ne from"path";var Lt="src/prototype/simulator/simulator-contract-schema.ts",ce="src/prototype/simulator/mock-generation-profile.ts",le="src/prototype/simulator/mock-scenarios.ts",zt="src/prototype/simulator/projection-resolvers.ts",At="src/prototype/simulator/data-load-handlers.ts",Ot="src/prototype/simulator/event-handlers.ts",Dt="src/prototype/simulator/mock-store.ts",$t="src/prototype/simulator/mock-backend-executor.ts",de="src/prototype/simulator/prototype-simulator.ts",Vt="scripts/check-simulator.ts",_t=String.raw`import { z } from "zod";
|
|
1014
|
+
|
|
1015
|
+
const camelCaseIdSchema = z
|
|
1016
|
+
.string()
|
|
1017
|
+
.regex(/^[a-z][a-zA-Z0-9]*$/, "Debe usar camelCase y comenzar con una letra minúscula.");
|
|
1018
|
+
|
|
1019
|
+
const dataReferenceSchema = z
|
|
1020
|
+
.string()
|
|
1021
|
+
.regex(
|
|
1022
|
+
/^[a-z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*$/,
|
|
1023
|
+
"Debe referenciar un campo como entidad.campo usando camelCase.",
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
const nonEmptyTextSchema = z.string().trim().min(1, "No puede quedar vacío.");
|
|
1027
|
+
|
|
1028
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
1029
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
|
1030
|
+
export type MockEntityRecord = Record<string, JsonValue>;
|
|
1031
|
+
|
|
1032
|
+
export const jsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>
|
|
1033
|
+
z.union([
|
|
1034
|
+
z.string(),
|
|
1035
|
+
z.number(),
|
|
1036
|
+
z.boolean(),
|
|
1037
|
+
z.null(),
|
|
1038
|
+
z.array(jsonValueSchema),
|
|
1039
|
+
z.record(z.string(), jsonValueSchema),
|
|
1040
|
+
]),
|
|
1041
|
+
);
|
|
1042
|
+
|
|
1043
|
+
export const mockEntityRecordSchema = z.record(z.string(), jsonValueSchema);
|
|
1044
|
+
|
|
1045
|
+
export const mockSemanticSchema = z.enum([
|
|
1046
|
+
"personName",
|
|
1047
|
+
"rutCl",
|
|
1048
|
+
"mobilePhoneCl",
|
|
1049
|
+
"landlinePhoneCl",
|
|
1050
|
+
"email",
|
|
1051
|
+
"clpAmount",
|
|
1052
|
+
"dateCl",
|
|
1053
|
+
"datetimeCl",
|
|
1054
|
+
"regionCl",
|
|
1055
|
+
"communeCl",
|
|
1056
|
+
"addressCl",
|
|
1057
|
+
"companyNameCl",
|
|
1058
|
+
"legalCaseReferenceCl",
|
|
1059
|
+
"freeText",
|
|
1060
|
+
"custom",
|
|
1061
|
+
]);
|
|
1062
|
+
|
|
1063
|
+
export const fieldMockHintSchema = z
|
|
1064
|
+
.object({
|
|
1065
|
+
semantic: mockSemanticSchema,
|
|
1066
|
+
description: nonEmptyTextSchema.optional(),
|
|
1067
|
+
examples: z.array(nonEmptyTextSchema).optional(),
|
|
1068
|
+
constraints: z
|
|
1069
|
+
.object({
|
|
1070
|
+
min: z.number().optional(),
|
|
1071
|
+
max: z.number().optional(),
|
|
1072
|
+
pattern: nonEmptyTextSchema.optional(),
|
|
1073
|
+
allowedValues: z.array(nonEmptyTextSchema).optional(),
|
|
1074
|
+
})
|
|
1075
|
+
.strict()
|
|
1076
|
+
.optional(),
|
|
1077
|
+
rutValidation: z.enum(["formatOnly", "validChecksum"]).optional(),
|
|
1078
|
+
})
|
|
1079
|
+
.strict();
|
|
1080
|
+
|
|
1081
|
+
export const mockGenerationProfileSchema = z
|
|
1082
|
+
.object({
|
|
1083
|
+
profileVersion: z.literal("1"),
|
|
1084
|
+
locale: z
|
|
1085
|
+
.object({
|
|
1086
|
+
language: z.literal("es"),
|
|
1087
|
+
locale: z.literal("es-CL"),
|
|
1088
|
+
country: z.literal("CL"),
|
|
1089
|
+
timezone: z.literal("America/Santiago"),
|
|
1090
|
+
currency: z.literal("CLP"),
|
|
1091
|
+
dateStorage: z.literal("iso8601"),
|
|
1092
|
+
dateDisplay: z.literal("dd-MM-yyyy"),
|
|
1093
|
+
timeDisplay: z.literal("24h"),
|
|
1094
|
+
})
|
|
1095
|
+
.strict(),
|
|
1096
|
+
safety: z
|
|
1097
|
+
.object({
|
|
1098
|
+
syntheticOnly: z.literal(true),
|
|
1099
|
+
allowProductionImports: z.literal(false),
|
|
1100
|
+
externalUse: z.literal(false),
|
|
1101
|
+
emailDomain: z.literal("example.com"),
|
|
1102
|
+
})
|
|
1103
|
+
.strict(),
|
|
1104
|
+
entityKeys: z.record(
|
|
1105
|
+
camelCaseIdSchema,
|
|
1106
|
+
z
|
|
1107
|
+
.object({
|
|
1108
|
+
field: dataReferenceSchema,
|
|
1109
|
+
})
|
|
1110
|
+
.strict(),
|
|
1111
|
+
),
|
|
1112
|
+
relationBindings: z.record(
|
|
1113
|
+
camelCaseIdSchema,
|
|
1114
|
+
z
|
|
1115
|
+
.object({
|
|
1116
|
+
relation: camelCaseIdSchema,
|
|
1117
|
+
fromField: dataReferenceSchema,
|
|
1118
|
+
toField: dataReferenceSchema,
|
|
1119
|
+
})
|
|
1120
|
+
.strict(),
|
|
1121
|
+
),
|
|
1122
|
+
fieldHints: z.record(dataReferenceSchema, fieldMockHintSchema),
|
|
1123
|
+
})
|
|
1124
|
+
.strict();
|
|
1125
|
+
|
|
1126
|
+
export const mockOperationErrorSchema = z
|
|
1127
|
+
.object({
|
|
1128
|
+
code: camelCaseIdSchema,
|
|
1129
|
+
message: nonEmptyTextSchema,
|
|
1130
|
+
recoverable: z.boolean(),
|
|
1131
|
+
})
|
|
1132
|
+
.strict();
|
|
1133
|
+
|
|
1134
|
+
export const mockDataLoadBehaviorSchema = z
|
|
1135
|
+
.object({
|
|
1136
|
+
latencyMs: z.number().int().min(0).optional(),
|
|
1137
|
+
outcome: z.enum(["normal", "empty", "error"]).optional(),
|
|
1138
|
+
error: mockOperationErrorSchema.optional(),
|
|
1139
|
+
})
|
|
1140
|
+
.strict()
|
|
1141
|
+
.superRefine((behavior, context) => {
|
|
1142
|
+
const outcome = behavior.outcome ?? "normal";
|
|
1143
|
+
if (outcome === "error" && !behavior.error) {
|
|
1144
|
+
context.addIssue({
|
|
1145
|
+
code: "custom",
|
|
1146
|
+
path: ["error"],
|
|
1147
|
+
message: "Un behavior error debe declarar el error controlado.",
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
if (outcome !== "error" && behavior.error) {
|
|
1151
|
+
context.addIssue({
|
|
1152
|
+
code: "custom",
|
|
1153
|
+
path: ["error"],
|
|
1154
|
+
message: "Solo un behavior error puede declarar error.",
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
export const mockEventBehaviorSchema = z
|
|
1160
|
+
.object({
|
|
1161
|
+
latencyMs: z.number().int().min(0).optional(),
|
|
1162
|
+
outcome: z.enum(["normal", "error"]).optional(),
|
|
1163
|
+
error: mockOperationErrorSchema.optional(),
|
|
1164
|
+
})
|
|
1165
|
+
.strict()
|
|
1166
|
+
.superRefine((behavior, context) => {
|
|
1167
|
+
const outcome = behavior.outcome ?? "normal";
|
|
1168
|
+
if (outcome === "error" && !behavior.error) {
|
|
1169
|
+
context.addIssue({
|
|
1170
|
+
code: "custom",
|
|
1171
|
+
path: ["error"],
|
|
1172
|
+
message: "Un behavior error debe declarar el error controlado.",
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
if (outcome !== "error" && behavior.error) {
|
|
1176
|
+
context.addIssue({
|
|
1177
|
+
code: "custom",
|
|
1178
|
+
path: ["error"],
|
|
1179
|
+
message: "Solo un behavior error puede declarar error.",
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
export const mockScenarioSchema = z
|
|
1185
|
+
.object({
|
|
1186
|
+
id: camelCaseIdSchema,
|
|
1187
|
+
name: nonEmptyTextSchema,
|
|
1188
|
+
description: nonEmptyTextSchema,
|
|
1189
|
+
entities: z.record(camelCaseIdSchema, z.array(mockEntityRecordSchema)),
|
|
1190
|
+
behaviors: z
|
|
1191
|
+
.object({
|
|
1192
|
+
dataLoads: z.record(camelCaseIdSchema, mockDataLoadBehaviorSchema).optional(),
|
|
1193
|
+
events: z.record(camelCaseIdSchema, mockEventBehaviorSchema).optional(),
|
|
1194
|
+
})
|
|
1195
|
+
.strict()
|
|
1196
|
+
.optional(),
|
|
1197
|
+
})
|
|
1198
|
+
.strict();
|
|
1199
|
+
|
|
1200
|
+
export const prototypeMockScenariosSchema = z
|
|
1201
|
+
.object({
|
|
1202
|
+
scenariosVersion: z.literal("1"),
|
|
1203
|
+
datasetVersion: z.number().int().positive(),
|
|
1204
|
+
defaultScenarioId: camelCaseIdSchema,
|
|
1205
|
+
scenarios: z.record(camelCaseIdSchema, mockScenarioSchema),
|
|
1206
|
+
})
|
|
1207
|
+
.strict()
|
|
1208
|
+
.superRefine((catalog, context) => {
|
|
1209
|
+
if (!catalog.scenarios[catalog.defaultScenarioId]) {
|
|
1210
|
+
context.addIssue({
|
|
1211
|
+
code: "custom",
|
|
1212
|
+
path: ["defaultScenarioId"],
|
|
1213
|
+
message: 'El escenario default "' + catalog.defaultScenarioId + '" no existe.',
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
for (const [scenarioKey, scenario] of Object.entries(catalog.scenarios)) {
|
|
1218
|
+
if (scenarioKey !== scenario.id) {
|
|
1219
|
+
context.addIssue({
|
|
1220
|
+
code: "custom",
|
|
1221
|
+
path: ["scenarios", scenarioKey, "id"],
|
|
1222
|
+
message: 'El id "' + scenario.id + '" debe coincidir con la key "' + scenarioKey + '".',
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
export type MockSemantic = z.infer<typeof mockSemanticSchema>;
|
|
1229
|
+
export type FieldMockHint = z.infer<typeof fieldMockHintSchema>;
|
|
1230
|
+
export type MockGenerationProfile = z.infer<typeof mockGenerationProfileSchema>;
|
|
1231
|
+
export type MockOperationError = z.infer<typeof mockOperationErrorSchema>;
|
|
1232
|
+
export type MockDataLoadBehavior = z.infer<typeof mockDataLoadBehaviorSchema>;
|
|
1233
|
+
export type MockEventBehavior = z.infer<typeof mockEventBehaviorSchema>;
|
|
1234
|
+
export type MockScenario = z.infer<typeof mockScenarioSchema>;
|
|
1235
|
+
export type PrototypeMockScenarios = z.infer<typeof prototypeMockScenariosSchema>;
|
|
1236
|
+
|
|
1237
|
+
export type SimulatorValidationIssue = {
|
|
1238
|
+
path: Array<string | number>;
|
|
1239
|
+
message: string;
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
type DataContractFieldLike = {
|
|
1243
|
+
dataType: "string" | "number" | "boolean" | "date" | "datetime" | "enum" | "identifier";
|
|
1244
|
+
required: boolean;
|
|
1245
|
+
enumValues?: string[];
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
type DataContractLike = {
|
|
1249
|
+
entities: Record<
|
|
1250
|
+
string,
|
|
1251
|
+
{
|
|
1252
|
+
fields: Record<string, DataContractFieldLike>;
|
|
1253
|
+
}
|
|
1254
|
+
>;
|
|
1255
|
+
relations: Array<{ id: string; fromEntity: string; toEntity: string }>;
|
|
1256
|
+
projections: Record<string, unknown>;
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
type EventEngineContractLike = {
|
|
1260
|
+
dataLoads: Record<string, unknown>;
|
|
1261
|
+
events: Record<string, unknown>;
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
const splitReference = (reference: string) => {
|
|
1265
|
+
const [entityId, fieldId] = reference.split(".");
|
|
1266
|
+
return { entityId, fieldId };
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
const getField = (dataContract: DataContractLike, reference: string) => {
|
|
1270
|
+
const { entityId, fieldId } = splitReference(reference);
|
|
1271
|
+
return {
|
|
1272
|
+
entityId,
|
|
1273
|
+
fieldId,
|
|
1274
|
+
entity: dataContract.entities[entityId],
|
|
1275
|
+
field: dataContract.entities[entityId]?.fields[fieldId],
|
|
1276
|
+
};
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
const isSemanticCompatible = (semantic: MockSemantic, field: DataContractFieldLike): boolean => {
|
|
1280
|
+
if (semantic === "custom") return true;
|
|
1281
|
+
if (semantic === "clpAmount") return field.dataType === "number";
|
|
1282
|
+
if (semantic === "dateCl") return field.dataType === "date";
|
|
1283
|
+
if (semantic === "datetimeCl") return field.dataType === "datetime";
|
|
1284
|
+
if (field.dataType === "enum") return ["regionCl", "communeCl", "custom"].includes(semantic);
|
|
1285
|
+
return ["string", "identifier"].includes(field.dataType);
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
const rutFormat = /^[0-9]{7,8}-[0-9K]$/;
|
|
1289
|
+
|
|
1290
|
+
const isValidRutChecksum = (rut: string) => {
|
|
1291
|
+
if (!rutFormat.test(rut)) return false;
|
|
1292
|
+
const [body, verifier] = rut.split("-");
|
|
1293
|
+
let sum = 0;
|
|
1294
|
+
let multiplier = 2;
|
|
1295
|
+
for (let index = body.length - 1; index >= 0; index -= 1) {
|
|
1296
|
+
sum += Number(body[index]) * multiplier;
|
|
1297
|
+
multiplier = multiplier === 7 ? 2 : multiplier + 1;
|
|
1298
|
+
}
|
|
1299
|
+
const result = 11 - (sum % 11);
|
|
1300
|
+
const expected = result === 11 ? "0" : result === 10 ? "K" : String(result);
|
|
1301
|
+
return verifier === expected;
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
const mobilePhoneFormat = /^\+56 9 [0-9]{4} [0-9]{4}$/;
|
|
1305
|
+
const landlinePhoneFormat = /^\+56 [2-9] [0-9]{4} [0-9]{4}$/;
|
|
1306
|
+
const isoDateFormat = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
1307
|
+
const datetimeWithOffsetFormat =
|
|
1308
|
+
/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{3})?(?:Z|[+-][0-9]{2}:[0-9]{2})$/;
|
|
1309
|
+
const emailFormat = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1310
|
+
const secretLikeFormat =
|
|
1311
|
+
/(sk-[a-z0-9_-]{12,}|ghp_[a-z0-9_]{12,}|api[_-]?key|secret|token|password|BEGIN PRIVATE KEY)/i;
|
|
1312
|
+
|
|
1313
|
+
const validateChileanValue = (input: {
|
|
1314
|
+
value: JsonValue;
|
|
1315
|
+
hint: FieldMockHint;
|
|
1316
|
+
emailDomain: string;
|
|
1317
|
+
}): string | null => {
|
|
1318
|
+
const { value, hint, emailDomain } = input;
|
|
1319
|
+
if (value === null) return null;
|
|
1320
|
+
|
|
1321
|
+
if (hint.semantic === "rutCl") {
|
|
1322
|
+
if (typeof value !== "string" || !rutFormat.test(value)) {
|
|
1323
|
+
return "El RUT debe usar formato 12345678-9 y K mayúscula.";
|
|
1324
|
+
}
|
|
1325
|
+
if (hint.rutValidation === "validChecksum" && !isValidRutChecksum(value)) {
|
|
1326
|
+
return "El RUT no cumple el dígito verificador módulo 11.";
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (
|
|
1331
|
+
hint.semantic === "mobilePhoneCl" &&
|
|
1332
|
+
(typeof value !== "string" || !mobilePhoneFormat.test(value))
|
|
1333
|
+
) {
|
|
1334
|
+
return "El móvil chileno debe usar formato +56 9 XXXX XXXX.";
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if (
|
|
1338
|
+
hint.semantic === "landlinePhoneCl" &&
|
|
1339
|
+
(typeof value !== "string" || !landlinePhoneFormat.test(value))
|
|
1340
|
+
) {
|
|
1341
|
+
return "El teléfono fijo chileno debe usar formato +56 X XXXX XXXX.";
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (hint.semantic === "dateCl" && (typeof value !== "string" || !isoDateFormat.test(value))) {
|
|
1345
|
+
return "La fecha de storage debe usar ISO YYYY-MM-DD.";
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (
|
|
1349
|
+
hint.semantic === "datetimeCl" &&
|
|
1350
|
+
(typeof value !== "string" || !datetimeWithOffsetFormat.test(value))
|
|
1351
|
+
) {
|
|
1352
|
+
return "El datetime debe usar ISO 8601 con offset o Z.";
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (hint.semantic === "clpAmount" && (!Number.isInteger(value) || typeof value !== "number")) {
|
|
1356
|
+
return "Los montos CLP deben almacenarse como enteros sin decimales.";
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (hint.semantic === "email") {
|
|
1360
|
+
if (typeof value !== "string" || !emailFormat.test(value)) {
|
|
1361
|
+
return "El correo debe tener formato válido.";
|
|
1362
|
+
}
|
|
1363
|
+
if (!value.endsWith("@" + emailDomain)) {
|
|
1364
|
+
return 'Los correos mock deben usar el dominio "' + emailDomain + '".';
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
return null;
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
const validateValueType = (field: DataContractFieldLike, value: JsonValue): string | null => {
|
|
1372
|
+
if (value === null) return null;
|
|
1373
|
+
if (field.dataType === "number" && typeof value !== "number") return "Debe ser number.";
|
|
1374
|
+
if (field.dataType === "boolean" && typeof value !== "boolean") return "Debe ser boolean.";
|
|
1375
|
+
if (
|
|
1376
|
+
["string", "identifier", "date", "datetime", "enum"].includes(field.dataType) &&
|
|
1377
|
+
typeof value !== "string"
|
|
1378
|
+
) {
|
|
1379
|
+
return "Debe ser string.";
|
|
1380
|
+
}
|
|
1381
|
+
if (field.dataType === "date" && typeof value === "string" && !isoDateFormat.test(value)) {
|
|
1382
|
+
return "Debe usar fecha ISO YYYY-MM-DD.";
|
|
1383
|
+
}
|
|
1384
|
+
if (
|
|
1385
|
+
field.dataType === "datetime" &&
|
|
1386
|
+
typeof value === "string" &&
|
|
1387
|
+
!datetimeWithOffsetFormat.test(value)
|
|
1388
|
+
) {
|
|
1389
|
+
return "Debe usar datetime ISO 8601 con offset o Z.";
|
|
1390
|
+
}
|
|
1391
|
+
if (
|
|
1392
|
+
field.dataType === "enum" &&
|
|
1393
|
+
typeof value === "string" &&
|
|
1394
|
+
!field.enumValues?.includes(value)
|
|
1395
|
+
) {
|
|
1396
|
+
return 'El valor "' + value + '" no existe en enumValues.';
|
|
1397
|
+
}
|
|
1398
|
+
return null;
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
const visitStringValues = (value: JsonValue, visitor: (text: string) => void): void => {
|
|
1402
|
+
if (typeof value === "string") {
|
|
1403
|
+
visitor(value);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
if (Array.isArray(value)) {
|
|
1407
|
+
for (const item of value) visitStringValues(item, visitor);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (value && typeof value === "object") {
|
|
1411
|
+
for (const item of Object.values(value)) visitStringValues(item, visitor);
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
export const validateMockProfileReferences = (input: {
|
|
1416
|
+
profile: MockGenerationProfile;
|
|
1417
|
+
dataContract: DataContractLike;
|
|
1418
|
+
}): SimulatorValidationIssue[] => {
|
|
1419
|
+
const issues: SimulatorValidationIssue[] = [];
|
|
1420
|
+
const { profile, dataContract } = input;
|
|
1421
|
+
|
|
1422
|
+
for (const [entityId, binding] of Object.entries(profile.entityKeys)) {
|
|
1423
|
+
const { entityId: fieldEntityId, fieldId, field } = getField(dataContract, binding.field);
|
|
1424
|
+
if (!dataContract.entities[entityId]) {
|
|
1425
|
+
issues.push({
|
|
1426
|
+
path: ["entityKeys", entityId],
|
|
1427
|
+
message: 'La entidad "' + entityId + '" no existe en la Spec 1.',
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
if (fieldEntityId !== entityId) {
|
|
1431
|
+
issues.push({
|
|
1432
|
+
path: ["entityKeys", entityId, "field"],
|
|
1433
|
+
message: "La key de entidad debe apuntar a un campo de la misma entidad.",
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
if (!field) {
|
|
1437
|
+
issues.push({
|
|
1438
|
+
path: ["entityKeys", entityId, "field"],
|
|
1439
|
+
message: 'El campo "' + fieldEntityId + "." + fieldId + '" no existe.',
|
|
1440
|
+
});
|
|
1441
|
+
} else if (!["identifier", "string", "number"].includes(field.dataType)) {
|
|
1442
|
+
issues.push({
|
|
1443
|
+
path: ["entityKeys", entityId, "field"],
|
|
1444
|
+
message: "La key debe ser identifier, string o number.",
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
for (const [bindingId, binding] of Object.entries(profile.relationBindings)) {
|
|
1450
|
+
const relation = dataContract.relations.find((candidate) => candidate.id === binding.relation);
|
|
1451
|
+
if (!relation) {
|
|
1452
|
+
issues.push({
|
|
1453
|
+
path: ["relationBindings", bindingId, "relation"],
|
|
1454
|
+
message: 'La relación "' + binding.relation + '" no existe en la Spec 1.',
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
for (const key of ["fromField", "toField"] as const) {
|
|
1458
|
+
const { entityId, fieldId, field } = getField(dataContract, binding[key]);
|
|
1459
|
+
if (!field) {
|
|
1460
|
+
issues.push({
|
|
1461
|
+
path: ["relationBindings", bindingId, key],
|
|
1462
|
+
message: 'El campo "' + entityId + "." + fieldId + '" no existe.',
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
for (const [reference, hint] of Object.entries(profile.fieldHints)) {
|
|
1469
|
+
const { entityId, fieldId, field } = getField(dataContract, reference);
|
|
1470
|
+
if (!field) {
|
|
1471
|
+
issues.push({
|
|
1472
|
+
path: ["fieldHints", reference],
|
|
1473
|
+
message: 'El campo "' + entityId + "." + fieldId + '" no existe.',
|
|
1474
|
+
});
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (!isSemanticCompatible(hint.semantic, field)) {
|
|
1478
|
+
issues.push({
|
|
1479
|
+
path: ["fieldHints", reference, "semantic"],
|
|
1480
|
+
message:
|
|
1481
|
+
'La semántica "' + hint.semantic + '" no es compatible con ' + field.dataType + ".",
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
if (field.dataType === "enum" && hint.constraints?.allowedValues) {
|
|
1485
|
+
for (const value of hint.constraints.allowedValues) {
|
|
1486
|
+
if (!field.enumValues?.includes(value)) {
|
|
1487
|
+
issues.push({
|
|
1488
|
+
path: ["fieldHints", reference, "constraints", "allowedValues"],
|
|
1489
|
+
message: 'El valor "' + value + '" no existe en enumValues.',
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
return issues;
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1499
|
+
export const validateMockScenarios = (input: {
|
|
1500
|
+
profile: MockGenerationProfile;
|
|
1501
|
+
scenarios: PrototypeMockScenarios;
|
|
1502
|
+
dataContract: DataContractLike;
|
|
1503
|
+
eventEngineContract: EventEngineContractLike;
|
|
1504
|
+
}): SimulatorValidationIssue[] => {
|
|
1505
|
+
const issues: SimulatorValidationIssue[] = [];
|
|
1506
|
+
const { profile, scenarios, dataContract, eventEngineContract } = input;
|
|
1507
|
+
|
|
1508
|
+
for (const [scenarioId, scenario] of Object.entries(scenarios.scenarios)) {
|
|
1509
|
+
for (const [entityId, records] of Object.entries(scenario.entities)) {
|
|
1510
|
+
const entity = dataContract.entities[entityId];
|
|
1511
|
+
if (!entity) {
|
|
1512
|
+
issues.push({
|
|
1513
|
+
path: ["scenarios", scenarioId, "entities", entityId],
|
|
1514
|
+
message: 'La entidad "' + entityId + '" no existe en la Spec 1.',
|
|
1515
|
+
});
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const keyBinding = profile.entityKeys[entityId];
|
|
1520
|
+
const seenKeys = new Set<JsonValue>();
|
|
1521
|
+
for (const [recordIndex, record] of records.entries()) {
|
|
1522
|
+
for (const fieldKey of Object.keys(record)) {
|
|
1523
|
+
if (!entity.fields[fieldKey]) {
|
|
1524
|
+
issues.push({
|
|
1525
|
+
path: ["scenarios", scenarioId, "entities", entityId, recordIndex, fieldKey],
|
|
1526
|
+
message: 'El campo "' + fieldKey + '" no existe en la entidad "' + entityId + '".',
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
for (const [fieldKey, field] of Object.entries(entity.fields)) {
|
|
1532
|
+
const value = record[fieldKey];
|
|
1533
|
+
if (field.required && value === undefined) {
|
|
1534
|
+
issues.push({
|
|
1535
|
+
path: ["scenarios", scenarioId, "entities", entityId, recordIndex, fieldKey],
|
|
1536
|
+
message: "El campo requerido está ausente.",
|
|
1537
|
+
});
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (value !== undefined) {
|
|
1541
|
+
const typeMessage = validateValueType(field, value);
|
|
1542
|
+
if (typeMessage) {
|
|
1543
|
+
issues.push({
|
|
1544
|
+
path: ["scenarios", scenarioId, "entities", entityId, recordIndex, fieldKey],
|
|
1545
|
+
message: typeMessage,
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const hint = profile.fieldHints[entityId + "." + fieldKey];
|
|
1550
|
+
if (hint) {
|
|
1551
|
+
const chileMessage = validateChileanValue({
|
|
1552
|
+
value,
|
|
1553
|
+
hint,
|
|
1554
|
+
emailDomain: profile.safety.emailDomain,
|
|
1555
|
+
});
|
|
1556
|
+
if (chileMessage) {
|
|
1557
|
+
issues.push({
|
|
1558
|
+
path: ["scenarios", scenarioId, "entities", entityId, recordIndex, fieldKey],
|
|
1559
|
+
message: chileMessage,
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
visitStringValues(value, (text) => {
|
|
1565
|
+
if (emailFormat.test(text) && !text.endsWith("@" + profile.safety.emailDomain)) {
|
|
1566
|
+
issues.push({
|
|
1567
|
+
path: ["scenarios", scenarioId, "entities", entityId, recordIndex, fieldKey],
|
|
1568
|
+
message:
|
|
1569
|
+
'Los correos mock deben usar el dominio "' + profile.safety.emailDomain + '".',
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
if (secretLikeFormat.test(text)) {
|
|
1573
|
+
issues.push({
|
|
1574
|
+
path: ["scenarios", scenarioId, "entities", entityId, recordIndex, fieldKey],
|
|
1575
|
+
message: "El valor parece contener un secreto, token o credencial.",
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (keyBinding) {
|
|
1583
|
+
const { fieldId } = splitReference(keyBinding.field);
|
|
1584
|
+
const keyValue = record[fieldId];
|
|
1585
|
+
if (keyValue !== undefined) {
|
|
1586
|
+
if (seenKeys.has(keyValue)) {
|
|
1587
|
+
issues.push({
|
|
1588
|
+
path: ["scenarios", scenarioId, "entities", entityId, recordIndex, fieldId],
|
|
1589
|
+
message: 'La key "' + String(keyValue) + '" está duplicada.',
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
seenKeys.add(keyValue);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
for (const [bindingId, binding] of Object.entries(profile.relationBindings)) {
|
|
1599
|
+
const from = splitReference(binding.fromField);
|
|
1600
|
+
const to = splitReference(binding.toField);
|
|
1601
|
+
const fromRecords = scenario.entities[from.entityId] ?? [];
|
|
1602
|
+
const toRecords = scenario.entities[to.entityId] ?? [];
|
|
1603
|
+
const targetValues = new Set(toRecords.map((record) => record[to.fieldId]));
|
|
1604
|
+
|
|
1605
|
+
for (const [recordIndex, record] of fromRecords.entries()) {
|
|
1606
|
+
const value = record[from.fieldId];
|
|
1607
|
+
if (value !== undefined && value !== null && !targetValues.has(value)) {
|
|
1608
|
+
issues.push({
|
|
1609
|
+
path: ["scenarios", scenarioId, "entities", from.entityId, recordIndex, from.fieldId],
|
|
1610
|
+
message:
|
|
1611
|
+
'Referencia "' +
|
|
1612
|
+
String(value) +
|
|
1613
|
+
'" inexistente para relationBinding "' +
|
|
1614
|
+
bindingId +
|
|
1615
|
+
'".',
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
for (const [loadId, behavior] of Object.entries(scenario.behaviors?.dataLoads ?? {})) {
|
|
1622
|
+
if (!eventEngineContract.dataLoads[loadId]) {
|
|
1623
|
+
issues.push({
|
|
1624
|
+
path: ["scenarios", scenarioId, "behaviors", "dataLoads", loadId],
|
|
1625
|
+
message: 'La carga "' + loadId + '" no existe en la Spec 2.',
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
if ((behavior.outcome ?? "normal") === "empty" && behavior.error) {
|
|
1629
|
+
issues.push({
|
|
1630
|
+
path: ["scenarios", scenarioId, "behaviors", "dataLoads", loadId, "error"],
|
|
1631
|
+
message: "El outcome empty no puede incluir error.",
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
for (const eventId of Object.keys(scenario.behaviors?.events ?? {})) {
|
|
1637
|
+
if (!eventEngineContract.events[eventId]) {
|
|
1638
|
+
issues.push({
|
|
1639
|
+
path: ["scenarios", scenarioId, "behaviors", "events", eventId],
|
|
1640
|
+
message: 'El evento "' + eventId + '" no existe en la Spec 2.',
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
return issues;
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1649
|
+
export function defineMockGenerationProfile(profile: MockGenerationProfile): MockGenerationProfile {
|
|
1650
|
+
return mockGenerationProfileSchema.parse(profile);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
export function definePrototypeMockScenarios(
|
|
1654
|
+
scenarios: PrototypeMockScenarios,
|
|
1655
|
+
): PrototypeMockScenarios {
|
|
1656
|
+
return prototypeMockScenariosSchema.parse(scenarios);
|
|
1657
|
+
}
|
|
1658
|
+
`,Ft=String.raw`import { defineMockGenerationProfile } from "./simulator-contract-schema";
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* Perfil para que el agente genere data mock coherente con Chile.
|
|
1662
|
+
*
|
|
1663
|
+
* La IA lo completa en autoría. El runtime solo consume objetos TypeScript
|
|
1664
|
+
* explícitos y deterministas.
|
|
1665
|
+
*/
|
|
1666
|
+
export const mockGenerationProfile = defineMockGenerationProfile({
|
|
1667
|
+
profileVersion: "1",
|
|
1668
|
+
locale: {
|
|
1669
|
+
language: "es",
|
|
1670
|
+
locale: "es-CL",
|
|
1671
|
+
country: "CL",
|
|
1672
|
+
timezone: "America/Santiago",
|
|
1673
|
+
currency: "CLP",
|
|
1674
|
+
dateStorage: "iso8601",
|
|
1675
|
+
dateDisplay: "dd-MM-yyyy",
|
|
1676
|
+
timeDisplay: "24h",
|
|
1677
|
+
},
|
|
1678
|
+
safety: {
|
|
1679
|
+
syntheticOnly: true,
|
|
1680
|
+
allowProductionImports: false,
|
|
1681
|
+
externalUse: false,
|
|
1682
|
+
emailDomain: "example.com",
|
|
1683
|
+
},
|
|
1684
|
+
entityKeys: {},
|
|
1685
|
+
relationBindings: {},
|
|
1686
|
+
fieldHints: {},
|
|
1687
|
+
});
|
|
1688
|
+
`,Nt=String.raw`import { definePrototypeMockScenarios } from "./simulator-contract-schema";
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Escenarios mock persistibles.
|
|
1692
|
+
*
|
|
1693
|
+
* Un proyecto fresco nace vacío y válido. El agente agrega fixtures explícitos
|
|
1694
|
+
* después del kickoff y sube datasetVersion cuando cambia el dataset.
|
|
1695
|
+
*/
|
|
1696
|
+
export const prototypeMockScenarios = definePrototypeMockScenarios({
|
|
1697
|
+
scenariosVersion: "1",
|
|
1698
|
+
datasetVersion: 1,
|
|
1699
|
+
defaultScenarioId: "base",
|
|
1700
|
+
scenarios: {
|
|
1701
|
+
base: {
|
|
1702
|
+
id: "base",
|
|
1703
|
+
name: "Recorrido principal",
|
|
1704
|
+
description: "Escenario inicial pendiente de completar durante el kickoff.",
|
|
1705
|
+
entities: {},
|
|
1706
|
+
},
|
|
1707
|
+
empty: {
|
|
1708
|
+
id: "empty",
|
|
1709
|
+
name: "Estado vacío",
|
|
1710
|
+
description: "Escenario sin resultados.",
|
|
1711
|
+
entities: {},
|
|
1712
|
+
},
|
|
1713
|
+
edgeCases: {
|
|
1714
|
+
id: "edgeCases",
|
|
1715
|
+
name: "Casos límite",
|
|
1716
|
+
description: "Escenario pendiente de completar según la experiencia.",
|
|
1717
|
+
entities: {},
|
|
1718
|
+
},
|
|
1719
|
+
},
|
|
1720
|
+
});
|
|
1721
|
+
`,Ht=String.raw`import type { ProjectionResolverMap } from "./prototype-simulator";
|
|
1722
|
+
|
|
1723
|
+
export const projectionResolvers: ProjectionResolverMap = {};
|
|
1724
|
+
`,Kt=String.raw`import type { MockDataLoadHandlerMap } from "./prototype-simulator";
|
|
1725
|
+
|
|
1726
|
+
export const dataLoadHandlers: MockDataLoadHandlerMap = {};
|
|
1727
|
+
`,qt=String.raw`import type { MockEventHandlerMap } from "./prototype-simulator";
|
|
1728
|
+
|
|
1729
|
+
export const eventHandlers: MockEventHandlerMap = {};
|
|
1730
|
+
`,Ut=String.raw`import type {
|
|
1731
|
+
JsonPrimitive,
|
|
1732
|
+
MockEntityRecord,
|
|
1733
|
+
PrototypeMockScenarios,
|
|
1734
|
+
} from "./simulator-contract-schema";
|
|
1735
|
+
|
|
1736
|
+
export type PublishedEventHistoryItem = {
|
|
1737
|
+
publicationId: string;
|
|
1738
|
+
eventId: string;
|
|
1739
|
+
technicalId: string;
|
|
1740
|
+
occurredAt: string;
|
|
1741
|
+
status: "published";
|
|
1742
|
+
};
|
|
1743
|
+
|
|
1744
|
+
export type PrototypeSimulatorState = {
|
|
1745
|
+
simulatorVersion: "1";
|
|
1746
|
+
datasetVersion: number;
|
|
1747
|
+
activeScenarioId: string;
|
|
1748
|
+
revision: number;
|
|
1749
|
+
entities: Record<string, MockEntityRecord[]>;
|
|
1750
|
+
entitySequences: Record<string, number>;
|
|
1751
|
+
publicationHistory: PublishedEventHistoryItem[];
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
export type SimulatorStorage = {
|
|
1755
|
+
read(): string | null;
|
|
1756
|
+
write(value: string): void;
|
|
1757
|
+
remove(): void;
|
|
1758
|
+
};
|
|
1759
|
+
|
|
1760
|
+
export type PrototypeMockStore = {
|
|
1761
|
+
getSnapshot(): PrototypeSimulatorState;
|
|
1762
|
+
subscribe(listener: () => void): () => void;
|
|
1763
|
+
transact(operation: (draft: PrototypeSimulatorState) => void): PrototypeSimulatorState;
|
|
1764
|
+
switchScenario(scenarioId: string): void;
|
|
1765
|
+
resetScenario(): void;
|
|
1766
|
+
clearPersistence(): void;
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
type CreatePrototypeMockStoreOptions = {
|
|
1770
|
+
projectName: string;
|
|
1771
|
+
scenarios: PrototypeMockScenarios;
|
|
1772
|
+
storage?: SimulatorStorage;
|
|
1773
|
+
onHydrationWarning?: (message: string) => void;
|
|
1774
|
+
};
|
|
1775
|
+
|
|
1776
|
+
const clone = <T>(value: T): T => {
|
|
1777
|
+
if (typeof structuredClone === "function") return structuredClone(value);
|
|
1778
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
1779
|
+
};
|
|
1780
|
+
|
|
1781
|
+
const buildSequences = (entities: Record<string, MockEntityRecord[]>) => {
|
|
1782
|
+
const sequences: Record<string, number> = {};
|
|
1783
|
+
for (const [entityId, records] of Object.entries(entities)) {
|
|
1784
|
+
sequences[entityId] = records.length + 1;
|
|
1785
|
+
}
|
|
1786
|
+
return sequences;
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
const createStateFromScenario = (
|
|
1790
|
+
scenarios: PrototypeMockScenarios,
|
|
1791
|
+
scenarioId: string,
|
|
1792
|
+
): PrototypeSimulatorState => {
|
|
1793
|
+
const scenario = scenarios.scenarios[scenarioId];
|
|
1794
|
+
if (!scenario) throw new Error('El escenario "' + scenarioId + '" no existe.');
|
|
1795
|
+
const entities = clone(scenario.entities);
|
|
1796
|
+
return {
|
|
1797
|
+
simulatorVersion: "1",
|
|
1798
|
+
datasetVersion: scenarios.datasetVersion,
|
|
1799
|
+
activeScenarioId: scenarioId,
|
|
1800
|
+
revision: 0,
|
|
1801
|
+
entities,
|
|
1802
|
+
entitySequences: buildSequences(entities),
|
|
1803
|
+
publicationHistory: [],
|
|
1804
|
+
};
|
|
1805
|
+
};
|
|
1806
|
+
|
|
1807
|
+
const isPersistedStateCompatible = (
|
|
1808
|
+
value: unknown,
|
|
1809
|
+
scenarios: PrototypeMockScenarios,
|
|
1810
|
+
): value is PrototypeSimulatorState => {
|
|
1811
|
+
if (!value || typeof value !== "object") return false;
|
|
1812
|
+
const state = value as Partial<PrototypeSimulatorState>;
|
|
1813
|
+
return (
|
|
1814
|
+
state.simulatorVersion === "1" &&
|
|
1815
|
+
state.datasetVersion === scenarios.datasetVersion &&
|
|
1816
|
+
typeof state.activeScenarioId === "string" &&
|
|
1817
|
+
Boolean(scenarios.scenarios[state.activeScenarioId]) &&
|
|
1818
|
+
typeof state.revision === "number" &&
|
|
1819
|
+
Boolean(state.entities && typeof state.entities === "object") &&
|
|
1820
|
+
Boolean(state.entitySequences && typeof state.entitySequences === "object") &&
|
|
1821
|
+
Array.isArray(state.publicationHistory)
|
|
1822
|
+
);
|
|
1823
|
+
};
|
|
1824
|
+
|
|
1825
|
+
export const createMemorySimulatorStorage = (): SimulatorStorage => {
|
|
1826
|
+
let current: string | null = null;
|
|
1827
|
+
return {
|
|
1828
|
+
read: () => current,
|
|
1829
|
+
write: (value) => {
|
|
1830
|
+
current = value;
|
|
1831
|
+
},
|
|
1832
|
+
remove: () => {
|
|
1833
|
+
current = null;
|
|
1834
|
+
},
|
|
1835
|
+
};
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
export const createLocalStorageSimulatorStorage = (key: string): SimulatorStorage => {
|
|
1839
|
+
const memory = createMemorySimulatorStorage();
|
|
1840
|
+
const getLocalStorage = () => {
|
|
1841
|
+
try {
|
|
1842
|
+
return globalThis.localStorage;
|
|
1843
|
+
} catch {
|
|
1844
|
+
return undefined;
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
return {
|
|
1849
|
+
read: () => {
|
|
1850
|
+
try {
|
|
1851
|
+
return getLocalStorage()?.getItem(key) ?? memory.read();
|
|
1852
|
+
} catch {
|
|
1853
|
+
return memory.read();
|
|
1854
|
+
}
|
|
1855
|
+
},
|
|
1856
|
+
write: (value) => {
|
|
1857
|
+
memory.write(value);
|
|
1858
|
+
try {
|
|
1859
|
+
getLocalStorage()?.setItem(key, value);
|
|
1860
|
+
} catch {
|
|
1861
|
+
// fallback en memoria
|
|
1862
|
+
}
|
|
1863
|
+
},
|
|
1864
|
+
remove: () => {
|
|
1865
|
+
memory.remove();
|
|
1866
|
+
try {
|
|
1867
|
+
getLocalStorage()?.removeItem(key);
|
|
1868
|
+
} catch {
|
|
1869
|
+
// fallback en memoria
|
|
1870
|
+
}
|
|
1871
|
+
},
|
|
1872
|
+
};
|
|
1873
|
+
};
|
|
1874
|
+
|
|
1875
|
+
export const simulatorStorageKey = (projectName: string) =>
|
|
1876
|
+
"lexy:prototype:" + projectName + ":simulator:v1";
|
|
1877
|
+
|
|
1878
|
+
export const createPrototypeMockStore = (
|
|
1879
|
+
options: CreatePrototypeMockStoreOptions,
|
|
1880
|
+
): PrototypeMockStore => {
|
|
1881
|
+
const storage =
|
|
1882
|
+
options.storage ?? createLocalStorageSimulatorStorage(simulatorStorageKey(options.projectName));
|
|
1883
|
+
const listeners = new Set<() => void>();
|
|
1884
|
+
|
|
1885
|
+
const hydrate = () => {
|
|
1886
|
+
const persisted = storage.read();
|
|
1887
|
+
if (!persisted)
|
|
1888
|
+
return createStateFromScenario(options.scenarios, options.scenarios.defaultScenarioId);
|
|
1889
|
+
|
|
1890
|
+
try {
|
|
1891
|
+
const parsed = JSON.parse(persisted) as unknown;
|
|
1892
|
+
if (isPersistedStateCompatible(parsed, options.scenarios)) return parsed;
|
|
1893
|
+
options.onHydrationWarning?.(
|
|
1894
|
+
"Estado persistido incompatible; se reinicia el simulador al escenario default.",
|
|
1895
|
+
);
|
|
1896
|
+
} catch {
|
|
1897
|
+
options.onHydrationWarning?.(
|
|
1898
|
+
"Estado persistido corrupto; se reinicia el simulador al escenario default.",
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
return createStateFromScenario(options.scenarios, options.scenarios.defaultScenarioId);
|
|
1902
|
+
};
|
|
1903
|
+
|
|
1904
|
+
let snapshot = hydrate();
|
|
1905
|
+
|
|
1906
|
+
const persist = () => {
|
|
1907
|
+
storage.write(JSON.stringify(snapshot));
|
|
1908
|
+
};
|
|
1909
|
+
|
|
1910
|
+
const notify = () => {
|
|
1911
|
+
for (const listener of listeners) listener();
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
return {
|
|
1915
|
+
getSnapshot: () => snapshot,
|
|
1916
|
+
subscribe: (listener) => {
|
|
1917
|
+
listeners.add(listener);
|
|
1918
|
+
return () => {
|
|
1919
|
+
listeners.delete(listener);
|
|
1920
|
+
};
|
|
1921
|
+
},
|
|
1922
|
+
transact: (operation) => {
|
|
1923
|
+
const draft = clone(snapshot);
|
|
1924
|
+
operation(draft);
|
|
1925
|
+
draft.revision = snapshot.revision + 1;
|
|
1926
|
+
snapshot = draft;
|
|
1927
|
+
persist();
|
|
1928
|
+
notify();
|
|
1929
|
+
return snapshot;
|
|
1930
|
+
},
|
|
1931
|
+
switchScenario: (scenarioId) => {
|
|
1932
|
+
snapshot = createStateFromScenario(options.scenarios, scenarioId);
|
|
1933
|
+
persist();
|
|
1934
|
+
notify();
|
|
1935
|
+
},
|
|
1936
|
+
resetScenario: () => {
|
|
1937
|
+
snapshot = createStateFromScenario(options.scenarios, snapshot.activeScenarioId);
|
|
1938
|
+
persist();
|
|
1939
|
+
notify();
|
|
1940
|
+
},
|
|
1941
|
+
clearPersistence: () => {
|
|
1942
|
+
storage.remove();
|
|
1943
|
+
},
|
|
1944
|
+
};
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
export const findRecordById = (
|
|
1948
|
+
records: readonly MockEntityRecord[],
|
|
1949
|
+
fieldId: string,
|
|
1950
|
+
id: JsonPrimitive,
|
|
1951
|
+
) => records.find((record) => record[fieldId] === id);
|
|
1952
|
+
`,Bt=String.raw`import type {
|
|
1953
|
+
EventPublicationEnvelope,
|
|
1954
|
+
PrototypeBackendExecutor,
|
|
1955
|
+
} from "../event-engine/event-engine";
|
|
1956
|
+
import type {
|
|
1957
|
+
JsonPrimitive,
|
|
1958
|
+
MockEntityRecord,
|
|
1959
|
+
MockGenerationProfile,
|
|
1960
|
+
MockOperationError,
|
|
1961
|
+
PrototypeMockScenarios,
|
|
1962
|
+
} from "./simulator-contract-schema";
|
|
1963
|
+
import {
|
|
1964
|
+
findRecordById,
|
|
1965
|
+
type PrototypeMockStore,
|
|
1966
|
+
type PrototypeSimulatorState,
|
|
1967
|
+
} from "./mock-store";
|
|
1968
|
+
import type {
|
|
1969
|
+
MockDataLoadHandlerMap,
|
|
1970
|
+
MockEventHandlerMap,
|
|
1971
|
+
ProjectionResolverContext,
|
|
1972
|
+
ProjectionResolverMap,
|
|
1973
|
+
} from "./prototype-simulator";
|
|
1974
|
+
|
|
1975
|
+
export class MockBackendError extends Error {
|
|
1976
|
+
code: string;
|
|
1977
|
+
recoverable: boolean;
|
|
1978
|
+
|
|
1979
|
+
constructor(error: MockOperationError) {
|
|
1980
|
+
super(error.message);
|
|
1981
|
+
this.name = "MockBackendError";
|
|
1982
|
+
this.code = error.code;
|
|
1983
|
+
this.recoverable = error.recoverable;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
type CreateMockBackendExecutorOptions = {
|
|
1988
|
+
profile: MockGenerationProfile;
|
|
1989
|
+
scenarios: PrototypeMockScenarios;
|
|
1990
|
+
store: PrototypeMockStore;
|
|
1991
|
+
projectionResolvers: ProjectionResolverMap;
|
|
1992
|
+
dataLoadHandlers: MockDataLoadHandlerMap;
|
|
1993
|
+
eventHandlers: MockEventHandlerMap;
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
const delay = (latencyMs: number | undefined, signal?: AbortSignal) =>
|
|
1997
|
+
new Promise<void>((resolve, reject) => {
|
|
1998
|
+
if (!latencyMs) {
|
|
1999
|
+
resolve();
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
if (signal?.aborted) {
|
|
2003
|
+
reject(new DOMException("Operación abortada.", "AbortError"));
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
const timeout = globalThis.setTimeout(resolve, latencyMs);
|
|
2007
|
+
signal?.addEventListener(
|
|
2008
|
+
"abort",
|
|
2009
|
+
() => {
|
|
2010
|
+
globalThis.clearTimeout(timeout);
|
|
2011
|
+
reject(new DOMException("Operación abortada.", "AbortError"));
|
|
2012
|
+
},
|
|
2013
|
+
{ once: true },
|
|
2014
|
+
);
|
|
2015
|
+
});
|
|
2016
|
+
|
|
2017
|
+
const splitReference = (reference: string) => {
|
|
2018
|
+
const [entityId, fieldId] = reference.split(".");
|
|
2019
|
+
return { entityId, fieldId };
|
|
2020
|
+
};
|
|
2021
|
+
|
|
2022
|
+
const getEntityKeyField = (profile: MockGenerationProfile, entityId: string) => {
|
|
2023
|
+
const field = profile.entityKeys[entityId]?.field;
|
|
2024
|
+
return field ? splitReference(field).fieldId : "id";
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
const createProjectionContext = (
|
|
2028
|
+
profile: MockGenerationProfile,
|
|
2029
|
+
state: PrototypeSimulatorState,
|
|
2030
|
+
): ProjectionResolverContext => ({
|
|
2031
|
+
entities: state.entities,
|
|
2032
|
+
getEntity: (entityId) => state.entities[entityId] ?? [],
|
|
2033
|
+
getById: (entityId, id) =>
|
|
2034
|
+
findRecordById(state.entities[entityId] ?? [], getEntityKeyField(profile, entityId), id),
|
|
2035
|
+
getRelated: (relationId, sourceId) => {
|
|
2036
|
+
const binding = Object.values(profile.relationBindings).find(
|
|
2037
|
+
(candidate) => candidate.relation === relationId,
|
|
2038
|
+
);
|
|
2039
|
+
if (!binding) return [];
|
|
2040
|
+
const target = splitReference(binding.toField);
|
|
2041
|
+
return (state.entities[target.entityId] ?? []).filter(
|
|
2042
|
+
(record) => record[target.fieldId] === sourceId,
|
|
2043
|
+
);
|
|
2044
|
+
},
|
|
2045
|
+
});
|
|
2046
|
+
|
|
2047
|
+
const nextEntityId = (
|
|
2048
|
+
profile: MockGenerationProfile,
|
|
2049
|
+
draft: PrototypeSimulatorState,
|
|
2050
|
+
entityId: string,
|
|
2051
|
+
): JsonPrimitive => {
|
|
2052
|
+
const keyField = getEntityKeyField(profile, entityId);
|
|
2053
|
+
const records = draft.entities[entityId] ?? [];
|
|
2054
|
+
const numericIds = records
|
|
2055
|
+
.map((record) => record[keyField])
|
|
2056
|
+
.filter((value): value is number => typeof value === "number");
|
|
2057
|
+
if (numericIds.length > 0) return Math.max(...numericIds) + 1;
|
|
2058
|
+
|
|
2059
|
+
const sequence = draft.entitySequences[entityId] ?? records.length + 1;
|
|
2060
|
+
draft.entitySequences[entityId] = sequence + 1;
|
|
2061
|
+
return entityId + "-" + sequence;
|
|
2062
|
+
};
|
|
2063
|
+
|
|
2064
|
+
export const createMockBackendExecutor = (
|
|
2065
|
+
options: CreateMockBackendExecutorOptions,
|
|
2066
|
+
): PrototypeBackendExecutor => ({
|
|
2067
|
+
async loadData(input) {
|
|
2068
|
+
const state = options.store.getSnapshot();
|
|
2069
|
+
const scenario = options.scenarios.scenarios[state.activeScenarioId];
|
|
2070
|
+
const behavior = scenario.behaviors?.dataLoads?.[input.loadId];
|
|
2071
|
+
await delay(behavior?.latencyMs, input.signal);
|
|
2072
|
+
|
|
2073
|
+
if (behavior?.outcome === "error" && behavior.error) throw new MockBackendError(behavior.error);
|
|
2074
|
+
if (behavior?.outcome === "empty") return [];
|
|
2075
|
+
|
|
2076
|
+
const handler = options.dataLoadHandlers[input.loadId];
|
|
2077
|
+
if (!handler) throw new Error('Falta handler para la carga "' + input.loadId + '".');
|
|
2078
|
+
|
|
2079
|
+
return handler({
|
|
2080
|
+
params: input.params,
|
|
2081
|
+
signal: input.signal,
|
|
2082
|
+
store: options.store,
|
|
2083
|
+
projections: {
|
|
2084
|
+
resolve: (projectionId, params) => {
|
|
2085
|
+
const resolver = options.projectionResolvers[projectionId];
|
|
2086
|
+
if (!resolver)
|
|
2087
|
+
throw new Error('Falta resolver para la proyección "' + projectionId + '".');
|
|
2088
|
+
return resolver(
|
|
2089
|
+
createProjectionContext(options.profile, options.store.getSnapshot()),
|
|
2090
|
+
params,
|
|
2091
|
+
);
|
|
2092
|
+
},
|
|
2093
|
+
},
|
|
2094
|
+
});
|
|
2095
|
+
},
|
|
2096
|
+
|
|
2097
|
+
async publishEvent(input) {
|
|
2098
|
+
const state = options.store.getSnapshot();
|
|
2099
|
+
const scenario = options.scenarios.scenarios[state.activeScenarioId];
|
|
2100
|
+
const behavior = scenario.behaviors?.events?.[input.eventId];
|
|
2101
|
+
await delay(behavior?.latencyMs);
|
|
2102
|
+
|
|
2103
|
+
if (behavior?.outcome === "error" && behavior.error) throw new MockBackendError(behavior.error);
|
|
2104
|
+
|
|
2105
|
+
const handler = options.eventHandlers[input.eventId];
|
|
2106
|
+
if (!handler) throw new Error('Falta handler para el evento "' + input.eventId + '".');
|
|
2107
|
+
|
|
2108
|
+
let receipt: unknown;
|
|
2109
|
+
options.store.transact((draft) => {
|
|
2110
|
+
const getById = (entityId: string, id: JsonPrimitive): MockEntityRecord | undefined =>
|
|
2111
|
+
findRecordById(
|
|
2112
|
+
draft.entities[entityId] ?? [],
|
|
2113
|
+
getEntityKeyField(options.profile, entityId),
|
|
2114
|
+
id,
|
|
2115
|
+
);
|
|
2116
|
+
|
|
2117
|
+
const result = handler({
|
|
2118
|
+
draft,
|
|
2119
|
+
envelope: input.envelope as EventPublicationEnvelope,
|
|
2120
|
+
getById,
|
|
2121
|
+
nextEntityId: (entityId) => nextEntityId(options.profile, draft, entityId),
|
|
2122
|
+
});
|
|
2123
|
+
receipt = result?.receipt;
|
|
2124
|
+
draft.publicationHistory.push({
|
|
2125
|
+
publicationId: input.envelope.publicationId,
|
|
2126
|
+
eventId: input.eventId,
|
|
2127
|
+
technicalId: input.envelope.technicalId,
|
|
2128
|
+
occurredAt: input.envelope.occurredAt,
|
|
2129
|
+
status: "published",
|
|
2130
|
+
});
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
return receipt === undefined ? {} : { receipt };
|
|
2134
|
+
},
|
|
2135
|
+
});
|
|
2136
|
+
`,Jt=String.raw`import type { EventPublicationEnvelope } from "../event-engine/event-engine";
|
|
2137
|
+
import { createPrototypeEventEngine } from "../event-engine/event-engine";
|
|
2138
|
+
import { prototypeDataContract } from "../data-contract/prototype-data-contract";
|
|
2139
|
+
import { dataLoadHandlers } from "./data-load-handlers";
|
|
2140
|
+
import { eventHandlers } from "./event-handlers";
|
|
2141
|
+
import { mockGenerationProfile } from "./mock-generation-profile";
|
|
2142
|
+
import { createMockBackendExecutor } from "./mock-backend-executor";
|
|
2143
|
+
import { createPrototypeMockStore, type PrototypeMockStore } from "./mock-store";
|
|
2144
|
+
import { prototypeMockScenarios } from "./mock-scenarios";
|
|
2145
|
+
import { projectionResolvers } from "./projection-resolvers";
|
|
2146
|
+
import type {
|
|
2147
|
+
JsonPrimitive,
|
|
2148
|
+
MockEntityRecord,
|
|
2149
|
+
MockGenerationProfile,
|
|
2150
|
+
PrototypeMockScenarios,
|
|
2151
|
+
} from "./simulator-contract-schema";
|
|
2152
|
+
|
|
2153
|
+
export type ProjectionResolverContext = {
|
|
2154
|
+
entities: Readonly<Record<string, readonly MockEntityRecord[]>>;
|
|
2155
|
+
getEntity(entityId: string): readonly MockEntityRecord[];
|
|
2156
|
+
getById(entityId: string, id: JsonPrimitive): MockEntityRecord | undefined;
|
|
2157
|
+
getRelated(relationId: string, sourceId: JsonPrimitive): readonly MockEntityRecord[];
|
|
2158
|
+
};
|
|
2159
|
+
|
|
2160
|
+
export type ProjectionResolver = (context: ProjectionResolverContext, params?: unknown) => unknown;
|
|
2161
|
+
|
|
2162
|
+
export type ProjectionResolverMap = Record<string, ProjectionResolver>;
|
|
2163
|
+
|
|
2164
|
+
export type MockDataLoadHandlerContext = {
|
|
2165
|
+
params: unknown;
|
|
2166
|
+
signal?: AbortSignal;
|
|
2167
|
+
store: PrototypeMockStore;
|
|
2168
|
+
projections: {
|
|
2169
|
+
resolve(projectionId: string, params?: unknown): unknown;
|
|
2170
|
+
};
|
|
2171
|
+
};
|
|
2172
|
+
|
|
2173
|
+
export type MockDataLoadHandler = (
|
|
2174
|
+
context: MockDataLoadHandlerContext,
|
|
2175
|
+
) => unknown | Promise<unknown>;
|
|
2176
|
+
|
|
2177
|
+
export type MockDataLoadHandlerMap = Record<string, MockDataLoadHandler>;
|
|
2178
|
+
|
|
2179
|
+
export type MockEventHandlerContext = {
|
|
2180
|
+
draft: import("./mock-store").PrototypeSimulatorState;
|
|
2181
|
+
envelope: EventPublicationEnvelope;
|
|
2182
|
+
getById(entityId: string, id: JsonPrimitive): MockEntityRecord | undefined;
|
|
2183
|
+
nextEntityId(entityId: string): JsonPrimitive;
|
|
2184
|
+
};
|
|
2185
|
+
|
|
2186
|
+
export type MockEventHandlerResult = {
|
|
2187
|
+
receipt?: unknown;
|
|
2188
|
+
};
|
|
2189
|
+
|
|
2190
|
+
export type MockEventHandler = (context: MockEventHandlerContext) => MockEventHandlerResult | void;
|
|
2191
|
+
|
|
2192
|
+
export type MockEventHandlerMap = Record<string, MockEventHandler>;
|
|
2193
|
+
|
|
2194
|
+
export type PrototypeSimulator = {
|
|
2195
|
+
executor: ReturnType<typeof createMockBackendExecutor>;
|
|
2196
|
+
store: PrototypeMockStore;
|
|
2197
|
+
getActiveScenario(): PrototypeMockScenarios["scenarios"][string];
|
|
2198
|
+
listScenarios(): Array<{ id: string; name: string; description: string }>;
|
|
2199
|
+
switchScenario(scenarioId: string): void;
|
|
2200
|
+
reset(): void;
|
|
2201
|
+
clearPersistence(): void;
|
|
2202
|
+
};
|
|
2203
|
+
|
|
2204
|
+
type CreatePrototypeSimulatorOptions = {
|
|
2205
|
+
profile: MockGenerationProfile;
|
|
2206
|
+
scenarios: PrototypeMockScenarios;
|
|
2207
|
+
projectionResolvers: ProjectionResolverMap;
|
|
2208
|
+
dataLoadHandlers: MockDataLoadHandlerMap;
|
|
2209
|
+
eventHandlers: MockEventHandlerMap;
|
|
2210
|
+
};
|
|
2211
|
+
|
|
2212
|
+
export const createPrototypeSimulator = (
|
|
2213
|
+
options: CreatePrototypeSimulatorOptions,
|
|
2214
|
+
): PrototypeSimulator => {
|
|
2215
|
+
const store = createPrototypeMockStore({
|
|
2216
|
+
projectName: prototypeDataContract.project.name,
|
|
2217
|
+
scenarios: options.scenarios,
|
|
2218
|
+
onHydrationWarning: (message) => {
|
|
2219
|
+
console.warn("[Lexy simulator] " + message);
|
|
2220
|
+
},
|
|
2221
|
+
});
|
|
2222
|
+
|
|
2223
|
+
const executor = createMockBackendExecutor({
|
|
2224
|
+
profile: options.profile,
|
|
2225
|
+
scenarios: options.scenarios,
|
|
2226
|
+
store,
|
|
2227
|
+
projectionResolvers: options.projectionResolvers,
|
|
2228
|
+
dataLoadHandlers: options.dataLoadHandlers,
|
|
2229
|
+
eventHandlers: options.eventHandlers,
|
|
2230
|
+
});
|
|
2231
|
+
|
|
2232
|
+
return {
|
|
2233
|
+
executor,
|
|
2234
|
+
store,
|
|
2235
|
+
getActiveScenario: () => options.scenarios.scenarios[store.getSnapshot().activeScenarioId],
|
|
2236
|
+
listScenarios: () =>
|
|
2237
|
+
Object.values(options.scenarios.scenarios).map((scenario) => ({
|
|
2238
|
+
id: scenario.id,
|
|
2239
|
+
name: scenario.name,
|
|
2240
|
+
description: scenario.description,
|
|
2241
|
+
})),
|
|
2242
|
+
switchScenario: (scenarioId) => store.switchScenario(scenarioId),
|
|
2243
|
+
reset: () => store.resetScenario(),
|
|
2244
|
+
clearPersistence: () => store.clearPersistence(),
|
|
2245
|
+
};
|
|
2246
|
+
};
|
|
2247
|
+
|
|
2248
|
+
export const prototypeSimulator = createPrototypeSimulator({
|
|
2249
|
+
profile: mockGenerationProfile,
|
|
2250
|
+
scenarios: prototypeMockScenarios,
|
|
2251
|
+
projectionResolvers,
|
|
2252
|
+
dataLoadHandlers,
|
|
2253
|
+
eventHandlers,
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2256
|
+
export const prototypeEventEngine = createPrototypeEventEngine({
|
|
2257
|
+
executor: prototypeSimulator.executor,
|
|
2258
|
+
});
|
|
2259
|
+
`,Gt=String.raw`import { ZodError } from "zod";
|
|
2260
|
+
|
|
2261
|
+
import { prototypeDataContractSchema } from "../src/prototype/data-contract/data-contract-schema";
|
|
2262
|
+
import { prototypeEventEngineContractSchema } from "../src/prototype/event-engine/event-engine-contract-schema";
|
|
2263
|
+
import { dataLoadHandlers } from "../src/prototype/simulator/data-load-handlers";
|
|
2264
|
+
import { eventHandlers } from "../src/prototype/simulator/event-handlers";
|
|
2265
|
+
import { mockGenerationProfile } from "../src/prototype/simulator/mock-generation-profile";
|
|
2266
|
+
import { prototypeMockScenarios } from "../src/prototype/simulator/mock-scenarios";
|
|
2267
|
+
import { projectionResolvers } from "../src/prototype/simulator/projection-resolvers";
|
|
2268
|
+
import {
|
|
2269
|
+
mockGenerationProfileSchema,
|
|
2270
|
+
prototypeMockScenariosSchema,
|
|
2271
|
+
validateMockProfileReferences,
|
|
2272
|
+
validateMockScenarios,
|
|
2273
|
+
type SimulatorValidationIssue,
|
|
2274
|
+
} from "../src/prototype/simulator/simulator-contract-schema";
|
|
2275
|
+
|
|
2276
|
+
const formatPath = (path: Array<string | number>) =>
|
|
2277
|
+
path.length === 0 ? "simulator" : path.map(String).join(".");
|
|
2278
|
+
|
|
2279
|
+
const printIssues = (issues: SimulatorValidationIssue[]) => {
|
|
2280
|
+
console.error("✗ Simulador inválido");
|
|
2281
|
+
for (const issue of issues) {
|
|
2282
|
+
console.error(" " + formatPath(issue.path));
|
|
2283
|
+
console.error(" " + issue.message);
|
|
2284
|
+
}
|
|
2285
|
+
};
|
|
2286
|
+
|
|
2287
|
+
try {
|
|
2288
|
+
const dataContractModule = await import("../src/prototype/data-contract/prototype-data-contract");
|
|
2289
|
+
const eventEngineModule =
|
|
2290
|
+
await import("../src/prototype/event-engine/prototype-event-engine-contract");
|
|
2291
|
+
|
|
2292
|
+
const dataContractResult = prototypeDataContractSchema.safeParse(
|
|
2293
|
+
dataContractModule.prototypeDataContract,
|
|
2294
|
+
);
|
|
2295
|
+
const eventEngineResult = prototypeEventEngineContractSchema.safeParse(
|
|
2296
|
+
eventEngineModule.prototypeEventEngineContract,
|
|
2297
|
+
);
|
|
2298
|
+
const profileResult = mockGenerationProfileSchema.safeParse(mockGenerationProfile);
|
|
2299
|
+
const scenariosResult = prototypeMockScenariosSchema.safeParse(prototypeMockScenarios);
|
|
2300
|
+
|
|
2301
|
+
const issues: SimulatorValidationIssue[] = [];
|
|
2302
|
+
for (const [prefix, result] of [
|
|
2303
|
+
["dataContract", dataContractResult],
|
|
2304
|
+
["eventEngine", eventEngineResult],
|
|
2305
|
+
["profile", profileResult],
|
|
2306
|
+
["scenarios", scenariosResult],
|
|
2307
|
+
] as const) {
|
|
2308
|
+
if (!result.success) {
|
|
2309
|
+
for (const issue of result.error.issues) {
|
|
2310
|
+
issues.push({
|
|
2311
|
+
path: [prefix, ...issue.path.map(String)],
|
|
2312
|
+
message: issue.message,
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
if (
|
|
2319
|
+
dataContractResult.success &&
|
|
2320
|
+
eventEngineResult.success &&
|
|
2321
|
+
profileResult.success &&
|
|
2322
|
+
scenariosResult.success
|
|
2323
|
+
) {
|
|
2324
|
+
issues.push(
|
|
2325
|
+
...validateMockProfileReferences({
|
|
2326
|
+
profile: profileResult.data,
|
|
2327
|
+
dataContract: dataContractResult.data,
|
|
2328
|
+
}),
|
|
2329
|
+
...validateMockScenarios({
|
|
2330
|
+
profile: profileResult.data,
|
|
2331
|
+
scenarios: scenariosResult.data,
|
|
2332
|
+
dataContract: dataContractResult.data,
|
|
2333
|
+
eventEngineContract: eventEngineResult.data,
|
|
2334
|
+
}),
|
|
2335
|
+
);
|
|
2336
|
+
|
|
2337
|
+
for (const projectionId of Object.keys(projectionResolvers)) {
|
|
2338
|
+
if (!dataContractResult.data.projections[projectionId]) {
|
|
2339
|
+
issues.push({
|
|
2340
|
+
path: ["projectionResolvers", projectionId],
|
|
2341
|
+
message: 'La proyección "' + projectionId + '" no existe en la Spec 1.',
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
const requiredProjections = new Set<string>();
|
|
2347
|
+
for (const load of Object.values(eventEngineResult.data.dataLoads)) {
|
|
2348
|
+
for (const projectionId of load.reads?.projections ?? [])
|
|
2349
|
+
requiredProjections.add(projectionId);
|
|
2350
|
+
}
|
|
2351
|
+
for (const projectionId of requiredProjections) {
|
|
2352
|
+
if (!projectionResolvers[projectionId]) {
|
|
2353
|
+
issues.push({
|
|
2354
|
+
path: ["projectionResolvers", projectionId],
|
|
2355
|
+
message: 'Falta resolver para la proyección consumida "' + projectionId + '".',
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
for (const loadId of Object.keys(eventEngineResult.data.dataLoads)) {
|
|
2361
|
+
if (!dataLoadHandlers[loadId]) {
|
|
2362
|
+
issues.push({
|
|
2363
|
+
path: ["dataLoadHandlers", loadId],
|
|
2364
|
+
message: 'Falta handler para la carga "' + loadId + '".',
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
for (const handlerId of Object.keys(dataLoadHandlers)) {
|
|
2369
|
+
if (!eventEngineResult.data.dataLoads[handlerId]) {
|
|
2370
|
+
issues.push({
|
|
2371
|
+
path: ["dataLoadHandlers", handlerId],
|
|
2372
|
+
message: 'El handler "' + handlerId + '" no corresponde a ninguna carga.',
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
for (const eventId of Object.keys(eventEngineResult.data.events)) {
|
|
2378
|
+
if (!eventHandlers[eventId]) {
|
|
2379
|
+
issues.push({
|
|
2380
|
+
path: ["eventHandlers", eventId],
|
|
2381
|
+
message: 'Falta handler para el evento "' + eventId + '".',
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
for (const handlerId of Object.keys(eventHandlers)) {
|
|
2386
|
+
if (!eventEngineResult.data.events[handlerId]) {
|
|
2387
|
+
issues.push({
|
|
2388
|
+
path: ["eventHandlers", handlerId],
|
|
2389
|
+
message: 'El handler "' + handlerId + '" no corresponde a ningún evento.',
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
if (issues.length > 0) {
|
|
2396
|
+
printIssues(issues);
|
|
2397
|
+
process.exitCode = 1;
|
|
2398
|
+
} else {
|
|
2399
|
+
console.log("✓ Simulador válido");
|
|
2400
|
+
console.log(" " + Object.keys(prototypeMockScenarios.scenarios).length + " escenarios");
|
|
2401
|
+
console.log(" datasetVersion " + prototypeMockScenarios.datasetVersion);
|
|
2402
|
+
}
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
if (error instanceof ZodError) {
|
|
2405
|
+
printIssues(
|
|
2406
|
+
error.issues.map((issue) => ({ path: issue.path.map(String), message: issue.message })),
|
|
2407
|
+
);
|
|
2408
|
+
} else {
|
|
2409
|
+
console.error("✗ No se pudo cargar el simulador");
|
|
2410
|
+
console.error(" " + (error instanceof Error ? error.message : String(error)));
|
|
2411
|
+
}
|
|
2412
|
+
process.exitCode = 1;
|
|
2413
|
+
}
|
|
2414
|
+
`,He=async e=>{let t=async(o,r)=>{let a=Ne.join(e,o);await Fe.mkdir(Ne.dirname(a),{recursive:!0}),await Fe.writeFile(a,r)};await t(Lt,_t),await t(ce,Ft),await t(le,Nt),await t(zt,Ht),await t(At,Kt),await t(Ot,qt),await t(Dt,Ut),await t($t,Bt),await t(de,Jt),await t(Vt,Gt)};var Zt={feature:["src/app","src/features","src/shared/assets","src/shared/components/base","src/shared/hooks","src/shared/services","src/shared/lib","src/shared/types"],layer:["src/assets","src/components/base","src/hooks","src/services","src/lib","src/types","src/views","src/stores"]},Wt="10.33.3",Xt=e=>{let t=e.architecture==="feature"?{"react-router":"^7.9.0"}:{};return JSON.stringify({name:e.projectName,private:!0,version:"0.0.0",type:"module",packageManager:`pnpm@${Wt}`,scripts:{dev:"vite","check:data-contract":"tsx scripts/check-data-contract.ts","check:event-engine":"tsx scripts/check-event-engine.ts","check:simulator":"tsx scripts/check-simulator.ts","check:prototype":"pnpm check:data-contract && pnpm check:event-engine && pnpm check:simulator","build:app":"tsc -b && vite build",build:"pnpm check:prototype && pnpm build:app",preview:"vite preview",lint:"eslint .",format:"prettier --write ."},dependencies:{clsx:"^2.1.1",react:"^19.2.6","react-dom":"^19.2.6",...t,"tailwind-merge":"^3.3.1",zod:"^4.1.0"},devDependencies:{"@babel/core":"^7.29.7","@eslint/js":"^10.0.1","@rolldown/plugin-babel":"^0.2.3","@tailwindcss/vite":"^4.3.0","@types/babel__core":"^7.20.5","@types/node":"^24.12.3","@types/react":"^19.2.14","@types/react-dom":"^19.2.3","@vitejs/plugin-react":"^6.0.1","babel-plugin-react-compiler":"^1.0.0",eslint:"^10.4.1","eslint-plugin-jsx-a11y":"^6.10.2","eslint-plugin-react-hooks":"^7.1.1","eslint-plugin-simple-import-sort":"^13.0.0",globals:"^17.6.0",prettier:"^3.8.4",tailwindcss:"^4.3.0",terser:"^5.48.0",tsx:"^4.19.0",typescript:"~5.9.3","typescript-eslint":"^8.61.0",vite:"^8.0.12"}},null,2)+`
|
|
2415
|
+
`},Yt=`import path from "node:path";
|
|
26
2416
|
|
|
27
2417
|
import babel from "@rolldown/plugin-babel";
|
|
28
2418
|
import tailwindcss from "@tailwindcss/vite";
|
|
@@ -52,7 +2442,7 @@ export default defineConfig({
|
|
|
52
2442
|
},
|
|
53
2443
|
},
|
|
54
2444
|
});
|
|
55
|
-
`,
|
|
2445
|
+
`,Qt=`import js from "@eslint/js";
|
|
56
2446
|
import jsxA11y from "eslint-plugin-jsx-a11y";
|
|
57
2447
|
import reactHooks from "eslint-plugin-react-hooks";
|
|
58
2448
|
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
|
@@ -81,14 +2471,14 @@ export default tseslint.config(
|
|
|
81
2471
|
},
|
|
82
2472
|
},
|
|
83
2473
|
);
|
|
84
|
-
`,
|
|
2474
|
+
`,eo=`{
|
|
85
2475
|
"printWidth": 100
|
|
86
2476
|
}
|
|
87
|
-
`,
|
|
2477
|
+
`,to=`pnpm-lock.yaml
|
|
88
2478
|
src/lexy-theme.css
|
|
89
2479
|
public/fonts/
|
|
90
2480
|
dist/
|
|
91
|
-
`,
|
|
2481
|
+
`,oo=`name: CI
|
|
92
2482
|
|
|
93
2483
|
on:
|
|
94
2484
|
push:
|
|
@@ -108,9 +2498,12 @@ jobs:
|
|
|
108
2498
|
- run: pnpm install --frozen-lockfile
|
|
109
2499
|
- run: pnpm lint
|
|
110
2500
|
- run: pnpm exec prettier --check .
|
|
111
|
-
- run: pnpm
|
|
2501
|
+
- run: pnpm check:data-contract
|
|
2502
|
+
- run: pnpm check:event-engine
|
|
2503
|
+
- run: pnpm check:simulator
|
|
2504
|
+
- run: pnpm build:app
|
|
112
2505
|
- run: pnpm audit --audit-level high
|
|
113
|
-
`,
|
|
2506
|
+
`,no=`{
|
|
114
2507
|
"files": [],
|
|
115
2508
|
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
|
|
116
2509
|
"compilerOptions": {
|
|
@@ -118,7 +2511,7 @@ jobs:
|
|
|
118
2511
|
"paths": { "@/*": ["./src/*"] }
|
|
119
2512
|
}
|
|
120
2513
|
}
|
|
121
|
-
`,
|
|
2514
|
+
`,ao=`{
|
|
122
2515
|
"compilerOptions": {
|
|
123
2516
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
124
2517
|
"target": "ES2022",
|
|
@@ -142,7 +2535,7 @@ jobs:
|
|
|
142
2535
|
},
|
|
143
2536
|
"include": ["src"]
|
|
144
2537
|
}
|
|
145
|
-
`,
|
|
2538
|
+
`,ro=`{
|
|
146
2539
|
"compilerOptions": {
|
|
147
2540
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
148
2541
|
"target": "ES2023",
|
|
@@ -159,7 +2552,7 @@ jobs:
|
|
|
159
2552
|
},
|
|
160
2553
|
"include": ["vite.config.ts"]
|
|
161
2554
|
}
|
|
162
|
-
`,
|
|
2555
|
+
`,io=e=>`<!doctype html>
|
|
163
2556
|
<html lang="es">
|
|
164
2557
|
<head>
|
|
165
2558
|
<meta charset="UTF-8" />
|
|
@@ -171,9 +2564,9 @@ jobs:
|
|
|
171
2564
|
<script type="module" src="/src/main.tsx"></script>
|
|
172
2565
|
</body>
|
|
173
2566
|
</html>
|
|
174
|
-
`,
|
|
2567
|
+
`,so=`@import "tailwindcss";
|
|
175
2568
|
@import "./lexy-theme.css";
|
|
176
|
-
`,
|
|
2569
|
+
`,co=e=>e.architecture==="feature"?`import "./index.css";
|
|
177
2570
|
|
|
178
2571
|
import { StrictMode } from "react";
|
|
179
2572
|
import { createRoot } from "react-dom/client";
|
|
@@ -200,18 +2593,18 @@ createRoot(document.getElementById("root")!).render(
|
|
|
200
2593
|
<App />
|
|
201
2594
|
</StrictMode>,
|
|
202
2595
|
);
|
|
203
|
-
`,
|
|
2596
|
+
`,Ke=` <main className="flex min-h-screen items-center justify-center bg-background text-foreground">
|
|
204
2597
|
<div className="text-center">
|
|
205
2598
|
<h1 className="text-2xl font-semibold">Proyecto Lexy listo</h1>
|
|
206
2599
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
207
2600
|
Trae tu primer componente: <code>npx create-lexy add button</code>
|
|
208
2601
|
</p>
|
|
209
2602
|
</div>
|
|
210
|
-
</main>`,
|
|
2603
|
+
</main>`,lo=e=>e.architecture==="feature"?`import { Route, Routes } from "react-router";
|
|
211
2604
|
|
|
212
2605
|
const Home = () => {
|
|
213
2606
|
return (
|
|
214
|
-
${
|
|
2607
|
+
${Ke}
|
|
215
2608
|
);
|
|
216
2609
|
};
|
|
217
2610
|
|
|
@@ -224,52 +2617,53 @@ export const App = () => {
|
|
|
224
2617
|
};
|
|
225
2618
|
`:`export const App = () => {
|
|
226
2619
|
return (
|
|
227
|
-
${
|
|
2620
|
+
${Ke}
|
|
228
2621
|
);
|
|
229
2622
|
};
|
|
230
|
-
`,
|
|
2623
|
+
`,po=`import { type ClassValue, clsx } from "clsx";
|
|
231
2624
|
import { twMerge } from "tailwind-merge";
|
|
232
2625
|
|
|
233
2626
|
export function cn(...inputs: ClassValue[]) {
|
|
234
2627
|
return twMerge(clsx(inputs));
|
|
235
2628
|
}
|
|
236
|
-
`,
|
|
2629
|
+
`,mo=`node_modules/
|
|
237
2630
|
dist/
|
|
238
2631
|
*.log
|
|
239
2632
|
.env
|
|
240
|
-
`,
|
|
2633
|
+
`,uo=e=>`# ${e.projectName}
|
|
241
2634
|
|
|
242
2635
|
Proyecto Lexy (React + Vite + TS + theme Lexy). Los componentes viven en tu proyecto: m\xEDralos con \`view\`, inst\xE1lalos con \`add\`, ed\xEDtalos con libertad.
|
|
243
2636
|
|
|
244
2637
|
\`\`\`bash
|
|
245
2638
|
pnpm dev # desarrollo
|
|
2639
|
+
pnpm check:prototype # validar datos, cargas/eventos y simulador
|
|
246
2640
|
npx create-lexy view button # ver un componente antes de instalarlo
|
|
247
2641
|
npx create-lexy add button # instalarlo (local y editable)
|
|
248
2642
|
npx create-lexy doctor # divergencia con el registry
|
|
249
2643
|
\`\`\`
|
|
250
|
-
`,
|
|
251
|
-
`},
|
|
252
|
-
`),await
|
|
2644
|
+
`,fo=e=>{let o=e.architecture==="feature"?"@/shared":"@";return JSON.stringify({$schema:"https://ui.shadcn.com/schema.json",style:"new-york",rsc:!1,tsx:!0,tailwind:{config:"",css:"src/index.css",baseColor:"neutral",cssVariables:!0},aliases:{components:`${o}/components`,ui:`${o}/components/base`,utils:`${o}/lib/utils/cn`,lib:`${o}/lib`,hooks:`${o}/hooks`},registries:{"@lexy":`${F}@latest/r/{name}.json`}},null,2)+`
|
|
2645
|
+
`},yo=e=>{let t=e.architecture==="feature";return{version:"2.0.0",generatedAt:new Date().toISOString(),architecture:e.architecture,world:e.world,registry:{package:"@lexydesign/registry",cdn:F},componentImportPattern:t?"@/shared/components/base/{Component}":"@/components/base/{Component}",paths:{components:t?"src/shared/components/base":"src/components/base",hooks:t?"src/shared/hooks":"src/hooks",services:t?"src/shared/services":"src/services",lib:t?"src/shared/lib":"src/lib",views:t?"src/features":"src/views"},prototype:{dataContractVersion:"1",dataContractPath:re,eventEngineVersion:"1",eventEngineContractPath:ie,eventEnginePath:se,simulatorVersion:"1",simulatorProfilePath:ce,simulatorScenariosPath:le,simulatorPath:de,defaultScenarioId:"base"},installed:{}}},qe=async(e,t)=>{let o=["package.json","src","index.html",".lexy"],r=[];for(let l of o)try{await R.access(T.join(t,l)),r.push(l)}catch{}if(r.length>0)throw new Error(`El directorio actual ya contiene archivos de un proyecto (${r.join(", ")}). Ejecuta el comando dentro de una carpeta vac\xEDa.`);for(let l of Zt[e.architecture])await R.mkdir(T.join(t,l),{recursive:!0});let a=(l,y)=>R.writeFile(T.join(t,l),y);await a("package.json",Xt(e)),await a("vite.config.ts",Yt),await a("tsconfig.json",no),await a("tsconfig.app.json",ao),await a("tsconfig.node.json",ro),await a("eslint.config.js",Qt),await a(".prettierrc",eo),await a(".prettierignore",to),await R.mkdir(T.join(t,".github/workflows"),{recursive:!0}),await a(".github/workflows/ci.yml",oo),await a("index.html",io(e)),await a(".gitignore",mo),await a("README.md",uo(e)),await a("src/index.css",so),await a("src/main.tsx",co(e)),await De(t,e.projectName),await _e(t),await He(t);let i=e.architecture==="feature"?"src/app":"src/views";await a(`${i}/App.tsx`,lo(e));let n=yo(e);await a(".lexy",JSON.stringify(n,null,2)+`
|
|
2646
|
+
`),await a("components.json",fo(e)),await R.copyFile(Pe,T.join(t,"src/lexy-theme.css")),await R.mkdir(T.join(t,"public/fonts"),{recursive:!0}),await R.cp(Ce,T.join(t,"public/fonts"),{recursive:!0});let p=T.join(t,n.paths.lib,"utils");await R.mkdir(p,{recursive:!0}),await R.writeFile(T.join(p,"cn.ts"),po)};var xo=["feature","layer"],bo=["cliente","crm","mixto"],Io=e=>new Promise((t,o)=>{ho("pnpm",["install"],{cwd:e,stdio:"ignore",shell:!0}).on("close",a=>a===0?t():o(new Error(`pnpm install fall\xF3 con c\xF3digo ${a}`)))}),Ue=async(e,t)=>{let o;!!(e&&t.type)?(xo.includes(t.type)||(console.error(E.red(`Error: la arquitectura "${t.type}" no es v\xE1lida. Usa "feature" o "layer".`)),process.exit(1)),t.world&&!bo.includes(t.world)&&(console.error(E.red(`Error: el mundo "${t.world}" no es v\xE1lido. Usa "cliente", "crm" o "mixto".`)),process.exit(1)),t.world||console.warn(E.yellow('Aviso: sin --world; se asume "mixto". ')+E.dim("Pasa --world cliente|crm para fijar densidad y voz desde el inicio.")),o={projectName:e,architecture:t.type,world:t.world??"mixto"},console.log(E.blue("Modo automatizado detectado. Saltando interfaz interactiva..."))):o=await ze(e),go(`Nombre: ${o.projectName}
|
|
253
2647
|
Arquitectura: ${o.architecture}
|
|
254
|
-
Mundo: ${o.world}`,"Configuraci\xF3n de Proyecto");let
|
|
255
|
-
|
|
256
|
-
`)+
|
|
257
|
-
`)+
|
|
258
|
-
`)+
|
|
259
|
-
`),
|
|
260
|
-
`),
|
|
261
|
-
`)};var
|
|
262
|
-
${
|
|
263
|
-
`));let p=!1;for(let
|
|
2648
|
+
Mundo: ${o.world}`,"Configuraci\xF3n de Proyecto");let a=So();a.start("Construyendo tu proyecto...");let i=process.cwd();try{await qe(o,i)}catch(n){throw a.stop(E.red("No se pudo crear el proyecto.")),n}t.install!==!1&&(a.message("Instalando dependencias base..."),await Io(i)),a.stop("\xA1Proyecto construido con \xE9xito!"),vo(E.green(`Todo listo en el directorio actual
|
|
2649
|
+
|
|
2650
|
+
`)+E.dim(`Siguientes pasos:
|
|
2651
|
+
`)+E.cyan(` pnpm dev
|
|
2652
|
+
`)+E.cyan(" npx create-lexy add button"))};import Eo from"fs/promises";import ko from"path";import z from"picocolors";import pe from"picocolors";var H=(e,t)=>{let o=e.split(`
|
|
2653
|
+
`),r=t.split(`
|
|
2654
|
+
`),a=o.length,i=r.length,n=Array.from({length:a+1},()=>new Array(i+1).fill(0));for(let u=a-1;u>=0;u--)for(let d=i-1;d>=0;d--)n[u][d]=o[u]===r[d]?n[u+1][d+1]+1:Math.max(n[u+1][d],n[u][d+1]);let p=[],l=0,y=0;for(;l<a&&y<i;)o[l]===r[y]?(p.push({type:"same",text:o[l]}),l++,y++):n[l+1][y]>=n[l][y+1]?p.push({type:"del",text:o[l++]}):p.push({type:"add",text:r[y++]});for(;l<a;)p.push({type:"del",text:o[l++]});for(;y<i;)p.push({type:"add",text:r[y++]});return p},X=e=>e.some(t=>t.type!=="same"),Be=(e,t=3)=>{if(!X(e))return null;let o=new Array(e.length).fill(!1);e.forEach((i,n)=>{if(i.type!=="same")for(let p=Math.max(0,n-t);p<=Math.min(e.length-1,n+t);p++)o[p]=!0});let r=[],a=!1;for(let i=0;i<e.length;i++){if(!o[i]){a||(r.push(pe.dim(" \u22EF")),a=!0);continue}a=!1;let n=e[i];n.type==="same"?r.push(` ${n.text}`):n.type==="del"?r.push(pe.red(`- ${n.text}`)):r.push(pe.green(`+ ${n.text}`))}return r.join(`
|
|
2655
|
+
`)};var Je=async e=>{let t=process.cwd(),o=await L(t),r=e.toLowerCase(),{item:a,version:i}=await x(r),n=o.installed[r];console.log(z.bold(`
|
|
2656
|
+
${a.title??r}`)+z.dim(` \xB7 instalado con registry@${n??"?"} \xB7 vigente registry@${i}
|
|
2657
|
+
`));let p=!1;for(let l of C(a,o)){let y=await Eo.readFile(ko.join(t,l.relPath),"utf-8").catch(()=>null);if(y===null){console.log(z.red(`\u2717 ${l.relPath} no existe localmente`)+z.dim(` \u2014 cr\xE9alo con \`create-lexy add ${r}\``)),p=!0;continue}let u=Be(H(y,l.content));u===null?console.log(z.green(`\u2713 ${l.relPath}`)+z.dim(" sin diferencias")):(p=!0,console.log(z.yellow(`\xB1 ${l.relPath}`)+z.dim(" (\u2212 local \xB7 + registry)")),console.log(u))}p&&(process.exitCode=1)};import A from"fs/promises";import f from"path";import k from"picocolors";var Ge=async e=>{let t=process.cwd(),o=0,r=s=>console.log(k.green(" \u2713 ")+s),a=s=>console.log(k.yellow(" \u2022 ")+s),i=(s,c)=>{o=1,console.log(k.red(" \u2717 ")+s),c&&console.log(k.dim(` \u2192 ${c}`))};console.log(k.bold(`
|
|
264
2658
|
Diagn\xF3stico del proyecto Lexy
|
|
265
|
-
`));let
|
|
266
|
-
Sin componentes instalados a\xFAn (create-lexy add <nombre>).`));else{let
|
|
267
|
-
Componentes instalados (registry vigente: ${
|
|
268
|
-
`));for(let[
|
|
2659
|
+
`));let n;try{n=await L(t),r(`.lexy v\xE1lido (arquitectura ${n.architecture}${n.world?`, mundo ${n.world}`:""})`)}catch(s){return i(s.message),1}await A.access(f.join(t,"src/lexy-theme.css")).then(()=>!0,()=>!1)?r("theme presente (src/lexy-theme.css)"):i("falta src/lexy-theme.css","re-cr\xE9alo desde un proyecto nuevo o copia el theme del registry");let l=f.join(n.paths.lib,"utils/cn.ts");await A.access(f.join(t,l)).then(()=>!0,()=>!1)?r(`helper cn presente (${l})`):i(`falta el helper cn (${l})`);let u=await A.readFile(f.join(t,"package.json"),"utf-8").then(s=>JSON.parse(s)).catch(()=>null),d=async(s,c)=>{await A.access(f.join(t,s)).then(()=>!0,()=>!1)?r(`${c} presente (${s})`):i(`falta ${c} (${s})`)},P=(s,c)=>{if(!u){i("package.json no se pudo leer para validar scripts del prototipo");return}let g=u.scripts?.[s];if(!g){i(`falta el script ${s} en package.json`,c?`agrega \`${c}\``:void 0);return}if(c&&!g.includes(c)){i(`el script ${s} no ejecuta ${c}`);return}r(`script ${s} configurado`)};if(!n.prototype)a("prototipo funcional no habilitado \u2014 proyecto anterior a las specs de datos/eventos/simulador");else{let s=n.prototype.dataContractPath;if(!s)a("contrato de datos no configurado \u2014 proyecto anterior a la Spec 1");else{await d(s,"contrato de datos");let c=f.join(f.dirname(s),"data-contract-schema.ts");await d(c,"schema del contrato"),await d("scripts/check-data-contract.ts","validador del contrato"),P("check:data-contract","tsx scripts/check-data-contract.ts")}n.prototype.eventEngineVersion?(n.prototype.eventEngineContractPath?(await d(n.prototype.eventEngineContractPath,"contrato del motor de eventos"),await d(f.join(f.dirname(n.prototype.eventEngineContractPath),"event-engine-contract-schema.ts"),"schema del motor de eventos"),await d(f.join(f.dirname(n.prototype.eventEngineContractPath),"payload-schema.ts"),"schemas runtime del motor de eventos")):i("eventEngineContractPath no est\xE1 configurado en .lexy"),n.prototype.eventEnginePath?await d(n.prototype.eventEnginePath,"engine de eventos"):i("eventEnginePath no est\xE1 configurado en .lexy"),await d("scripts/check-event-engine.ts","validador del motor de eventos"),P("check:event-engine","tsx scripts/check-event-engine.ts")):a("motor de eventos no habilitado \u2014 proyecto anterior a la Spec 2"),n.prototype.simulatorVersion?(n.prototype.simulatorProfilePath?(await d(n.prototype.simulatorProfilePath,"perfil del simulador"),await d(f.join(f.dirname(n.prototype.simulatorProfilePath),"simulator-contract-schema.ts"),"schema del simulador"),await d(f.join(f.dirname(n.prototype.simulatorProfilePath),"projection-resolvers.ts"),"resolvers de proyecci\xF3n del simulador"),await d(f.join(f.dirname(n.prototype.simulatorProfilePath),"data-load-handlers.ts"),"handlers de carga del simulador"),await d(f.join(f.dirname(n.prototype.simulatorProfilePath),"event-handlers.ts"),"handlers de evento del simulador"),await d(f.join(f.dirname(n.prototype.simulatorProfilePath),"mock-store.ts"),"store persistente del simulador"),await d(f.join(f.dirname(n.prototype.simulatorProfilePath),"mock-backend-executor.ts"),"executor mock del simulador")):i("simulatorProfilePath no est\xE1 configurado en .lexy"),n.prototype.simulatorScenariosPath?await d(n.prototype.simulatorScenariosPath,"escenarios del simulador"):i("simulatorScenariosPath no est\xE1 configurado en .lexy"),n.prototype.simulatorPath?await d(n.prototype.simulatorPath,"ensamblaje del simulador"):i("simulatorPath no est\xE1 configurado en .lexy"),await d("scripts/check-simulator.ts","validador del simulador"),P("check:simulator","tsx scripts/check-simulator.ts"),P("check:prototype","check:simulator")):a("simulador persistente no habilitado \u2014 proyecto anterior a la Spec 3")}let V=f.join(t,"ai/lexy-ai-manifest.json"),m=await A.readFile(V,"utf-8").catch(()=>null);if(m===null)console.log(k.dim(" \xB7 capa de IA no instalada (npx @lexydesign/ai install) \u2014 opcional"));else try{let s=JSON.parse(m),c=s.files??[],g=[];for(let I of c)await A.access(f.join(t,I)).then(()=>!0,()=>!1)||g.push(I);g.length>0?i(`capa de IA ${s.aiVersion??"?"} incompleta \u2014 faltan ${g.length} archivo(s) (${g.slice(0,3).join(", ")}${g.length>3?", \u2026":""})`,"npx @lexydesign/ai install"):r(`capa de IA ${s.aiVersion??"?"} completa (${c.length} archivos)`)}catch{i("ai/lexy-ai-manifest.json no es JSON v\xE1lido","regenera con `npx @lexydesign/ai install`")}let h=Object.entries(n.installed);if(h.length===0)console.log(k.dim(`
|
|
2660
|
+
Sin componentes instalados a\xFAn (create-lexy add <nombre>).`));else{let s=await G();console.log(k.bold(`
|
|
2661
|
+
Componentes instalados (registry vigente: ${s})
|
|
2662
|
+
`));for(let[c,g]of h){let I;try{I=await x(c)}catch(D){i(`${c}: ${D.message}`);continue}let q=C(I.item,n),v=[],O=!1;for(let D of q){let U=await A.readFile(f.join(t,D.relPath),"utf-8").catch(()=>null);U===null?v.push(D.relPath):X(H(U,D.content))&&(O=!0)}if(v.length>0){i(`${c}: archivos faltantes (${v.join(", ")})`,`create-lexy add ${c}`);continue}if(!O){r(`${c} al d\xEDa (${I.version})`);continue}let ee=!0;if(g&&g!==I.version)try{let D=await x(c,g),U=C(D.item,n);ee=!1;for(let ve of U){let Se=await A.readFile(f.join(t,ve.relPath),"utf-8").catch(()=>null);(Se===null||X(H(Se,ve.content)))&&(ee=!0)}}catch{}ee?e.strict?i(`${c}: editado localmente (difiere del registry vigente)`,`create-lexy diff ${c}`):a(`${c}: editado localmente \u2014 divergencia deliberada (m\xEDrala con \`create-lexy diff ${c}\`)`):i(`${c}: desactualizado (instalado con ${g}, vigente ${I.version})`,`create-lexy diff ${c} \xB7 create-lexy add ${c} --overwrite`)}}return console.log(o===0?k.green(`
|
|
269
2663
|
Todo en orden.
|
|
270
|
-
`):
|
|
2664
|
+
`):k.red(`
|
|
271
2665
|
Hay problemas que atender (exit 1).
|
|
272
|
-
`)),o};import
|
|
2666
|
+
`)),o};import me from"fs/promises";import Ze from"path";import{note as wo,outro as Po}from"@clack/prompts";import ue from"picocolors";var Y="LEXY-KICKOFF.md",Co=`# Kickoff de proyecto Lexy \u2014 protocolo de entrevista
|
|
273
2667
|
|
|
274
2668
|
> **Para el agente (Claude Code):** este archivo es tu guion. La persona que
|
|
275
2669
|
> tienes enfrente es **dise\xF1adora, no programadora**: no le preguntes por
|
|
@@ -291,8 +2685,20 @@ hazlas de a poco, repregunta y resume lo que entendiste. Cubre:
|
|
|
291
2685
|
dashboard, listados, vistas de detalle)?
|
|
292
2686
|
- **Pantallas y flujos clave** \u2014 \xBFqu\xE9 vistas importan y qu\xE9 hace la persona en
|
|
293
2687
|
cada una? \xBFC\xF3mo se mueve entre ellas?
|
|
294
|
-
- **Datos
|
|
295
|
-
|
|
2688
|
+
- **Datos de la experiencia** \u2014 \xBFqu\xE9 objetos principales existen? \xBFQu\xE9 necesita
|
|
2689
|
+
ver o modificar la persona? \xBFQu\xE9 relaciones necesita entender? \xBFQu\xE9 estados
|
|
2690
|
+
cambian lo que aparece en pantalla? \xBFQu\xE9 supuestos todav\xEDa debe validar TI?
|
|
2691
|
+
- **Cargas de datos** \u2014 \xBFqu\xE9 informaci\xF3n debe llegar desde backend para cada
|
|
2692
|
+
pantalla? \xBFQu\xE9 significa que una carga venga vac\xEDa? \xBFQu\xE9 errores o latencias
|
|
2693
|
+
vale la pena poder probar?
|
|
2694
|
+
- **Eventos publicados** \u2014 \xBFqu\xE9 escrituras hace la interfaz? \xBFQu\xE9 hecho ocurri\xF3
|
|
2695
|
+
cuando se publican? \xBFQu\xE9 entidad cambia visualmente despu\xE9s de ver "Evento
|
|
2696
|
+
publicado"? \xBFQu\xE9 debe mantenerse intacto si falla?
|
|
2697
|
+
- **Datos mock y escenarios** \u2014 \xBFqu\xE9 situaciones reales debe representar el
|
|
2698
|
+
prototipo? \xBFQu\xE9 casos l\xEDmite importan? \xBFQu\xE9 datos deben verse chilenos? \xBFQu\xE9
|
|
2699
|
+
campos son sensibles aunque sean sint\xE9ticos?
|
|
2700
|
+
- **Referencias** \u2014 \xBFhay un Figma, capturas o un producto de referencia que
|
|
2701
|
+
mande sobre composiciones gen\xE9ricas?
|
|
296
2702
|
|
|
297
2703
|
## 2. Traducir las respuestas a flags (no se lo muestres a la persona)
|
|
298
2704
|
|
|
@@ -318,22 +2724,43 @@ npx @lexydesign/ai install
|
|
|
318
2724
|
|
|
319
2725
|
## 4. Volcar la entrevista en el brief vivo
|
|
320
2726
|
|
|
321
|
-
Abre \`ai/PROJECT-CONTEXT.md\` (lo cre\xF3 \`@lexydesign/ai install\`) y
|
|
322
|
-
con lo que recopilaste: **qu\xE9 se construye**, **para qui\xE9n**,
|
|
323
|
-
flujos clave**,
|
|
324
|
-
|
|
2727
|
+
1. Abre \`ai/PROJECT-CONTEXT.md\` (lo cre\xF3 \`@lexydesign/ai install\`) y
|
|
2728
|
+
compl\xE9talo con lo que recopilaste: **qu\xE9 se construye**, **para qui\xE9n**,
|
|
2729
|
+
**pantallas y flujos clave**, un resumen de **datos principales**,
|
|
2730
|
+
**referencias** y **decisiones/restricciones** ya tomadas.
|
|
2731
|
+
2. Traduce los objetos, datos, relaciones, estados y supuestos conversados a
|
|
2732
|
+
\`src/prototype/data-contract/prototype-data-contract.ts\`. No inventes
|
|
2733
|
+
backend: lo que nazca desde usabilidad queda como
|
|
2734
|
+
\`origin: "generatedByUsability"\` y
|
|
2735
|
+
\`technicalValidation.status: "pendingTi"\`.
|
|
2736
|
+
3. Declara las comunicaciones externas en
|
|
2737
|
+
\`src/prototype/event-engine/prototype-event-engine-contract.ts\`: lecturas
|
|
2738
|
+
remotas como **cargas de datos** y escrituras como **eventos publicados**.
|
|
2739
|
+
No declares interacciones locales.
|
|
2740
|
+
4. Completa \`src/prototype/simulator/\` con perfil, escenarios, resolvers y
|
|
2741
|
+
handlers m\xEDnimos. Usa datos sint\xE9ticos, deterministas y formato chileno.
|
|
2742
|
+
5. Ejecuta \`pnpm check:prototype\` y corrige cualquier inconsistencia antes de
|
|
2743
|
+
empezar a implementar pantallas.
|
|
2744
|
+
|
|
2745
|
+
\`PROJECT-CONTEXT.md\` es el resumen humano. El contrato TypeScript es la fuente
|
|
2746
|
+
estructurada de datos para las pr\xF3ximas sesiones. El motor explica qu\xE9 se carga
|
|
2747
|
+
o publica y el simulador entrega datos mock persistentes sin backend real.
|
|
325
2748
|
|
|
326
2749
|
## 5. Cerrar
|
|
327
2750
|
|
|
328
|
-
Borra este archivo (\`${
|
|
2751
|
+
Borra este archivo (\`${Y}\`): su trabajo termin\xF3. Conf\xEDrmale a la
|
|
329
2752
|
persona que su proyecto est\xE1 listo y prop\xF3n el primer paso concreto
|
|
330
2753
|
(p. ej. "\xBFDise\xF1amos la pantalla de inicio?").
|
|
331
|
-
`,
|
|
2754
|
+
`,We=async e=>{let t=process.cwd();for(let a of["package.json",".lexy"])if(await me.access(Ze.join(t,a)).then(()=>!0,()=>!1))throw new Error(`El directorio actual ya contiene un proyecto (${a}). \`kickoff\` es para empezar de cero en una carpeta vac\xEDa.`);let o=Ze.join(t,Y);if(await me.access(o).then(()=>!0,()=>!1)&&!e.force)throw new Error(`${Y} ya existe. Usa --force para regenerarlo.`);await me.writeFile(o,Co),wo(`Se cre\xF3 ${ue.cyan(Y)} en esta carpeta.
|
|
332
2755
|
|
|
333
|
-
Abre la carpeta con Claude Code y di: `+
|
|
334
|
-
El agente te entrevistar\xE1 y montar\xE1 el proyecto por ti.`,"Kickoff listo"),
|
|
2756
|
+
Abre la carpeta con Claude Code y di: `+ue.bold("\xABay\xFAdame a arrancar mi proyecto Lexy\xBB")+`.
|
|
2757
|
+
El agente te entrevistar\xE1 y montar\xE1 el proyecto por ti.`,"Kickoff listo"),Po(ue.green("Sin flags, sin prompts t\xE9cnicos \u2014 solo conversa con el agente."))};import fe from"fs/promises";import Q from"path";import w from"picocolors";var he="@lexydesign/kit",Xe=`https://cdn.jsdelivr.net/npm/${he}`,Ye=8e3,ye=async e=>{let t=await fetch(e,{signal:AbortSignal.timeout(Ye)});if(!t.ok){let o=new Error(`HTTP ${t.status} al pedir ${e}`);throw o.status=t.status,o}return Buffer.from(await t.arrayBuffer())},jo=async()=>{try{let t=await fetch(`https://registry.npmjs.org/${he}/latest`,{signal:AbortSignal.timeout(Ye)});if(t.ok)return(await t.json()).version}catch{}return JSON.parse((await ye(`${Xe}@latest/package.json`)).toString("utf-8")).version},Qe=async e=>{let t=process.cwd(),o=Q.resolve(t,e.dir??"lexy-kit"),r=Q.relative(t,o)||".";if(await fe.access(o).then(()=>!0).catch(()=>!1)&&!e.force)throw new Error(`Ya existe ${w.bold(r)}. Usa ${w.cyan("--force")} para sobrescribir o ${w.cyan("--dir <ruta>")} para otra carpeta.`);let i=await jo();console.log(w.dim(`
|
|
2758
|
+
Trayendo ${he}@${i} \u2192 ${r}/
|
|
2759
|
+
`));let n=`${Xe}@${i}/kit`,p=JSON.parse((await ye(`${n}/manifest.json`)).toString("utf-8")),l=["index.html","kit.css","kit.js","manifest.json","README.md","AGENTS.md","llms.txt","llms-full.txt","fonts/noto-sans-latin.woff2","fonts/OFL-NotoSans.txt",...p.guides?.pautas??[],...p.components.map(u=>u.file),...p.blocks.map(u=>u.file)],y=0;for(let u of l)try{let d=await ye(`${n}/${u}`),P=Q.join(o,u);await fe.mkdir(Q.dirname(P),{recursive:!0}),await fe.writeFile(P,d),y+=1}catch(d){console.warn(w.yellow(`\u26A0 no se pudo traer ${u}`)+w.dim(` (${d.message})`))}console.log(w.green("\u2713 Kit listo")+w.dim(` \u2014 ${y} archivo(s), ${p.components.length} componente(s) + ${p.blocks.length} block(s)
|
|
2760
|
+
`)),console.log(` Abre ${w.bold(`${r}/index.html`)} en el navegador.`),console.log(w.dim(` El cat\xE1logo m\xE1quina-legible est\xE1 en ${r}/manifest.json.
|
|
2761
|
+
`))};import Ro from"fs/promises";import ge from"path";import b from"picocolors";var K=e=>console.log(b.bold(b.cyan(`
|
|
335
2762
|
\u2500\u2500 ${e} \u2500\u2500
|
|
336
|
-
`))),
|
|
337
|
-
${
|
|
338
|
-
`),console.log(
|
|
339
|
-
Error: `)+t.message),process.exit(1)}};
|
|
2763
|
+
`))),et=async(e,t)=>{if(t.list||!e){let{index:n,version:p,source:l}=await we();K(`Cat\xE1logo Lexy \xB7 @lexydesign/registry@${p}${l==="bundle"?" (bundle local)":""}`);for(let y of n.items)console.log(` ${b.bold(y.name.padEnd(20))} ${b.dim(y.description??"")}`);console.log(b.dim(`
|
|
2764
|
+
${n.items.length} componente(s). Instala con: create-lexy add <nombre>`));return}let o=e.toLowerCase();if(t.installed){let n=process.cwd(),p=await L(n),l=p.installed[o];if(!l){console.error(b.red(`"${o}" no figura como instalado en .lexy. Inst\xE1lalo con \`create-lexy add ${o}\`.`)),process.exitCode=1;return}let{item:y}=await x(o,l).catch(()=>x(o));K(`${y.title??o} \xB7 copia local (instalado con registry@${l})`);for(let u of C(y,p)){let d=await Ro.readFile(ge.join(n,u.relPath),"utf-8").catch(()=>null);K(d===null?`${u.relPath} (FALTANTE)`:u.relPath),d!==null&&console.log(d)}return}let{item:r,version:a,source:i}=await x(o);K(`${r.title??r.name} \xB7 @lexydesign/registry@${a}${i==="bundle"?" (bundle local)":""}`),r.description&&console.log(r.description+`
|
|
2765
|
+
`),console.log(b.dim("tipo: ")+r.type),console.log(b.dim("deps npm: ")+((r.dependencies??[]).join(", ")||"\u2014")),console.log(b.dim("deps registry: ")+((r.registryDependencies??[]).join(", ")||"\u2014")),console.log(b.dim("archivos: ")+r.files.map(n=>ge.posix.basename(n.path)).join(", ")),console.log(b.dim("instalar: ")+`create-lexy add ${r.name}`);for(let n of r.files)typeof n.content=="string"&&(K(ge.posix.basename(n.path)),console.log(n.content))};var Ao=tt.dirname(Mo(import.meta.url)),Oo=JSON.parse(To(tt.join(Ao,"../package.json"),"utf-8")),M=new Lo,$=e=>async()=>{try{await e()}catch(t){console.error(zo.red(`
|
|
2766
|
+
Error: `)+t.message),process.exit(1)}};M.name("create-lexy").description("CLI del Lexy Design System: crea proyectos y trae componentes a demanda.\nLos componentes viven en tu proyecto \u2014 m\xEDralos con `view`, inst\xE1lalos con `add`, ed\xEDtalos con libertad.").version(Oo.version);M.command("create",{isDefault:!0}).alias("init").description("Crea un proyecto Lexy vac\xEDo (React + Vite + TS + theme); los componentes llegan con `add`").argument("[project-name]","Nombre del proyecto").option("-t, --type <architecture>","Arquitectura (feature o layer) \u2014 activa el modo no interactivo").option("-w, --world <world>","Mundo de dise\xF1o (cliente, crm o mixto)").option("--no-install","No correr pnpm install al final").action((e,t)=>$(()=>Ue(e,t))());M.command("kickoff").description("Deja un .md de entrevista para que el agente conduzca el arranque de un dise\xF1ador (carpeta vac\xEDa)").option("--force","Regenerar el .md de kickoff si ya existe").action(e=>$(()=>We(e))());M.command("add").description("Instala componentes del registry en tu proyecto (local y editable), con sus dependencias").argument("<components...>","Nombres en el registry (ej: button card)").option("--overwrite","Sobrescribir copias locales con cambios sin preguntar").option("--no-install","No instalar dependencias npm").action((e,t)=>$(()=>Re(e,t))());M.command("view").description("Muestra c\xF3digo + doc + metadata de un componente antes de instalarlo").argument("[component]","Nombre en el registry (sin nombre: lista el cat\xE1logo)").option("--installed","Mostrar la copia local instalada en vez de la del registry").option("--list","Listar el cat\xE1logo completo").action((e,t)=>$(()=>et(e,t))());M.command("kit").description("Trae el kit HTML/low-fi (mismos componentes y blocks en HTML plano) a una carpeta").option("--dir <path>","Carpeta destino (por defecto: lexy-kit)").option("--force","Sobrescribir la carpeta si ya existe").action(e=>$(()=>Qe(e))());M.command("diff").description("Copia local vs versi\xF3n vigente del registry (exit 1 si hay diferencias)").argument("<component>","Nombre en el registry").action(e=>$(()=>Je(e))());M.command("doctor").description("Drift de todo lo instalado contra el registry (solo lectura; exit 1 con problemas)").option("--strict","Las ediciones locales tambi\xE9n hacen fallar").action(e=>$(async()=>{process.exitCode=await Ge(e)})());M.parse(process.argv);
|