refactor(engine): mover limpieza de instancias y validación required al motor [4E]
applyAnswer y isRequiredAndUnanswered pasan de wizard-form.tsx al motor como funciones puras — eran lógica de flujo en el presentador (viola ADR-005). Limpieza de respuestas huérfanas al remover un vínculo y validación required por instancia, ahora testeables. 20/20 tests. Verificado end-to-end con navegador. 4E sin construcción pendiente: el mini-wizard es propiedad emergente del motor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -176,6 +176,52 @@ export function setAnswer(
|
||||
}
|
||||
}
|
||||
|
||||
function isAnswerEmpty(v: AnswerValue | undefined): boolean {
|
||||
return v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica una respuesta y limpia las respuestas de pantallas que dejaron de
|
||||
* ser visibles como CONSECUENCIA de ese cambio (ej.: sacar un vínculo de 4.1
|
||||
* borra las respuestas de 4.2/4.3 de esa instancia puntual; las de las demás
|
||||
* instancias quedan intactas porque conservan su propio answerKey). Esto es
|
||||
* consecuencia de flujo (ADR-005), no presentación — por eso vive en el
|
||||
* motor y no en el componente.
|
||||
*/
|
||||
export function applyAnswer(
|
||||
state: WizardState,
|
||||
schema: FormSchema,
|
||||
code: string,
|
||||
value: AnswerValue,
|
||||
instanceId: string = DEFAULT_INSTANCE
|
||||
): WizardState {
|
||||
const updated = setAnswer(state, code, value, instanceId)
|
||||
const before = resolveScreens(schema, state.answers)
|
||||
const after = resolveScreens(schema, updated.answers)
|
||||
const afterIds = new Set(after.map((s) => s.id))
|
||||
const disappeared = before.filter((s) => !afterIds.has(s.id) && s.question)
|
||||
|
||||
if (disappeared.length === 0) return updated
|
||||
|
||||
const clearedAnswers = { ...updated.answers }
|
||||
for (const s of disappeared) {
|
||||
delete clearedAnswers[answerKey(s.question!.code, s.instanceId ?? undefined)]
|
||||
}
|
||||
return { ...updated, answers: clearedAnswers }
|
||||
}
|
||||
|
||||
/**
|
||||
* true si la pantalla es una pregunta `required` y todavía no tiene
|
||||
* respuesta — incluye preguntas de instancia (cada instancia se evalúa por
|
||||
* su propio answerKey, independiente de las demás). El seed v3 actual no
|
||||
* marca ninguna pregunta de instancia como required, pero el camino queda
|
||||
* cubierto para cuando exista una.
|
||||
*/
|
||||
export function isRequiredAndUnanswered(screen: Screen, answers: FormAnswers): boolean {
|
||||
if (!screen.question?.required) return false
|
||||
return isAnswerEmpty(answers[answerKey(screen.question.code, screen.instanceId ?? undefined)])
|
||||
}
|
||||
|
||||
/**
|
||||
* Avanza una pantalla. Recalcula la lista de pantallas visibles desde las
|
||||
* respuestas actuales y busca la pantalla actual por ID (nunca por índice
|
||||
|
||||
Reference in New Issue
Block a user