Open questions
- {phase.open_questions.map((q, i) => (
+ {openQuestions.map((q, i) => (
- {q}
))}
diff --git a/frontend/src/viewers/artifactV2.ts b/frontend/src/viewers/artifactV2.ts
index caca0e09..6f6a62d0 100644
--- a/frontend/src/viewers/artifactV2.ts
+++ b/frontend/src/viewers/artifactV2.ts
@@ -119,3 +119,18 @@ export interface PhaseSummaryV2 {
tables?: PhaseTable[];
open_questions?: string[];
}
+
+/** Defensive compatibility for malformed v2 artifacts persisted before the gate
+ * enforced `open_questions: string[]`. New payloads stay strictly typed as strings. */
+export function phaseOpenQuestionText(value: unknown): string | null {
+ if (typeof value === "string") return value.trim() || null;
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
+
+ const record = value as Record
;
+ for (const field of ["question", "label"]) {
+ const text = record[field];
+ if (typeof text === "string" && text.trim()) return text.trim();
+ }
+ return null;
+}
diff --git a/harness/.pi/extensions/gate/__tests__/artifact_contracts.test.js b/harness/.pi/extensions/gate/__tests__/artifact_contracts.test.js
index 7c023f82..6b604f5b 100644
--- a/harness/.pi/extensions/gate/__tests__/artifact_contracts.test.js
+++ b/harness/.pi/extensions/gate/__tests__/artifact_contracts.test.js
@@ -151,6 +151,22 @@ test("validatePhaseSummaryV2: rejects a section missing title or items", () => {
assert.ok(out.errors.some((e) => /sections\[0\].*title/.test(e)));
});
+test("validatePhaseSummaryV2: rejects structured open_questions instead of crashing the UI", () => {
+ const out = validatePhaseSummaryV2({
+ schema_version: 2,
+ summary: "s",
+ open_questions: [
+ {
+ label: "pazienti_finale restituisce 0 righe",
+ question: "Verificare se i filtri sono troppo restrittivi",
+ },
+ ],
+ });
+
+ assert.equal(out.ok, false);
+ assert.ok(out.errors.some((error) => /open_questions\[0\]/.test(error)));
+});
+
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);
diff --git a/harness/.pi/extensions/gate/artifact-contracts.js b/harness/.pi/extensions/gate/artifact-contracts.js
index 116b2b93..1ca6e89a 100644
--- a/harness/.pi/extensions/gate/artifact-contracts.js
+++ b/harness/.pi/extensions/gate/artifact-contracts.js
@@ -115,6 +115,17 @@ function validatePhaseSummaryV2(data) {
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 };
}
diff --git a/harness/.pi/skills/tht-sessione/SKILL.md b/harness/.pi/skills/tht-sessione/SKILL.md
index 454a5780..1bc9625b 100644
--- a/harness/.pi/skills/tht-sessione/SKILL.md
+++ b/harness/.pi/skills/tht-sessione/SKILL.md
@@ -100,6 +100,8 @@ substantive decisions.
"columns":[{"name":"cod_paz","value_filter":""}]}],
"open_questions":[]}
```
+ `open_questions` MUST be an array of plain strings (`string[]`). Never put objects
+ such as `{label, question}` in it; express each open question as one complete string.
Legacy free-text recaps still work (no `schema_version`), but prefer v2. Note: the
F4 schema-linking recap travels in `tables` of this v2 phase payload — do NOT reuse
`kind:"schema_linking"` for a phase recap.