opsward.score#

Pure scoring functions. ScanResult -> DiagnosisReport.

All functions are pure — same input, same output.

Functions

claude_md_size_overrun(content, *[, ...])

Return a SizeOverrun if content exceeds max_chars, else None.

diagnose(scan_result, *[, ...])

Score a ScanResult and return a DiagnosisReport.

docs_dir_hint(sr)

Project-relative docs dir to suggest moving sections into.

skill_reachability_findings(sr, *, suggestions)

Why a project skill may never trigger, as note lines (and one suggestion each).

validate_hooks_config(cfg)

Validate a hooks config against Claude Code's expected shape.

validate_skill_spec(skill)

Validate a SkillInfo against the agentskills.io specification.

Classes

SizeOverrun(chars, max_chars, penalty, ...)

A CLAUDE.md over its size budget, with the sections worth moving out.

class opsward.score.SizeOverrun(chars, max_chars, penalty, sections, docs_dir)[source]#

Bases: object

A CLAUDE.md over its size budget, with the sections worth moving out.

describe(*, deducted=None)[source]#

One-paragraph finding naming the longest sections and where to move them.

deducted is the points actually lost (the line-count rule may have left fewer than penalty to take); omit it to leave the figure out.

Return type:

str

opsward.score.claude_md_size_overrun(content, *, max_chars=10000, docs_dir='misc/docs', top=3)[source]#

Return a SizeOverrun if content exceeds max_chars, else None.

The penalty is proportional to the overrun: _CONCISE_SIZE_PENALTY_MAX points at twice the threshold (at least 1 once over it).

>>> claude_md_size_overrun("x" * 11, max_chars=10).penalty
1
>>> claude_md_size_overrun("short", max_chars=10) is None
True
Return type:

Optional[SizeOverrun]

opsward.score.diagnose(scan_result, *, claude_md_max_chars=10000, listing_budget_chars=30000)[source]#

Score a ScanResult and return a DiagnosisReport.

claude_md_max_chars is the CLAUDE.md size above which Conciseness loses points (see claude_md_size_overrun()).

>>> from pathlib import Path
>>> from opsward.base import ScanResult
>>> r = diagnose(ScanResult(project_root=Path('/tmp/empty')))
>>> r.grade
'F'
Return type:

DiagnosisReport

opsward.score.docs_dir_hint(sr)[source]#

Project-relative docs dir to suggest moving sections into.

Return type:

str

opsward.score.skill_reachability_findings(sr, *, suggestions, listing_budget_chars=30000)[source]#

Why a project skill may never trigger, as note lines (and one suggestion each).

Three shapes from the #23 report: a project skill shadowed by a global one of the same name (Claude Code resolves personal > project, verified in a fresh session); an unmigrated bridge (a real directory under .claude/skills while the repo also holds the skill at its canonical location); and a listing budget that the global scope plus this project’s skills cannot fit, so descriptions get dropped least-invoked-first. The global scope comes from ScanResult.global_skills; with none (scan(..., claude_config_dir=None)) only the bridge check runs.

>>> from pathlib import Path
>>> from opsward.base import ScanResult, SkillInfo
>>> g = SkillInfo(name='lint', path=Path('/g/lint'), has_skill_md=True, description='Global lint.', real_path=Path('/g/lint'))
>>> p = SkillInfo(name='lint', path=Path('/p/.claude/skills/lint'), has_skill_md=True, description='Project lint.', real_path=Path('/p/.claude/skills/lint'))
>>> sr = ScanResult(project_root=Path('/p'), skills=[p], global_skills=[g])
>>> s = []
>>> skill_reachability_findings(sr, suggestions=s)[0][:46]
'Skill "lint": shadowed by the global skill of '
>>> len(s)
1
Return type:

list[str]

opsward.score.validate_hooks_config(cfg)[source]#

Validate a hooks config against Claude Code’s expected shape.

Returns a list of human-readable violation strings (empty = valid). Catches the silent-failure traps: a non-string matcher, an unknown event name, or an entry with no runnable command hook — none of which Claude Code honors, so the hook never fires even though the file “looks” configured.

>>> validate_hooks_config({'hooks': {'PostToolUse': [
...     {'matcher': 'Edit', 'hooks': [{'type': 'command', 'command': 'x'}]}]}})
[]
>>> validate_hooks_config({'pre_commit': ['ruff']})
['no top-level `hooks` key']
>>> 'matcher' in validate_hooks_config({'hooks': {'PostToolUse': [
...     {'matcher': {'tool_name': 'Edit'}, 'hooks': [
...         {'type': 'command', 'command': 'x'}]}]}})[0]
True
Return type:

list[str]

opsward.score.validate_skill_spec(skill)[source]#

Validate a SkillInfo against the agentskills.io specification.

Returns a list of human-readable violation strings (empty = compliant).

>>> from pathlib import Path
>>> from opsward.base import SkillInfo
>>> s = SkillInfo(name='good-skill', path=Path('.'), has_skill_md=True,
...     frontmatter={'name': 'good-skill', 'description': 'Does X.'}, line_count=50)
>>> validate_skill_spec(s)
[]
Return type:

list[str]