Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions modules/cli/status_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from modules.doctor.doctor import run_doctor
from modules.validation.workspace_validation import validate_workspace_schemas
from modules.runtime.sample_run import run_sample
from modules.evidence.evidence import verify_sample_evidence
from modules.evidence.evidence import verify_file_state_log, verify_sample_evidence


def build_status_screen(repo_root: Path) -> tuple[int, str]:
Expand Down Expand Up @@ -56,7 +56,6 @@ def build_status_screen(repo_root: Path) -> tuple[int, str]:
try:
evidence_res = verify_sample_evidence(sample=True)
state_log_path = repo_root / "examples/workspace/evidence-state-log.sample.json"
from modules.evidence.evidence import verify_file_state_log
state_log_res = verify_file_state_log(filepath=state_log_path)
evidence_ok = bool(evidence_res.get("ok")) and bool(state_log_res.get("ok"))
except Exception:
Expand Down
2 changes: 2 additions & 0 deletions modules/evidence/evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ def verify_file_evidence(*, filepath: str | Path) -> dict[str, Any]:

def verify_state_log_chain(entries: list[dict[str, Any]]) -> dict[str, Any]:
"""Verify sequence, previous state hashes, git commit refs, and content hashes in a state log chain."""
if not entries:
return {"ok": False, "error": "State log chain must contain at least 1 entry"}
previous_hash = GENESIS_HASH
for expected_sequence, entry in enumerate(entries):
if not isinstance(entry, dict):
Expand Down
11 changes: 9 additions & 2 deletions modules/validation/schema_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,16 @@ def validate_json_schema_instance(schema: dict[str, Any], instance: Any, locatio
raise ValueError(f"{location} must be one of {schema['enum']}")

if "pattern" in schema and isinstance(instance, str):
pattern = schema["pattern"]
if not isinstance(pattern, str):
raise ValueError(f"{location} pattern must be a string")
import re
if not re.match(schema["pattern"], instance):
raise ValueError(f"{location} does not match pattern {schema['pattern']}")
try:
compiled_pattern = re.compile(pattern)
except re.error as e:
raise ValueError(f"{location} has invalid regex pattern '{pattern}': {e}")
if not compiled_pattern.search(instance):
raise ValueError(f"{location} does not match pattern {pattern}")

if "minItems" in schema and isinstance(instance, list):
if len(instance) < schema["minItems"]:
Expand Down
6 changes: 6 additions & 0 deletions tests/evidence/test_state_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ def test_verify_state_log_chain_valid() -> None:
assert len(result["root_hash"]) == 64


def test_verify_state_log_chain_empty() -> None:
result = verify_state_log_chain([])
assert result["ok"] is False
assert "at least 1 entry" in result["error"]


def test_verify_file_state_log_valid() -> None:
sample_file = ROOT / "examples" / "workspace" / "evidence-state-log.sample.json"
result = verify_file_state_log(filepath=sample_file)
Expand Down
22 changes: 22 additions & 0 deletions tests/validation/test_schema_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,25 @@ def test_validate_json_schema_instance_rejects_unsupported_types() -> None:
def test_validate_json_schema_instance_does_not_treat_boolean_as_integer() -> None:
with pytest.raises(ValueError, match="must be integer"):
validate_json_schema_instance({"type": "integer"}, True)


def test_validate_json_schema_instance_pattern_search_semantics() -> None:
# pattern "foo" accepts "xxfooyy" (search semantics)
validate_json_schema_instance({"type": "string", "pattern": "foo"}, "xxfooyy")

# pattern "^foo$" accepts "foo"
validate_json_schema_instance({"type": "string", "pattern": "^foo$"}, "foo")

# pattern "^foo$" rejects "xxfooyy"
with pytest.raises(ValueError, match="does not match pattern"):
validate_json_schema_instance({"type": "string", "pattern": "^foo$"}, "xxfooyy")


def test_validate_json_schema_instance_pattern_failures() -> None:
# pattern as non-string raises ValueError
with pytest.raises(ValueError, match="pattern must be a string"):
validate_json_schema_instance({"type": "string", "pattern": 123}, "some_string")

# invalid regex pattern raises ValueError
with pytest.raises(ValueError, match="invalid regex pattern"):
validate_json_schema_instance({"type": "string", "pattern": "[invalid"}, "some_string")
Loading