@jesdi/skills 0.0.9 → 0.0.11
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.
- package/package.json +1 -1
- package/skills/crap-gate/SKILL.md +167 -0
- package/skills/crap-gate/crap.py +210 -0
- package/skills/crap-gate/crap_config.py +124 -0
- package/skills/crap-gate/crap_coverage.py +85 -0
- package/skills/crap-gate/crap_git.py +73 -0
- package/skills/crap-gate/crap_model.py +33 -0
- package/skills/crap-gate/crap_python.py +69 -0
- package/skills/crap-gate/crap_report.py +99 -0
- package/skills/crap-gate/crap_score.py +65 -0
- package/skills/crap-gate/crap_typescript.mjs +110 -0
- package/skills/crap-gate/crap_typescript.py +30 -0
- package/skills/crap-gate/test_cli.py +152 -0
- package/skills/crap-gate/test_config.py +94 -0
- package/skills/crap-gate/test_coverage.py +88 -0
- package/skills/crap-gate/test_git.py +71 -0
- package/skills/crap-gate/test_python.py +94 -0
- package/skills/crap-gate/test_score.py +49 -0
- package/skills/crap-gate/test_typescript.py +73 -0
- package/skills-manifest.json +7 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from crap_config import Target
|
|
7
|
+
from crap_coverage import CoveragePyReport, IstanbulReport, load_report
|
|
8
|
+
from crap_model import Func, ToolError
|
|
9
|
+
|
|
10
|
+
COVERAGE_PY = {
|
|
11
|
+
"files": {
|
|
12
|
+
"services/x.py": {
|
|
13
|
+
"functions": {
|
|
14
|
+
"f": {"start_line": 3, "summary": {"percent_covered": 75.0}},
|
|
15
|
+
"K.m": {"start_line": 10, "summary": {"percent_covered": 0.0}},
|
|
16
|
+
"": {"start_line": 1, "summary": {"percent_covered": 100.0}},
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_coverage_py_matches_by_start_line_and_prefixes_root():
|
|
24
|
+
r = CoveragePyReport(COVERAGE_PY, "backend")
|
|
25
|
+
assert r.has_file("backend/services/x.py")
|
|
26
|
+
assert not r.has_file("services/x.py")
|
|
27
|
+
assert r.function_coverage(Func("backend/services/x.py", "f", 3, 8, 2), []) == 0.75
|
|
28
|
+
assert r.function_coverage(Func("backend/services/x.py", "K.m", 10, 12, 2), []) == 0.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_coverage_py_falls_back_to_first_entry_inside_range_else_zero():
|
|
32
|
+
r = CoveragePyReport(COVERAGE_PY, "backend")
|
|
33
|
+
# decorated function: analyzer says def at 2, coverage.py says 3
|
|
34
|
+
assert r.function_coverage(Func("backend/services/x.py", "f", 2, 8, 2), []) == 0.75
|
|
35
|
+
assert r.function_coverage(Func("backend/services/x.py", "ghost", 20, 25, 2), []) == 0.0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def istanbul(repo_root):
|
|
39
|
+
path = str(repo_root / "frontend/src/a.tsx")
|
|
40
|
+
return {
|
|
41
|
+
path: {
|
|
42
|
+
"path": path,
|
|
43
|
+
"statementMap": {
|
|
44
|
+
"0": {"start": {"line": 2, "column": 0}, "end": {"line": 2, "column": 10}},
|
|
45
|
+
"1": {"start": {"line": 3, "column": 0}, "end": {"line": 3, "column": 10}},
|
|
46
|
+
"2": {"start": {"line": 5, "column": 0}, "end": {"line": 5, "column": 10}}, # inside child
|
|
47
|
+
"3": {"start": {"line": 9, "column": 0}, "end": {"line": 9, "column": 10}}, # outside
|
|
48
|
+
},
|
|
49
|
+
"s": {"0": 1, "1": 0, "2": 0, "3": 1},
|
|
50
|
+
"branchMap": {
|
|
51
|
+
"0": {"loc": {"start": {"line": 3, "column": 4}, "end": {"line": 3, "column": 9}},
|
|
52
|
+
"type": "cond-expr", "locations": [], "line": 3},
|
|
53
|
+
},
|
|
54
|
+
"b": {"0": [1, 0]},
|
|
55
|
+
"fnMap": {},
|
|
56
|
+
"f": {},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_istanbul_counts_statements_and_branches_in_range_excluding_children(tmp_path):
|
|
62
|
+
r = IstanbulReport(istanbul(tmp_path), tmp_path)
|
|
63
|
+
assert r.has_file("frontend/src/a.tsx")
|
|
64
|
+
parent = Func("frontend/src/a.tsx", "P", 1, 7, 3)
|
|
65
|
+
child = Func("frontend/src/a.tsx", "P.c", 4, 6, 1)
|
|
66
|
+
# statements 0 (hit), 1 (miss) + branches [1, 0] → 2 of 4
|
|
67
|
+
assert r.function_coverage(parent, [child]) == 0.5
|
|
68
|
+
# child has one statement, missed
|
|
69
|
+
assert r.function_coverage(child, []) == 0.0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_istanbul_function_without_statements_is_fully_covered(tmp_path):
|
|
73
|
+
r = IstanbulReport(istanbul(tmp_path), tmp_path)
|
|
74
|
+
assert r.function_coverage(Func("frontend/src/a.tsx", "T", 20, 21, 1), []) == 1.0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def target(fmt, report, root="."):
|
|
78
|
+
return Target("t", ["**"], [], "python", ".", fmt, report, "", root)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_load_report_dispatches_and_errors(tmp_path):
|
|
82
|
+
(tmp_path / "cov.json").write_text(json.dumps(COVERAGE_PY))
|
|
83
|
+
r = load_report(target("coverage.py", "cov.json", "backend"), tmp_path)
|
|
84
|
+
assert isinstance(r, CoveragePyReport)
|
|
85
|
+
(tmp_path / "ist.json").write_text(json.dumps(istanbul(tmp_path)))
|
|
86
|
+
assert isinstance(load_report(target("istanbul", "ist.json"), tmp_path), IstanbulReport)
|
|
87
|
+
with pytest.raises(ToolError, match="missing.json"):
|
|
88
|
+
load_report(target("istanbul", "missing.json"), tmp_path)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from crap_git import changed_files, merge_base, show, toplevel, touched_lines, tracked_files
|
|
7
|
+
from crap_model import ToolError
|
|
8
|
+
|
|
9
|
+
GIT = ["git", "-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def git(root, *args):
|
|
13
|
+
return subprocess.run([*GIT, *args], cwd=root, check=True, capture_output=True, text=True).stdout
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def repo(tmp_path):
|
|
18
|
+
git(tmp_path, "init", "-q", "-b", "main")
|
|
19
|
+
(tmp_path / "pkg").mkdir()
|
|
20
|
+
(tmp_path / "pkg" / "a.py").write_text("def f(x):\n return x\n\n\ndef g(y):\n return y\n")
|
|
21
|
+
(tmp_path / "pkg" / "old.py").write_text("def h():\n return 1\n")
|
|
22
|
+
git(tmp_path, "add", ".")
|
|
23
|
+
git(tmp_path, "commit", "-q", "-m", "base")
|
|
24
|
+
git(tmp_path, "switch", "-q", "-c", "feature")
|
|
25
|
+
return tmp_path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_toplevel_and_merge_base(repo):
|
|
29
|
+
assert toplevel(repo / "pkg") == repo
|
|
30
|
+
assert merge_base(repo, "main") == git(repo, "rev-parse", "main").strip()
|
|
31
|
+
with pytest.raises(ToolError, match="nope"):
|
|
32
|
+
merge_base(repo, "nope")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_changed_files_covers_modified_added_renamed_untracked_and_skips_deleted(repo):
|
|
36
|
+
base = merge_base(repo, "main")
|
|
37
|
+
(repo / "pkg" / "a.py").write_text("def f(x):\n return x + 1\n\n\ndef g(y):\n return y\n")
|
|
38
|
+
git(repo, "mv", "pkg/old.py", "pkg/new.py")
|
|
39
|
+
(repo / "pkg" / "added.py").write_text("def k():\n return 2\n")
|
|
40
|
+
git(repo, "add", "pkg/added.py")
|
|
41
|
+
git(repo, "commit", "-q", "-m", "work")
|
|
42
|
+
(repo / "pkg" / "untracked.py").write_text("def u():\n return 3\n")
|
|
43
|
+
(repo / "pkg" / "gone.py").write_text("x = 1\n")
|
|
44
|
+
git(repo, "add", "pkg/gone.py")
|
|
45
|
+
git(repo, "commit", "-q", "-m", "add gone")
|
|
46
|
+
(repo / "pkg" / "gone.py").unlink()
|
|
47
|
+
assert changed_files(repo, base) == [
|
|
48
|
+
("pkg/a.py", "pkg/a.py"),
|
|
49
|
+
("pkg/added.py", None),
|
|
50
|
+
("pkg/new.py", "pkg/old.py"),
|
|
51
|
+
("pkg/untracked.py", None),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_touched_lines_include_pure_deletions(repo):
|
|
56
|
+
base = merge_base(repo, "main")
|
|
57
|
+
# change line 2, delete lines 5-6 (function g)
|
|
58
|
+
(repo / "pkg" / "a.py").write_text("def f(x):\n return x * 2\n\n\n")
|
|
59
|
+
assert touched_lines(repo, base, "pkg/a.py") == [(2, 2), (4, 5)]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_show_returns_base_content_or_none(repo):
|
|
63
|
+
base = merge_base(repo, "main")
|
|
64
|
+
assert show(repo, base, "pkg/old.py") == "def h():\n return 1\n"
|
|
65
|
+
assert show(repo, base, "pkg/missing.py") is None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_tracked_files_includes_untracked_and_excludes_deleted(repo):
|
|
69
|
+
(repo / "pkg" / "u.py").write_text("")
|
|
70
|
+
(repo / "pkg" / "old.py").unlink()
|
|
71
|
+
assert tracked_files(repo) == ["pkg/a.py", "pkg/u.py"]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from crap_model import Func, ToolError
|
|
4
|
+
from crap_python import functions
|
|
5
|
+
|
|
6
|
+
SRC = '''
|
|
7
|
+
def simple(x):
|
|
8
|
+
return x
|
|
9
|
+
|
|
10
|
+
def branchy(items, flag):
|
|
11
|
+
total = 0
|
|
12
|
+
for i in items:
|
|
13
|
+
if i and flag:
|
|
14
|
+
total += 1
|
|
15
|
+
elif i > 2:
|
|
16
|
+
total += 2
|
|
17
|
+
return total if total else -1
|
|
18
|
+
|
|
19
|
+
class K:
|
|
20
|
+
def method(self, xs):
|
|
21
|
+
return [x for x in xs if x]
|
|
22
|
+
|
|
23
|
+
def outer(f):
|
|
24
|
+
def inner(y):
|
|
25
|
+
return y if y else 0
|
|
26
|
+
handler = lambda z: z if z else 1
|
|
27
|
+
return inner(f) + handler(f)
|
|
28
|
+
'''
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def by_name(funcs):
|
|
32
|
+
return {f.name: f for f in funcs}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_named_functions_with_qualified_names_and_ranges():
|
|
36
|
+
got = by_name(functions(SRC, "m.py"))
|
|
37
|
+
assert set(got) == {"simple", "branchy", "K.method", "outer", "outer.inner"}
|
|
38
|
+
assert got["simple"] == Func("m.py", "simple", 2, 3, 1)
|
|
39
|
+
assert (got["branchy"].start, got["branchy"].end) == (5, 12)
|
|
40
|
+
assert (got["outer"].start, got["outer"].end) == (18, 22)
|
|
41
|
+
assert (got["outer.inner"].start, got["outer.inner"].end) == (19, 20)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_cyclomatic_complexity_counts_branches_boolops_ternaries_comprehensions():
|
|
45
|
+
got = by_name(functions(SRC, "m.py"))
|
|
46
|
+
assert got["branchy"].cc == 6 # for, if, `and`, elif, ternary
|
|
47
|
+
assert got["K.method"].cc == 3 # comprehension for + if
|
|
48
|
+
assert got["outer"].cc == 2 # lambda's ternary folds into outer
|
|
49
|
+
assert got["outer.inner"].cc == 2 # its own ternary, not counted in outer
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_try_except_match_and_async():
|
|
53
|
+
src = '''
|
|
54
|
+
async def fetch(url):
|
|
55
|
+
try:
|
|
56
|
+
return await get(url)
|
|
57
|
+
except ValueError:
|
|
58
|
+
return None
|
|
59
|
+
except KeyError:
|
|
60
|
+
return {}
|
|
61
|
+
|
|
62
|
+
def route(cmd):
|
|
63
|
+
match cmd:
|
|
64
|
+
case "a":
|
|
65
|
+
return 1
|
|
66
|
+
case "b":
|
|
67
|
+
return 2
|
|
68
|
+
while True:
|
|
69
|
+
break
|
|
70
|
+
'''
|
|
71
|
+
got = by_name(functions(src, "m.py"))
|
|
72
|
+
assert got["fetch"].cc == 3 # two except handlers
|
|
73
|
+
assert got["route"].cc == 4 # two cases + while
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_defs_under_module_level_if_and_decorators():
|
|
77
|
+
src = '''
|
|
78
|
+
import typing
|
|
79
|
+
if typing.TYPE_CHECKING:
|
|
80
|
+
def stub(a):
|
|
81
|
+
return a or 1
|
|
82
|
+
|
|
83
|
+
@decorator
|
|
84
|
+
def deco(b):
|
|
85
|
+
return b
|
|
86
|
+
'''
|
|
87
|
+
got = by_name(functions(src, "m.py"))
|
|
88
|
+
assert got["stub"].cc == 2
|
|
89
|
+
assert got["deco"].start == 8 # the `def` line, not the decorator line
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_syntax_error_is_a_tool_error():
|
|
93
|
+
with pytest.raises(ToolError, match="m.py"):
|
|
94
|
+
functions("def (:", "m.py")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
from crap_model import Func
|
|
4
|
+
from crap_score import crap_score, hint, max_cc, required_coverage, score
|
|
5
|
+
|
|
6
|
+
THRESHOLDS = {"existing": 15, "new": 9}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_formula_matches_savoia_definition():
|
|
10
|
+
assert crap_score(22, 0.0) == 506
|
|
11
|
+
assert crap_score(9, 1.0) == 9
|
|
12
|
+
assert crap_score(5, 0.0) == 30
|
|
13
|
+
assert math.isclose(crap_score(41, 0.86), 41 * 41 * 0.14**3 + 41)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_required_coverage_is_none_when_cc_alone_exceeds_limit():
|
|
17
|
+
assert required_coverage(10, 9) is None
|
|
18
|
+
assert required_coverage(16, 15) is None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_required_coverage_at_the_edge_is_full():
|
|
22
|
+
assert required_coverage(9, 9) == 1.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_required_coverage_scales_with_cc():
|
|
26
|
+
assert required_coverage(2, 9) == 0.0
|
|
27
|
+
assert math.isclose(required_coverage(5, 9), 1 - (4 / 25) ** (1 / 3))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_max_cc_is_largest_passing_complexity_at_given_coverage():
|
|
31
|
+
assert max_cc(0.0, 9) == 2 # CC 3 at 0% = 12 > 9
|
|
32
|
+
assert max_cc(1.0, 15) == 15
|
|
33
|
+
assert max_cc(1.0, 9) == 9
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_hint_offers_coverage_when_reachable_and_split_when_cc_too_high():
|
|
37
|
+
assert hint(5, 0.0, 9) == "cover ≥ 46% or split to CC ≤ 2"
|
|
38
|
+
assert hint(22, 0.0, 15) == "split to CC ≤ 3"
|
|
39
|
+
assert hint(9, 0.5, 9) == "cover ≥ 100% or split to CC ≤ 5" # 25·0.125+5 = 8.1 ok, 36·0.125+6 = 10.5 not
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_score_uses_new_limit_for_new_functions():
|
|
43
|
+
f = Func("a.py", "f", 1, 5, 5)
|
|
44
|
+
new = score("backend", f, 0.0, "new", THRESHOLDS)
|
|
45
|
+
old = score("backend", f, 0.0, "existing", THRESHOLDS)
|
|
46
|
+
assert (new.limit, new.status) == (9, "FAIL")
|
|
47
|
+
assert (old.limit, old.status) == (15, "FAIL")
|
|
48
|
+
ok = score("backend", f, 1.0, "new", THRESHOLDS, note="n")
|
|
49
|
+
assert (ok.status, ok.hint, ok.note, ok.crap) == ("ok", "", "n", 5)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from crap_model import Func, ToolError
|
|
6
|
+
from crap_typescript import functions
|
|
7
|
+
|
|
8
|
+
REPO_ROOT = Path(__file__).resolve().parents[2] # has node_modules/typescript (devDependency)
|
|
9
|
+
|
|
10
|
+
TSX = """import { memo, useMemo } from 'react';
|
|
11
|
+
export function Plain(a: number) { return a; }
|
|
12
|
+
export const Widget = ({ items, flag }: { items: number[]; flag?: boolean }) => {
|
|
13
|
+
const tiles = useMemo(() => items.filter((i) => i > 1 && flag), [items, flag]);
|
|
14
|
+
const onKey = (e: { key: string }) => (e.key === 'a' ? 1 : 0);
|
|
15
|
+
return <div>{flag ? tiles.map((t) => <span key={t}>{t}</span>) : null}</div>;
|
|
16
|
+
};
|
|
17
|
+
export default memo(({ x }: { x?: number }) => x ?? 0);
|
|
18
|
+
class Store { get(k: string) { return k || 'x'; } }
|
|
19
|
+
export const Wrapped = memo(function Inner({ n }: { n: number }) { return n?.toString() ?? ''; });
|
|
20
|
+
button.addEventListener('click', () => { if (ready) go(); });
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def write(tmp_path, name, text):
|
|
25
|
+
p = tmp_path / name
|
|
26
|
+
p.write_text(text)
|
|
27
|
+
return p
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def by_name(funcs):
|
|
31
|
+
return {f.name: f for f in funcs}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_named_functions_folding_and_cc(tmp_path):
|
|
35
|
+
p = write(tmp_path, "w.tsx", TSX)
|
|
36
|
+
got = by_name(functions([(p, "src/w.tsx")], REPO_ROOT)["src/w.tsx"])
|
|
37
|
+
assert set(got) == {"Plain", "Widget", "Widget.onKey", "default", "Store.get", "Wrapped"}
|
|
38
|
+
assert got["Plain"] == Func("src/w.tsx", "Plain", 2, 2, 1)
|
|
39
|
+
assert (got["Widget"].start, got["Widget"].end, got["Widget"].cc) == (3, 7, 3) # `&&` in filter cb + ternary in JSX
|
|
40
|
+
assert got["Widget.onKey"].cc == 2
|
|
41
|
+
assert got["default"].cc == 2 # `??`
|
|
42
|
+
assert got["Store.get"].cc == 2 # `||`
|
|
43
|
+
assert got["Wrapped"].cc == 2 # `??`; optional chaining not counted
|
|
44
|
+
# the addEventListener callback has no named ancestor → ignored, not a function
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_switch_loops_catch_and_logical_assignment(tmp_path):
|
|
48
|
+
src = """export function f(k: string, xs: number[]) {
|
|
49
|
+
let n = 0;
|
|
50
|
+
switch (k) { case 'a': n = 1; break; case 'b': n = 2; break; default: n = 3; }
|
|
51
|
+
for (const x of xs) { while (x > n) { n++; } }
|
|
52
|
+
try { n ||= 4; } catch (e) { n &&= 5; }
|
|
53
|
+
do { n--; } while (n > 0);
|
|
54
|
+
return n;
|
|
55
|
+
}
|
|
56
|
+
"""
|
|
57
|
+
p = write(tmp_path, "f.ts", src)
|
|
58
|
+
got = by_name(functions([(p, "src/f.ts")], REPO_ROOT)["src/f.ts"])
|
|
59
|
+
assert got["f"].cc == 9 # 2 cases, for-of, while, catch, ||=, &&=, do-while
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_batch_returns_every_label_even_when_empty(tmp_path):
|
|
63
|
+
a = write(tmp_path, "a.ts", "export const n = 1;\n")
|
|
64
|
+
b = write(tmp_path, "b.ts", "export const g = () => 1;\n")
|
|
65
|
+
got = functions([(a, "a.ts"), (b, "b.ts")], REPO_ROOT)
|
|
66
|
+
assert got["a.ts"] == []
|
|
67
|
+
assert [f.name for f in got["b.ts"]] == ["g"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_missing_typescript_package_is_a_tool_error(tmp_path):
|
|
71
|
+
p = write(tmp_path, "a.ts", "export const g = () => 1;\n")
|
|
72
|
+
with pytest.raises(ToolError, match="typescript"):
|
|
73
|
+
functions([(p, "a.ts")], tmp_path)
|
package/skills-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-
|
|
3
|
+
"generatedAt": "2026-08-30T22:42:44.950Z",
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
6
6
|
"name": "backlog",
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
"hash": "sha256-5f239c09fd46ba4c1887da88374a75e12e3df31ff18e93ed9367bdd06f3b386c",
|
|
21
21
|
"description": "CEO/founder-mode plan review. Rethink the problem, find the 10-star product,\nchallenge premises, expand scope when it creates a better product. Four modes:\nSCOPE EXPANSION (dream big), SELECTIVE EXPANSION (hold scope + cherry-pick\nexpansions), HOLD SCOPE (maximum rigor), SCOPE REDUCTION (strip to essentials).\nUse when asked to \"think bigger\", \"expand scope\", \"strategy review\", \"rethink this\",\n\"what would you do differently\", or when reviewing project direction and goals.\nProduces a structured review document with analysis and recommendations.\n"
|
|
22
22
|
},
|
|
23
|
+
{
|
|
24
|
+
"name": "crap-gate",
|
|
25
|
+
"version": "0.1.1",
|
|
26
|
+
"hash": "sha256-c4d66903f2d8b3d17b65e193f75ee0eea6dff9b8f47bd04a520a4f10778d895c",
|
|
27
|
+
"description": "Score every function a branch changed with CRAP (cyclomatic complexity × untested fraction), fail on existing functions > 15 and new functions > 9, and drive a fixer + independent reviewer loop until the gate passes. Use when implementation is done and before pushing, when the user says \"run the crap gate\", \"check CRAP\", \"is this change tested enough\", or when a push gate reports CRAP violations."
|
|
28
|
+
},
|
|
23
29
|
{
|
|
24
30
|
"name": "file-bug-issue",
|
|
25
31
|
"version": "0.1.0",
|