mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-12 12:06:29 +03:00
fix: relocate binary analysis skill
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
"""Project state machine — lifecycle transitions and staleness detection.
|
||||
|
||||
Enforces strict state transitions per the architecture:
|
||||
CREATED -> IMPORTED -> ANALYZING -> READY
|
||||
READY -> STALE -> ANALYZING
|
||||
Any state -> FAILED (with diagnostics preserved)
|
||||
|
||||
Provides:
|
||||
- Transition validation (reject invalid transitions).
|
||||
- FAILED transition helpers (preserve diagnostics, release locks).
|
||||
- Staleness detection (SHA-256 comparison on source change).
|
||||
- State-aware operation guards (clean only FAILED, migrate only unlocked).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from binary_analysis.domain.enums import ProjectState
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Valid transition map
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Each state maps to a set of allowed target states
|
||||
_VALID_TRANSITIONS: dict[ProjectState, set[ProjectState]] = {
|
||||
ProjectState.CREATED: {ProjectState.IMPORTED, ProjectState.FAILED},
|
||||
ProjectState.IMPORTED: {ProjectState.ANALYZING, ProjectState.FAILED},
|
||||
ProjectState.ANALYZING: {ProjectState.READY, ProjectState.FAILED},
|
||||
ProjectState.READY: {ProjectState.STALE, ProjectState.FAILED},
|
||||
ProjectState.STALE: {ProjectState.ANALYZING, ProjectState.FAILED},
|
||||
ProjectState.FAILED: {ProjectState.CREATED}, # Clean resets to CREATED
|
||||
}
|
||||
|
||||
# States from which analyze can be started (re-transition)
|
||||
_ANALYZABLE_STATES: set[ProjectState] = {
|
||||
ProjectState.IMPORTED,
|
||||
ProjectState.STALE,
|
||||
ProjectState.READY, # Can detect staleness without re-analyzing
|
||||
}
|
||||
|
||||
# States from which import is allowed
|
||||
_IMPORTABLE_STATES: set[ProjectState] = {
|
||||
ProjectState.CREATED,
|
||||
ProjectState.IMPORTED,
|
||||
}
|
||||
|
||||
# States from which clean is allowed (only FAILED)
|
||||
_CLEANABLE_STATES: set[ProjectState] = {
|
||||
ProjectState.FAILED,
|
||||
}
|
||||
|
||||
# States from which migrate is rejected (locked projects)
|
||||
_MIGRATE_BLOCKED_STATES: set[ProjectState] = {
|
||||
ProjectState.ANALYZING,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transition validation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def is_valid_transition(from_state: ProjectState, to_state: ProjectState) -> bool:
|
||||
"""Check if a state transition is allowed by the state machine.
|
||||
|
||||
Args:
|
||||
from_state: Current project state.
|
||||
to_state: Desired target state.
|
||||
|
||||
Returns:
|
||||
True if the transition is valid.
|
||||
"""
|
||||
allowed = _VALID_TRANSITIONS.get(from_state, set())
|
||||
return to_state in allowed
|
||||
|
||||
|
||||
def can_analyze(state: ProjectState) -> bool:
|
||||
"""Check if analysis can be started from the given state."""
|
||||
return state in _ANALYZABLE_STATES
|
||||
|
||||
|
||||
def can_import(state: ProjectState) -> bool:
|
||||
"""Check if a binary import is allowed in the given state."""
|
||||
return state in _IMPORTABLE_STATES
|
||||
|
||||
|
||||
def can_clean(state: ProjectState) -> bool:
|
||||
"""Check if clean is allowed in the given state (only FAILED)."""
|
||||
return state in _CLEANABLE_STATES
|
||||
|
||||
|
||||
def should_reject_migrate(state: ProjectState, is_locked: bool) -> bool:
|
||||
"""Check if migrate should be rejected due to project state or lock.
|
||||
|
||||
Args:
|
||||
state: Current project state.
|
||||
is_locked: Whether the project has an active lock.
|
||||
|
||||
Returns:
|
||||
True if migrate should be rejected.
|
||||
"""
|
||||
if is_locked:
|
||||
return True
|
||||
return state in _MIGRATE_BLOCKED_STATES
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transition helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def transition_to_failed(
|
||||
manifest: dict[str, Any],
|
||||
from_state: ProjectState,
|
||||
diagnostics: list[dict[str, Any]],
|
||||
release_lock_fn: Any | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Transition a project to FAILED state, preserving context from the source state.
|
||||
|
||||
Handles specific preservation rules per source state:
|
||||
- CREATED->FAILED: Preserve diagnostics, no lock to release.
|
||||
- IMPORTED->FAILED: Preserve binary record (binary_count, binary data),
|
||||
release lock if held.
|
||||
- ANALYZING->FAILED: Release lock, preserve crash diagnostics,
|
||||
clear lock from manifest.
|
||||
- STALE->FAILED: Capture both staleness cause and analysis failure,
|
||||
preserve binary record.
|
||||
|
||||
Args:
|
||||
manifest: The current project manifest (mutated in place).
|
||||
from_state: The state before failure.
|
||||
diagnostics: Failure diagnostics to preserve.
|
||||
release_lock_fn: Optional function to release the project lock.
|
||||
|
||||
Returns:
|
||||
The updated manifest dict.
|
||||
"""
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
# Preserve existing diagnostics
|
||||
existing_diags = manifest.get("diagnostics", [])
|
||||
if not isinstance(existing_diags, list):
|
||||
existing_diags = []
|
||||
|
||||
# Merge diagnostics, ensuring we don't lose staleness context
|
||||
merged_diags = existing_diags + diagnostics
|
||||
|
||||
# Update manifest
|
||||
manifest["state"] = ProjectState.FAILED.value
|
||||
manifest["diagnostics"] = merged_diags
|
||||
manifest["updated_at"] = now
|
||||
|
||||
# Release lock if transitioning from ANALYZING
|
||||
if from_state == ProjectState.ANALYZING:
|
||||
manifest["lock"] = None
|
||||
if release_lock_fn is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
release_lock_fn()
|
||||
|
||||
# Preserve binary record for IMPORTED->FAILED and STALE->FAILED
|
||||
# (binary_count and is_stale are preserved by default since we don't clear them)
|
||||
|
||||
return manifest
|
||||
Reference in New Issue
Block a user