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
4 changes: 2 additions & 2 deletions src/agentready/assessors/documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ def _check_template_compliance(self, sample_files: list) -> int:

required_sections = ["status", "context", "decision", "consequences"]
total_points = 0
max_points_per_file = 20 // len(sample_files)
max_points_per_file = 20 / len(sample_files)

for adr_file in sample_files:
try:
Expand All @@ -662,7 +662,7 @@ def _check_template_compliance(self, sample_files: list) -> int:
except OSError:
continue # Skip unreadable files

return int(total_points)
return round(total_points)

def _create_remediation(self) -> Remediation:
"""Create remediation guidance for missing/inadequate ADRs."""
Expand Down
34 changes: 34 additions & 0 deletions src/agentready/models/assessment.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,40 @@ def __post_init__(self):
f"attributes total ({self.attributes_total})"
)

@classmethod
def from_dict(cls, data: dict) -> "Assessment":
"""Create Assessment from dictionary.

Handles the key mapping where to_dict() serializes
'attributes_not_assessed' as 'attributes_skipped'.
"""
from datetime import datetime

metadata_data = data.get("metadata")
config_data = data.get("config")

return cls(
repository=Repository.from_dict(data["repository"]),
timestamp=datetime.fromisoformat(data["timestamp"]),
overall_score=data["overall_score"],
certification_level=data["certification_level"],
attributes_assessed=data["attributes_assessed"],
attributes_not_assessed=data.get(
"attributes_skipped", data.get("attributes_not_assessed", 0)
),
attributes_total=data["attributes_total"],
findings=[Finding.from_dict(f) for f in data.get("findings", [])],
config=Config.model_validate(config_data) if config_data else None,
duration_seconds=data.get("duration_seconds", 0.0),
discovered_skills=[
DiscoveredSkill.from_dict(s) for s in data.get("discovered_skills", [])
],
metadata=(
AssessmentMetadata.from_dict(metadata_data) if metadata_data else None
),
schema_version=data.get("schema_version", cls.CURRENT_SCHEMA_VERSION),
)

def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
Expand Down
13 changes: 13 additions & 0 deletions src/agentready/models/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ def __post_init__(self):
f"Default weight must be in range [0.0, 1.0]: {self.default_weight}"
)

@classmethod
def from_dict(cls, data: dict) -> "Attribute":
"""Create Attribute from dictionary."""
return cls(
id=data["id"],
name=data["name"],
category=data["category"],
tier=data["tier"],
description=data["description"],
criteria=data["criteria"],
default_weight=data["default_weight"],
)

def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
Expand Down
10 changes: 10 additions & 0 deletions src/agentready/models/citation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ def __post_init__(self):
if not self.relevance:
raise ValueError("Citation relevance must be non-empty")

@classmethod
def from_dict(cls, data: dict) -> "Citation":
"""Create Citation from dictionary."""
return cls(
source=data["source"],
title=data["title"],
url=data.get("url"),
relevance=data["relevance"],
)

def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
Expand Down
16 changes: 16 additions & 0 deletions src/agentready/models/discovered_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ def __post_init__(self):
if not self.pattern_summary:
raise ValueError("Pattern summary must be non-empty")

@classmethod
def from_dict(cls, data: dict) -> "DiscoveredSkill":
"""Create DiscoveredSkill from dictionary."""
return cls(
skill_id=data["skill_id"],
name=data["name"],
description=data["description"],
confidence=data["confidence"],
source_attribute_id=data["source_attribute_id"],
reusability_score=data["reusability_score"],
impact_score=data["impact_score"],
pattern_summary=data["pattern_summary"],
code_examples=data.get("code_examples", []),
citations=[Citation.from_dict(c) for c in data.get("citations", [])],
)

def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
Expand Down
32 changes: 32 additions & 0 deletions src/agentready/models/finding.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ def __post_init__(self):
if not self.steps:
raise ValueError("Remediation must have at least one step")

@classmethod
def from_dict(cls, data: dict) -> "Remediation":
"""Create Remediation from dictionary."""
steps = data.get("steps")
if not steps:
steps = [data["summary"]]
return cls(
summary=data["summary"],
steps=steps,
tools=data.get("tools", []),
commands=data.get("commands", []),
examples=data.get("examples", []),
citations=[Citation.from_dict(c) for c in data.get("citations", [])],
)
Comment on lines +37 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

from_dict can generate invalid Remediation instances.

Line 42 defaults steps to [], but __post_init__ rejects empty steps. Missing steps in input will fail deserialization instead of recovering safely.

Proposed fix (preserve invariant on missing steps)
     def from_dict(cls, data: dict) -> "Remediation":
         """Create Remediation from dictionary."""
+        steps = data.get("steps")
+        if not steps:
+            steps = [data["summary"]]
         return cls(
             summary=data["summary"],
-            steps=data.get("steps", []),
+            steps=steps,
             tools=data.get("tools", []),
             commands=data.get("commands", []),
             examples=data.get("examples", []),
             citations=[Citation.from_dict(c) for c in data.get("citations", [])],
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agentready/models/finding.py` around lines 37 - 47, The from_dict in
Remediation currently defaults steps to [] which violates the non-empty steps
invariant enforced by __post_init__; change from_dict (Remediation.from_dict) to
require and validate a non-empty steps list: read steps = data.get("steps") (or
data["steps"]) and if steps is missing or empty raise a clear ValueError (e.g.
"Remediation.from_dict requires a non-empty 'steps'"), otherwise pass the
validated steps into the constructor; keep the rest of the fields (tools,
commands, examples, citations) as-is.


def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
Expand Down Expand Up @@ -88,6 +103,23 @@ def __post_init__(self):
if self.status == "error" and not self.error_message:
raise ValueError("Error message required for status error")

@classmethod
def from_dict(cls, data: dict) -> "Finding":
"""Create Finding from dictionary."""
remediation_data = data.get("remediation")
return cls(
attribute=Attribute.from_dict(data["attribute"]),
status=data["status"],
score=data.get("score"),
measured_value=data.get("measured_value"),
threshold=data.get("threshold"),
evidence=data.get("evidence", []),
remediation=(
Remediation.from_dict(remediation_data) if remediation_data else None
),
error_message=data.get("error_message"),
)

def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
Expand Down
13 changes: 13 additions & 0 deletions src/agentready/models/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ class AssessmentMetadata:
command: str
working_directory: str

@classmethod
def from_dict(cls, data: dict) -> "AssessmentMetadata":
"""Create AssessmentMetadata from dictionary."""
return cls(
agentready_version=data["agentready_version"],
research_version=data["research_version"],
assessment_timestamp=data["assessment_timestamp"],
assessment_timestamp_human=data["assessment_timestamp_human"],
executed_by=data["executed_by"],
command=data["command"],
working_directory=data["working_directory"],
)

def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
Expand Down
19 changes: 19 additions & 0 deletions src/agentready/models/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,25 @@ def get_short_commit_hash(self) -> str:
"""
return shorten_commit_hash(self.commit_hash)

@classmethod
def from_dict(cls, data: dict) -> "Repository":
"""Create Repository from dictionary.

Skips filesystem validation so cached assessments remain readable
even if the original repository path no longer exists on disk.
"""
repo = object.__new__(cls)
repo.path = Path(data["path"])
repo.name = data["name"]
repo.url = data.get("url")
repo.branch = data["branch"]
repo.commit_hash = data["commit_hash"]
repo.languages = data.get("languages", {})
repo.total_files = data.get("total_files", 0)
repo.total_lines = data.get("total_lines", 0)
repo.config = None
return repo

@property
def primary_language(self) -> str:
"""Get the primary programming language (most files).
Expand Down
24 changes: 11 additions & 13 deletions src/agentready/services/assessment_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from pathlib import Path
from typing import Optional

from pydantic import ValidationError

from ..models import Assessment


Expand Down Expand Up @@ -108,7 +110,14 @@ def get(self, repository_url: str, commit_hash: str) -> Optional[Assessment]:
assessment_data = json.loads(assessment_json)
return self._deserialize_assessment(assessment_data)

except (sqlite3.Error, json.JSONDecodeError, ValueError):
except (
sqlite3.Error,
json.JSONDecodeError,
ValueError,
KeyError,
TypeError,
ValidationError,
):
return None

def set(
Expand Down Expand Up @@ -250,15 +259,4 @@ def _deserialize_assessment(data: dict) -> Assessment:
Returns:
Assessment object
"""
# This is a simplified deserialization
# In practice, you'd need full deserialization logic
# For now, we'll use a placeholder that assumes the cached JSON
# has the correct structure

# Note: This is a simplified approach. In production, you'd need
# proper deserialization that reconstructs all nested objects
raise NotImplementedError(
"Full assessment deserialization not yet implemented. "
"Consider caching assessment JSON directly and providing "
"a proper deserializer factory."
)
return Assessment.from_dict(data)
Loading
Loading