Files
ThothII/harness/.pi/extensions/gate/__tests__/artifact_contracts.test.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

158 lines
6.0 KiB
JavaScript

// Soft-validators for the v2 artifact payloads (contracts.md A/B/C). Pure, no I/O.
// A payload without schema_version===2 is NOT an error -- it's legacy, and the caller
// falls back to the current (pre-v2) rendering path.
const test = require("node:test");
const assert = require("node:assert");
const {
validateCtePlanV2,
validateCteResultThin,
validatePhaseSummaryV2,
} = require("../artifact-contracts.js");
// --- validateCtePlanV2 (payload A) ------------------------------------------------
test("validateCtePlanV2: legacy (no schema_version) is ok:true, legacy:true", () => {
const out = validateCtePlanV2({ some: "legacy shape" });
assert.deepEqual(out, { ok: true, legacy: true });
});
test("validateCtePlanV2: legacy when data is a plain string (markdown)", () => {
const out = validateCtePlanV2("### Piano CTE\n1. foo");
assert.deepEqual(out, { ok: true, legacy: true });
});
test("validateCtePlanV2: valid v2 payload", () => {
const out = validateCtePlanV2({
schema_version: 2,
question: "domanda",
strategy: "razionale",
ctes: [
{ name: "base_pazienti", purpose: "p", rationale: "r", depends_on: [], tables: [], keys: [], filters: [], output_columns: [] },
{ name: "eventi", purpose: "p2", rationale: "r2", depends_on: ["base_pazienti"], tables: [], keys: [], filters: [], output_columns: [] },
],
});
assert.equal(out.ok, true);
assert.ok(!out.errors || out.errors.length === 0);
});
test("validateCtePlanV2: rejects missing ctes array", () => {
const out = validateCtePlanV2({ schema_version: 2, question: "q", strategy: "s" });
assert.equal(out.ok, false);
assert.ok(out.errors.some((e) => /ctes/.test(e)));
});
test("validateCtePlanV2: rejects a CTE missing name", () => {
const out = validateCtePlanV2({
schema_version: 2, question: "q", strategy: "s",
ctes: [{ purpose: "p" }],
});
assert.equal(out.ok, false);
assert.ok(out.errors.some((e) => /ctes\[0\].*name/.test(e)));
});
test("validateCtePlanV2: rejects depends_on referencing an unknown/forward CTE", () => {
const out = validateCtePlanV2({
schema_version: 2, question: "q", strategy: "s",
ctes: [
{ name: "a", depends_on: ["b"] }, // b does not precede a
{ name: "b", depends_on: [] },
],
});
assert.equal(out.ok, false);
assert.ok(out.errors.some((e) => /depends_on/.test(e) && /'b'/.test(e)));
});
test("validateCtePlanV2: depends_on referencing an earlier CTE is fine", () => {
const out = validateCtePlanV2({
schema_version: 2, question: "q", strategy: "s",
ctes: [
{ name: "a", depends_on: [] },
{ name: "b", depends_on: ["a"] },
],
});
assert.equal(out.ok, true);
});
// --- validateCteResultThin (payload B, model side is THIN) ------------------------
test("validateCteResultThin: legacy (string/markdown) is ok:true, legacy:true", () => {
assert.deepEqual(validateCteResultThin("**CTE risultato**: ok"), { ok: true, legacy: true });
assert.deepEqual(validateCteResultThin({ some: "legacy" }), { ok: true, legacy: true });
});
test("validateCteResultThin: valid thin v2 payload (all fields optional)", () => {
const out = validateCteResultThin({ schema_version: 2 });
assert.equal(out.ok, true);
});
test("validateCteResultThin: valid thin v2 payload with purpose/rationale/note", () => {
const out = validateCteResultThin({
schema_version: 2, purpose: "seleziona pazienti", rationale: "base per la catena", note: "ok",
});
assert.equal(out.ok, true);
});
test("validateCteResultThin: rejects non-string purpose/rationale/note", () => {
const out = validateCteResultThin({ schema_version: 2, purpose: 42 });
assert.equal(out.ok, false);
assert.ok(out.errors.some((e) => /purpose/.test(e)));
});
test("validateCteResultThin: permissive on extra fields (e.g. a model that also sends sql) -- guidance against this lives in SKILL.md, not the validator", () => {
const out = validateCteResultThin({ schema_version: 2, sql: "WITH a AS (SELECT 1)" });
assert.equal(out.ok, true);
});
// --- validatePhaseSummaryV2 (payload C) -------------------------------------------
test("validatePhaseSummaryV2: legacy (no schema_version) is ok:true, legacy:true", () => {
assert.deepEqual(validatePhaseSummaryV2({ recap: "legacy text" }), { ok: true, legacy: true });
assert.deepEqual(validatePhaseSummaryV2("legacy markdown"), { ok: true, legacy: true });
});
test("validatePhaseSummaryV2: valid minimal v2 payload", () => {
const out = validatePhaseSummaryV2({ schema_version: 2, summary: "riepilogo" });
assert.equal(out.ok, true);
});
test("validatePhaseSummaryV2: valid full v2 payload", () => {
const out = validatePhaseSummaryV2({
schema_version: 2,
summary: "riepilogo",
checks: [{ label: "schema valido", status: "ok" }],
sections: [{ title: "Criteri", items: [{ label: "flag attivo", table: "t", column: "c", value: "TRUE", kind: "filter" }] }],
tables: [{ name: "t", role: "promoted", columns: [{ name: "c" }] }],
open_questions: [],
});
assert.equal(out.ok, true);
});
test("validatePhaseSummaryV2: rejects missing summary", () => {
const out = validatePhaseSummaryV2({ schema_version: 2 });
assert.equal(out.ok, false);
assert.ok(out.errors.some((e) => /summary/.test(e)));
});
test("validatePhaseSummaryV2: rejects a check with an invalid status", () => {
const out = validatePhaseSummaryV2({
schema_version: 2, summary: "s",
checks: [{ label: "x", status: "maybe" }],
});
assert.equal(out.ok, false);
assert.ok(out.errors.some((e) => /checks\[0\].*status/.test(e)));
});
test("validatePhaseSummaryV2: rejects a section missing title or items", () => {
const out = validatePhaseSummaryV2({
schema_version: 2, summary: "s",
sections: [{ items: [] }],
});
assert.equal(out.ok, false);
assert.ok(out.errors.some((e) => /sections\[0\].*title/.test(e)));
});
test("validatePhaseSummaryV2: permissive on extra unknown fields", () => {
const out = validatePhaseSummaryV2({ schema_version: 2, summary: "s", extra_field_from_model: "whatever" });
assert.equal(out.ok, true);
});