personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(schemas): strict-portable the 13 Class-A schemas + provider-parity guard (req_bfbdbux6 Lode 1/4)

WHAT: Apply the canonical Appendix A transform to the 13 Class-A *.schema.json files so they validate natively under Google response_json_schema, OpenAI strict:true, and Anthropic output_config json_schema. The transform strips $schema/$comment/length-numeric-items bounds, collapses oneOf, requires every property, and makes previously-optional properties nullable. meeting.box_2d and participation.participation_confidence are the only newly-required-nullable contract changes; this was pre-verified backward-compatible because consumers null/length-guard and journals validate on write only, so no migration is needed. The 3 dropped $comment notes are relocated as code/docstring comments.

GUARDS: Add tests/test_schema_strict_portability.py as the offline make ci guard, with a temporary PENDING_PORTABILITY allowlist covering the 4 Class-B schemas, chat, and build_rollup_schema(3). The guard also asserts allowlisted schemas still violate so the allowlist cannot rot. Add tests/integration/test_schema_provider_parity.py as an integration+requires_api raw-SDK strict parity test that skips cleanly without tests/fixtures/.env.

RATIFIED SCOPE DEVIATION (founder-approved during this session): scope §2/AC3 said tests/test_anthropic.py must be byte-unchanged, but its two sanitizes_describe_schema tests consumed the real describe.schema.json as a dirty-input fixture. Jer approved a minimal surgical edit: _load_describe_schema() now returns an inline dirty schema and the two tests plus that helper are the ONLY test_anthropic.py changes. All other test_anthropic.py tests and the test_sense_schema.py test_hydrate_runtime_enums_* block are byte-unchanged. The Anthropic sanitizer/forced-tool fallback in solstone/think/providers/ is intentionally unchanged because it remains load-bearing for Class B/C until Lodes 2-4.

SHIP OBLIGATION (AC7): the gated tests/integration/test_schema_provider_parity.py is run OUT-OF-BAND by VPE against the founder's solstone-scoped keys before ship and must pass 39/39 native (13x3, no fallback); that result is recorded in the ship/decision note. It was not run live in this lode because tests/fixtures/.env is absent.

SCOPE: this is Lode 1 of a founder-directed 4-lode program; req_bfbdbux6 closes when Lode 4 ships, not this commit. Lodes 2-4 remove their PENDING_PORTABILITY entries; Lode 4 deletes the allowlist mechanism and, after B/C/D land, the provider fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+887 -179
+18 -14
solstone/apps/entities/talent/entity_observer.schema.json
··· 1 1 { 2 - "$schema": "https://json-schema.org/draft/2020-12/schema", 3 2 "type": "object", 4 3 "additionalProperties": false, 5 - "required": ["observations", "skipped", "summary"], 4 + "required": [ 5 + "observations", 6 + "skipped", 7 + "summary" 8 + ], 6 9 "properties": { 7 10 "observations": { 8 11 "type": "array", 9 12 "items": { 10 13 "type": "object", 11 14 "additionalProperties": false, 12 - "required": ["entity_id", "items"], 15 + "required": [ 16 + "entity_id", 17 + "items" 18 + ], 13 19 "properties": { 14 20 "entity_id": { 15 - "type": "string", 16 - "minLength": 1 21 + "type": "string" 17 22 }, 18 23 "items": { 19 24 "type": "array", 20 25 "items": { 21 26 "type": "object", 22 27 "additionalProperties": false, 23 - "required": ["content", "reasoning"], 28 + "required": [ 29 + "content", 30 + "reasoning" 31 + ], 24 32 "properties": { 25 33 "content": { 26 - "type": "string", 27 - "minLength": 1 34 + "type": "string" 28 35 }, 29 36 "reasoning": { 30 - "type": "string", 31 - "minLength": 1 37 + "type": "string" 32 38 } 33 39 } 34 40 } ··· 39 45 "skipped": { 40 46 "type": "array", 41 47 "items": { 42 - "type": "string", 43 - "minLength": 1 48 + "type": "string" 44 49 } 45 50 }, 46 51 "summary": { 47 - "type": "string", 48 - "minLength": 1 52 + "type": "string" 49 53 } 50 54 } 51 55 }
+5 -1
solstone/apps/timeline/talent/segment_summary.schema.json
··· 10 10 "description": "ONE sentence, MAX 10 words, MAX 60 characters, third person, present-tense, verb-led. Examples: 'Maps the app surface and names rebuild priorities.' 'Restarts display manager to recover the desktop session.' 'Fixes panel placement and terminal artifacts in Plasma.' No first-person. No times. No segment IDs." 11 11 } 12 12 }, 13 - "required": ["title", "description"] 13 + "required": [ 14 + "title", 15 + "description" 16 + ], 17 + "additionalProperties": false 14 18 }
+69 -29
solstone/observe/categories/meeting.schema.json
··· 1 1 { 2 - "$schema": "https://json-schema.org/draft/2020-12/schema", 3 - "$comment": "Meeting extraction output contract. Source of truth for the shape is observe/categories/meeting.md.", 4 2 "type": "object", 5 3 "additionalProperties": false, 6 - "required": ["platform", "participants", "screen_share"], 4 + "required": [ 5 + "platform", 6 + "participants", 7 + "screen_share" 8 + ], 7 9 "properties": { 8 10 "platform": { 9 11 "type": "string", 10 - "enum": ["zoom", "meet", "teams", "slack", "discord", "webex", "other"] 12 + "enum": [ 13 + "zoom", 14 + "meet", 15 + "teams", 16 + "slack", 17 + "discord", 18 + "webex", 19 + "other" 20 + ] 11 21 }, 12 22 "participants": { 13 23 "type": "array", 14 24 "items": { 15 25 "type": "object", 16 26 "additionalProperties": false, 17 - "required": ["name", "status", "video"], 27 + "required": [ 28 + "name", 29 + "status", 30 + "video", 31 + "box_2d" 32 + ], 18 33 "properties": { 19 - "name": {"type": "string", "minLength": 1}, 34 + "name": { 35 + "type": "string" 36 + }, 20 37 "status": { 21 38 "type": "string", 22 - "enum": ["speaking", "muted", "active", "presenting", "unknown"] 39 + "enum": [ 40 + "speaking", 41 + "muted", 42 + "active", 43 + "presenting", 44 + "unknown" 45 + ] 23 46 }, 24 - "video": {"type": "boolean"}, 47 + "video": { 48 + "type": "boolean" 49 + }, 25 50 "box_2d": { 26 - "type": "array", 27 - "items": {"type": "integer", "minimum": 0}, 28 - "minItems": 4, 29 - "maxItems": 4 51 + "type": [ 52 + "array", 53 + "null" 54 + ], 55 + "items": { 56 + "type": "integer" 57 + } 30 58 } 31 59 } 32 60 } 33 61 }, 34 62 "screen_share": { 35 - "oneOf": [ 36 - {"type": "null"}, 37 - { 38 - "type": "object", 39 - "additionalProperties": false, 40 - "required": ["box_2d", "presenter", "description", "formatted_text"], 41 - "properties": { 42 - "box_2d": { 43 - "type": "array", 44 - "items": {"type": "integer", "minimum": 0}, 45 - "minItems": 4, 46 - "maxItems": 4 47 - }, 48 - "presenter": {"type": ["string", "null"]}, 49 - "description": {"type": "string"}, 50 - "formatted_text": {"type": "string"} 63 + "type": [ 64 + "object", 65 + "null" 66 + ], 67 + "additionalProperties": false, 68 + "required": [ 69 + "box_2d", 70 + "presenter", 71 + "description", 72 + "formatted_text" 73 + ], 74 + "properties": { 75 + "box_2d": { 76 + "type": "array", 77 + "items": { 78 + "type": "integer" 51 79 } 80 + }, 81 + "presenter": { 82 + "type": [ 83 + "string", 84 + "null" 85 + ] 86 + }, 87 + "description": { 88 + "type": "string" 89 + }, 90 + "formatted_text": { 91 + "type": "string" 52 92 } 53 - ] 93 + } 54 94 } 55 95 } 56 96 }
+2
solstone/observe/describe.py
··· 113 113 if prompt_content.text.strip(): 114 114 metadata["prompt"] = prompt_content.text 115 115 116 + # Per-category output contract from <category>.schema.json; e.g. meeting.schema.json: Source of truth for the shape is observe/categories/meeting.md 116 117 schema_path = md_path.with_suffix(".schema.json") 117 118 if schema_path.exists(): 118 119 metadata["json_schema"] = json.loads(schema_path.read_text("utf-8")) ··· 181 182 # Build categorization prompt from template 182 183 CATEGORIZATION_PROMPT = _build_categorization_prompt() 183 184 185 + # The enums in `primary` and `secondary` MUST match the filenames under observe/categories/*.md. 184 186 _SCHEMA = json.loads( 185 187 (Path(__file__).parent / "describe.schema.json").read_text(encoding="utf-8") 186 188 )
+41 -7
solstone/observe/describe.schema.json
··· 1 1 { 2 - "$schema": "https://json-schema.org/draft/2020-12/schema", 3 - "$comment": "Frame categorization schema for observe/describe.py. The enums in `primary` and `secondary` MUST match the filenames under observe/categories/*.md. Enforced by tests/test_observe_describe_schema.py::test_category_enum_matches_registry. If you add/remove/rename a category file, update both enums here.", 4 2 "type": "object", 5 3 "additionalProperties": false, 6 - "required": ["visual_description", "primary", "secondary", "overlap"], 4 + "required": [ 5 + "visual_description", 6 + "primary", 7 + "secondary", 8 + "overlap" 9 + ], 7 10 "properties": { 8 - "visual_description": {"type": "string", "minLength": 1}, 9 - "primary": {"type": "string", "enum": ["browsing", "code", "gaming", "media", "meeting", "messaging", "productivity", "reading", "terminal"]}, 10 - "secondary": {"type": "string", "enum": ["browsing", "code", "gaming", "media", "meeting", "messaging", "productivity", "reading", "terminal", "none"]}, 11 - "overlap": {"type": "boolean"} 11 + "visual_description": { 12 + "type": "string" 13 + }, 14 + "primary": { 15 + "type": "string", 16 + "enum": [ 17 + "browsing", 18 + "code", 19 + "gaming", 20 + "media", 21 + "meeting", 22 + "messaging", 23 + "productivity", 24 + "reading", 25 + "terminal" 26 + ] 27 + }, 28 + "secondary": { 29 + "type": "string", 30 + "enum": [ 31 + "browsing", 32 + "code", 33 + "gaming", 34 + "media", 35 + "meeting", 36 + "messaging", 37 + "productivity", 38 + "reading", 39 + "terminal", 40 + "none" 41 + ] 42 + }, 43 + "overlap": { 44 + "type": "boolean" 45 + } 12 46 } 13 47 }
+25 -8
solstone/observe/enrich.schema.json
··· 1 1 { 2 - "$schema": "https://json-schema.org/draft/2020-12/schema", 3 2 "type": "object", 4 3 "additionalProperties": false, 5 - "required": ["statements", "topics", "setting", "warning"], 4 + "required": [ 5 + "statements", 6 + "topics", 7 + "setting", 8 + "warning" 9 + ], 6 10 "properties": { 7 11 "statements": { 8 12 "type": "array", 9 13 "items": { 10 14 "type": "object", 11 15 "additionalProperties": false, 12 - "required": ["corrected", "emotion"], 16 + "required": [ 17 + "corrected", 18 + "emotion" 19 + ], 13 20 "properties": { 14 - "corrected": {"type": "string"}, 15 - "emotion": {"type": "string"} 21 + "corrected": { 22 + "type": "string" 23 + }, 24 + "emotion": { 25 + "type": "string" 26 + } 16 27 } 17 28 } 18 29 }, 19 - "topics": {"type": "string"}, 20 - "setting": {"type": "string"}, 21 - "warning": {"type": "string"} 30 + "topics": { 31 + "type": "string" 32 + }, 33 + "setting": { 34 + "type": "string" 35 + }, 36 + "warning": { 37 + "type": "string" 38 + } 22 39 } 23 40 }
+10 -7
solstone/observe/transcribe/gemini.schema.json
··· 1 1 { 2 - "$schema": "https://json-schema.org/draft/2020-12/schema", 3 2 "type": "object", 4 3 "additionalProperties": false, 5 - "required": ["segments"], 4 + "required": [ 5 + "segments" 6 + ], 6 7 "properties": { 7 8 "segments": { 8 9 "type": "array", 9 10 "items": { 10 11 "type": "object", 11 12 "additionalProperties": false, 12 - "required": ["start", "speaker", "text"], 13 + "required": [ 14 + "start", 15 + "speaker", 16 + "text" 17 + ], 13 18 "properties": { 14 19 "start": { 15 20 "type": "string", 16 21 "pattern": "^\\d{2}:\\d{2}$" 17 22 }, 18 23 "speaker": { 19 - "type": "string", 20 - "minLength": 1 24 + "type": "string" 21 25 }, 22 26 "text": { 23 - "type": "string", 24 - "minLength": 1 27 + "type": "string" 25 28 } 26 29 } 27 30 }
+4 -2
solstone/talent/daily_schedule.schema.json
··· 1 1 { 2 - "$schema": "https://json-schema.org/draft/2020-12/schema", 3 2 "type": "object", 4 3 "additionalProperties": false, 5 - "required": ["primary", "fallback"], 4 + "required": [ 5 + "primary", 6 + "fallback" 7 + ], 6 8 "properties": { 7 9 "primary": { 8 10 "type": "string",
+29 -13
solstone/talent/participation.schema.json
··· 1 1 { 2 - "$schema": "https://json-schema.org/draft/2020-12/schema", 3 2 "type": "object", 4 3 "additionalProperties": false, 5 - "required": ["participation"], 4 + "required": [ 5 + "participation", 6 + "participation_confidence" 7 + ], 6 8 "properties": { 7 9 "participation": { 8 10 "type": "array", 9 11 "items": { 10 12 "type": "object", 11 13 "additionalProperties": false, 12 - "required": ["name", "role", "source", "confidence", "context", "entity_id"], 14 + "required": [ 15 + "name", 16 + "role", 17 + "source", 18 + "confidence", 19 + "context", 20 + "entity_id" 21 + ], 13 22 "properties": { 14 23 "name": { 15 - "type": "string", 16 - "minLength": 1 24 + "type": "string" 17 25 }, 18 26 "role": { 19 27 "type": "string", 20 - "enum": ["attendee", "mentioned"] 28 + "enum": [ 29 + "attendee", 30 + "mentioned" 31 + ] 21 32 }, 22 33 "source": { 23 34 "type": "string", 24 - "enum": ["voice", "speaker_label", "transcript", "screen", "other"] 35 + "enum": [ 36 + "voice", 37 + "speaker_label", 38 + "transcript", 39 + "screen", 40 + "other" 41 + ] 25 42 }, 26 43 "confidence": { 27 - "type": "number", 28 - "minimum": 0, 29 - "maximum": 1 44 + "type": "number" 30 45 }, 31 46 "context": { 32 47 "type": "string" ··· 38 53 } 39 54 }, 40 55 "participation_confidence": { 41 - "type": "number", 42 - "minimum": 0, 43 - "maximum": 1 56 + "type": [ 57 + "number", 58 + "null" 59 + ] 44 60 } 45 61 } 46 62 }
+21 -9
solstone/talent/participation_entry.schema.json
··· 1 1 { 2 - "$schema": "https://json-schema.org/draft/2020-12/schema", 3 2 "type": "object", 4 3 "additionalProperties": false, 5 - "required": ["name", "role", "source", "confidence", "context", "entity_id"], 4 + "required": [ 5 + "name", 6 + "role", 7 + "source", 8 + "confidence", 9 + "context", 10 + "entity_id" 11 + ], 6 12 "properties": { 7 13 "name": { 8 - "type": "string", 9 - "minLength": 1 14 + "type": "string" 10 15 }, 11 16 "role": { 12 17 "type": "string", 13 - "enum": ["attendee", "mentioned"] 18 + "enum": [ 19 + "attendee", 20 + "mentioned" 21 + ] 14 22 }, 15 23 "source": { 16 24 "type": "string", 17 - "enum": ["voice", "speaker_label", "transcript", "screen", "other"] 25 + "enum": [ 26 + "voice", 27 + "speaker_label", 28 + "transcript", 29 + "screen", 30 + "other" 31 + ] 18 32 }, 19 33 "confidence": { 20 - "type": "number", 21 - "minimum": 0, 22 - "maximum": 1 34 + "type": "number" 23 35 }, 24 36 "context": { 25 37 "type": "string"
+141 -23
solstone/talent/sense.schema.json
··· 1 1 { 2 - "$schema": "https://json-schema.org/draft/2020-12/schema", 3 2 "type": "object", 4 3 "additionalProperties": false, 5 - "required": ["density","content_type","activity_summary","entities","facets","meeting_detected","speakers","recommend","emotional_register"], 4 + "required": [ 5 + "density", 6 + "content_type", 7 + "activity_summary", 8 + "entities", 9 + "facets", 10 + "meeting_detected", 11 + "speakers", 12 + "recommend", 13 + "emotional_register" 14 + ], 6 15 "properties": { 7 - "density": {"type": "string", "enum": ["active","low_change","idle"]}, 8 - "content_type": {"type": "string", "enum": ["meeting","coding","browsing","email","messaging","ai_conversation","writing","reading","video","gaming","social","planning","productivity","terminal","design","music","idle"]}, 9 - "activity_summary": {"type": "string", "minLength": 1}, 16 + "density": { 17 + "type": "string", 18 + "enum": [ 19 + "active", 20 + "low_change", 21 + "idle" 22 + ] 23 + }, 24 + "content_type": { 25 + "type": "string", 26 + "enum": [ 27 + "meeting", 28 + "coding", 29 + "browsing", 30 + "email", 31 + "messaging", 32 + "ai_conversation", 33 + "writing", 34 + "reading", 35 + "video", 36 + "gaming", 37 + "social", 38 + "planning", 39 + "productivity", 40 + "terminal", 41 + "design", 42 + "music", 43 + "idle" 44 + ] 45 + }, 46 + "activity_summary": { 47 + "type": "string" 48 + }, 10 49 "entities": { 11 50 "type": "array", 12 51 "items": { 13 52 "type": "object", 14 53 "additionalProperties": false, 15 - "required": ["type","name","role","source","context"], 54 + "required": [ 55 + "type", 56 + "name", 57 + "role", 58 + "source", 59 + "context" 60 + ], 16 61 "properties": { 17 - "type": {"type": "string", "enum": ["Person","Company","Project","Tool"]}, 18 - "name": {"type": "string", "minLength": 1}, 19 - "role": {"type": "string", "enum": ["attendee","mentioned"]}, 20 - "source": {"type": "string", "enum": ["voice","speaker_label","transcript","screen","other"]}, 21 - "context": {"type": "string", "minLength": 1} 62 + "type": { 63 + "type": "string", 64 + "enum": [ 65 + "Person", 66 + "Company", 67 + "Project", 68 + "Tool" 69 + ] 70 + }, 71 + "name": { 72 + "type": "string" 73 + }, 74 + "role": { 75 + "type": "string", 76 + "enum": [ 77 + "attendee", 78 + "mentioned" 79 + ] 80 + }, 81 + "source": { 82 + "type": "string", 83 + "enum": [ 84 + "voice", 85 + "speaker_label", 86 + "transcript", 87 + "screen", 88 + "other" 89 + ] 90 + }, 91 + "context": { 92 + "type": "string" 93 + } 22 94 } 23 95 } 24 96 }, 25 97 "facets": { 26 98 "type": "array", 27 - "minItems": 1, 28 99 "items": { 29 100 "type": "object", 30 101 "additionalProperties": false, 31 - "required": ["facet","activity","level"], 102 + "required": [ 103 + "facet", 104 + "activity", 105 + "level" 106 + ], 32 107 "properties": { 33 - "facet": {"type": "string", "enum": ["__RUNTIME_FACETS__"]}, 34 - "activity": {"type": "string", "minLength": 1}, 35 - "level": {"type": "string", "enum": ["high","medium","low"]} 108 + "facet": { 109 + "type": "string", 110 + "enum": [ 111 + "__RUNTIME_FACETS__" 112 + ] 113 + }, 114 + "activity": { 115 + "type": "string" 116 + }, 117 + "level": { 118 + "type": "string", 119 + "enum": [ 120 + "high", 121 + "medium", 122 + "low" 123 + ] 124 + } 36 125 } 37 126 } 38 127 }, 39 - "meeting_detected": {"type": "boolean"}, 40 - "speakers": {"type": "array", "items": {"type": "string"}}, 128 + "meeting_detected": { 129 + "type": "boolean" 130 + }, 131 + "speakers": { 132 + "type": "array", 133 + "items": { 134 + "type": "string" 135 + } 136 + }, 41 137 "recommend": { 42 138 "type": "object", 43 139 "additionalProperties": false, 44 - "required": ["screen_record","speaker_attribution","pulse_update"], 140 + "required": [ 141 + "screen_record", 142 + "speaker_attribution", 143 + "pulse_update" 144 + ], 45 145 "properties": { 46 - "screen_record": {"type": "boolean"}, 47 - "speaker_attribution": {"type": "boolean"}, 48 - "pulse_update": {"type": "boolean"} 146 + "screen_record": { 147 + "type": "boolean" 148 + }, 149 + "speaker_attribution": { 150 + "type": "boolean" 151 + }, 152 + "pulse_update": { 153 + "type": "boolean" 154 + } 49 155 } 50 156 }, 51 - "emotional_register": {"type": "string", "enum": ["high_energy","tense","focused","collaborative","flat","celebratory","strained","neutral"]} 157 + "emotional_register": { 158 + "type": "string", 159 + "enum": [ 160 + "high_energy", 161 + "tense", 162 + "focused", 163 + "collaborative", 164 + "flat", 165 + "celebratory", 166 + "strained", 167 + "neutral" 168 + ] 169 + } 52 170 } 53 171 }
+79 -22
solstone/talent/story.schema.json
··· 1 1 { 2 - "$schema": "https://json-schema.org/draft/2020-12/schema", 3 2 "type": "object", 4 3 "additionalProperties": false, 5 - "required": ["body","topics","confidence","commitments","closures","decisions"], 4 + "required": [ 5 + "body", 6 + "topics", 7 + "confidence", 8 + "commitments", 9 + "closures", 10 + "decisions" 11 + ], 6 12 "properties": { 7 - "body": {"type": "string", "minLength": 1}, 13 + "body": { 14 + "type": "string" 15 + }, 8 16 "topics": { 9 17 "type": "array", 10 - "items": {"type": "string"}, 11 - "maxItems": 10 18 + "items": { 19 + "type": "string" 20 + } 21 + }, 22 + "confidence": { 23 + "type": "number" 12 24 }, 13 - "confidence": {"type": "number", "minimum": 0, "maximum": 1}, 14 25 "commitments": { 15 26 "type": "array", 16 27 "items": { 17 28 "type": "object", 18 29 "additionalProperties": false, 19 - "required": ["owner","action","counterparty","when","context"], 30 + "required": [ 31 + "owner", 32 + "action", 33 + "counterparty", 34 + "when", 35 + "context" 36 + ], 20 37 "properties": { 21 - "owner": {"type": "string", "minLength": 1}, 22 - "action": {"type": "string", "minLength": 1}, 23 - "counterparty": {"type": "string", "minLength": 1}, 24 - "when": {"type": "string", "minLength": 1}, 25 - "context": {"type": "string", "minLength": 1} 38 + "owner": { 39 + "type": "string" 40 + }, 41 + "action": { 42 + "type": "string" 43 + }, 44 + "counterparty": { 45 + "type": "string" 46 + }, 47 + "when": { 48 + "type": "string" 49 + }, 50 + "context": { 51 + "type": "string" 52 + } 26 53 } 27 54 } 28 55 }, ··· 31 58 "items": { 32 59 "type": "object", 33 60 "additionalProperties": false, 34 - "required": ["owner","action","counterparty","resolution","context"], 61 + "required": [ 62 + "owner", 63 + "action", 64 + "counterparty", 65 + "resolution", 66 + "context" 67 + ], 35 68 "properties": { 36 - "owner": {"type": "string", "minLength": 1}, 37 - "action": {"type": "string", "minLength": 1}, 38 - "counterparty": {"type": "string", "minLength": 1}, 69 + "owner": { 70 + "type": "string" 71 + }, 72 + "action": { 73 + "type": "string" 74 + }, 75 + "counterparty": { 76 + "type": "string" 77 + }, 39 78 "resolution": { 40 79 "type": "string", 41 - "enum": ["sent","done","signed","dropped","deferred"] 80 + "enum": [ 81 + "sent", 82 + "done", 83 + "signed", 84 + "dropped", 85 + "deferred" 86 + ] 42 87 }, 43 - "context": {"type": "string", "minLength": 1} 88 + "context": { 89 + "type": "string" 90 + } 44 91 } 45 92 } 46 93 }, ··· 49 96 "items": { 50 97 "type": "object", 51 98 "additionalProperties": false, 52 - "required": ["owner","action","context"], 99 + "required": [ 100 + "owner", 101 + "action", 102 + "context" 103 + ], 53 104 "properties": { 54 - "owner": {"type": "string", "minLength": 1}, 55 - "action": {"type": "string", "minLength": 1}, 56 - "context": {"type": "string", "minLength": 1} 105 + "owner": { 106 + "type": "string" 107 + }, 108 + "action": { 109 + "type": "string" 110 + }, 111 + "context": { 112 + "type": "string" 113 + } 57 114 } 58 115 } 59 116 }
+13 -5
solstone/think/detect_created.schema.json
··· 1 1 { 2 - "$schema": "https://json-schema.org/draft/2020-12/schema", 3 2 "type": "object", 4 3 "additionalProperties": false, 5 - "required": ["day", "time", "confidence", "source", "utc"], 4 + "required": [ 5 + "day", 6 + "time", 7 + "confidence", 8 + "source", 9 + "utc" 10 + ], 6 11 "properties": { 7 12 "day": { 8 13 "type": "string", ··· 14 19 }, 15 20 "confidence": { 16 21 "type": "string", 17 - "enum": ["high", "medium", "low"] 22 + "enum": [ 23 + "high", 24 + "medium", 25 + "low" 26 + ] 18 27 }, 19 28 "source": { 20 - "type": "string", 21 - "minLength": 1 29 + "type": "string" 22 30 }, 23 31 "utc": { 24 32 "type": "boolean"
+1
solstone/think/detect_transcript.py
··· 17 17 encoding="utf-8" 18 18 ) 19 19 ) 20 + # Source of truth is think/detect_transcript_json.md. 20 21 _JSON_SCHEMA = json.loads( 21 22 (Path(__file__).parent / "detect_transcript_json.schema.json").read_text( 22 23 encoding="utf-8"
+26 -9
solstone/think/detect_transcript_json.schema.json
··· 1 1 { 2 - "$schema": "https://json-schema.org/draft/2020-12/schema", 3 - "$comment": "Output contract for detect_transcript_json(). Source of truth is think/detect_transcript_json.md.", 4 2 "type": "object", 5 3 "additionalProperties": false, 6 - "required": ["entries", "topics", "setting"], 4 + "required": [ 5 + "entries", 6 + "topics", 7 + "setting" 8 + ], 7 9 "properties": { 8 10 "entries": { 9 11 "type": "array", 10 12 "items": { 11 13 "type": "object", 12 14 "additionalProperties": false, 13 - "required": ["start", "speaker", "text"], 15 + "required": [ 16 + "start", 17 + "speaker", 18 + "text" 19 + ], 14 20 "properties": { 15 - "start": {"type": "string", "pattern": "^\\d{2}:\\d{2}:\\d{2}$"}, 16 - "speaker": {"type": "string", "minLength": 1}, 17 - "text": {"type": "string", "minLength": 1} 21 + "start": { 22 + "type": "string", 23 + "pattern": "^\\d{2}:\\d{2}:\\d{2}$" 24 + }, 25 + "speaker": { 26 + "type": "string" 27 + }, 28 + "text": { 29 + "type": "string" 30 + } 18 31 } 19 32 } 20 33 }, 21 - "topics": {"type": "string"}, 22 - "setting": {"type": "string"} 34 + "topics": { 35 + "type": "string" 36 + }, 37 + "setting": { 38 + "type": "string" 39 + } 23 40 } 24 41 }
+233
tests/integration/test_schema_provider_parity.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Raw-SDK strict schema parity tests for req_bfbdbux6 Class-A schemas.""" 5 + 6 + from __future__ import annotations 7 + 8 + import json 9 + import os 10 + from pathlib import Path 11 + from typing import Any, Callable 12 + 13 + import pytest 14 + from dotenv import load_dotenv 15 + from jsonschema import Draft202012Validator 16 + 17 + from solstone.think.models import CLAUDE_SONNET_4, GEMINI_FLASH, GPT_5 18 + 19 + REPO_ROOT = Path(__file__).resolve().parents[2] 20 + FACET_SENTINEL = "__RUNTIME_FACETS__" 21 + 22 + CLASS_A = [ 23 + ( 24 + "describe", 25 + "solstone/observe/describe.schema.json", 26 + "Categorize a hypothetical code-editor-with-terminal screenshot.", 27 + ), 28 + ( 29 + "meeting", 30 + "solstone/observe/categories/meeting.schema.json", 31 + "Hypothetical 2-person Zoom: Alice speaking video-on box [10,20,30,40]; " 32 + "Bob muted video-off; no screen share.", 33 + ), 34 + ( 35 + "enrich", 36 + "solstone/observe/enrich.schema.json", 37 + "Enrich a hypothetical transcript: one corrected statement, neutral emotion; " 38 + "topics/setting/warning short.", 39 + ), 40 + ( 41 + "transcribe_gemini", 42 + "solstone/observe/transcribe/gemini.schema.json", 43 + "One transcript segment: start '00:05', speaker 'Alice', text 'hello'.", 44 + ), 45 + ( 46 + "detect_created", 47 + "solstone/think/detect_created.schema.json", 48 + "Detect created date: day '20260518', time '143000', confidence high, " 49 + "source 'header', utc false.", 50 + ), 51 + ( 52 + "detect_transcript_json", 53 + "solstone/think/detect_transcript_json.schema.json", 54 + "One entry: start '00:00:05', speaker 'Alice', text 'hi'; " 55 + "topics/setting short.", 56 + ), 57 + ( 58 + "daily_schedule", 59 + "solstone/talent/daily_schedule.schema.json", 60 + "primary '09:00', fallback '13:30'.", 61 + ), 62 + ( 63 + "entity_observer", 64 + "solstone/apps/entities/talent/entity_observer.schema.json", 65 + "One observation entity_id 'e1' with one item content 'c' reasoning 'r'; " 66 + "skipped []; summary 's'.", 67 + ), 68 + ( 69 + "sense", 70 + "solstone/talent/sense.schema.json", 71 + "Hypothetical idle frame: density idle, content_type idle, summary 'idle', " 72 + "no entities, one facet, meeting_detected false, no speakers, all recommend " 73 + "false, emotional_register neutral.", 74 + ), 75 + ( 76 + "story", 77 + "solstone/talent/story.schema.json", 78 + "Short story body 's', topics ['t'], confidence 0.5, no commitments/" 79 + "closures/decisions.", 80 + ), 81 + ( 82 + "segment_summary", 83 + "solstone/apps/timeline/talent/segment_summary.schema.json", 84 + "title 'Dev Env', description 'Sets up the environment.'", 85 + ), 86 + ( 87 + "participation", 88 + "solstone/talent/participation.schema.json", 89 + "One participant: name 'Alice', role attendee, source voice, confidence 0.9, " 90 + "context 'c', entity_id null; participation_confidence 0.8.", 91 + ), 92 + ( 93 + "participation_entry", 94 + "solstone/talent/participation_entry.schema.json", 95 + "name 'Alice', role attendee, source voice, confidence 0.9, context 'c', " 96 + "entity_id null.", 97 + ), 98 + ] 99 + 100 + 101 + def get_fixtures_env(api_key_name: str): 102 + fixtures_env = Path(__file__).parent.parent / "fixtures" / ".env" 103 + if not fixtures_env.exists(): 104 + return None, None, None 105 + load_dotenv(fixtures_env, override=True) 106 + return fixtures_env, os.getenv(api_key_name), os.getenv("SOLSTONE_JOURNAL") 107 + 108 + 109 + def hydrate(node: Any) -> None: 110 + if isinstance(node, dict): 111 + if node.get("enum") == [FACET_SENTINEL]: 112 + node["enum"] = ["work", "personal", "health"] 113 + for value in node.values(): 114 + hydrate(value) 115 + elif isinstance(node, list): 116 + for value in node: 117 + hydrate(value) 118 + 119 + 120 + def load_schema(rel_path: str) -> dict[str, Any]: 121 + schema = json.loads((REPO_ROOT / rel_path).read_text(encoding="utf-8")) 122 + hydrate(schema) 123 + return schema 124 + 125 + 126 + def conforms(schema: dict[str, Any], text: str) -> tuple[bool, str]: 127 + try: 128 + obj = json.loads(text) 129 + except Exception as exc: 130 + return False, f"not JSON: {exc}: {text[:120]!r}" 131 + errors = sorted(Draft202012Validator(schema).iter_errors(obj), key=str) 132 + if errors: 133 + return False, f"NONCONFORM: {errors[0].message[:160]}" 134 + return True, json.dumps(obj)[:140] 135 + 136 + 137 + def call_anthropic(schema: dict[str, Any], prompt: str, api_key: str) -> str: 138 + from anthropic import Anthropic 139 + 140 + message = Anthropic(api_key=api_key).messages.create( 141 + model=CLAUDE_SONNET_4, 142 + max_tokens=2048, 143 + temperature=0.3, 144 + messages=[{"role": "user", "content": prompt + " Respond JSON only."}], 145 + output_config={"format": {"type": "json_schema", "schema": schema}}, 146 + ) 147 + return "".join( 148 + block.text 149 + for block in message.content 150 + if getattr(block, "type", None) == "text" 151 + ) 152 + 153 + 154 + def call_openai(schema: dict[str, Any], prompt: str, api_key: str) -> str: 155 + import openai 156 + 157 + response = openai.OpenAI(api_key=api_key).responses.create( 158 + model=GPT_5, 159 + input=prompt + " Respond JSON only.", 160 + max_output_tokens=2048, 161 + text={ 162 + "format": { 163 + "type": "json_schema", 164 + "name": "r", 165 + "schema": schema, 166 + "strict": True, 167 + } 168 + }, 169 + ) 170 + return response.output_text or "" 171 + 172 + 173 + def call_google(schema: dict[str, Any], prompt: str, api_key: str) -> str: 174 + from google import genai 175 + from google.genai import types 176 + 177 + response = genai.Client(api_key=api_key, vertexai=False).models.generate_content( 178 + model=GEMINI_FLASH, 179 + contents=[prompt + " Respond JSON only."], 180 + config=types.GenerateContentConfig( 181 + temperature=0.3, 182 + max_output_tokens=2048, 183 + response_mime_type="application/json", 184 + response_json_schema=schema, 185 + thinking_config=types.ThinkingConfig(thinking_budget=0), 186 + ), 187 + ) 188 + return response.text or "" 189 + 190 + 191 + PROVIDERS: tuple[tuple[str, str, Callable[[dict[str, Any], str, str], str]], ...] = ( 192 + ("google", "GOOGLE_API_KEY", call_google), 193 + ("openai-strict", "OPENAI_API_KEY", call_openai), 194 + ("anthropic-strict", "ANTHROPIC_API_KEY", call_anthropic), 195 + ) 196 + 197 + 198 + @pytest.mark.integration 199 + @pytest.mark.requires_api 200 + @pytest.mark.parametrize( 201 + ("provider_name", "api_key_name", "caller"), 202 + [pytest.param(*provider, id=provider[0]) for provider in PROVIDERS], 203 + ) 204 + @pytest.mark.parametrize( 205 + ("schema_name", "schema_path", "prompt"), 206 + [pytest.param(*schema_case, id=schema_case[0]) for schema_case in CLASS_A], 207 + ) 208 + def test_class_a_schema_provider_parity( 209 + provider_name: str, 210 + api_key_name: str, 211 + caller: Callable[[dict[str, Any], str, str], str], 212 + schema_name: str, 213 + schema_path: str, 214 + prompt: str, 215 + ) -> None: 216 + fixtures_env, api_key, journal_path = get_fixtures_env(api_key_name) 217 + if not fixtures_env: 218 + pytest.skip("tests/fixtures/.env not found") 219 + if not api_key: 220 + pytest.skip(f"{api_key_name} not found in tests/fixtures/.env file") 221 + if not journal_path: 222 + pytest.skip("SOLSTONE_JOURNAL not found in tests/fixtures/.env file") 223 + 224 + schema = load_schema(schema_path) 225 + try: 226 + output = caller(schema, prompt, api_key) 227 + except Exception as exc: 228 + pytest.fail( 229 + f"{provider_name} rejected {schema_name}: {type(exc).__name__}: {exc}" 230 + ) 231 + 232 + ok, detail = conforms(schema, output) 233 + assert ok, f"{provider_name} returned invalid {schema_name}: {detail}"
+13 -4
tests/test_anthropic.py
··· 43 43 44 44 45 45 def _load_describe_schema() -> dict: 46 - schema_path = ( 47 - Path(__file__).resolve().parents[1] / "solstone/observe/describe.schema.json" 48 - ) 49 - return json.loads(schema_path.read_text(encoding="utf-8")) 46 + return { 47 + "$schema": "https://json-schema.org/draft/2020-12/schema", 48 + "$comment": f"Inline dirty fixture for {Path('describe.schema.json').name}", 49 + "type": "object", 50 + "additionalProperties": False, 51 + "required": ["visual_description", "primary"], 52 + "properties": { 53 + "visual_description": {"type": "string", "minLength": 1}, 54 + "primary": {"type": "string", "enum": ["browsing", "code"]}, 55 + }, 56 + } 50 57 51 58 52 59 def _assert_no_schema_metadata(value): ··· 781 788 assert "$comment" in schema 782 789 assert json.dumps(schema, sort_keys=True) == original_json 783 790 assert output_schema["additionalProperties"] is False 791 + assert schema["properties"]["visual_description"]["minLength"] == 1 784 792 assert output_schema["properties"]["visual_description"]["minLength"] == 1 785 793 assert "code" in output_schema["properties"]["primary"]["enum"] 786 794 _validate_json_response(result, True) ··· 813 821 assert "$comment" in schema 814 822 assert json.dumps(schema, sort_keys=True) == original_json 815 823 assert output_schema["additionalProperties"] is False 824 + assert schema["properties"]["visual_description"]["minLength"] == 1 816 825 assert output_schema["properties"]["visual_description"]["minLength"] == 1 817 826 assert "code" in output_schema["properties"]["primary"]["enum"] 818 827 _validate_json_response(result, True)
+1 -1
tests/test_detect_created_schema.py
··· 51 51 assert not validator.is_valid({**valid, "time": "14:30:52"}) 52 52 assert not validator.is_valid({**valid, "confidence": "certain"}) 53 53 assert not validator.is_valid({**valid, "extra": "x"}) 54 - assert not validator.is_valid({**valid, "source": ""}) 54 + assert not validator.is_valid({**valid, "source": 42}) 55 55 56 56 57 57 def test_detect_created_passes_schema_to_generate(monkeypatch):
+1 -1
tests/test_detect_transcript_schema.py
··· 83 83 assert not validator.is_valid( 84 84 { 85 85 **valid, 86 - "entries": [{"start": "12:34:56", "speaker": "Alice", "text": ""}], 86 + "entries": [{"start": "12:34:56", "speaker": "Alice", "text": 7}], 87 87 } 88 88 ) 89 89 assert not validator.is_valid({**valid, "extra": "x"})
+1 -1
tests/test_entity_observer_schema.py
··· 130 130 "observations": [ 131 131 { 132 132 "entity_id": "alice_johnson", 133 - "items": [{"content": "", "reasoning": "Durable preference."}], 133 + "items": [{"content": 7, "reasoning": "Durable preference."}], 134 134 } 135 135 ], 136 136 "skipped": [],
+6 -6
tests/test_meeting_schema.py
··· 34 34 { 35 35 "platform": "zoom", 36 36 "participants": [ 37 - {"name": "Alice", "status": "active", "video": True}, 37 + {"name": "Alice", "status": "active", "video": True, "box_2d": None}, 38 38 ], 39 39 "screen_share": None, 40 40 } ··· 62 62 { 63 63 "platform": "hangouts", 64 64 "participants": [ 65 - {"name": "Alice", "status": "active", "video": True}, 65 + {"name": "Alice", "status": "active", "video": True, "box_2d": None}, 66 66 ], 67 67 "screen_share": None, 68 68 } ··· 71 71 { 72 72 "platform": "zoom", 73 73 "participants": [ 74 - {"name": "Alice", "status": "talking", "video": True}, 74 + {"name": "Alice", "status": "talking", "video": True, "box_2d": None}, 75 75 ], 76 76 "screen_share": None, 77 77 } ··· 87 87 { 88 88 "platform": "zoom", 89 89 "participants": [ 90 - {"name": "Alice", "status": "active", "video": True}, 90 + {"name": "Alice", "status": "active", "video": True, "box_2d": None}, 91 91 ], 92 92 "screen_share": None, 93 93 "extra": True, ··· 97 97 { 98 98 "platform": "zoom", 99 99 "participants": [ 100 - {"status": "active", "video": True}, 100 + {"status": "active", "video": True, "box_2d": None}, 101 101 ], 102 102 "screen_share": None, 103 103 } ··· 106 106 { 107 107 "platform": "zoom", 108 108 "participants": [ 109 - {"name": "", "status": "active", "video": True}, 109 + {"name": "Alice", "status": "active", "video": True}, 110 110 ], 111 111 "screen_share": None, 112 112 }
+2 -1
tests/test_observe_describe_schema.py
··· 85 85 ) 86 86 assert not validator.is_valid( 87 87 { 88 - "visual_description": "", 88 + "visual_description": 7, 89 89 "primary": "productivity", 90 90 "secondary": "none", 91 91 "overlap": False, ··· 119 119 120 120 121 121 def test_category_enum_matches_registry(): 122 + """The enums in `primary` and `secondary` MUST match the filenames under observe/categories/*.md.""" 122 123 categories_dir = Path(describe_mod.__file__).resolve().parent / "categories" 123 124 on_disk = {p.stem for p in categories_dir.glob("*.md")} 124 125
-3
tests/test_participation_schema.py
··· 22 22 23 23 Draft202012Validator.check_schema(schema) 24 24 25 - assert schema["$schema"] == "https://json-schema.org/draft/2020-12/schema" 26 - 27 25 28 26 def test_participation_schema_is_valid_and_matches_loaded(): 29 27 schema = _load_json(PARTICIPATION_SCHEMA_PATH) ··· 39 37 40 38 items = dict(schema["properties"]["participation"]["items"]) 41 39 fragment_without_schema = dict(fragment) 42 - fragment_without_schema.pop("$schema") 43 40 44 41 assert items == fragment_without_schema
+18 -2
tests/test_schedule_schema.py
··· 42 42 return schema 43 43 44 44 45 + def _strip_portability_annotations(value): 46 + if isinstance(value, dict): 47 + for key in ("minLength", "minimum", "maximum"): 48 + value.pop(key, None) 49 + for child in value.values(): 50 + _strip_portability_annotations(child) 51 + elif isinstance(value, list): 52 + for child in value: 53 + _strip_portability_annotations(child) 54 + 55 + 45 56 def _expected_schedule_activity_ids() -> set[str]: 46 57 # Why: `meeting` is emitted by both the schedule talent and sense; the 47 58 # other 9 are schedule-only (their instructions carry the marker). ··· 171 182 172 183 assert isinstance(fragment, dict) 173 184 fragment_without_schema = dict(fragment) 174 - fragment_without_schema.pop("$schema") 175 185 fragment_without_schema["properties"] = dict(fragment_without_schema["properties"]) 176 186 fragment_without_schema["properties"].pop("entity_id") 177 187 fragment_without_schema["required"] = [ 178 188 key for key in fragment_without_schema["required"] if key != "entity_id" 179 189 ] 180 190 181 - inline_items = dict( 191 + raw_inline_items = dict( 182 192 schedule_schema["items"]["properties"]["participation"]["items"] 183 193 ) 194 + assert "entity_id" in fragment["properties"] 195 + assert "entity_id" not in raw_inline_items["properties"] 196 + assert raw_inline_items != fragment 197 + 198 + inline_items = json.loads(json.dumps(raw_inline_items)) 199 + _strip_portability_annotations(inline_items) 184 200 185 201 assert inline_items == fragment_without_schema 186 202
+126
tests/test_schema_strict_portability.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Offline CI gate for req_bfbdbux6 strict schema portability. 5 + 6 + The allowlist is temporary: later portability lodes remove entries as they 7 + portabilize Class-B/C/D schemas, then delete this allowlist mechanism entirely. 8 + """ 9 + 10 + from __future__ import annotations 11 + 12 + import json 13 + from pathlib import Path 14 + from typing import Any 15 + 16 + import pytest 17 + 18 + from solstone.apps.timeline.rollup import build_rollup_schema 19 + 20 + REPO_ROOT = Path(__file__).resolve().parents[1] 21 + BANNED_KEYS = frozenset( 22 + { 23 + "$schema", 24 + "$comment", 25 + "minLength", 26 + "maxLength", 27 + "minItems", 28 + "maxItems", 29 + "minimum", 30 + "maximum", 31 + } 32 + ) 33 + 34 + # Lodes 2-4 of req_bfbdbux6 remove their entries as those classes are 35 + # portabilized; Lode 4 deletes this allowlist mechanism entirely. 36 + PENDING_PORTABILITY = frozenset( 37 + { 38 + "solstone/observe/extract.schema.json", 39 + "solstone/talent/schedule.schema.json", 40 + "solstone/talent/speaker_attribution.schema.json", 41 + "solstone/think/detect_transcript_segment.schema.json", 42 + "solstone/talent/chat.schema.json", 43 + "build_rollup_schema(3)", 44 + } 45 + ) 46 + 47 + 48 + def _discover_schemas() -> tuple[tuple[str, dict[str, Any]], ...]: 49 + discovered: list[tuple[str, dict[str, Any]]] = [] 50 + for path in sorted((REPO_ROOT / "solstone").glob("**/*.schema.json")): 51 + schema_id = path.relative_to(REPO_ROOT).as_posix() 52 + discovered.append((schema_id, json.loads(path.read_text(encoding="utf-8")))) 53 + discovered.append(("build_rollup_schema(3)", build_rollup_schema(3))) 54 + return tuple(discovered) 55 + 56 + 57 + SCHEMAS = _discover_schemas() 58 + 59 + 60 + def violations(schema: dict[str, Any]) -> list[str]: 61 + found: list[str] = [] 62 + 63 + root_is_object = schema.get("type") == "object" or ( 64 + "properties" in schema and "type" not in schema 65 + ) 66 + if not root_is_object: 67 + found.append("$: root schema must be an object") 68 + 69 + def walk(node: Any, path: str) -> None: 70 + if isinstance(node, dict): 71 + for key in node: 72 + if key in BANNED_KEYS: 73 + found.append(f"{path}: banned key {key!r}") 74 + if key == "oneOf": 75 + found.append(f"{path}: banned key 'oneOf'") 76 + 77 + if node.get("type") == "object" or "properties" in node: 78 + if node.get("additionalProperties") is not False: 79 + found.append(f"{path}: object missing additionalProperties:false") 80 + properties = node.get("properties") or {} 81 + required = node.get("required") or [] 82 + missing = sorted(set(properties) - set(required)) 83 + if missing: 84 + found.append(f"{path}: properties not required {missing!r}") 85 + 86 + for key, value in node.items(): 87 + walk(value, f"{path}/{key}") 88 + elif isinstance(node, list): 89 + for index, value in enumerate(node): 90 + walk(value, f"{path}[{index}]") 91 + 92 + walk(schema, "$") 93 + return found 94 + 95 + 96 + @pytest.mark.parametrize( 97 + ("schema_id", "schema"), 98 + [pytest.param(schema_id, schema, id=schema_id) for schema_id, schema in SCHEMAS], 99 + ) 100 + def test_pending_portability_set_matches_discovery( 101 + schema_id: str, schema: dict[str, Any] 102 + ) -> None: 103 + schema_violations = violations(schema) 104 + if schema_id in PENDING_PORTABILITY: 105 + assert schema_violations, f"{schema_id} is still allowlisted but is portable" 106 + else: 107 + assert schema_violations == [], f"{schema_id}: {schema_violations}" 108 + 109 + 110 + @pytest.mark.parametrize( 111 + "schema", 112 + [ 113 + { 114 + "type": "object", 115 + "$comment": "bad", 116 + "properties": { 117 + "a": {"type": "array", "minItems": 1}, 118 + "b": {"type": "string"}, 119 + }, 120 + "required": ["a"], 121 + "additionalProperties": False, 122 + } 123 + ], 124 + ) 125 + def test_strict_portability_guard_rejects_bad_schema(schema: dict[str, Any]) -> None: 126 + assert violations(schema)
-9
tests/test_sense_schema.py
··· 93 93 assert hydrated["properties"]["facet"]["enum"] == ["alpha", "valid_one"] 94 94 95 95 96 - def test_sense_schema_facets_array_requires_minItems_one(): 97 - schema = json.loads(SENSE_SCHEMA_PATH.read_text(encoding="utf-8")) 98 - facets_node = schema["properties"]["facets"] 99 - 100 - assert facets_node["type"] == "array" 101 - assert facets_node.get("minItems") == 1 102 - Draft202012Validator.check_schema(schema) 103 - 104 - 105 96 def test_hydrate_runtime_enums_preserves_facet_minItems_when_facets_exist( 106 97 monkeypatch, 107 98 ):
+2 -2
tests/test_transcribe_gemini_schema.py
··· 68 68 } 69 69 ) 70 70 assert not validator.is_valid( 71 - {"segments": [{"start": "01:23", "speaker": "Speaker 1", "text": ""}]} 71 + {"segments": [{"start": "01:23", "speaker": "Speaker 1", "text": 7}]} 72 72 ) 73 73 assert not validator.is_valid( 74 - {"segments": [{"start": "01:23", "speaker": "", "text": "hi"}]} 74 + {"segments": [{"start": "01:23", "speaker": 7, "text": "hi"}]} 75 75 ) 76 76 assert not validator.is_valid( 77 77 {"segments": [{"start": "1:23", "speaker": "Speaker 1", "text": "hi"}]}