Skip to content

Commit b0ca6e2

Browse files
nhortonclaude
andauthored
fix: handle YAML files in quality gate json_schema validation (#338)
* fix: handle YAML files in quality gate json_schema validation validate_json_schemas() used json.loads() unconditionally, failing on .yml/.yaml files. Now checks file extension and uses yaml.safe_load() for YAML files, matching the approach already used in deepschema_write.py. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: handle YAML files in quality gate json_schema validation - Update JOBS-REQ-004.2.2 spec to cover YAML parsing based on file extension - Update JOBS-REQ-004.2.3 spec wording (JSON parsing -> file parsing) - Add traceability comments to new YAML tests - Fix incorrect traceability ref: JOBS-REQ-001.4.8 -> JOBS-REQ-004.2.6 - Update doc/job_yml_guidance.md to reflect YAML support - Add changelog entry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: use yaml.safe_load unconditionally since YAML is a JSON superset Simplifies the implementation — no need to branch on file extension. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update changelog entry to reflect unconditional yaml.safe_load approach Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move yaml import to top-level and narrow exception handling - Move `import yaml` from loop body to module-level (PyYAML is a runtime dep) - Narrow `except Exception` to `(yaml.YAMLError, UnicodeDecodeError)` - Fix doc wording: remove stale "based on file extension" references - Fix extra blank line flagged by ruff Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore required blank line before top-level comment block Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 879c0d1 commit b0ca6e2

5 files changed

Lines changed: 64 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626

2727
### Fixed
2828

29+
- Quality gate `validate_json_schemas()` now uses `yaml.safe_load` (a JSON superset) instead of `json.loads`, so YAML output files are validated correctly
30+
2931
### Removed
3032
## [0.12.0] - 2026-04-03
3133

doc/job_yml_guidance.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ You define quality criteria once, and they apply everywhere. If three workflows
6464

6565
### `json_schema`
6666

67-
Only applies to `file_path` arguments. When set, the framework parses each output file as JSON and validates it against the schema **before any reviews run**. If validation fails, `finished_step` returns the error immediately -- reviews are skipped entirely. This is a hard gate, not a soft review. Use for structured outputs where format correctness is non-negotiable.
67+
Only applies to `file_path` arguments. When set, the framework parses each output file (JSON or YAML -- both are supported since YAML is a JSON superset) and validates it against the schema **before any reviews run**. If validation fails, `finished_step` returns the error immediately -- reviews are skipped entirely. This is a hard gate, not a soft review. Use for structured outputs where format correctness is non-negotiable.
6868

6969
---
7070

@@ -152,7 +152,7 @@ A map of step_argument names to output configuration. When the agent calls `fini
152152

153153
1. **Completeness**: All required outputs must be present. No unknown output names allowed.
154154
2. **Type validation**: `file_path` values must point to existing files. `string` values must be strings.
155-
3. **JSON schema**: If the step_argument has `json_schema`, file contents are parsed and validated. Failures are returned immediately; reviews are skipped.
155+
3. **JSON schema**: If the step_argument has `json_schema`, file contents are parsed (JSON or YAML) and validated. Failures are returned immediately; reviews are skipped.
156156
4. **Quality reviews**: Dynamic reviews from the output ref and step_argument, plus .deepreview rules.
157157

158158
**Important**: The agent must provide ALL required outputs on every `finished_step` call, even outputs whose files have not changed since a previous attempt. The framework re-validates everything each time.

specs/deepwork/jobs/JOBS-REQ-004-quality-review-system.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ The quality review system evaluates step outputs against defined quality criteri
1515
### JOBS-REQ-004.2: JSON Schema Validation
1616

1717
1. `validate_json_schemas()` MUST check all `file_path` type outputs that have a `json_schema` defined on their `StepArgument`.
18-
2. For each such output, the file content MUST be parsed as JSON and validated against the schema.
19-
3. If JSON parsing fails, the error MUST be included in the returned error list.
18+
2. For each such output, the file content MUST be parsed as YAML (which is a superset of JSON) and validated against the schema.
19+
3. If file parsing fails, the error MUST be included in the returned error list.
2020
4. If schema validation fails, the error MUST be included in the returned error list.
2121
5. Files that do not exist MUST be skipped (not treated as errors by this function).
2222
6. `run_quality_gate()` MUST run JSON schema validation before building review rules. If schema errors exist, it MUST return an error message listing them without proceeding to reviews.

src/deepwork/jobs/mcp/quality_gate.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77

88
from __future__ import annotations
99

10-
import json
1110
import logging
1211
from pathlib import Path
1312

13+
import yaml
14+
1415
from deepwork.deepschema.review_bridge import generate_review_rules as gen_schema_rules
1516
from deepwork.jobs.mcp.schemas import ArgumentValue
1617
from deepwork.jobs.parser import (
@@ -60,9 +61,9 @@ def validate_json_schemas(
6061
continue
6162
try:
6263
content = full_path.read_text(encoding="utf-8")
63-
parsed = json.loads(content)
64-
except (json.JSONDecodeError, UnicodeDecodeError) as e:
65-
errors.append(f"Output '{output_name}' file '{path}': failed to parse as JSON: {e}")
64+
parsed = yaml.safe_load(content)
65+
except (yaml.YAMLError, UnicodeDecodeError) as e:
66+
errors.append(f"Output '{output_name}' file '{path}': failed to parse: {e}")
6667
continue
6768

6869
try:

tests/unit/jobs/mcp/test_quality_gate.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,22 +92,22 @@ def test_passes_when_json_schema_validates(self, tmp_path: Path) -> None:
9292

9393
# THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.2.3).
9494
# YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES
95-
def test_fails_when_json_is_invalid(self, tmp_path: Path) -> None:
96-
"""Non-JSON content in the output file produces an error."""
95+
def test_fails_when_file_is_unparseable(self, tmp_path: Path) -> None:
96+
"""Unparseable content in the output file produces an error."""
9797
schema = {"type": "object"}
9898
arg = StepArgument(
99-
name="data", description="JSON data", type="file_path", json_schema=schema
99+
name="data", description="data file", type="file_path", json_schema=schema
100100
)
101101
output_ref = StepOutputRef(argument_name="data", required=True)
102102
step = WorkflowStep(name="generate", outputs={"data": output_ref})
103103
job, _ = _make_job(tmp_path, [arg], step)
104104

105-
data_file = tmp_path / "data.json"
106-
data_file.write_text("not json {{{")
105+
data_file = tmp_path / "data.yml"
106+
data_file.write_text(":\n bad: [yaml\n unclosed")
107107

108-
errors = validate_json_schemas({"data": "data.json"}, step, job, tmp_path)
108+
errors = validate_json_schemas({"data": "data.yml"}, step, job, tmp_path)
109109
assert len(errors) == 1
110-
assert "failed to parse as JSON" in errors[0]
110+
assert "failed to parse" in errors[0]
111111

112112
# THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.2.4).
113113
# YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES
@@ -147,6 +147,51 @@ def test_skips_string_type_arguments(self, tmp_path: Path) -> None:
147147
errors = validate_json_schemas({"data": "just a string value"}, step, job, tmp_path)
148148
assert errors == []
149149

150+
# THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.2.2).
151+
# YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES
152+
def test_passes_when_yaml_file_matches_schema(self, tmp_path: Path) -> None:
153+
"""YAML files should be parsed with yaml.safe_load, not json.loads."""
154+
schema = {
155+
"type": "object",
156+
"properties": {"title": {"type": "string"}},
157+
"required": ["title"],
158+
}
159+
arg = StepArgument(
160+
name="data", description="YAML data", type="file_path", json_schema=schema
161+
)
162+
output_ref = StepOutputRef(argument_name="data", required=True)
163+
step = WorkflowStep(name="generate", outputs={"data": output_ref})
164+
job, _ = _make_job(tmp_path, [arg], step)
165+
166+
data_file = tmp_path / "data.yml"
167+
data_file.write_text("title: Hello\n")
168+
169+
errors = validate_json_schemas({"data": "data.yml"}, step, job, tmp_path)
170+
assert errors == []
171+
172+
# THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.2.4).
173+
# YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES
174+
def test_fails_when_yaml_file_violates_schema(self, tmp_path: Path) -> None:
175+
"""YAML files that don't match the schema produce errors."""
176+
schema = {
177+
"type": "object",
178+
"properties": {"count": {"type": "integer"}},
179+
"required": ["count"],
180+
}
181+
arg = StepArgument(
182+
name="data", description="YAML data", type="file_path", json_schema=schema
183+
)
184+
output_ref = StepOutputRef(argument_name="data", required=True)
185+
step = WorkflowStep(name="generate", outputs={"data": output_ref})
186+
job, _ = _make_job(tmp_path, [arg], step)
187+
188+
data_file = tmp_path / "data.yaml"
189+
data_file.write_text("count: not_an_integer\n")
190+
191+
errors = validate_json_schemas({"data": "data.yaml"}, step, job, tmp_path)
192+
assert len(errors) == 1
193+
assert "schema validation failed" in errors[0]
194+
150195

151196
# ---------------------------------------------------------------------------
152197
# TestBuildDynamicReviewRules
@@ -440,7 +485,7 @@ def test_returns_none_when_no_review_blocks_defined(self, tmp_path: Path) -> Non
440485

441486
assert result is None
442487

443-
# THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-001.4.8).
488+
# THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.2.6).
444489
# YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES
445490
def test_returns_feedback_when_json_schema_fails(self, tmp_path: Path) -> None:
446491
"""Schema validation failure returns an error string without running reviews."""

0 commit comments

Comments
 (0)