diff --git a/talent/conversation.md b/talent/conversation.md index 5530023f8..218d1fa12 100644 --- a/talent/conversation.md +++ b/talent/conversation.md @@ -7,6 +7,7 @@ "activities": ["meeting", "call", "messaging", "email"], "priority": 20, "output": "json", + "schema": "story.schema.json", "hook": {"post": "story"}, "load": { "transcripts": true, diff --git a/talent/event.md b/talent/event.md index f76d51af0..52bae355b 100644 --- a/talent/event.md +++ b/talent/event.md @@ -7,6 +7,7 @@ "activities": ["appointment", "event", "travel", "errand", "celebration", "deadline", "reminder"], "priority": 20, "output": "json", + "schema": "story.schema.json", "hook": {"post": "story"}, "load": { "transcripts": true, diff --git a/talent/story.schema.json b/talent/story.schema.json new file mode 100644 index 000000000..8b377dae8 --- /dev/null +++ b/talent/story.schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": ["body","topics","confidence","commitments","closures","decisions"], + "properties": { + "body": {"type": "string", "minLength": 1}, + "topics": { + "type": "array", + "items": {"type": "string"}, + "maxItems": 10 + }, + "confidence": {"type": "number", "minimum": 0, "maximum": 1}, + "commitments": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["owner","action","counterparty","when","context"], + "properties": { + "owner": {"type": "string", "minLength": 1}, + "action": {"type": "string", "minLength": 1}, + "counterparty": {"type": "string", "minLength": 1}, + "when": {"type": "string", "minLength": 1}, + "context": {"type": "string", "minLength": 1} + } + } + }, + "closures": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["owner","action","counterparty","resolution","context"], + "properties": { + "owner": {"type": "string", "minLength": 1}, + "action": {"type": "string", "minLength": 1}, + "counterparty": {"type": "string", "minLength": 1}, + "resolution": { + "type": "string", + "enum": ["sent","done","signed","dropped","deferred"] + }, + "context": {"type": "string", "minLength": 1} + } + } + }, + "decisions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["owner","action","context"], + "properties": { + "owner": {"type": "string", "minLength": 1}, + "action": {"type": "string", "minLength": 1}, + "context": {"type": "string", "minLength": 1} + } + } + } + } +} diff --git a/talent/work.md b/talent/work.md index 79e7f911a..c28e9ec3d 100644 --- a/talent/work.md +++ b/talent/work.md @@ -7,6 +7,7 @@ "activities": ["coding", "browsing", "reading"], "priority": 20, "output": "json", + "schema": "story.schema.json", "hook": {"post": "story"}, "load": { "transcripts": true, diff --git a/tests/baselines/api/stats/stats.json b/tests/baselines/api/stats/stats.json index 24edfcfa6..23cdd1d3a 100644 --- a/tests/baselines/api/stats/stats.json +++ b/tests/baselines/api/stats/stats.json @@ -22,6 +22,7 @@ "path": "/talent/conversation.md", "priority": 20, "schedule": "activity", + "schema": "story.schema.json", "source": "system", "title": "Conversation Story", "type": "generate" @@ -144,6 +145,7 @@ "path": "/talent/event.md", "priority": 20, "schedule": "activity", + "schema": "story.schema.json", "source": "system", "title": "Event Story", "type": "generate" @@ -301,6 +303,7 @@ "path": "/talent/work.md", "priority": 20, "schedule": "activity", + "schema": "story.schema.json", "source": "system", "title": "Work Story", "type": "generate" diff --git a/tests/test_story_schema.py b/tests/test_story_schema.py new file mode 100644 index 000000000..8f7fd1284 --- /dev/null +++ b/tests/test_story_schema.py @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +import json +from pathlib import Path + +from jsonschema import Draft202012Validator + +from talent.story import ALLOWED_RESOLUTIONS +from tests.test_story_hook import _valid_result +from think.talent import get_talent + +STORY_SCHEMA_PATH = Path(__file__).resolve().parents[1] / "talent" / "story.schema.json" + + +def _load_story_schema() -> dict: + return json.loads(STORY_SCHEMA_PATH.read_text(encoding="utf-8")) + + +def test_story_schema_file_is_valid_draft_2020_12(): + Draft202012Validator.check_schema(_load_story_schema()) + + +def test_story_talents_load_shared_schema(): + on_disk = _load_story_schema() + + for talent_name in ["conversation", "work", "event"]: + assert get_talent(talent_name)["json_schema"] == on_disk + + +def test_story_schema_mirrors_hook_requirements(): + schema = _load_story_schema() + properties = schema["properties"] + + assert set(schema["required"]) == { + "body", + "topics", + "confidence", + "commitments", + "closures", + "decisions", + } + assert set(properties["commitments"]["items"]["required"]) == { + "owner", + "action", + "counterparty", + "when", + "context", + } + assert set(properties["closures"]["items"]["required"]) == { + "owner", + "action", + "counterparty", + "resolution", + "context", + } + assert ( + set(properties["closures"]["items"]["properties"]["resolution"]["enum"]) + == ALLOWED_RESOLUTIONS + ) + assert set(properties["decisions"]["items"]["required"]) == { + "owner", + "action", + "context", + } + + +def test_story_hook_fixtures_validate_against_schema(): + schema = _load_story_schema() + payload = json.loads(_valid_result()) + + errors = list(Draft202012Validator(schema).iter_errors(payload)) + + assert errors == []