fix: enforce strict Codex output schemas - #14
Conversation
📝 WalkthroughWalkthroughThe pull request tightens two structured-output JSON schemas, adds recursive schema validation for workflow-referenced schemas, and invokes that validator from the test script. ChangesStructured output schema validation
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
schemas/maintenance-plan.schema.json (1)
10-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate
instructionDigestsschema block — extract to$defs.The identical
instructionDigestsobject (shared/phaseTemplate/eventContract with sha256 pattern) is spelled out twice: once underagentContract(Lines 16-25) and once undercompletionAssessment(Lines 104-112). Any future change to the digest pattern or required fields must be applied in both places or the two contract checkpoints silently drift apart.♻️ Proposed refactor using a shared definition
+ "$defs": { + "instructionDigests": { + "type": "object", + "additionalProperties": false, + "required": ["shared", "phaseTemplate", "eventContract"], + "properties": { + "shared": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "phaseTemplate": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "eventContract": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"} + } + } + }, "properties": { ... "agentContract": { "type": "object", "additionalProperties": false, "required": ["contractVersion", "instructionDigests"], "properties": { "contractVersion": {"type": "integer", "const": 1}, - "instructionDigests": { - "type": "object", - "additionalProperties": false, - "required": ["shared", "phaseTemplate", "eventContract"], - "properties": { - "shared": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, - "phaseTemplate": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, - "eventContract": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"} - } - } + "instructionDigests": {"$ref": "`#/`$defs/instructionDigests"} } }, ... "completionAssessment": { ... - "instructionDigests": { - "type": "object", - "additionalProperties": false, - "required": ["shared", "phaseTemplate", "eventContract"], - "properties": { - "shared": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, - "phaseTemplate": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, - "eventContract": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"} - } - }, + "instructionDigests": {"$ref": "`#/`$defs/instructionDigests"}, }Note:
scripts/validate-structured-output-schemas(context snippet 3) recurses into every dict node regardless of$refindirection, so a$defsentry following the same closed-object pattern would still pass its checks. Please confirm the exact Codex structured-output subset in use (codex-version: 0.145.0, per.github/workflows/maintenance-consumer.yml) actually supports$ref/$defsbefore adopting, since unsupported keywords would break the live consumer run.Also applies to: 98-113
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@schemas/maintenance-plan.schema.json` around lines 10 - 27, Extract the duplicated instructionDigests object into a shared $defs entry and replace both agentContract and completionAssessment copies with references to it, preserving the required fields, closed-object constraint, and SHA-256 pattern. Before applying the refactor, verify that Codex structured-output version 0.145.0 supports $ref/$defs; if unsupported, retain the duplicated schema and avoid introducing unsupported keywords.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@schemas/maintenance-plan.schema.json`:
- Around line 28-51: Update the evidence schema to reject duplicate captureId
values, not merely duplicate full evidence objects. Add uniqueItems: true and a
capture-level uniqueness constraint using the project’s supported schema
mechanism, while preserving the existing four-item bound and allowed captureId
enum.
In `@scripts/validate-structured-output-schemas`:
- Line 14: Make workflow schema discovery format-complete by updating
OUTPUT_SCHEMA_RE to accept normal whitespace around the --output-schema argument
and the static argument forms used by workflows, not only the exact comma-quote
sequence. Update the workflow globbing logic at the related discovery sites to
scan both .yml and .yaml files, while preserving schema validation across all
discovered files.
- Around line 30-43: Update the schema validation logic around the
declared_type/types handling so any schema containing properties, required, or
additionalProperties is treated as object-shaped and must explicitly include
"object" in types. Reject such schemas when "object" is absent, while preserving
the existing strict additionalProperties, properties, and required validations
for explicitly typed objects.
---
Nitpick comments:
In `@schemas/maintenance-plan.schema.json`:
- Around line 10-27: Extract the duplicated instructionDigests object into a
shared $defs entry and replace both agentContract and completionAssessment
copies with references to it, preserving the required fields, closed-object
constraint, and SHA-256 pattern. Before applying the refactor, verify that Codex
structured-output version 0.145.0 supports $ref/$defs; if unsupported, retain
the duplicated schema and avoid introducing unsupported keywords.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b9b946bd-c07a-4d77-b82e-52e171602ee2
📒 Files selected for processing (4)
schemas/agent-completion-assessment.schema.jsonschemas/maintenance-plan.schema.jsonscripts/test.shscripts/validate-structured-output-schemas
| "evidence": { | ||
| "type": "array", | ||
| "minItems": 4, | ||
| "maxItems": 4, | ||
| "items": { | ||
| "type": "object", | ||
| "required": ["captureId", "digest", "locator"], | ||
| "additionalProperties": false, | ||
| "required": ["captureId", "digest", "claim", "locator"], | ||
| "properties": { | ||
| "captureId": {"enum": ["php_bin_policy_selector", "php_bin_state", "support_policy", "policy_invariants"]}, | ||
| "captureId": {"type": "string", "enum": ["php_bin_policy_selector", "php_bin_state", "support_policy", "policy_invariants"]}, | ||
| "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, | ||
| "claim": {"type": "string"}, | ||
| "locator": { | ||
| "type": "object", | ||
| "additionalProperties": false, | ||
| "required": ["kind", "value"], | ||
| "properties": { | ||
| "kind": {"const": "json_pointer"}, | ||
| "kind": {"type": "string", "const": "json_pointer"}, | ||
| "value": {"type": "string", "pattern": "^/"} | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)schemas/maintenance-plan\.schema\.json$|maintenance-plan' || true
echo "== schema excerpt =="
if [ -f schemas/maintenance-plan.schema.json ]; then
cat -n schemas/maintenance-plan.schema.json | sed -n '1,120p'
fi
echo "== uniqueItems occurrences =="
if [ -f schemas/maintenance-plan.schema.json ]; then
python3 - <<'PY'
import json
p='schemas/maintenance-plan.schema.json'
data=json.load(open(p))
for path, val in json_paths({'$': data}, '$'):
if isinstance(val, dict) and val.get('uniqueItems') is True:
print(path)
PY
fi
echo "== captureId occurrences in repo =="
rg -n '"captureId"|captureId' -S . || true
echo "== validator behavior probe with python jsonschema if available =="
python3 - <<'PY'
import json, sys
try:
from jsonschema import validate, ValidationError
except Exception as e:
print("jsonschema unavailable:", type(e).__name__, str(e))
sys.exit(0)
schema = json.load(open('schemas/maintenance-plan.schema.json'))
duplicate = {
"evidence": [
{"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","claim":"first","locator":{"kind":"json_pointer","value":"/a"}},
{"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000001","claim":"second","locator":{"kind":"json_pointer","value":"/a"}},
{"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","claim":"third","locator":{"kind":"json_pointer","value":"/a"}},
{"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000003","claim":"fourth","locator":{"kind":"json_pointer","value":"/a"}},
],
}
try:
validate(duplicate, schema)
print("duplicate-first captureId VALID without uniqueItems")
except ValidationError as err:
print("duplicate-first captureId INVALID:", str(err))
with_unique = json.loads(json.dumps(schema))
with_unique['properties']['evidence']['uniqueItems'] = True
try:
validate(duplicate, with_unique)
print("duplicate-first captureId VALID with uniqueItems")
except ValidationError as err:
print("duplicate-first captureId INVALID with uniqueItems:", str(err))
unique = duplicate.copy()
unique["evidence"] = [
{"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","claim":"first","locator":{"kind":"json_pointer","value":"/a"}},
{"captureId":"php_bin_state","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000001","claim":"second","locator":{"kind":"json_pointer","value":"/b"}},
{"captureId":"support_policy","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","claim":"third","locator":{"kind":"json_pointer","value":"/c"}},
{"captureId":"policy_invariants","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000003","claim":"fourth","locator":{"kind":"json_pointer","value":"/d"}},
]
try:
validate(unique, with_unique)
print("all-four distinct captureIds VALID with uniqueItems")
except ValidationError as err:
print("all-four distinct captureIds INVALID with uniqueItems:", str(err))
PYRepository: Bigpixelrocket/mise-php
Length of output: 6722
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json
schema = json.load(open('schemas/maintenance-plan.schema.json'))
def scan(obj, path='$'):
if isinstance(obj, dict):
yield path, obj
for k, v in obj.items():
yield from scan(v, f'{path}.{k}')
elif isinstance(obj, list):
for i, v in enumerate(obj):
yield from scan(v, f'{path}[{i}]')
for path, val in scan(schema):
if isinstance(val, dict) and val.get('uniqueItems') is True:
parent_path = path.rsplit('.', 1)[0] if '.' in path else '/properties/' + path
print(path, 'in', parent_path)
required_capture = schema['properties']['evidence']['items']['properties']['captureId'].get('enum')
print(
"evidence length:", schema['properties']['evidence'].get('minItems'), schema['properties']['evidence'].get('maxItems'),
"captureIds:", required_capture,
"evidence uniqueItems:", schema['properties']['evidence'].get('uniqueItems')
)
items_props = schema['properties']['evidence']['items']['properties']
print("evidence item required:", schema['properties']['evidence']['items']['required'])
PY
echo
if python3 - <<'PY'
import importlib.util
print(importlib.util.find_spec("jsonschema"))
PY
then
python3 - <<'PY'
from jsonschema import validate, ValidationError
import json
schema = json.load(open('schemas/maintenance-plan.schema.json'))
duplicate = {
"evidence": [
{"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","claim":"first","locator":{"kind":"json_pointer","value":"/a"}},
{"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000001","claim":"second","locator":{"kind":"json_pointer","value":"/a"}},
{"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","claim":"third","locator":{"kind":"json_pointer","value":"/a"}},
{"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000003","claim":"fourth","locator":{"kind":"json_pointer","value":"/a"}},
],
}
try:
validate(duplicate, schema)
print("duplicate-first captureId VALID without uniqueItems")
except ValidationError as err:
print("duplicate-first captureId INVALID:", str(err))
with_unique = json.loads(json.dumps(schema))
with_unique['properties']['evidence']['uniqueItems'] = True
try:
validate(duplicate, with_unique)
print("duplicate-first captureId VALID with uniqueItems")
except ValidationError as err:
print("duplicate-first captureId INVALID with uniqueItems:", str(err))
unique = duplicate.copy()
unique["evidence"] = [
{"captureId":"php_bin_policy_selector","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","claim":"first","locator":{"kind":"json_pointer","value":"/a"}},
{"captureId":"php_bin_state","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000001","claim":"second","locator":{"kind":"json_pointer","value":"/b"}},
{"captureId":"support_policy","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000002","claim":"third","locator":{"kind":"json_pointer","value":"/c"}},
{"captureId":"policy_invariants","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000003","claim":"fourth","locator":{"kind":"json_pointer","value":"/d"}},
]
try:
validate(unique, with_unique)
print("all-four distinct captureIds VALID with uniqueItems")
except ValidationError as err:
print("all-four distinct captureIds INVALID with uniqueItems:", str(err))
PY
else
echo "jsonschema library not available"
echo "Behavioral summary remains deterministic: JSON Schema uniqueItems compares array items by JSON equality; two evidence objects differ because their non-captureId fields (digest/claim/locator) differ, so uniqueItems alone does not enforce distinct captureIds. It only prevents exact duplicate evidence entries."
fiRepository: Bigpixelrocket/mise-php
Length of output: 687
Prevent duplicate evidence captureIds in the schema.
evidence is bounded to 4 items from a 4-value captureId enum, but without uniqueItems plus capture-level uniqueness it still accepts entries that omit a required evidence type. Add uniqueItems: true and another check that forces distinct captureId values, for example via a separate requiresCaptureIds keyword, enum constraint on each item, or an unevaluatedProperties rule.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@schemas/maintenance-plan.schema.json` around lines 28 - 51, Update the
evidence schema to reject duplicate captureId values, not merely duplicate full
evidence objects. Add uniqueItems: true and a capture-level uniqueness
constraint using the project’s supported schema mechanism, while preserving the
existing four-item bound and allowed captureId enum.
|
|
||
|
|
||
| ROOT = pathlib.Path(__file__).resolve().parents[1] | ||
| OUTPUT_SCHEMA_RE = re.compile(r'--output-schema","([^"]+\.json)"') |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Make workflow schema discovery format-complete.
The .yml-only glob and exact --output-schema","…" regex can miss .yaml workflows or normally spaced command arguments; validation can still pass if another schema is discovered. Scan both extensions and support the static argument forms used by workflows.
Also applies to: 53-55
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/validate-structured-output-schemas` at line 14, Make workflow schema
discovery format-complete by updating OUTPUT_SCHEMA_RE to accept normal
whitespace around the --output-schema argument and the static argument forms
used by workflows, not only the exact comma-quote sequence. Update the workflow
globbing logic at the related discovery sites to scan both .yml and .yaml files,
while preserving schema validation across all discovered files.
| declared_type = node.get("type") | ||
| types = {declared_type} if isinstance(declared_type, str) else set(declared_type or []) | ||
| if "object" in types: | ||
| if node.get("additionalProperties") is not False: | ||
| fail(f"{location} must set additionalProperties to false") | ||
| properties = node.get("properties") | ||
| if not isinstance(properties, dict): | ||
| fail(f"{location} must declare object properties") | ||
| required = node.get("required") | ||
| if not isinstance(required, list) or set(required) != set(properties): | ||
| fail(f"{location} must require every declared property exactly once") | ||
| if len(required) != len(set(required)): | ||
| fail(f"{location} contains duplicate required properties") | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Detect object-shaped schemas that omit type: object.
At Line 32, validation runs only for explicitly typed objects. JSON Schema allows properties, required, or additionalProperties without type, so an open object can bypass this strictness check. Reject object keywords unless type includes object.
Proposed fix
+OBJECT_KEYWORDS = frozenset(
+ {"properties", "required", "additionalProperties", "patternProperties",
+ "propertyNames", "dependentRequired", "dependentSchemas",
+ "unevaluatedProperties", "minProperties", "maxProperties"}
+)
+
def validate_node(node: Any, location: str) -> None:
...
declared_type = node.get("type")
types = {declared_type} if isinstance(declared_type, str) else set(declared_type or [])
+ if OBJECT_KEYWORDS.intersection(node) and "object" not in types:
+ fail(f"{location} uses object keywords without type: object")
if "object" in types:📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| declared_type = node.get("type") | |
| types = {declared_type} if isinstance(declared_type, str) else set(declared_type or []) | |
| if "object" in types: | |
| if node.get("additionalProperties") is not False: | |
| fail(f"{location} must set additionalProperties to false") | |
| properties = node.get("properties") | |
| if not isinstance(properties, dict): | |
| fail(f"{location} must declare object properties") | |
| required = node.get("required") | |
| if not isinstance(required, list) or set(required) != set(properties): | |
| fail(f"{location} must require every declared property exactly once") | |
| if len(required) != len(set(required)): | |
| fail(f"{location} contains duplicate required properties") | |
| OBJECT_KEYWORDS = frozenset( | |
| { | |
| "properties", | |
| "required", | |
| "additionalProperties", | |
| "patternProperties", | |
| "propertyNames", | |
| "dependentRequired", | |
| "dependentSchemas", | |
| "unevaluatedProperties", | |
| "minProperties", | |
| "maxProperties", | |
| } | |
| ) | |
| declared_type = node.get("type") | |
| types = {declared_type} if isinstance(declared_type, str) else set(declared_type or []) | |
| if OBJECT_KEYWORDS.intersection(node) and "object" not in types: | |
| fail(f"{location} uses object keywords without type: object") | |
| if "object" in types: | |
| if node.get("additionalProperties") is not False: | |
| fail(f"{location} must set additionalProperties to false") | |
| properties = node.get("properties") | |
| if not isinstance(properties, dict): | |
| fail(f"{location} must declare object properties") | |
| required = node.get("required") | |
| if not isinstance(required, list) or set(required) != set(properties): | |
| fail(f"{location} must require every declared property exactly once") | |
| if len(required) != len(set(required)): | |
| fail(f"{location} contains duplicate required properties") |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/validate-structured-output-schemas` around lines 30 - 43, Update the
schema validation logic around the declared_type/types handling so any schema
containing properties, required, or additionalProperties is treated as
object-shaped and must explicitly include "object" in types. Reject such schemas
when "object" is absent, while preserving the existing strict
additionalProperties, properties, and required validations for explicitly typed
objects.
Summary
Verification
Security and licensing