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"`.