Files
ThothII/harness/.pi/extensions/gate/artifact-contracts.js
T
marcopanandClaude Fable 5 3ad93cd02f 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>
2026-07-07 00:42:48 +02:00

126 lines
4.0 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.");
}
return { ok: errors.length === 0, errors };
}
module.exports = {
validateCtePlanV2,
validateCteResultThin,
validatePhaseSummaryV2,
};