From 07bc7b8e102469e312adfc37450f3f3f8ee1286c Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Sun, 19 Apr 2026 17:27:36 -0600 Subject: [PATCH] talents/participation: migrate to external participation.schema.json + shared entry fragment Next L3 structured-outputs migration: constrain the participation talent with an external schema. Follows sense (8c952dc4), story (50693752), daily_schedule (0e098e7b), and detect_created (b58bd862). Introduces talent/participation.schema.json plus a shared participation_entry.schema.json fragment. The top-level schema inlines that fragment shape (Option B) instead of using cross-file $ref because think/talent.py::_load_talent_schema has no cross-file registry plumbing; drift is guarded by tests/test_participation_schema.py::test_participation_schema_items_match_fragment. No post-hook changes: talent/participation.py is byte-identical, and the prompt body in talent/participation.md is untouched. Provider-side validation remains deferred per precedent; advisory local validation continues through the existing dispatcher path. Forward note: talent/schedule.md:55-63 is the logical next adoption site for the shared fragment, but its current participation example does not include entity_id, which the fragment requires, so that follow-up lode will need to update the example or introduce a variant. --- talent/participation.md | 1 + talent/participation.schema.json | 46 ++++++++++++++++++++++++++ talent/participation_entry.schema.json | 31 +++++++++++++++++ tests/baselines/api/stats/stats.json | 1 + tests/test_participation_schema.py | 44 ++++++++++++++++++++++++ tests/test_participation_talent.py | 1 + 6 files changed, 124 insertions(+) create mode 100644 talent/participation.schema.json create mode 100644 talent/participation_entry.schema.json create mode 100644 tests/test_participation_schema.py diff --git a/talent/participation.md b/talent/participation.md index 1667ceddc..c11c0165a 100644 --- a/talent/participation.md +++ b/talent/participation.md @@ -8,6 +8,7 @@ "priority": 10, "tier": 3, "output": "json", + "schema": "participation.schema.json", "load": { "transcripts": true, "percepts": true, diff --git a/talent/participation.schema.json b/talent/participation.schema.json new file mode 100644 index 000000000..cf12a44e8 --- /dev/null +++ b/talent/participation.schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": ["participation"], + "properties": { + "participation": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "role", "source", "confidence", "context", "entity_id"], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "enum": ["attendee", "mentioned"] + }, + "source": { + "type": "string", + "enum": ["voice", "speaker_label", "transcript", "screen", "other"] + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "context": { + "type": "string" + }, + "entity_id": { + "type": "null" + } + } + } + }, + "participation_confidence": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + } +} diff --git a/talent/participation_entry.schema.json b/talent/participation_entry.schema.json new file mode 100644 index 000000000..7d104387c --- /dev/null +++ b/talent/participation_entry.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": ["name", "role", "source", "confidence", "context", "entity_id"], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "enum": ["attendee", "mentioned"] + }, + "source": { + "type": "string", + "enum": ["voice", "speaker_label", "transcript", "screen", "other"] + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "context": { + "type": "string" + }, + "entity_id": { + "type": "null" + } + } +} diff --git a/tests/baselines/api/stats/stats.json b/tests/baselines/api/stats/stats.json index a700baf68..fb3e93d3a 100644 --- a/tests/baselines/api/stats/stats.json +++ b/tests/baselines/api/stats/stats.json @@ -172,6 +172,7 @@ "path": "/talent/participation.md", "priority": 10, "schedule": "activity", + "schema": "participation.schema.json", "source": "system", "tier": 3, "title": "Participation", diff --git a/tests/test_participation_schema.py b/tests/test_participation_schema.py new file mode 100644 index 000000000..4eb36a17b --- /dev/null +++ b/tests/test_participation_schema.py @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +import json +from pathlib import Path + +from jsonschema import Draft202012Validator + +from think.talent import get_talent + +TALENT_DIR = Path(__file__).resolve().parents[1] / "talent" +PARTICIPATION_ENTRY_SCHEMA_PATH = TALENT_DIR / "participation_entry.schema.json" +PARTICIPATION_SCHEMA_PATH = TALENT_DIR / "participation.schema.json" + + +def _load_json(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def test_participation_entry_schema_is_valid_draft_2020_12(): + schema = _load_json(PARTICIPATION_ENTRY_SCHEMA_PATH) + + Draft202012Validator.check_schema(schema) + + assert schema["$schema"] == "https://json-schema.org/draft/2020-12/schema" + + +def test_participation_schema_is_valid_and_matches_loaded(): + schema = _load_json(PARTICIPATION_SCHEMA_PATH) + + Draft202012Validator.check_schema(schema) + + assert get_talent("participation")["json_schema"] == schema + + +def test_participation_schema_items_match_fragment(): + schema = _load_json(PARTICIPATION_SCHEMA_PATH) + fragment = _load_json(PARTICIPATION_ENTRY_SCHEMA_PATH) + + items = dict(schema["properties"]["participation"]["items"]) + fragment_without_schema = dict(fragment) + fragment_without_schema.pop("$schema") + + assert items == fragment_without_schema diff --git a/tests/test_participation_talent.py b/tests/test_participation_talent.py index 728e41767..8520e6ea7 100644 --- a/tests/test_participation_talent.py +++ b/tests/test_participation_talent.py @@ -15,6 +15,7 @@ def test_participation_talent_frontmatter_and_placeholders(): assert post.metadata["activities"] == ["*"] assert post.metadata["tier"] == 3 assert post.metadata["output"] == "json" + assert post.metadata["schema"] == "participation.schema.json" assert post.metadata["priority"] == 10 assert post.metadata["load"]["talents"]["sense"] is True -- 2.51.2