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 +0 -0
- drskill/checks/__init__.py +82 -0
- drskill/checks/budget.py +40 -0
- drskill/checks/duplicates.py +103 -0
- drskill/checks/filesystem.py +36 -0
- drskill/checks/heuristics.py +249 -0
- drskill/checks/injection.py +523 -0
- drskill/checks/lockfile.py +129 -0
- drskill/checks/shadowing.py +144 -0
- drskill/checks/spec.py +108 -0
- drskill/cli.py +521 -0
- drskill/data/__init__.py +0 -0
- drskill/data/harnesses.toml +973 -0
- drskill/deep.py +240 -0
- drskill/deep_llm.py +84 -0
- drskill/discovery.py +96 -0
- drskill/harnesses.py +62 -0
- drskill/interactive.py +48 -0
- drskill/ledger.py +133 -0
- drskill/models.py +79 -0
- drskill/pipeline.py +58 -0
- drskill/report.py +271 -0
- drskill/resolution.py +222 -0
- drskill/state.py +42 -0
- drskill/text.py +113 -0
- drskill/tokens.py +30 -0
- drskill_core-0.3.0.dist-info/METADATA +234 -0
- drskill_core-0.3.0.dist-info/RECORD +31 -0
- drskill_core-0.3.0.dist-info/WHEEL +4 -0
- drskill_core-0.3.0.dist-info/entry_points.txt +2 -0
- drskill_core-0.3.0.dist-info/licenses/LICENSE +21 -0
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
|
+
)
|
drskill/checks/budget.py
ADDED
|
@@ -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
|
+
]
|