drskill-core 0.3.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.
drskill/__init__.py ADDED
File without changes
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from collections.abc import Callable
5
+
6
+ from drskill.ledger import Config
7
+ from drskill.models import Contributor, Finding
8
+ from drskill.resolution import World
9
+
10
+ CheckFn = Callable[[World, Config], list[Finding]]
11
+ REGISTRY: dict[str, CheckFn] = {}
12
+
13
+
14
+ def check(check_id: str):
15
+ def deco(fn: CheckFn) -> CheckFn:
16
+ REGISTRY[check_id] = fn
17
+ return fn
18
+
19
+ return deco
20
+
21
+
22
+ def fingerprint(
23
+ check_id: str,
24
+ contributors: list[Contributor],
25
+ extra: str = "",
26
+ texts: list[str] | None = None,
27
+ ) -> str:
28
+ """Fingerprint over the material the check judged. By default that is
29
+ each contributor's whole normalized content; a check that only judged a
30
+ slice (e.g. descriptions) passes `texts` so acks survive unrelated edits."""
31
+ if texts is None:
32
+ parts = sorted(c.content_hash for c in contributors)
33
+ else:
34
+ parts = sorted(hashlib.sha256(t.encode()).hexdigest() for t in texts)
35
+ payload = "|".join([check_id, *parts, extra])
36
+ return "sha256:" + hashlib.sha256(payload.encode()).hexdigest()
37
+
38
+
39
+ def make_finding(
40
+ check_id: str,
41
+ severity: str,
42
+ contributors: list[Contributor],
43
+ message: str,
44
+ *,
45
+ harnesses: list[str] | None = None,
46
+ fix_commands: list[str] | None = None,
47
+ extra_key: str = "",
48
+ fingerprint_texts: list[str] | None = None,
49
+ ) -> Finding:
50
+ if harnesses is None:
51
+ harnesses = sorted({d.harness for c in contributors for d in c.deployments})
52
+ return Finding(
53
+ check_id=check_id,
54
+ severity=severity,
55
+ contributors=[c.id for c in contributors],
56
+ contributor_names=sorted({c.name for c in contributors}),
57
+ harnesses=harnesses,
58
+ message=message,
59
+ fix_commands=fix_commands or [],
60
+ fingerprint=fingerprint(check_id, contributors, extra_key, fingerprint_texts),
61
+ )
62
+
63
+
64
+ def run_all(world: World, config: Config) -> list[Finding]:
65
+ # Import registers every check module exactly once.
66
+ from drskill.checks import budget, duplicates, filesystem, heuristics, injection, lockfile, shadowing, spec # noqa: F401
67
+
68
+ findings: list[Finding] = []
69
+ for fn in REGISTRY.values():
70
+ findings.extend(fn(world, config))
71
+ merged: dict[str, Finding] = {}
72
+ for f in findings:
73
+ if f.fingerprint in merged:
74
+ prior = merged[f.fingerprint]
75
+ merged[f.fingerprint] = prior.model_copy(
76
+ update={"harnesses": sorted(set(prior.harnesses) | set(f.harnesses))}
77
+ )
78
+ else:
79
+ merged[f.fingerprint] = f
80
+ return sorted(
81
+ merged.values(), key=lambda f: (f.severity, f.check_id, f.message)
82
+ )
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from drskill.checks import check, make_finding
4
+ from drskill.ledger import Config
5
+ from drskill.models import Finding
6
+ from drskill.resolution import World
7
+
8
+
9
+ @check("budget-catalog-tokens")
10
+ def budget_catalog_tokens(world: World, config: Config) -> list[Finding]:
11
+ out = []
12
+ for hid, hdef in world.harnesses.items():
13
+ contributors = world.effective(hid)
14
+ total = sum(c.token_cost.catalog_tokens for c in contributors)
15
+ if total > config.budget.catalog_tokens_max:
16
+ out.append(
17
+ make_finding(
18
+ "budget-catalog-tokens", "warning", contributors,
19
+ f"{hdef.display_name} startup catalog is ~{total} tokens "
20
+ f"(budget {config.budget.catalog_tokens_max})",
21
+ harnesses=[hid],
22
+ extra_key=hid,
23
+ fix_commands=[f"drskill list --tokens --harness {hid}"],
24
+ )
25
+ )
26
+ return out
27
+
28
+
29
+ @check("budget-body-tokens")
30
+ def budget_body_tokens(world: World, config: Config) -> list[Finding]:
31
+ return [
32
+ make_finding(
33
+ "budget-body-tokens", "warning", [c],
34
+ f"'{c.name}' body is ~{c.token_cost.body_tokens} tokens "
35
+ f"(warn ceiling {config.budget.body_tokens_warn})",
36
+ fix_commands=[f"Split or trim {c.id}; move reference material into bundled files"],
37
+ )
38
+ for c in world.contributors.values()
39
+ if c.token_cost.body_tokens > config.budget.body_tokens_warn
40
+ ]
@@ -0,0 +1,103 @@
1
+ """Exact and near duplicate detection. MinHash is hand rolled and uses
2
+ zlib.crc32 because builtin str hash() is salted per process."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import shlex
7
+ import zlib
8
+ from itertools import combinations
9
+
10
+ from drskill.checks import check, make_finding
11
+ from drskill.ledger import Config
12
+ from drskill.models import Contributor, Finding
13
+ from drskill.resolution import World
14
+
15
+ SHINGLE_WORDS = 5
16
+ NUM_HASHES = 128
17
+
18
+
19
+ def shingles(text: str, k: int = SHINGLE_WORDS) -> set[str]:
20
+ words = text.lower().split()
21
+ if len(words) <= k:
22
+ return {" ".join(words)} if words else set()
23
+ return {" ".join(words[i : i + k]) for i in range(len(words) - k + 1)}
24
+
25
+
26
+ def signature(sh: set[str]) -> list[int]:
27
+ if not sh:
28
+ return [0] * NUM_HASHES
29
+ return [
30
+ min(zlib.crc32(f"{seed}:{s}".encode()) for s in sh)
31
+ for seed in range(NUM_HASHES)
32
+ ]
33
+
34
+
35
+ def estimate(a: list[int], b: list[int]) -> float:
36
+ return sum(x == y for x, y in zip(a, b)) / len(a)
37
+
38
+
39
+ def _text(c: Contributor) -> str:
40
+ return f"{c.routing_text}\n{c.body}"
41
+
42
+
43
+ def _harnesses_loading_both(world: World, a: Contributor, b: Contributor) -> set[str]:
44
+ both = set()
45
+ for hid in world.harnesses:
46
+ eff = {c.id for c in world.effective(hid)}
47
+ if a.id in eff and b.id in eff:
48
+ both.add(hid)
49
+ return both
50
+
51
+
52
+ @check("exact-duplicate")
53
+ def exact_duplicate(world: World, config: Config) -> list[Finding]:
54
+ by_hash: dict[str, list[Contributor]] = {}
55
+ for c in world.contributors.values():
56
+ by_hash.setdefault(c.content_hash, []).append(c)
57
+ out = []
58
+ for group in by_hash.values():
59
+ if len(group) < 2:
60
+ continue
61
+ # pairs co-loaded by one harness belong to double-load, not here
62
+ clean_pairs = [
63
+ (a, b)
64
+ for a, b in combinations(group, 2)
65
+ if not _harnesses_loading_both(world, a, b)
66
+ ]
67
+ if clean_pairs:
68
+ out.append(
69
+ make_finding(
70
+ "exact-duplicate", "warning", group,
71
+ "identical skills installed in more than one place: "
72
+ + ", ".join(sorted(c.id for c in group)),
73
+ fix_commands=[
74
+ f"npx skills remove {shlex.quote(group[0].name)}"
75
+ " # from the copies you don't want"
76
+ ],
77
+ )
78
+ )
79
+ return out
80
+
81
+
82
+ @check("near-duplicate")
83
+ def near_duplicate(world: World, config: Config) -> list[Finding]:
84
+ cs = list(world.contributors.values())
85
+ sigs = {c.id: signature(shingles(_text(c))) for c in cs}
86
+ out = []
87
+ for a, b in combinations(cs, 2):
88
+ if a.content_hash == b.content_hash:
89
+ continue
90
+ sim = estimate(sigs[a.id], sigs[b.id])
91
+ if sim >= config.thresholds.near_duplicate:
92
+ out.append(
93
+ make_finding(
94
+ "near-duplicate", "warning", [a, b],
95
+ f"'{a.name}' and '{b.name}' are ~{sim:.0%} similar; "
96
+ "likely the same skill twice",
97
+ fix_commands=[
98
+ f"Compare and keep one: diff {shlex.quote(a.id)} {shlex.quote(b.id)}",
99
+ f"npx skills remove {shlex.quote(b.name)}",
100
+ ],
101
+ )
102
+ )
103
+ return out
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+
5
+ from drskill.checks import check, make_finding
6
+ from drskill.ledger import Config
7
+ from drskill.models import Finding
8
+ from drskill.resolution import World
9
+
10
+
11
+ @check("broken-symlink")
12
+ def broken_symlink(world: World, config: Config) -> list[Finding]:
13
+ return [
14
+ make_finding(
15
+ "broken-symlink", "error", [],
16
+ f"broken symlink: {b.path} points at nothing",
17
+ harnesses=[b.harness],
18
+ extra_key=str(b.path),
19
+ fix_commands=[f"rm {shlex.quote(str(b.path))}"],
20
+ )
21
+ for b in world.broken_symlinks
22
+ ]
23
+
24
+
25
+ @check("unreadable-skill")
26
+ def unreadable_skill(world: World, config: Config) -> list[Finding]:
27
+ return [
28
+ make_finding(
29
+ "unreadable-skill", "warning", [],
30
+ f"cannot read {path}: skipped",
31
+ harnesses=[harness],
32
+ extra_key=str(path),
33
+ fix_commands=[f"Check permissions: ls -l {shlex.quote(str(path))}"],
34
+ )
35
+ for harness, path in world.unreadable
36
+ ]
@@ -0,0 +1,249 @@
1
+ """Tier 2 heuristic checks: deterministic, threshold-tuned, always ack-able."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from itertools import combinations
7
+ from pathlib import Path
8
+
9
+ from drskill import text
10
+ from drskill.checks import check, make_finding
11
+ from drskill.checks.duplicates import estimate, shingles, signature
12
+ from drskill.ledger import Config
13
+ from drskill.models import Contributor, Finding
14
+ from drskill.resolution import World
15
+
16
+
17
+ def _skill_md(world: World) -> list[Contributor]:
18
+ return [
19
+ c
20
+ for c in world.contributors.values()
21
+ if Path(c.id).name == "SKILL.md" and c.frontmatter_valid
22
+ ]
23
+
24
+
25
+ @check("missing-activation")
26
+ def missing_activation(world: World, config: Config) -> list[Finding]:
27
+ offenders = sorted(
28
+ (
29
+ c for c in _skill_md(world)
30
+ if c.routing_text.strip() and not text.has_activation(c.routing_text)
31
+ ),
32
+ key=lambda c: c.name,
33
+ )
34
+ if not offenders:
35
+ return []
36
+ n = len(offenders)
37
+ head = (
38
+ f"{n} skill{'s' if n != 1 else ''} never "
39
+ f"{'say' if n != 1 else 'says'} when to use "
40
+ f"{'them' if n != 1 else 'it'}; the router has to guess:"
41
+ )
42
+ member_lines = "".join(f"\n {c.name}: {c.id}" for c in offenders)
43
+ return [
44
+ make_finding(
45
+ "missing-activation", "warning", offenders,
46
+ head + member_lines,
47
+ fix_commands=[
48
+ "Start each description with a condition, e.g. 'Use when ...'"
49
+ ],
50
+ fingerprint_texts=[f"{c.name}\n{c.routing_text}" for c in offenders],
51
+ )
52
+ ]
53
+
54
+
55
+ _IMPERATIVE = re.compile(r"\b(always|never)\s+((?:\w+[ \t]){0,3}\w+)", re.IGNORECASE)
56
+
57
+ # Unlike text.STOPWORDS, verbs stay: "use tabs" needs "use". Only glue words
58
+ # and location/degree adverbs are dropped.
59
+ # Corpus tuning 2026-07-20: set-intersection matching fired 119 times and
60
+ # set-containment 248 times on the hermes corpus (179 skills), almost all
61
+ # single shared verbs. The shipped rule is strict verb+object bigram
62
+ # equality: the first two non-glue tokens after always/never must match
63
+ # exactly. Very low recall, near-zero noise.
64
+ _IMPERATIVE_DROP = frozenset(
65
+ """a an the and or for in on at to of with from by anywhere everywhere
66
+ nowhere here there always never all any this that these those it its
67
+ your you""".split()
68
+ )
69
+
70
+
71
+ _SNIPPET_MAX = 100
72
+
73
+
74
+ def _snippet(body: str, start: int, end: int) -> str:
75
+ line_start = body.rfind("\n", 0, start) + 1
76
+ line_end = body.find("\n", end)
77
+ if line_end == -1:
78
+ line_end = len(body)
79
+ line = body[line_start:line_end].strip()
80
+ if len(line) > _SNIPPET_MAX:
81
+ line = line[: _SNIPPET_MAX - 1].rstrip() + "…"
82
+ return line
83
+
84
+
85
+ def _imperative_phrases(c: Contributor) -> dict[str, dict[tuple[str, str], str]]:
86
+ """kind -> {verb+object bigram: snippet of the first line saying it}."""
87
+ out: dict[str, dict[tuple[str, str], str]] = {"always": {}, "never": {}}
88
+ for m in _IMPERATIVE.finditer(c.body):
89
+ toks = [t for t in text.tokenize(m.group(2)) if t not in _IMPERATIVE_DROP]
90
+ if len(toks) >= 2:
91
+ out[m.group(1).lower()].setdefault(
92
+ (toks[0], toks[1]), _snippet(c.body, m.start(), m.end())
93
+ )
94
+ return out
95
+
96
+
97
+ @check("opposing-imperatives")
98
+ def opposing_imperatives(world: World, config: Config) -> list[Finding]:
99
+ cs = _skill_md(world)
100
+ phrases = {c.id: _imperative_phrases(c) for c in cs}
101
+ out = []
102
+ for a, b in combinations(cs, 2):
103
+ seen: set[str] = set()
104
+ for kind_a, kind_b in (("always", "never"), ("never", "always")):
105
+ for pair in sorted(set(phrases[a.id][kind_a]) & set(phrases[b.id][kind_b])):
106
+ phrase = " ".join(pair)
107
+ if phrase in seen:
108
+ continue
109
+ seen.add(phrase)
110
+ lines = [
111
+ f"'{a.name}' and '{b.name}' give opposite orders about "
112
+ f"'{phrase}':",
113
+ f' {a.name}: "{phrases[a.id][kind_a][pair]}"',
114
+ f" {a.id}",
115
+ f' {b.name}: "{phrases[b.id][kind_b][pair]}"',
116
+ f" {b.id}",
117
+ " (exact-match check; paraphrased contradictions are"
118
+ " not detected)",
119
+ ]
120
+ out.append(
121
+ make_finding(
122
+ "opposing-imperatives", "warning", [a, b],
123
+ "\n".join(lines),
124
+ fix_commands=[
125
+ "Align the two instructions, or scope each to its own condition"
126
+ ],
127
+ extra_key=phrase,
128
+ fingerprint_texts=[f"{a.name}\n{a.body}", f"{b.name}\n{b.body}"],
129
+ )
130
+ )
131
+ return out
132
+
133
+
134
+ def _is_duplicate_pair(
135
+ a: Contributor,
136
+ b: Contributor,
137
+ near_threshold: float,
138
+ sigs: dict[str, list[int]],
139
+ ) -> bool:
140
+ if a.content_hash == b.content_hash:
141
+ return True
142
+ return estimate(sigs[a.id], sigs[b.id]) >= near_threshold
143
+
144
+
145
+ @check("description-overlap")
146
+ def description_overlap(world: World, config: Config) -> list[Finding]:
147
+ cs = [c for c in _skill_md(world) if c.routing_text.strip()]
148
+ vecs = {c.id: text.shingle_vector(c.routing_text) for c in cs}
149
+ sigs = {c.id: signature(shingles(f"{c.routing_text}\n{c.body}")) for c in cs}
150
+
151
+ # Collapse duplicate groups to one representative each BEFORE clustering.
152
+ # Skipping only the direct edge is not enough: a carved-out duplicate
153
+ # pair could re-enter one cluster through a third skill that overlaps
154
+ # both (found in review). Duplicates are the stronger diagnosis and are
155
+ # reported by their own checks.
156
+ rep_parent = {c.id: c.id for c in cs}
157
+
158
+ def rep_find(x: str) -> str:
159
+ while rep_parent[x] != x:
160
+ rep_parent[x] = rep_parent[rep_parent[x]]
161
+ x = rep_parent[x]
162
+ return x
163
+
164
+ for a, b in combinations(cs, 2):
165
+ # Same-name pairs are diverged-copies territory; duplicate pairs are
166
+ # exact/near-duplicate territory. Both collapse to one representative.
167
+ if a.name == b.name or _is_duplicate_pair(
168
+ a, b, config.thresholds.near_duplicate, sigs
169
+ ):
170
+ rep_parent[rep_find(a.id)] = rep_find(b.id)
171
+ by_id = {c.id: c for c in cs}
172
+ reps = [by_id[cid] for cid in sorted({rep_find(c.id) for c in cs})]
173
+
174
+ parent = {c.id: c.id for c in reps}
175
+
176
+ def find(x: str) -> str:
177
+ while parent[x] != x:
178
+ parent[x] = parent[parent[x]]
179
+ x = parent[x]
180
+ return x
181
+
182
+ for a, b in combinations(reps, 2):
183
+ if text.cosine(vecs[a.id], vecs[b.id]) >= config.thresholds.description_overlap:
184
+ parent[find(a.id)] = find(b.id)
185
+
186
+ clusters: dict[str, list[Contributor]] = {}
187
+ for c in reps:
188
+ clusters.setdefault(find(c.id), []).append(c)
189
+
190
+ out = []
191
+ for members in clusters.values():
192
+ if len(members) < 2:
193
+ continue
194
+ members = sorted(members, key=lambda c: c.name)
195
+ phrases = text.shared_phrases([m.routing_text for m in members])[:3]
196
+ if phrases:
197
+ quoted = ", ".join(f"'{p}'" for p in phrases)
198
+ claim = f" all claim {quoted}"
199
+ else:
200
+ claim = " have near-identical descriptions"
201
+ # Same-name members collapse to one representative above, so names
202
+ # inside a cluster are unique and need no path disambiguation.
203
+ names = ", ".join(m.name for m in members)
204
+ member_lines = "".join(f"\n {m.name}: {m.id}" for m in members)
205
+ out.append(
206
+ make_finding(
207
+ "description-overlap", "warning", members,
208
+ f"{len(members)} skills ({names}){claim}; "
209
+ "none states an exclusive condition, so routing between them "
210
+ f"is a coin flip{member_lines}",
211
+ fix_commands=[
212
+ "Give each description an exclusive 'use when' condition the others lack"
213
+ ],
214
+ fingerprint_texts=[f"{m.name}\n{m.routing_text}" for m in members],
215
+ )
216
+ )
217
+ return out
218
+
219
+
220
+ @check("generic-description")
221
+ def generic_description(world: World, config: Config) -> list[Finding]:
222
+ offenders = []
223
+ for c in sorted(_skill_md(world), key=lambda c: c.name):
224
+ if not c.routing_text.strip():
225
+ continue
226
+ distinct = {
227
+ t for t in text.content_tokens(c.routing_text)
228
+ if t not in text.GENERIC_VOCAB
229
+ }
230
+ if len(distinct) < config.thresholds.generic_min_distinct_tokens:
231
+ offenders.append(c)
232
+ if not offenders:
233
+ return []
234
+ n = len(offenders)
235
+ head = (
236
+ f"{n} skill description{'s have' if n != 1 else ' has'} "
237
+ "no distinguishing words to route on:"
238
+ )
239
+ member_lines = "".join(f"\n {c.name}: {c.id}" for c in offenders)
240
+ return [
241
+ make_finding(
242
+ "generic-description", "warning", offenders,
243
+ head + member_lines,
244
+ fix_commands=[
245
+ "Name the concrete inputs, outputs, or domain in each description"
246
+ ],
247
+ fingerprint_texts=[f"{c.name}\n{c.routing_text}" for c in offenders],
248
+ )
249
+ ]