opsward.score#
Pure scoring functions. ScanResult -> DiagnosisReport.
All functions are pure — same input, same output.
Functions
|
Return a |
|
Score a ScanResult and return a DiagnosisReport. |
|
Project-relative docs dir to suggest moving sections into. |
|
Why a project skill may never trigger, as note lines (and one suggestion each). |
Validate a hooks config against Claude Code's expected shape. |
|
|
Validate a SkillInfo against the agentskills.io specification. |
Classes
|
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:
objectA CLAUDE.md over its size budget, with the sections worth moving out.
- opsward.score.claude_md_size_overrun(content, *, max_chars=10000, docs_dir='misc/docs', top=3)[source]#
Return a
SizeOverrunif content exceeds max_chars, else None.The penalty is proportional to the overrun:
_CONCISE_SIZE_PENALTY_MAXpoints 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:
- 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_charsis the CLAUDE.md size above which Conciseness loses points (seeclaude_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:
- opsward.score.docs_dir_hint(sr)[source]#
Project-relative docs dir to suggest moving sections into.
- Return type:
- 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/skillswhile 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 fromScanResult.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
- 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 runnablecommandhook — 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
- 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) []