137 lines
4.3 KiB
JavaScript
137 lines
4.3 KiB
JavaScript
// 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,
|
|
};
|