From 339ef8a100bd4bae274be0122d8b1d2fb523227e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 20:20:30 +0000 Subject: [PATCH 1/3] Add rule_library with YAML schema validation rule Create a top-level rule_library folder for example rules. Add yaml-schema-validation rule that: - Triggers on all .yml and .yaml files - Detects $schema declarations (URLs or local paths) - Validates files against their declared JSON Schema - Returns pass for valid files or blocking JSON with error details - Uses compare_to: prompt Includes validation script and test examples. --- rule_library/examples/invalid-example.yaml | 3 + rule_library/examples/test-schema.json | 13 ++ rule_library/examples/valid-example.yaml | 3 + rule_library/scripts/validate_yaml_schema.py | 210 +++++++++++++++++++ rule_library/yaml-schema-validation.md | 59 ++++++ 5 files changed, 288 insertions(+) create mode 100644 rule_library/examples/invalid-example.yaml create mode 100644 rule_library/examples/test-schema.json create mode 100644 rule_library/examples/valid-example.yaml create mode 100755 rule_library/scripts/validate_yaml_schema.py create mode 100644 rule_library/yaml-schema-validation.md diff --git a/rule_library/examples/invalid-example.yaml b/rule_library/examples/invalid-example.yaml new file mode 100644 index 00000000..351c4e7b --- /dev/null +++ b/rule_library/examples/invalid-example.yaml @@ -0,0 +1,3 @@ +$schema: ./test-schema.json +name: example +version: 123 diff --git a/rule_library/examples/test-schema.json b/rule_library/examples/test-schema.json new file mode 100644 index 00000000..74177cc7 --- /dev/null +++ b/rule_library/examples/test-schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "type": "object", + "required": ["name", "version"], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + } +} diff --git a/rule_library/examples/valid-example.yaml b/rule_library/examples/valid-example.yaml new file mode 100644 index 00000000..6340da2b --- /dev/null +++ b/rule_library/examples/valid-example.yaml @@ -0,0 +1,3 @@ +$schema: ./test-schema.json +name: example +version: "1.0.0" diff --git a/rule_library/scripts/validate_yaml_schema.py b/rule_library/scripts/validate_yaml_schema.py new file mode 100755 index 00000000..cf30694c --- /dev/null +++ b/rule_library/scripts/validate_yaml_schema.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +""" +Validate YAML files against their declared JSON Schema. + +Looks for a $schema declaration at the top of YAML files and validates +the file content against that schema. The schema can be a URL or a local path. + +Exit codes: + 0 - Validation passed (or no schema declared) + 1 - Validation failed (outputs JSON with failure details) + 2 - Error fetching/loading schema +""" + +import json +import sys +import urllib.request +import urllib.error +from pathlib import Path + +try: + import yaml +except ImportError: + print(json.dumps({ + "status": "error", + "message": "PyYAML is not installed. Run: pip install pyyaml" + })) + sys.exit(2) + +try: + import jsonschema + from jsonschema import Draft7Validator, ValidationError +except ImportError: + print(json.dumps({ + "status": "error", + "message": "jsonschema is not installed. Run: pip install jsonschema" + })) + sys.exit(2) + + +def load_yaml_file(file_path: str) -> tuple[dict | list | None, str | None]: + """Load a YAML file and return its contents.""" + try: + with open(file_path, "r", encoding="utf-8") as f: + content = yaml.safe_load(f) + return content, None + except yaml.YAMLError as e: + return None, f"Invalid YAML syntax: {e}" + except FileNotFoundError: + return None, f"File not found: {file_path}" + except Exception as e: + return None, f"Error reading file: {e}" + + +def extract_schema_reference(content: dict) -> str | None: + """Extract the $schema reference from YAML content.""" + if not isinstance(content, dict): + return None + return content.get("$schema") + + +def fetch_schema_from_url(url: str) -> tuple[dict | None, str | None]: + """Fetch a JSON Schema from a URL.""" + try: + req = urllib.request.Request( + url, + headers={"User-Agent": "yaml-schema-validator/1.0"} + ) + with urllib.request.urlopen(req, timeout=30) as response: + schema_content = response.read().decode("utf-8") + + # Try to parse as JSON first, then YAML + try: + return json.loads(schema_content), None + except json.JSONDecodeError: + try: + return yaml.safe_load(schema_content), None + except yaml.YAMLError as e: + return None, f"Invalid schema format at URL: {e}" + + except urllib.error.URLError as e: + return None, f"Failed to fetch schema from URL: {e}" + except Exception as e: + return None, f"Error fetching schema: {e}" + + +def load_schema_from_path(schema_path: str, yaml_file_path: str) -> tuple[dict | None, str | None]: + """Load a JSON Schema from a local file path.""" + # Resolve relative paths relative to the YAML file's directory + path = Path(schema_path) + if not path.is_absolute(): + yaml_dir = Path(yaml_file_path).parent + path = yaml_dir / path + + path = path.resolve() + + if not path.exists(): + return None, f"Schema file not found: {path}" + + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + # Try to parse as JSON first, then YAML + try: + return json.loads(content), None + except json.JSONDecodeError: + try: + return yaml.safe_load(content), None + except yaml.YAMLError as e: + return None, f"Invalid schema format: {e}" + + except Exception as e: + return None, f"Error reading schema file: {e}" + + +def load_schema(schema_ref: str, yaml_file_path: str) -> tuple[dict | None, str | None]: + """Load a schema from either a URL or local path.""" + if schema_ref.startswith(("http://", "https://")): + return fetch_schema_from_url(schema_ref) + else: + return load_schema_from_path(schema_ref, yaml_file_path) + + +def validate_against_schema(content: dict, schema: dict) -> list[dict]: + """Validate content against a JSON Schema and return list of errors.""" + validator = Draft7Validator(schema) + errors = [] + + for error in sorted(validator.iter_errors(content), key=lambda e: e.path): + error_info = { + "message": error.message, + "path": "/" + "/".join(str(p) for p in error.absolute_path) if error.absolute_path else "/", + "schema_path": "/" + "/".join(str(p) for p in error.absolute_schema_path) if error.absolute_schema_path else "/", + } + + # Add the failing value if it's simple enough to display + if error.instance is not None and not isinstance(error.instance, (dict, list)): + error_info["value"] = error.instance + + errors.append(error_info) + + return errors + + +def main(): + if len(sys.argv) < 2: + print(json.dumps({ + "status": "error", + "message": "Usage: validate_yaml_schema.py " + })) + sys.exit(2) + + file_path = sys.argv[1] + + # Load the YAML file + content, error = load_yaml_file(file_path) + if error: + print(json.dumps({ + "status": "error", + "file": file_path, + "message": error + })) + sys.exit(2) + + # Check for $schema reference + schema_ref = extract_schema_reference(content) + if not schema_ref: + # No schema declared - pass silently + print(json.dumps({ + "status": "pass", + "file": file_path, + "message": "No $schema declared, skipping validation" + })) + sys.exit(0) + + # Load the schema + schema, error = load_schema(schema_ref, file_path) + if error: + print(json.dumps({ + "status": "error", + "file": file_path, + "schema": schema_ref, + "message": error + })) + sys.exit(2) + + # Validate + errors = validate_against_schema(content, schema) + + if not errors: + print(json.dumps({ + "status": "pass", + "file": file_path, + "schema": schema_ref, + "message": "Validation passed" + })) + sys.exit(0) + else: + print(json.dumps({ + "status": "fail", + "file": file_path, + "schema": schema_ref, + "error_count": len(errors), + "errors": errors + }, indent=2)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/rule_library/yaml-schema-validation.md b/rule_library/yaml-schema-validation.md new file mode 100644 index 00000000..e7950338 --- /dev/null +++ b/rule_library/yaml-schema-validation.md @@ -0,0 +1,59 @@ +--- +name: YAML Schema Validation +trigger: + - "**/*.yml" + - "**/*.yaml" +action: + command: python3 rule_library/scripts/validate_yaml_schema.py {file} + run_for: each_match +compare_to: prompt +--- +Validates YAML files against their declared JSON Schema. + +This rule triggers on any `.yml` or `.yaml` file that is modified. It looks for +a `$schema` declaration at the top of the file: + +```yaml +$schema: https://json-schema.org/draft-07/schema +# or +$schema: ./schemas/my-schema.json +``` + +## Behavior + +- **Pass**: File validates against the declared schema, or no schema is declared +- **Fail**: Returns a blocking JSON response with validation error details + +## Example Output + +On validation failure: +```json +{ + "status": "fail", + "file": "config.yaml", + "schema": "https://example.com/schemas/config.json", + "error_count": 2, + "errors": [ + { + "message": "'name' is a required property", + "path": "/", + "schema_path": "/required" + }, + { + "message": "42 is not of type 'string'", + "path": "/version", + "schema_path": "/properties/version/type", + "value": 42 + } + ] +} +``` + +## Requirements + +The validation script requires: +- Python 3.10+ +- `pyyaml` package +- `jsonschema` package + +Install with: `pip install pyyaml jsonschema` From 67352eb084c5cebdb5800089f7d652375cf975e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 20:35:12 +0000 Subject: [PATCH 2/3] Restructure rule_library and add JSON file support - Move rule to rule_library/json_validation/ subfolder - Add JSON file support (.json in addition to .yml/.yaml) - Optimize validation with quick text scan for $schema before parsing - Only fully parse files that have a schema declaration - Add JSON examples (valid and invalid) --- rule_library/examples/test-schema.json | 13 -- rule_library/examples/valid-example.yaml | 3 - .../examples/invalid-example.json | 6 + .../examples/invalid-example.yaml | 2 +- .../json_validation/examples/test-schema.json | 22 +++ .../examples/valid-example.json | 6 + .../examples/valid-example.yaml | 4 + .../json_validation/schema-validation.md | 79 +++++++++++ .../scripts/validate_schema.py} | 133 +++++++++++++----- rule_library/yaml-schema-validation.md | 59 -------- 10 files changed, 215 insertions(+), 112 deletions(-) delete mode 100644 rule_library/examples/test-schema.json delete mode 100644 rule_library/examples/valid-example.yaml create mode 100644 rule_library/json_validation/examples/invalid-example.json rename rule_library/{ => json_validation}/examples/invalid-example.yaml (70%) create mode 100644 rule_library/json_validation/examples/test-schema.json create mode 100644 rule_library/json_validation/examples/valid-example.json create mode 100644 rule_library/json_validation/examples/valid-example.yaml create mode 100644 rule_library/json_validation/schema-validation.md rename rule_library/{scripts/validate_yaml_schema.py => json_validation/scripts/validate_schema.py} (54%) delete mode 100644 rule_library/yaml-schema-validation.md diff --git a/rule_library/examples/test-schema.json b/rule_library/examples/test-schema.json deleted file mode 100644 index 74177cc7..00000000 --- a/rule_library/examples/test-schema.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft-07/schema", - "type": "object", - "required": ["name", "version"], - "properties": { - "name": { - "type": "string" - }, - "version": { - "type": "string" - } - } -} diff --git a/rule_library/examples/valid-example.yaml b/rule_library/examples/valid-example.yaml deleted file mode 100644 index 6340da2b..00000000 --- a/rule_library/examples/valid-example.yaml +++ /dev/null @@ -1,3 +0,0 @@ -$schema: ./test-schema.json -name: example -version: "1.0.0" diff --git a/rule_library/json_validation/examples/invalid-example.json b/rule_library/json_validation/examples/invalid-example.json new file mode 100644 index 00000000..386f9d5a --- /dev/null +++ b/rule_library/json_validation/examples/invalid-example.json @@ -0,0 +1,6 @@ +{ + "$schema": "./test-schema.json", + "name": "", + "version": "not-a-version", + "extra_field": "not allowed" +} diff --git a/rule_library/examples/invalid-example.yaml b/rule_library/json_validation/examples/invalid-example.yaml similarity index 70% rename from rule_library/examples/invalid-example.yaml rename to rule_library/json_validation/examples/invalid-example.yaml index 351c4e7b..b76d9967 100644 --- a/rule_library/examples/invalid-example.yaml +++ b/rule_library/json_validation/examples/invalid-example.yaml @@ -1,3 +1,3 @@ $schema: ./test-schema.json -name: example +name: my-project version: 123 diff --git a/rule_library/json_validation/examples/test-schema.json b/rule_library/json_validation/examples/test-schema.json new file mode 100644 index 00000000..aee0164f --- /dev/null +++ b/rule_library/json_validation/examples/test-schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["name", "version"], + "properties": { + "$schema": { + "type": "string" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "description": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/rule_library/json_validation/examples/valid-example.json b/rule_library/json_validation/examples/valid-example.json new file mode 100644 index 00000000..c98b41b6 --- /dev/null +++ b/rule_library/json_validation/examples/valid-example.json @@ -0,0 +1,6 @@ +{ + "$schema": "./test-schema.json", + "name": "my-project", + "version": "2.0.0", + "description": "A sample JSON project" +} diff --git a/rule_library/json_validation/examples/valid-example.yaml b/rule_library/json_validation/examples/valid-example.yaml new file mode 100644 index 00000000..65491749 --- /dev/null +++ b/rule_library/json_validation/examples/valid-example.yaml @@ -0,0 +1,4 @@ +$schema: ./test-schema.json +name: my-project +version: "1.0.0" +description: A sample project diff --git a/rule_library/json_validation/schema-validation.md b/rule_library/json_validation/schema-validation.md new file mode 100644 index 00000000..88bea756 --- /dev/null +++ b/rule_library/json_validation/schema-validation.md @@ -0,0 +1,79 @@ +--- +name: Schema Validation +trigger: + - "**/*.yml" + - "**/*.yaml" + - "**/*.json" +action: + command: python3 rule_library/json_validation/scripts/validate_schema.py {file} + run_for: each_match +compare_to: prompt +--- +Validates YAML and JSON files against their declared JSON Schema. + +This rule triggers on any `.yml`, `.yaml`, or `.json` file that is modified. It +performs a quick text scan for a `$schema` declaration before doing any parsing. +Only files with a schema reference are fully parsed and validated. + +## Schema Declaration + +**YAML files:** +```yaml +$schema: https://json-schema.org/draft-07/schema +# or +$schema: ./schemas/my-schema.json +``` + +**JSON files:** +```json +{ + "$schema": "https://json-schema.org/draft-07/schema", + "name": "example" +} +``` + +## Behavior + +1. **Quick scan**: Searches first 4KB for `$schema` pattern (no parsing) +2. **Skip if none**: Files without schema declaration pass immediately +3. **Full parse**: Only files with schema are fully parsed +4. **Validate**: Content validated against the declared schema + +### Exit Codes + +- **0 (pass)**: File validates against schema, or no schema declared +- **1 (fail)**: Validation failed - returns blocking JSON with error details +- **2 (error)**: Could not load schema or parse file + +## Example Output + +On validation failure: +```json +{ + "status": "fail", + "file": "config.json", + "schema": "https://example.com/schemas/config.json", + "error_count": 2, + "errors": [ + { + "message": "'name' is a required property", + "path": "/", + "schema_path": "/required" + }, + { + "message": "42 is not of type 'string'", + "path": "/version", + "schema_path": "/properties/version/type", + "value": 42 + } + ] +} +``` + +## Requirements + +- Python 3.10+ +- `jsonschema` package (required) +- `pyyaml` package (required for YAML files) + +Install with: `pip install jsonschema pyyaml` diff --git a/rule_library/scripts/validate_yaml_schema.py b/rule_library/json_validation/scripts/validate_schema.py similarity index 54% rename from rule_library/scripts/validate_yaml_schema.py rename to rule_library/json_validation/scripts/validate_schema.py index cf30694c..ed944832 100755 --- a/rule_library/scripts/validate_yaml_schema.py +++ b/rule_library/json_validation/scripts/validate_schema.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 """ -Validate YAML files against their declared JSON Schema. +Validate YAML and JSON files against their declared JSON Schema. -Looks for a $schema declaration at the top of YAML files and validates -the file content against that schema. The schema can be a URL or a local path. +This script first performs a quick text search for $schema in the file. +Only if a schema reference is found does it fully parse the file and validate. + +Supported file types: .yml, .yaml, .json Exit codes: 0 - Validation passed (or no schema declared) @@ -12,6 +14,7 @@ """ import json +import re import sys import urllib.request import urllib.error @@ -20,15 +23,11 @@ try: import yaml except ImportError: - print(json.dumps({ - "status": "error", - "message": "PyYAML is not installed. Run: pip install pyyaml" - })) - sys.exit(2) + yaml = None try: import jsonschema - from jsonschema import Draft7Validator, ValidationError + from jsonschema import Draft7Validator except ImportError: print(json.dumps({ "status": "error", @@ -37,12 +36,59 @@ sys.exit(2) -def load_yaml_file(file_path: str) -> tuple[dict | list | None, str | None]: - """Load a YAML file and return its contents.""" +# Pattern to quickly detect $schema in file content without full parsing +# Matches both JSON ("$schema": "...") and YAML ($schema: ...) +SCHEMA_PATTERN = re.compile( + r'''["']?\$schema["']?\s*[:=]\s*["']?([^"'\s,}\]]+)''', + re.IGNORECASE +) + + +def quick_detect_schema(file_path: str) -> str | None: + """ + Quickly scan file for $schema declaration without full parsing. + Returns the schema reference if found, None otherwise. + """ try: with open(file_path, "r", encoding="utf-8") as f: - content = yaml.safe_load(f) - return content, None + # Read first 4KB - schema should be near the top + content = f.read(4096) + + match = SCHEMA_PATTERN.search(content) + if match: + return match.group(1).rstrip("'\"") + return None + + except Exception: + return None + + +def parse_file(file_path: str) -> tuple[dict | list | None, str | None]: + """Parse a YAML or JSON file and return its contents.""" + path = Path(file_path) + suffix = path.suffix.lower() + + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + if suffix == ".json": + return json.loads(content), None + elif suffix in (".yml", ".yaml"): + if yaml is None: + return None, "PyYAML is not installed. Run: pip install pyyaml" + return yaml.safe_load(content), None + else: + # Try JSON first, then YAML + try: + return json.loads(content), None + except json.JSONDecodeError: + if yaml: + return yaml.safe_load(content), None + return None, f"Unsupported file type: {suffix}" + + except json.JSONDecodeError as e: + return None, f"Invalid JSON syntax: {e}" except yaml.YAMLError as e: return None, f"Invalid YAML syntax: {e}" except FileNotFoundError: @@ -52,7 +98,7 @@ def load_yaml_file(file_path: str) -> tuple[dict | list | None, str | None]: def extract_schema_reference(content: dict) -> str | None: - """Extract the $schema reference from YAML content.""" + """Extract the $schema reference from parsed content.""" if not isinstance(content, dict): return None return content.get("$schema") @@ -63,7 +109,7 @@ def fetch_schema_from_url(url: str) -> tuple[dict | None, str | None]: try: req = urllib.request.Request( url, - headers={"User-Agent": "yaml-schema-validator/1.0"} + headers={"User-Agent": "schema-validator/1.0"} ) with urllib.request.urlopen(req, timeout=30) as response: schema_content = response.read().decode("utf-8") @@ -72,10 +118,12 @@ def fetch_schema_from_url(url: str) -> tuple[dict | None, str | None]: try: return json.loads(schema_content), None except json.JSONDecodeError: - try: - return yaml.safe_load(schema_content), None - except yaml.YAMLError as e: - return None, f"Invalid schema format at URL: {e}" + if yaml: + try: + return yaml.safe_load(schema_content), None + except yaml.YAMLError as e: + return None, f"Invalid schema format at URL: {e}" + return None, "Invalid JSON schema format at URL" except urllib.error.URLError as e: return None, f"Failed to fetch schema from URL: {e}" @@ -83,13 +131,13 @@ def fetch_schema_from_url(url: str) -> tuple[dict | None, str | None]: return None, f"Error fetching schema: {e}" -def load_schema_from_path(schema_path: str, yaml_file_path: str) -> tuple[dict | None, str | None]: +def load_schema_from_path(schema_path: str, source_file_path: str) -> tuple[dict | None, str | None]: """Load a JSON Schema from a local file path.""" - # Resolve relative paths relative to the YAML file's directory + # Resolve relative paths relative to the source file's directory path = Path(schema_path) if not path.is_absolute(): - yaml_dir = Path(yaml_file_path).parent - path = yaml_dir / path + source_dir = Path(source_file_path).parent + path = source_dir / path path = path.resolve() @@ -104,21 +152,23 @@ def load_schema_from_path(schema_path: str, yaml_file_path: str) -> tuple[dict | try: return json.loads(content), None except json.JSONDecodeError: - try: - return yaml.safe_load(content), None - except yaml.YAMLError as e: - return None, f"Invalid schema format: {e}" + if yaml: + try: + return yaml.safe_load(content), None + except yaml.YAMLError as e: + return None, f"Invalid schema format: {e}" + return None, "Invalid JSON schema format" except Exception as e: return None, f"Error reading schema file: {e}" -def load_schema(schema_ref: str, yaml_file_path: str) -> tuple[dict | None, str | None]: +def load_schema(schema_ref: str, source_file_path: str) -> tuple[dict | None, str | None]: """Load a schema from either a URL or local path.""" if schema_ref.startswith(("http://", "https://")): return fetch_schema_from_url(schema_ref) else: - return load_schema_from_path(schema_ref, yaml_file_path) + return load_schema_from_path(schema_ref, source_file_path) def validate_against_schema(content: dict, schema: dict) -> list[dict]: @@ -146,14 +196,25 @@ def main(): if len(sys.argv) < 2: print(json.dumps({ "status": "error", - "message": "Usage: validate_yaml_schema.py " + "message": "Usage: validate_schema.py " })) sys.exit(2) file_path = sys.argv[1] - # Load the YAML file - content, error = load_yaml_file(file_path) + # Step 1: Quick detection - scan for $schema without parsing + quick_schema = quick_detect_schema(file_path) + if not quick_schema: + # No schema found in quick scan - pass without full parsing + print(json.dumps({ + "status": "pass", + "file": file_path, + "message": "No $schema declared, skipping validation" + })) + sys.exit(0) + + # Step 2: Schema detected - now do full parsing + content, error = parse_file(file_path) if error: print(json.dumps({ "status": "error", @@ -162,10 +223,10 @@ def main(): })) sys.exit(2) - # Check for $schema reference + # Get the actual schema reference from parsed content schema_ref = extract_schema_reference(content) if not schema_ref: - # No schema declared - pass silently + # Quick scan found something but it wasn't actually a $schema field print(json.dumps({ "status": "pass", "file": file_path, @@ -173,7 +234,7 @@ def main(): })) sys.exit(0) - # Load the schema + # Step 3: Load the schema schema, error = load_schema(schema_ref, file_path) if error: print(json.dumps({ @@ -184,7 +245,7 @@ def main(): })) sys.exit(2) - # Validate + # Step 4: Validate errors = validate_against_schema(content, schema) if not errors: diff --git a/rule_library/yaml-schema-validation.md b/rule_library/yaml-schema-validation.md deleted file mode 100644 index e7950338..00000000 --- a/rule_library/yaml-schema-validation.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: YAML Schema Validation -trigger: - - "**/*.yml" - - "**/*.yaml" -action: - command: python3 rule_library/scripts/validate_yaml_schema.py {file} - run_for: each_match -compare_to: prompt ---- -Validates YAML files against their declared JSON Schema. - -This rule triggers on any `.yml` or `.yaml` file that is modified. It looks for -a `$schema` declaration at the top of the file: - -```yaml -$schema: https://json-schema.org/draft-07/schema -# or -$schema: ./schemas/my-schema.json -``` - -## Behavior - -- **Pass**: File validates against the declared schema, or no schema is declared -- **Fail**: Returns a blocking JSON response with validation error details - -## Example Output - -On validation failure: -```json -{ - "status": "fail", - "file": "config.yaml", - "schema": "https://example.com/schemas/config.json", - "error_count": 2, - "errors": [ - { - "message": "'name' is a required property", - "path": "/", - "schema_path": "/required" - }, - { - "message": "42 is not of type 'string'", - "path": "/version", - "schema_path": "/properties/version/type", - "value": 42 - } - ] -} -``` - -## Requirements - -The validation script requires: -- Python 3.10+ -- `pyyaml` package -- `jsonschema` package - -Install with: `pip install pyyaml jsonschema` From 210eee19c3ae34e71bea943f7251221fa6b0bb46 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Jan 2026 18:39:02 +0000 Subject: [PATCH 3/3] Restructure to library/jobs and library/rules with symlinks - Move job_library to library/jobs - Move rule_library to library/rules - Move json_validation rule to .deepwork/rules/ (source of truth) - Add symlink library/rules/json_validation -> .deepwork/rules/json_validation - Add symlink library/jobs/commit -> .deepwork/jobs/commit - Update AGENTS.md with new library structure and examples - Update claude.md references from library_jobs to library/jobs Library items can now be either standalone directories or symlinks to repo-specific rules/jobs that serve as good examples. --- .../examples/invalid-example.json | 0 .../examples/invalid-example.yaml | 0 .../json_validation/examples/test-schema.json | 0 .../examples/valid-example.json | 0 .../examples/valid-example.yaml | 0 .../json_validation/schema-validation.md | 2 +- .../scripts/validate_schema.py | 0 AGENTS.md | 35 +++++++++++-------- claude.md | 8 +++-- {job_library => library/jobs}/README.md | 6 ++-- library/jobs/commit | 1 + library/rules/json_validation | 1 + 12 files changed, 32 insertions(+), 21 deletions(-) rename {rule_library => .deepwork/rules}/json_validation/examples/invalid-example.json (100%) rename {rule_library => .deepwork/rules}/json_validation/examples/invalid-example.yaml (100%) rename {rule_library => .deepwork/rules}/json_validation/examples/test-schema.json (100%) rename {rule_library => .deepwork/rules}/json_validation/examples/valid-example.json (100%) rename {rule_library => .deepwork/rules}/json_validation/examples/valid-example.yaml (100%) rename {rule_library => .deepwork/rules}/json_validation/schema-validation.md (95%) rename {rule_library => .deepwork/rules}/json_validation/scripts/validate_schema.py (100%) rename {job_library => library/jobs}/README.md (88%) create mode 120000 library/jobs/commit create mode 120000 library/rules/json_validation diff --git a/rule_library/json_validation/examples/invalid-example.json b/.deepwork/rules/json_validation/examples/invalid-example.json similarity index 100% rename from rule_library/json_validation/examples/invalid-example.json rename to .deepwork/rules/json_validation/examples/invalid-example.json diff --git a/rule_library/json_validation/examples/invalid-example.yaml b/.deepwork/rules/json_validation/examples/invalid-example.yaml similarity index 100% rename from rule_library/json_validation/examples/invalid-example.yaml rename to .deepwork/rules/json_validation/examples/invalid-example.yaml diff --git a/rule_library/json_validation/examples/test-schema.json b/.deepwork/rules/json_validation/examples/test-schema.json similarity index 100% rename from rule_library/json_validation/examples/test-schema.json rename to .deepwork/rules/json_validation/examples/test-schema.json diff --git a/rule_library/json_validation/examples/valid-example.json b/.deepwork/rules/json_validation/examples/valid-example.json similarity index 100% rename from rule_library/json_validation/examples/valid-example.json rename to .deepwork/rules/json_validation/examples/valid-example.json diff --git a/rule_library/json_validation/examples/valid-example.yaml b/.deepwork/rules/json_validation/examples/valid-example.yaml similarity index 100% rename from rule_library/json_validation/examples/valid-example.yaml rename to .deepwork/rules/json_validation/examples/valid-example.yaml diff --git a/rule_library/json_validation/schema-validation.md b/.deepwork/rules/json_validation/schema-validation.md similarity index 95% rename from rule_library/json_validation/schema-validation.md rename to .deepwork/rules/json_validation/schema-validation.md index 88bea756..3e3c5919 100644 --- a/rule_library/json_validation/schema-validation.md +++ b/.deepwork/rules/json_validation/schema-validation.md @@ -5,7 +5,7 @@ trigger: - "**/*.yaml" - "**/*.json" action: - command: python3 rule_library/json_validation/scripts/validate_schema.py {file} + command: python3 .deepwork/rules/json_validation/scripts/validate_schema.py {file} run_for: each_match compare_to: prompt --- diff --git a/rule_library/json_validation/scripts/validate_schema.py b/.deepwork/rules/json_validation/scripts/validate_schema.py similarity index 100% rename from rule_library/json_validation/scripts/validate_schema.py rename to .deepwork/rules/json_validation/scripts/validate_schema.py diff --git a/AGENTS.md b/AGENTS.md index 5f327754..d61c8b5a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,20 +21,18 @@ When creating or modifying jobs in this repository, you MUST understand which ty - NEVER edit the installed copies in `.deepwork/jobs/` directly - After editing, run `deepwork install --platform claude` to sync -### 2. Library Jobs (`library_jobs/`) +### 2. Library Jobs (`library/jobs/`) -**What they are**: Example or reusable jobs that any repository is welcome to use, but are NOT auto-installed. Users must explicitly copy or import these into their projects. +**What they are**: Example or reusable jobs that any repository is welcome to use, but are NOT auto-installed. Users must explicitly copy or import these into their projects. Some library jobs may be symlinks to bespoke jobs that serve as good examples. -**Location**: `library_jobs/[job_name]/` +**Location**: `library/jobs/[job_name]/` -**Examples** (potential): -- Competitive research workflows -- Code review processes -- Documentation generation -- Release management +**Examples**: +- `commit` - Lint, test, and commit workflow (symlink to `.deepwork/jobs/commit`) **Editing rules**: -- Edit directly in `library_jobs/[job_name]/` +- If the job is a symlink, edit the source in `.deepwork/jobs/[job_name]/` +- If the job is a standalone directory, edit directly in `library/jobs/[job_name]/` - These are templates/examples for users to adopt - Should be well-documented and self-contained @@ -77,10 +75,17 @@ deepwork/ ├── src/deepwork/standard_jobs/ # Standard jobs (source of truth) │ ├── deepwork_jobs/ │ └── deepwork_rules/ -├── library_jobs/ # Library/example jobs -│ └── [example_job]/ -└── .deepwork/jobs/ # Installed standard jobs + bespoke jobs - ├── deepwork_jobs/ # ← Installed copy, NOT source of truth - ├── deepwork_rules/ # ← Installed copy, NOT source of truth - └── [bespoke_job]/ # ← Source of truth for bespoke only +├── library/ # Library of examples +│ ├── jobs/ # Library jobs (may be symlinks) +│ │ ├── commit -> ../../.deepwork/jobs/commit +│ │ └── README.md +│ └── rules/ # Library rules (may be symlinks) +│ └── json_validation -> ../../.deepwork/rules/json_validation +└── .deepwork/ # Repo-specific configuration + ├── jobs/ # Installed standard jobs + bespoke jobs + │ ├── deepwork_jobs/ # ← Installed copy, NOT source of truth + │ ├── deepwork_rules/ # ← Installed copy, NOT source of truth + │ └── commit/ # ← Bespoke job (also exposed in library/) + └── rules/ # Repo-specific rules + └── json_validation/ # ← Bespoke rule (also exposed in library/) ``` diff --git a/claude.md b/claude.md index a90782ca..d6e36fe9 100644 --- a/claude.md +++ b/claude.md @@ -47,7 +47,9 @@ deepwork/ │ │ └── deepwork_rules/ │ ├── schemas/ # Job definition schemas │ └── utils/ # Utilities (fs, git, yaml, validation) -├── library_jobs/ # Reusable example jobs (not auto-installed) +├── library/ # Reusable examples (not auto-installed) +│ ├── jobs/ # Example jobs (some may be symlinks) +│ └── rules/ # Example rules (some may be symlinks) ├── tests/ # Test suite ├── doc/ # Documentation └── doc/architecture.md # Detailed architecture document @@ -191,7 +193,7 @@ my-project/ | Type | Location | Purpose | |------|----------|---------| | **Standard Jobs** | `src/deepwork/standard_jobs/` | Framework core, auto-installed to users | -| **Library Jobs** | `library_jobs/` | Reusable examples users can adopt | +| **Library Jobs** | `library/jobs/` | Reusable examples users can adopt | | **Bespoke Jobs** | `.deepwork/jobs/` (if not in standard_jobs) | This repo's internal workflows only | ### Editing Standard Jobs @@ -218,7 +220,7 @@ Instead, follow this workflow: ### How to Identify Job Types - **Standard jobs**: Exist in `src/deepwork/standard_jobs/` (currently: `deepwork_jobs`, `deepwork_rules`) -- **Library jobs**: Exist in `library_jobs/` +- **Library jobs**: Exist in `library/jobs/` - **Bespoke jobs**: Exist ONLY in `.deepwork/jobs/` with no corresponding standard_jobs entry **When creating a new job, always clarify which type it should be.** If uncertain, ask the user. diff --git a/job_library/README.md b/library/jobs/README.md similarity index 88% rename from job_library/README.md rename to library/jobs/README.md index e6cbd4d2..3e871155 100644 --- a/job_library/README.md +++ b/library/jobs/README.md @@ -15,8 +15,8 @@ The job library provides: Each job in this library follows the same structure as the `.deepwork/jobs` subfolders in your local project: ``` -job_library/ -├── [job-name]/ +library/jobs/ +├── [job-name]/ # May be actual folder or symlink to .deepwork/jobs/ │ ├── job.yml # Job definition (name, steps, dependencies) │ └── steps/ │ ├── step_one.md # Instructions for step one @@ -29,6 +29,8 @@ job_library/ └── README.md ``` +**Note**: Some jobs in this library may be symlinks to `.deepwork/jobs/` where the actual job definitions live. This allows the library to expose jobs that are actively used in this repository. + ### job.yml The job definition file contains: diff --git a/library/jobs/commit b/library/jobs/commit new file mode 120000 index 00000000..ad92446f --- /dev/null +++ b/library/jobs/commit @@ -0,0 +1 @@ +../../.deepwork/jobs/commit \ No newline at end of file diff --git a/library/rules/json_validation b/library/rules/json_validation new file mode 120000 index 00000000..2a47ead3 --- /dev/null +++ b/library/rules/json_validation @@ -0,0 +1 @@ +../../.deepwork/rules/json_validation \ No newline at end of file