From 1de3e949900f24d5e4f96abed826c6d761da999d Mon Sep 17 00:00:00 2001 From: kami Date: Sun, 14 Jun 2026 03:13:22 +0400 Subject: [PATCH] fix(research): flatten source_dossier schema to correx's flat JsonSchema The schema declared sources as an array of objects, which correx's flat JsonSchema model can't represent (JsonSchemaProperty has no nested properties). The strict config loader rejected it at startup, silently disabling the source_dossier artifact kind. Flatten sources to an array of strings (URL-prefixed per-source notes), update the gather prompt to match, and add ResearchSchemaLoadTest to guard every shipped research schema against the strict loader. --- .../artifacts/kind/ResearchSchemaLoadTest.kt | 47 +++++++++++++++++++ docs/schemas/source_dossier.json | 20 +------- examples/workflows/prompts/research_gather.md | 4 +- 3 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 core/artifacts/src/test/kotlin/com/correx/core/artifacts/kind/ResearchSchemaLoadTest.kt diff --git a/core/artifacts/src/test/kotlin/com/correx/core/artifacts/kind/ResearchSchemaLoadTest.kt b/core/artifacts/src/test/kotlin/com/correx/core/artifacts/kind/ResearchSchemaLoadTest.kt new file mode 100644 index 00000000..d858e60d --- /dev/null +++ b/core/artifacts/src/test/kotlin/com/correx/core/artifacts/kind/ResearchSchemaLoadTest.kt @@ -0,0 +1,47 @@ +package com.correx.core.artifacts.kind + +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.readText + +/** + * The research artifact schemas (docs/schemas/research_*.json, source_dossier.json) must parse with + * the same strict deserializer the server uses to load config artifact kinds. correx's JsonSchema is + * flat — array properties take only scalar `items` — so a schema with array-of-object items fails to + * load at startup (silently disabling the kind). This guards every shipped research schema. + */ +class ResearchSchemaLoadTest { + + private fun repoFile(relative: String): Path { + var dir: Path? = Path.of("").toAbsolutePath() + while (dir != null) { + val candidate = dir.resolve(relative) + if (candidate.exists()) return candidate + dir = dir.parent + } + error("could not locate $relative") + } + + private fun load(name: String): JsonSchema = + Json.decodeFromString(JsonSchema.serializer(), repoFile("docs/schemas/$name").readText()) + + @Test + fun `research schemas parse with the strict loader`() { + for (name in listOf("research_plan.json", "source_dossier.json", "research_report.json")) { + val schema = load(name) + assertEquals("object", schema.type, "$name root must be an object") + } + } + + @Test + fun `source dossier sources is an array of strings`() { + val sources = load("source_dossier.json").properties.getValue("sources") + assertEquals("array", sources.type) + assertEquals("string", sources.items?.type, "items must be scalar — the flat JsonSchema model") + assertTrue(sources.minItems >= 1) + } +} diff --git a/docs/schemas/source_dossier.json b/docs/schemas/source_dossier.json index c5e8ee7f..f96e4d51 100644 --- a/docs/schemas/source_dossier.json +++ b/docs/schemas/source_dossier.json @@ -3,25 +3,9 @@ "properties": { "sources": { "type": "array", - "description": "one entry per fetched source — each summarized on its own (per-source synthesis). This is the compression step: raw page text must never be copied here, only what the source contributes.", + "description": "one entry per fetched source — each a self-contained note in your own words (per-source synthesis; the compression step). Start each entry with the source URL, then summarize what it contributes and which sub-question(s) it bears on. Never copy raw page text. Example: 'https://example.com/x — explains Y; bears on sub-question 2'.", "minItems": 1, - "items": { - "type": "object", - "properties": { - "url": { "type": "string", "description": "the fetched source URL (a citation handle)" }, - "title": { "type": "string" }, - "summary": { - "type": "string", - "description": "what this source contributes to the question, in your own words — not a copy of the page" - }, - "relevance": { - "type": "string", - "description": "which sub-question(s) this source bears on, and how strongly" - } - }, - "required": ["url", "summary"], - "additionalProperties": true - } + "items": { "type": "string" } } }, "required": ["sources"], diff --git a/examples/workflows/prompts/research_gather.md b/examples/workflows/prompts/research_gather.md index 8b2b8611..3efd352b 100644 --- a/examples/workflows/prompts/research_gather.md +++ b/examples/workflows/prompts/research_gather.md @@ -18,4 +18,6 @@ Steps: Never copy raw page text into a summary. Cite each source by its exact URL. Emit your result as the `source_dossier` artifact (JSON, schema provided): -- `sources`: one entry per fetched source, each with `url`, `title`, `summary`, and `relevance`. +- `sources`: an array of strings, one per fetched source. Start each entry with the source URL, + then your summary and which sub-question(s) it bears on — e.g. + `"https://example.com/x — explains Y; bears on sub-question 2"`.