harness-eval 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- harness_eval/__init__.py +3 -0
- harness_eval/analysis/__init__.py +0 -0
- harness_eval/analysis/budget.py +74 -0
- harness_eval/analysis/context_utilization.py +90 -0
- harness_eval/analysis/dependencies.py +111 -0
- harness_eval/analysis/system.py +95 -0
- harness_eval/analysis/triggers.py +63 -0
- harness_eval/analysis/types.py +40 -0
- harness_eval/cli/__init__.py +13 -0
- harness_eval/cli/_helpers.py +14 -0
- harness_eval/cli/lint.py +223 -0
- harness_eval/cli/review.py +166 -0
- harness_eval/cli/security.py +446 -0
- harness_eval/cli/skill.py +229 -0
- harness_eval/config/__init__.py +0 -0
- harness_eval/config/presets.py +214 -0
- harness_eval/core/__init__.py +0 -0
- harness_eval/core/discoverers/__init__.py +5 -0
- harness_eval/core/discoverers/base.py +62 -0
- harness_eval/core/discoverers/claude.py +264 -0
- harness_eval/core/discoverers/copilot.py +97 -0
- harness_eval/core/discoverers/cursor.py +154 -0
- harness_eval/core/discoverers/gemini.py +64 -0
- harness_eval/core/discoverers/opencode.py +88 -0
- harness_eval/core/discoverers/registry.py +25 -0
- harness_eval/core/discoverers/third_party.py +89 -0
- harness_eval/core/fingerprint.py +89 -0
- harness_eval/core/setup.py +138 -0
- harness_eval/core/types.py +53 -0
- harness_eval/data/__init__.py +25 -0
- harness_eval/data/builtins.json +8 -0
- harness_eval/data/tautological_patterns.json +29 -0
- harness_eval/inspection/__init__.py +3 -0
- harness_eval/inspection/engine.py +728 -0
- harness_eval/inspection/fixer.py +66 -0
- harness_eval/inspection/parsers.py +340 -0
- harness_eval/inspection/registry.py +37 -0
- harness_eval/inspection/rules/__init__.py +184 -0
- harness_eval/inspection/rules/agents/__init__.py +0 -0
- harness_eval/inspection/rules/agents/constraint_body_match.py +95 -0
- harness_eval/inspection/rules/agents/data_exfiltration.py +43 -0
- harness_eval/inspection/rules/agents/description_required.py +42 -0
- harness_eval/inspection/rules/agents/disallowed_tools_parseable.py +47 -0
- harness_eval/inspection/rules/agents/model_specified.py +46 -0
- harness_eval/inspection/rules/agents/no_credential_access.py +48 -0
- harness_eval/inspection/rules/agents/no_prompt_injection.py +45 -0
- harness_eval/inspection/rules/agents/obfuscation_detection.py +43 -0
- harness_eval/inspection/rules/agents/referenced_skills_exist.py +44 -0
- harness_eval/inspection/rules/agents/reverse_shell_detection.py +45 -0
- harness_eval/inspection/rules/claude_md/__init__.py +0 -0
- harness_eval/inspection/rules/claude_md/exists.py +39 -0
- harness_eval/inspection/rules/claude_md/generic_advice.py +61 -0
- harness_eval/inspection/rules/claude_md/skill_duplication.py +58 -0
- harness_eval/inspection/rules/commands/__init__.py +0 -0
- harness_eval/inspection/rules/commands/data_exfiltration.py +43 -0
- harness_eval/inspection/rules/commands/description_required.py +45 -0
- harness_eval/inspection/rules/commands/duplicate_detection.py +64 -0
- harness_eval/inspection/rules/commands/no_credential_access.py +51 -0
- harness_eval/inspection/rules/commands/no_prompt_injection.py +45 -0
- harness_eval/inspection/rules/commands/obfuscation_detection.py +43 -0
- harness_eval/inspection/rules/commands/references_nonexistent_skill.py +59 -0
- harness_eval/inspection/rules/commands/reverse_shell_detection.py +45 -0
- harness_eval/inspection/rules/commands/script_exists.py +51 -0
- harness_eval/inspection/rules/commands/shadows_builtin.py +43 -0
- harness_eval/inspection/rules/commands/skill_overlap.py +54 -0
- harness_eval/inspection/rules/content/__init__.py +0 -0
- harness_eval/inspection/rules/content/broken_references.py +91 -0
- harness_eval/inspection/rules/content/circular_references.py +110 -0
- harness_eval/inspection/rules/content/duplicate_detection.py +65 -0
- harness_eval/inspection/rules/content/token_budget.py +74 -0
- harness_eval/inspection/rules/frontmatter/__init__.py +0 -0
- harness_eval/inspection/rules/frontmatter/description_quality.py +107 -0
- harness_eval/inspection/rules/frontmatter/description_required.py +40 -0
- harness_eval/inspection/rules/frontmatter/format_valid.py +54 -0
- harness_eval/inspection/rules/hooks/__init__.py +0 -0
- harness_eval/inspection/rules/hooks/dangerous_command.py +58 -0
- harness_eval/inspection/rules/hooks/env_leakage.py +63 -0
- harness_eval/inspection/rules/hooks/network_access.py +57 -0
- harness_eval/inspection/rules/hooks/script_boundary.py +70 -0
- harness_eval/inspection/rules/hooks/valid_structure.py +91 -0
- harness_eval/inspection/rules/mcp/__init__.py +0 -0
- harness_eval/inspection/rules/mcp/duplicate_server.py +65 -0
- harness_eval/inspection/rules/mcp/no_wildcard_tools.py +59 -0
- harness_eval/inspection/rules/mcp/suspicious_endpoint.py +73 -0
- harness_eval/inspection/rules/mcp/valid_config.py +106 -0
- harness_eval/inspection/rules/quality/__init__.py +0 -0
- harness_eval/inspection/rules/quality/_patterns.py +20 -0
- harness_eval/inspection/rules/quality/example_gap.py +80 -0
- harness_eval/inspection/rules/quality/imprecise_instruction.py +93 -0
- harness_eval/inspection/rules/quality/negative_only.py +94 -0
- harness_eval/inspection/rules/quality/redundant_guidance.py +216 -0
- harness_eval/inspection/rules/quality/scope_overreach.py +89 -0
- harness_eval/inspection/rules/quality/stale_references.py +134 -0
- harness_eval/inspection/rules/quality/trigger_manipulation.py +106 -0
- harness_eval/inspection/rules/quality/unfinished_content.py +176 -0
- harness_eval/inspection/rules/security/__init__.py +0 -0
- harness_eval/inspection/rules/security/_shared.py +141 -0
- harness_eval/inspection/rules/security/ast_behavioral.py +206 -0
- harness_eval/inspection/rules/security/bash_taint_tracking.py +168 -0
- harness_eval/inspection/rules/security/coercive_override.py +73 -0
- harness_eval/inspection/rules/security/cve_lookup.py +202 -0
- harness_eval/inspection/rules/security/data_exfiltration.py +57 -0
- harness_eval/inspection/rules/security/mcp_least_privilege.py +160 -0
- harness_eval/inspection/rules/security/mcp_tool_poisoning.py +210 -0
- harness_eval/inspection/rules/security/no_credential_access.py +78 -0
- harness_eval/inspection/rules/security/no_prompt_injection.py +91 -0
- harness_eval/inspection/rules/security/obfuscation_detection.py +57 -0
- harness_eval/inspection/rules/security/prompt_exfiltration.py +76 -0
- harness_eval/inspection/rules/security/reverse_shell_detection.py +56 -0
- harness_eval/inspection/rules/security/stealth_persistence.py +61 -0
- harness_eval/inspection/rules/security/taint_tracking.py +236 -0
- harness_eval/inspection/rules/security/yara_scan.py +160 -0
- harness_eval/inspection/rules/structural/__init__.py +0 -0
- harness_eval/inspection/rules/structural/skill_md_exists.py +34 -0
- harness_eval/inspection/suppression.py +87 -0
- harness_eval/inspection/types.py +218 -0
- harness_eval/output/__init__.py +0 -0
- harness_eval/output/metadata.py +60 -0
- harness_eval/output/report.py +353 -0
- harness_eval/output/sarif.py +83 -0
- harness_eval/py.typed +0 -0
- harness_eval/rubric/__init__.py +0 -0
- harness_eval/rubric/dimensions.py +158 -0
- harness_eval/rubric/prompts/adjudication.py +56 -0
- harness_eval/rubric/prompts/batch-template.md +37 -0
- harness_eval/rubric/prompts/issue-template.md +93 -0
- harness_eval/rubric/prompts/system.md +39 -0
- harness_eval/rubric/prompts.py +61 -0
- harness_eval/rubric/scorer.py +272 -0
- harness_eval/rubric/types.py +36 -0
- harness_eval/utils/__init__.py +0 -0
- harness_eval/utils/llm.py +93 -0
- harness_eval/utils/parsing.py +100 -0
- harness_eval/utils/paths.py +35 -0
- harness_eval/utils/similarity.py +17 -0
- harness_eval/utils/tokens.py +57 -0
- harness_eval/watch.py +135 -0
- harness_eval-5.0.0.dist-info/METADATA +124 -0
- harness_eval-5.0.0.dist-info/RECORD +142 -0
- harness_eval-5.0.0.dist-info/WHEEL +4 -0
- harness_eval-5.0.0.dist-info/entry_points.txt +2 -0
- harness_eval-5.0.0.dist-info/licenses/LICENSE +190 -0
harness_eval/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Token budget analysis for the setup as a whole."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from harness_eval.core.types import ComponentType, ParsedComponent, Setup
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class BudgetReport:
|
|
12
|
+
"""Token budget breakdown for a setup."""
|
|
13
|
+
|
|
14
|
+
total_tokens: int = 0
|
|
15
|
+
always_loaded_tokens: int = 0
|
|
16
|
+
on_demand_tokens: int = 0
|
|
17
|
+
always_loaded_ratio: float = 0.0
|
|
18
|
+
by_type: dict[str, int] = field(default_factory=dict)
|
|
19
|
+
by_component: list[tuple[str, str, int]] = field(default_factory=list)
|
|
20
|
+
heaviest_component_name: str = ""
|
|
21
|
+
heaviest_component_ratio: float = 0.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_EXCLUDED_FROM_BUDGET = {ComponentType.UNCATEGORIZED}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _is_always_loaded(comp: ParsedComponent) -> bool:
|
|
28
|
+
if comp.component_type == ComponentType.HOOKS:
|
|
29
|
+
return True
|
|
30
|
+
if comp.component_type != ComponentType.CLAUDE_MD:
|
|
31
|
+
return False
|
|
32
|
+
if comp.source_tool != "cursor":
|
|
33
|
+
return True
|
|
34
|
+
return bool(comp.frontmatter and comp.frontmatter.get("alwaysApply") is True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def analyze_budget(setup: Setup) -> BudgetReport:
|
|
38
|
+
"""Analyze token budget distribution across a setup."""
|
|
39
|
+
by_type: dict[str, int] = {}
|
|
40
|
+
by_component: list[tuple[str, str, int]] = []
|
|
41
|
+
always_loaded = 0
|
|
42
|
+
on_demand = 0
|
|
43
|
+
heaviest_name = ""
|
|
44
|
+
heaviest_tokens = 0
|
|
45
|
+
|
|
46
|
+
for comp in setup.components:
|
|
47
|
+
if comp.component_type in _EXCLUDED_FROM_BUDGET:
|
|
48
|
+
continue
|
|
49
|
+
type_key = comp.component_type.value
|
|
50
|
+
by_type[type_key] = by_type.get(type_key, 0) + comp.token_count
|
|
51
|
+
by_component.append((type_key, comp.name, comp.token_count))
|
|
52
|
+
|
|
53
|
+
if _is_always_loaded(comp):
|
|
54
|
+
always_loaded += comp.token_count
|
|
55
|
+
else:
|
|
56
|
+
on_demand += comp.token_count
|
|
57
|
+
|
|
58
|
+
if comp.token_count > heaviest_tokens:
|
|
59
|
+
heaviest_tokens = comp.token_count
|
|
60
|
+
heaviest_name = f"{type_key}/{comp.name}"
|
|
61
|
+
|
|
62
|
+
total = always_loaded + on_demand
|
|
63
|
+
by_component.sort(key=lambda x: x[2], reverse=True)
|
|
64
|
+
|
|
65
|
+
return BudgetReport(
|
|
66
|
+
total_tokens=total,
|
|
67
|
+
always_loaded_tokens=always_loaded,
|
|
68
|
+
on_demand_tokens=on_demand,
|
|
69
|
+
always_loaded_ratio=always_loaded / total if total else 0.0,
|
|
70
|
+
by_type=by_type,
|
|
71
|
+
by_component=by_component,
|
|
72
|
+
heaviest_component_name=heaviest_name,
|
|
73
|
+
heaviest_component_ratio=heaviest_tokens / total if total else 0.0,
|
|
74
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Context utilization analysis: how much of each model's context window the setup consumes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from harness_eval.analysis.budget import BudgetReport
|
|
8
|
+
from harness_eval.core.types import Setup
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ModelSpec:
|
|
13
|
+
"""A model with a known context window size."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
context_window: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ModelUtilization:
|
|
21
|
+
"""Context utilization for one model."""
|
|
22
|
+
|
|
23
|
+
model: str
|
|
24
|
+
context_window: int
|
|
25
|
+
always_loaded_pct: float
|
|
26
|
+
peak_load_pct: float
|
|
27
|
+
remaining_pct: float
|
|
28
|
+
warning: bool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ContextUtilizationReport:
|
|
33
|
+
"""Context utilization across all target models."""
|
|
34
|
+
|
|
35
|
+
always_loaded_tokens: int = 0
|
|
36
|
+
peak_tokens: int = 0
|
|
37
|
+
models: list[ModelUtilization] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
PEAK_WARNING_THRESHOLD = 0.20
|
|
41
|
+
PEAK_CRITICAL_THRESHOLD = 0.50
|
|
42
|
+
ALWAYS_LOADED_WARNING_THRESHOLD = 0.10
|
|
43
|
+
|
|
44
|
+
DEFAULT_MODELS: list[ModelSpec] = [
|
|
45
|
+
ModelSpec("claude-haiku-4.5", 200_000),
|
|
46
|
+
ModelSpec("claude-sonnet-4.6", 200_000),
|
|
47
|
+
ModelSpec("claude-opus-4.6", 200_000),
|
|
48
|
+
ModelSpec("claude-opus-4.7", 200_000),
|
|
49
|
+
ModelSpec("claude-opus-4.8", 200_000),
|
|
50
|
+
ModelSpec("claude-opus-4.6-1m", 1_000_000),
|
|
51
|
+
ModelSpec("claude-opus-4.7-1m", 1_000_000),
|
|
52
|
+
ModelSpec("claude-opus-4.8-1m", 1_000_000),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def analyze_context_utilization(
|
|
57
|
+
setup: Setup,
|
|
58
|
+
budget: BudgetReport,
|
|
59
|
+
models: list[ModelSpec] | None = None,
|
|
60
|
+
) -> ContextUtilizationReport:
|
|
61
|
+
"""Compute context window utilization per model."""
|
|
62
|
+
target_models = models if models is not None else DEFAULT_MODELS
|
|
63
|
+
always_loaded = budget.always_loaded_tokens
|
|
64
|
+
peak = budget.total_tokens
|
|
65
|
+
|
|
66
|
+
utilizations: list[ModelUtilization] = []
|
|
67
|
+
for spec in target_models:
|
|
68
|
+
if spec.context_window <= 0:
|
|
69
|
+
continue
|
|
70
|
+
al_pct = always_loaded / spec.context_window
|
|
71
|
+
peak_pct = peak / spec.context_window
|
|
72
|
+
remaining = max(0.0, 1.0 - peak_pct)
|
|
73
|
+
utilizations.append(
|
|
74
|
+
ModelUtilization(
|
|
75
|
+
model=spec.name,
|
|
76
|
+
context_window=spec.context_window,
|
|
77
|
+
always_loaded_pct=al_pct,
|
|
78
|
+
peak_load_pct=peak_pct,
|
|
79
|
+
remaining_pct=remaining,
|
|
80
|
+
warning=peak_pct > PEAK_WARNING_THRESHOLD,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
utilizations.sort(key=lambda m: m.peak_load_pct, reverse=True)
|
|
85
|
+
|
|
86
|
+
return ContextUtilizationReport(
|
|
87
|
+
always_loaded_tokens=always_loaded,
|
|
88
|
+
peak_tokens=peak,
|
|
89
|
+
models=utilizations,
|
|
90
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Dependency mapping: which components reference which, and are any broken?"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from harness_eval.core.types import ComponentType, Setup
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class DependencyEdge:
|
|
13
|
+
"""A reference from one component to another."""
|
|
14
|
+
|
|
15
|
+
source_type: str
|
|
16
|
+
source_name: str
|
|
17
|
+
target_type: str
|
|
18
|
+
target_name: str
|
|
19
|
+
exists: bool
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DependencyReport:
|
|
24
|
+
"""Dependency analysis results."""
|
|
25
|
+
|
|
26
|
+
edges: list[DependencyEdge] = field(default_factory=list)
|
|
27
|
+
broken_refs: list[DependencyEdge] = field(default_factory=list)
|
|
28
|
+
orphan_components: list[str] = field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_SKILL_REF_PATTERNS = [
|
|
32
|
+
re.compile(r"skills?[/\\](\w[\w-]*)"),
|
|
33
|
+
re.compile(r"/(\w[\w-]*)\s+skill"),
|
|
34
|
+
re.compile(r"skill\s+['\"](\w[\w-]*)['\"]"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def analyze_dependencies(setup: Setup) -> DependencyReport:
|
|
39
|
+
"""Map dependencies between components and find broken references."""
|
|
40
|
+
known_skills = {c.name for c in setup.by_type(ComponentType.SKILL)}
|
|
41
|
+
known_commands = {c.name for c in setup.by_type(ComponentType.COMMAND)}
|
|
42
|
+
all_known = known_skills | known_commands
|
|
43
|
+
|
|
44
|
+
edges: list[DependencyEdge] = []
|
|
45
|
+
|
|
46
|
+
for comp in setup.by_type(ComponentType.AGENT):
|
|
47
|
+
if comp.frontmatter:
|
|
48
|
+
referenced = comp.frontmatter.get("skills", []) or []
|
|
49
|
+
if isinstance(referenced, str):
|
|
50
|
+
referenced = [s.strip() for s in referenced.split(",")]
|
|
51
|
+
for skill_name in referenced:
|
|
52
|
+
if not skill_name:
|
|
53
|
+
continue
|
|
54
|
+
exists = skill_name in known_skills
|
|
55
|
+
edges.append(
|
|
56
|
+
DependencyEdge(
|
|
57
|
+
source_type="agent",
|
|
58
|
+
source_name=comp.name,
|
|
59
|
+
target_type="skill",
|
|
60
|
+
target_name=skill_name,
|
|
61
|
+
exists=exists,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
for comp in setup.components:
|
|
66
|
+
if comp.component_type in (ComponentType.MCP_CONFIG, ComponentType.HOOKS):
|
|
67
|
+
continue
|
|
68
|
+
for pattern in _SKILL_REF_PATTERNS:
|
|
69
|
+
for match in pattern.finditer(comp.content):
|
|
70
|
+
ref_name = match.group(1)
|
|
71
|
+
if ref_name in all_known and ref_name != comp.name:
|
|
72
|
+
edges.append(
|
|
73
|
+
DependencyEdge(
|
|
74
|
+
source_type=comp.component_type.value,
|
|
75
|
+
source_name=comp.name,
|
|
76
|
+
target_type="skill" if ref_name in known_skills else "command",
|
|
77
|
+
target_name=ref_name,
|
|
78
|
+
exists=True,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
seen = set()
|
|
83
|
+
deduped: list[DependencyEdge] = []
|
|
84
|
+
for e in edges:
|
|
85
|
+
key = (e.source_type, e.source_name, e.target_type, e.target_name)
|
|
86
|
+
if key not in seen:
|
|
87
|
+
seen.add(key)
|
|
88
|
+
deduped.append(e)
|
|
89
|
+
|
|
90
|
+
broken = [e for e in deduped if not e.exists]
|
|
91
|
+
|
|
92
|
+
referenced_names = set()
|
|
93
|
+
for e in deduped:
|
|
94
|
+
referenced_names.add(e.target_name)
|
|
95
|
+
referenced_names.add(e.source_name)
|
|
96
|
+
|
|
97
|
+
orphans = []
|
|
98
|
+
# Skills are activated by description matching, not by explicit references,
|
|
99
|
+
# so an unreferenced skill is not orphaned. Only flag commands and agents.
|
|
100
|
+
orphan_types = (ComponentType.COMMAND, ComponentType.AGENT)
|
|
101
|
+
for comp in setup.components:
|
|
102
|
+
if comp.component_type not in orphan_types:
|
|
103
|
+
continue
|
|
104
|
+
if comp.name not in referenced_names and len(setup.components) > 3:
|
|
105
|
+
orphans.append(f"{comp.component_type.value}/{comp.name}")
|
|
106
|
+
|
|
107
|
+
return DependencyReport(
|
|
108
|
+
edges=deduped,
|
|
109
|
+
broken_refs=broken,
|
|
110
|
+
orphan_components=orphans,
|
|
111
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""System-level analysis: evaluate the setup as a whole, not component-by-component."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from harness_eval.analysis.budget import BudgetReport, analyze_budget
|
|
8
|
+
from harness_eval.analysis.context_utilization import (
|
|
9
|
+
ContextUtilizationReport,
|
|
10
|
+
analyze_context_utilization,
|
|
11
|
+
)
|
|
12
|
+
from harness_eval.analysis.dependencies import DependencyReport, analyze_dependencies
|
|
13
|
+
from harness_eval.analysis.triggers import TriggerReport, analyze_triggers
|
|
14
|
+
from harness_eval.core.types import Setup
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class SystemReport:
|
|
19
|
+
"""Complete system-level analysis of a setup."""
|
|
20
|
+
|
|
21
|
+
setup_name: str
|
|
22
|
+
component_count: int
|
|
23
|
+
budget: BudgetReport
|
|
24
|
+
triggers: TriggerReport
|
|
25
|
+
dependencies: DependencyReport
|
|
26
|
+
context_utilization: ContextUtilizationReport
|
|
27
|
+
findings: list[str] = field(default_factory=list)
|
|
28
|
+
uncategorized_files: list[str] = field(default_factory=list)
|
|
29
|
+
detected_tools: tuple[str, ...] = ()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def analyze_system(setup: Setup) -> SystemReport:
|
|
33
|
+
"""Run all system-level analyses on a setup."""
|
|
34
|
+
budget = analyze_budget(setup)
|
|
35
|
+
triggers = analyze_triggers(setup)
|
|
36
|
+
dependencies = analyze_dependencies(setup)
|
|
37
|
+
context_utilization = analyze_context_utilization(setup, budget)
|
|
38
|
+
|
|
39
|
+
findings: list[str] = []
|
|
40
|
+
|
|
41
|
+
tool_set = set(setup.detected_tools)
|
|
42
|
+
tool_set.discard("Third-party modules")
|
|
43
|
+
if not tool_set:
|
|
44
|
+
always_label = "system instructions"
|
|
45
|
+
elif tool_set == {"Cursor"}:
|
|
46
|
+
always_label = "cursor rules"
|
|
47
|
+
elif len(tool_set) > 1:
|
|
48
|
+
always_label = "system instructions"
|
|
49
|
+
elif "Gemini CLI" in tool_set:
|
|
50
|
+
always_label = "GEMINI.md"
|
|
51
|
+
elif "OpenCode" in tool_set:
|
|
52
|
+
always_label = "AGENTS.md"
|
|
53
|
+
elif "Copilot" in tool_set:
|
|
54
|
+
always_label = "system instructions"
|
|
55
|
+
else:
|
|
56
|
+
always_label = "CLAUDE.md"
|
|
57
|
+
|
|
58
|
+
if budget.always_loaded_ratio > 0.7:
|
|
59
|
+
findings.append(
|
|
60
|
+
f"Inverted budget: {budget.always_loaded_ratio:.0%} of tokens are "
|
|
61
|
+
f"always-loaded ({always_label}). Most content should be in on-demand skills."
|
|
62
|
+
)
|
|
63
|
+
elif budget.always_loaded_ratio > 0.5:
|
|
64
|
+
findings.append(
|
|
65
|
+
f"Heavy always-loaded ratio: {budget.always_loaded_ratio:.0%} of tokens "
|
|
66
|
+
f"are always-loaded. Consider moving content to on-demand skills."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if budget.heaviest_component_ratio > 0.5:
|
|
70
|
+
findings.append(
|
|
71
|
+
f"One component ({budget.heaviest_component_name}) uses "
|
|
72
|
+
f"{budget.heaviest_component_ratio:.0%} of the total token budget."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
for pair in triggers.overlap_pairs:
|
|
76
|
+
findings.append(
|
|
77
|
+
f"Trigger overlap: '{pair[0]}' and '{pair[1]}' have "
|
|
78
|
+
f"{pair[2]:.0%} description similarity, may load together."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
from harness_eval.core.types import ComponentType as CT
|
|
82
|
+
|
|
83
|
+
uncategorized = [c.name for c in setup.by_type(CT.UNCATEGORIZED)]
|
|
84
|
+
|
|
85
|
+
return SystemReport(
|
|
86
|
+
setup_name=setup.name,
|
|
87
|
+
component_count=len([c for c in setup.components if c.component_type != CT.UNCATEGORIZED]),
|
|
88
|
+
budget=budget,
|
|
89
|
+
triggers=triggers,
|
|
90
|
+
dependencies=dependencies,
|
|
91
|
+
context_utilization=context_utilization,
|
|
92
|
+
findings=findings,
|
|
93
|
+
uncategorized_files=uncategorized,
|
|
94
|
+
detected_tools=setup.detected_tools,
|
|
95
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Trigger overlap analysis: detect skills that would load simultaneously."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from harness_eval.core.types import ComponentType, Setup
|
|
8
|
+
from harness_eval.utils.similarity import tfidf_similarity
|
|
9
|
+
|
|
10
|
+
TRIGGER_OVERLAP_THRESHOLD = 0.50
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class TriggerReport:
|
|
15
|
+
"""Trigger overlap analysis results."""
|
|
16
|
+
|
|
17
|
+
skill_count: int = 0
|
|
18
|
+
skills_with_description: int = 0
|
|
19
|
+
skills_without_description: int = 0
|
|
20
|
+
overlap_pairs: list[tuple[str, str, float]] = field(default_factory=list)
|
|
21
|
+
missing_use_when: list[str] = field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def analyze_triggers(setup: Setup) -> TriggerReport:
|
|
25
|
+
"""Analyze skill trigger descriptions for overlap and quality."""
|
|
26
|
+
skills = setup.by_type(ComponentType.SKILL)
|
|
27
|
+
report = TriggerReport(skill_count=len(skills))
|
|
28
|
+
|
|
29
|
+
descriptions: dict[str, str] = {}
|
|
30
|
+
|
|
31
|
+
for comp in skills:
|
|
32
|
+
desc = ""
|
|
33
|
+
if comp.frontmatter and isinstance(comp.frontmatter.get("description"), str):
|
|
34
|
+
desc = comp.frontmatter["description"]
|
|
35
|
+
|
|
36
|
+
if desc:
|
|
37
|
+
report.skills_with_description += 1
|
|
38
|
+
descriptions[comp.name] = desc
|
|
39
|
+
|
|
40
|
+
desc_lower = desc.lower()
|
|
41
|
+
use_when_phrases = [
|
|
42
|
+
"use when",
|
|
43
|
+
"use for",
|
|
44
|
+
"applies to",
|
|
45
|
+
"relevant for",
|
|
46
|
+
"triggered by",
|
|
47
|
+
"invoke when",
|
|
48
|
+
]
|
|
49
|
+
if not any(phrase in desc_lower for phrase in use_when_phrases):
|
|
50
|
+
report.missing_use_when.append(comp.name)
|
|
51
|
+
else:
|
|
52
|
+
report.skills_without_description += 1
|
|
53
|
+
|
|
54
|
+
names = list(descriptions.keys())
|
|
55
|
+
for i in range(len(names)):
|
|
56
|
+
for j in range(i + 1, len(names)):
|
|
57
|
+
sim = tfidf_similarity(descriptions[names[i]], descriptions[names[j]])
|
|
58
|
+
if sim >= TRIGGER_OVERLAP_THRESHOLD:
|
|
59
|
+
report.overlap_pairs.append((names[i], names[j], sim))
|
|
60
|
+
|
|
61
|
+
report.overlap_pairs.sort(key=lambda x: x[2], reverse=True)
|
|
62
|
+
|
|
63
|
+
return report
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Types for cross-analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class SetupComparison:
|
|
10
|
+
"""Comparison of inspection results between two setups."""
|
|
11
|
+
|
|
12
|
+
setup_a: str
|
|
13
|
+
setup_b: str
|
|
14
|
+
shared_components: list[str] = field(default_factory=list)
|
|
15
|
+
only_in_a: list[str] = field(default_factory=list)
|
|
16
|
+
only_in_b: list[str] = field(default_factory=list)
|
|
17
|
+
score_deltas: dict[str, int] = field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Correlation:
|
|
22
|
+
"""A correlation between a component difference and an inspection score difference."""
|
|
23
|
+
|
|
24
|
+
component_name: str
|
|
25
|
+
component_type: str
|
|
26
|
+
setup_name: str
|
|
27
|
+
error_count: int
|
|
28
|
+
warning_count: int
|
|
29
|
+
impact: str # "positive", "negative", "neutral"
|
|
30
|
+
explanation: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class CrossAnalysisResult:
|
|
35
|
+
"""Complete cross-analysis result."""
|
|
36
|
+
|
|
37
|
+
comparison: SetupComparison
|
|
38
|
+
correlations: list[Correlation] = field(default_factory=list)
|
|
39
|
+
summary: str = ""
|
|
40
|
+
setup_scores: dict[str, dict[str, int]] = field(default_factory=dict)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""CLI entry point for harness-eval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
cli = click.Group(name="harness-eval", help="Evaluate AI agent setups.")
|
|
8
|
+
click.version_option(package_name="harness-eval")(cli)
|
|
9
|
+
|
|
10
|
+
from harness_eval.cli import lint as _lint # noqa: E402, F401
|
|
11
|
+
from harness_eval.cli import review as _review # noqa: E402, F401
|
|
12
|
+
from harness_eval.cli import security as _security # noqa: E402, F401
|
|
13
|
+
from harness_eval.cli import skill as _skill # noqa: E402, F401
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Shared CLI helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def emit_output(text: str, output_path: str | None) -> None:
|
|
11
|
+
if output_path:
|
|
12
|
+
Path(output_path).write_text(text)
|
|
13
|
+
else:
|
|
14
|
+
click.echo(text)
|