feat(gate): deterministic v2 review-gate payloads (cte_plan/cte_result/phase)
The tht-gate.js reviewer_confirm now builds structured v2 artifacts before the
widget so the reviewer approves gate-derived data, not raw model text:
- new pure modules gate/artifact-contracts.js (soft validators, {ok,errors},
legacy-passthrough) and gate/enrich.js (index/description enrichment,
buildCteResultV2 fusing thin model data with `tht cte info`, phase enrichment)
- cte_plan v2: validate + enrich + persist via `tht cte plan --name … --doc -`
(names derived from data.ctes[]); legacy `names` param kept as fallback
- cte_result v2: rebuild from `tht cte next`/`tht cte info` (sql + preview from
the persisted test record); null/error last_test -> actionable textResult
- phase v2: soft-validate + fill phase from meta + catalog descriptions
- prepareReviewerArguments coerces artifact.data too (GLM double-stringify);
legacy markdown strings pass through unchanged
- SKILL.md: Phase 6 cte_plan payload A + thin cte_result guidance; Discipline 6
payload C example; Discipline 7 reworded for gate-rebuilt cte_result
Legacy (non-v2) paths unchanged. TypeBox stays Type.Any() for artifact.data;
validation is soft (textResult) so models self-correct instead of looping.
All 102 gate JS tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
// 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.");
|
||||
}
|
||||
return { ok: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateCtePlanV2,
|
||||
validateCteResultThin,
|
||||
validatePhaseSummaryV2,
|
||||
};
|
||||
Reference in New Issue
Block a user