// artifact-contracts.js -- PURE soft-validators for the v2 gate payloads (WS2). // // No I/O, no TypeBox: hand-rolled, permissive on extra fields. Each validator // returns { ok: boolean, errors: string[] } with actionable messages so the model // can self-correct (the gate returns them as a textResult, never a hard TypeBox // failure -- models loop when validation fails BEFORE execute()). // // A payload without `schema_version === 2` is NOT an error: it returns // { ok: true, legacy: true } and the caller proceeds on the legacy path. "use strict"; function isNonEmptyString(v) { return typeof v === "string" && v.length > 0; } function isArray(v) { return Array.isArray(v); } const VALID_CHECK_STATUS = new Set(["ok", "warn", "fail"]); // Contract A -- cte_plan v2. Validates the model-owned skeleton; catalog-derived // fields (index, table/filter descriptions) are filled later by enrich, so they // are NOT required here. function validateCtePlanV2(data) { if (!data || typeof data !== "object" || data.schema_version !== 2) { return { ok: true, legacy: true }; } const errors = []; if (!isArray(data.ctes) || data.ctes.length === 0) { errors.push("ctes deve essere un array non vuoto."); return { ok: false, errors }; } const seen = []; data.ctes.forEach((c, i) => { if (!c || typeof c !== "object") { errors.push(`ctes[${i}] deve essere un oggetto.`); return; } if (!isNonEmptyString(c.name)) { errors.push(`ctes[${i}].name mancante o vuoto.`); } if (isArray(c.depends_on)) { for (const dep of c.depends_on) { if (!seen.includes(dep)) { errors.push( `ctes[${i}].depends_on contiene '${dep}' che non รจ un CTE precedente.`, ); } } } if (isNonEmptyString(c.name)) seen.push(c.name); }); return { ok: errors.length === 0, errors }; } // Contract B -- cte_result THIN (what the model sends). The gate builds the full // payload from `tht cte info`; the model only supplies {purpose?, rationale?, note?}. // Everything is optional, so a thin v2 artifact is valid as long as it declares v2. function validateCteResultThin(data) { if (!data || typeof data !== "object" || data.schema_version !== 2) { return { ok: true, legacy: true }; } const errors = []; for (const field of ["purpose", "rationale", "note"]) { if (data[field] !== undefined && typeof data[field] !== "string") { errors.push(`${field} deve essere una stringa se presente.`); } } return { ok: errors.length === 0, errors }; } // Contract C -- phase summary v2. function validatePhaseSummaryV2(data) { if (!data || typeof data !== "object" || data.schema_version !== 2) { return { ok: true, legacy: true }; } const errors = []; if (!isNonEmptyString(data.summary)) { errors.push("summary mancante o vuoto."); } if (data.checks !== undefined) { if (!isArray(data.checks)) { errors.push("checks deve essere un array se presente."); } else { data.checks.forEach((c, i) => { if (!c || typeof c !== "object" || !isNonEmptyString(c.label)) { errors.push(`checks[${i}].label mancante o vuoto.`); } if (!c || !VALID_CHECK_STATUS.has(c.status)) { errors.push(`checks[${i}].status deve essere uno tra ok|warn|fail.`); } }); } } if (data.sections !== undefined) { if (!isArray(data.sections)) { errors.push("sections deve essere un array se presente."); } else { data.sections.forEach((s, i) => { if (!s || typeof s !== "object") { errors.push(`sections[${i}] deve essere un oggetto.`); return; } if (!isNonEmptyString(s.title)) { errors.push(`sections[${i}].title mancante o vuoto.`); } if (s.items !== undefined && !isArray(s.items)) { errors.push(`sections[${i}].items deve essere un array se presente.`); } }); } } if (data.tables !== undefined && !isArray(data.tables)) { errors.push("tables deve essere un array se presente."); } if (data.open_questions !== undefined) { if (!isArray(data.open_questions)) { errors.push("open_questions deve essere un array di stringhe se presente."); } else { data.open_questions.forEach((question, i) => { if (typeof question !== "string") { errors.push(`open_questions[${i}] deve essere una stringa.`); } }); } } return { ok: errors.length === 0, errors }; } module.exports = { validateCtePlanV2, validateCteResultThin, validatePhaseSummaryV2, };