#!/usr/bin/env node /** * question-slots — трекинг заданных вопросов разговорной фазы как СЛОТОВ с id * (условие R7 spec 2026-06-05): механическая проверка «нет повисших вопросов» * работает только если вопрос помечается слотом (задан/отвечен), а не вольным * текстом. Чистый модуль, иммутабельные операции ($0, без LLM). */ function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; } /** Добавить заданный вопрос (новый слот answered:false). id — непустой уникальный. */ export function addQuestion(slots, { id, text } = {}) { const list = Array.isArray(slots) ? slots : []; if (!isNonEmptyString(id)) throw new Error('question id: непустая строка обязательна'); if (!isNonEmptyString(text)) throw new Error('question text: непустая строка обязательна'); if (list.some((s) => s.id === id)) throw new Error(`question id дублируется: ${id}`); return [...list, { id, text, answered: false, answer: null }]; } /** Пометить вопрос отвеченным (непустой ответ). Неизвестный id → ошибка. */ export function answerQuestion(slots, id, answer) { const list = Array.isArray(slots) ? slots : []; if (!list.some((s) => s.id === id)) throw new Error(`неизвестный question id: ${id}`); if (!isNonEmptyString(answer)) throw new Error('answer: непустая строка обязательна'); return list.map((s) => (s.id === id ? { ...s, answered: true, answer } : s)); } /** Повисшие вопросы — заданные, но не отвеченные. */ export function hangingQuestions(slots) { return (Array.isArray(slots) ? slots : []).filter((s) => !s.answered); } /** Ни один заданный вопрос не повис (пустой список → true, вопросов нет). */ export function allAnswered(slots) { return hangingQuestions(slots).length === 0; }