whycode-cli 0.2.4__tar.gz → 0.2.6__tar.gz
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.
- {whycode_cli-0.2.4/src/whycode_cli.egg-info → whycode_cli-0.2.6}/PKG-INFO +3 -2
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/README.md +2 -1
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/pyproject.toml +1 -1
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode/__init__.py +1 -1
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode/cli.py +165 -0
- whycode_cli-0.2.6/src/whycode/templates/github-workflow.yml +64 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6/src/whycode_cli.egg-info}/PKG-INFO +3 -2
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/tests/test_cli.py +81 -0
- whycode_cli-0.2.4/src/whycode/templates/github-workflow.yml +0 -40
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/LICENSE +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/setup.cfg +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode/__main__.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode/git_facts.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode/ignore.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode/mcp_server.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode/risk_card.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode/scorer.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode/signals.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode/suppressions.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode/templates/__init__.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode/templates/pre-commit +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode_cli.egg-info/SOURCES.txt +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode_cli.egg-info/dependency_links.txt +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode_cli.egg-info/entry_points.txt +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode_cli.egg-info/requires.txt +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/src/whycode_cli.egg-info/top_level.txt +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/tests/test_git_facts.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/tests/test_ignore.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/tests/test_scorer.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/tests/test_signals.py +0 -0
- {whycode_cli-0.2.4 → whycode_cli-0.2.6}/tests/test_suppressions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: whycode-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Tells you what to be afraid of before you touch a file.
|
|
5
5
|
Author: Kevin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -87,8 +87,9 @@ Requires Python 3.11+.
|
|
|
87
87
|
```bash
|
|
88
88
|
cd /path/to/your/repo
|
|
89
89
|
|
|
90
|
+
whycode tour # the one command to run first
|
|
90
91
|
whycode init # one-command setup: CI workflow + pre-commit gate
|
|
91
|
-
whycode highlights #
|
|
92
|
+
whycode highlights # repo-wide treasure map: top decisions + incidents
|
|
92
93
|
whycode why src/some/file.py # the Risk Card for one file
|
|
93
94
|
whycode why src/some/file.py -b # one-line summary (for triage / scripts)
|
|
94
95
|
whycode why src/some/file.py --at <sha> # risk as of a past commit
|
|
@@ -59,8 +59,9 @@ Requires Python 3.11+.
|
|
|
59
59
|
```bash
|
|
60
60
|
cd /path/to/your/repo
|
|
61
61
|
|
|
62
|
+
whycode tour # the one command to run first
|
|
62
63
|
whycode init # one-command setup: CI workflow + pre-commit gate
|
|
63
|
-
whycode highlights #
|
|
64
|
+
whycode highlights # repo-wide treasure map: top decisions + incidents
|
|
64
65
|
whycode why src/some/file.py # the Risk Card for one file
|
|
65
66
|
whycode why src/some/file.py -b # one-line summary (for triage / scripts)
|
|
66
67
|
whycode why src/some/file.py --at <sha> # risk as of a past commit
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Commands
|
|
4
4
|
--------
|
|
5
|
+
- ``whycode tour`` — first-run walkthrough: highlights + top risk + MCP setup.
|
|
5
6
|
- ``whycode why <path>`` — print the Risk Card for a single file.
|
|
6
7
|
- ``whycode why <path> --at SHA`` — risk card as of a past commit.
|
|
7
8
|
- ``whycode why <path> --mute KIND`` — locally suppress a noisy signal kind.
|
|
@@ -238,6 +239,14 @@ def diff(
|
|
|
238
239
|
json_out: bool = typer.Option(
|
|
239
240
|
False, "--json", help="Emit machine-readable JSON instead of a table."
|
|
240
241
|
),
|
|
242
|
+
markdown: bool = typer.Option(
|
|
243
|
+
False,
|
|
244
|
+
"--markdown",
|
|
245
|
+
help=(
|
|
246
|
+
"Emit GitHub-flavoured markdown suitable for posting as a PR comment. "
|
|
247
|
+
"Pipe into a workflow step that calls `gh pr comment`."
|
|
248
|
+
),
|
|
249
|
+
),
|
|
241
250
|
fail_on: str | None = typer.Option(
|
|
242
251
|
None,
|
|
243
252
|
"--fail-on",
|
|
@@ -305,6 +314,37 @@ def diff(
|
|
|
305
314
|
|
|
306
315
|
flagged = [c for c in cards if _is_actionable(c)]
|
|
307
316
|
quiet_n = len(cards) - len(flagged)
|
|
317
|
+
scope_md = "files staged for commit" if staged else f"files changed vs `{actual_base}`"
|
|
318
|
+
if markdown:
|
|
319
|
+
# Stable marker so a follow-up workflow step can find-and-update the
|
|
320
|
+
# same comment on subsequent pushes instead of stacking new ones.
|
|
321
|
+
print("<!-- whycode-comment -->")
|
|
322
|
+
print("## WhyCode risk briefing")
|
|
323
|
+
print()
|
|
324
|
+
print(f"**{len(files)} {scope_md}**")
|
|
325
|
+
print()
|
|
326
|
+
if not flagged:
|
|
327
|
+
print("Nothing flagged. Read the diff anyway.")
|
|
328
|
+
else:
|
|
329
|
+
print("| Score | Band | File | Top signal |")
|
|
330
|
+
print("| ----: | ---- | ---- | ---------- |")
|
|
331
|
+
for c in flagged:
|
|
332
|
+
top_signal = c.signals[0].headline.replace("|", "\\|")
|
|
333
|
+
print(
|
|
334
|
+
f"| {c.score.value} | {c.score.band.value} | "
|
|
335
|
+
f"`{c.path}` | {top_signal} |"
|
|
336
|
+
)
|
|
337
|
+
if quiet_n:
|
|
338
|
+
print()
|
|
339
|
+
print(f"_+ {quiet_n} file(s) changed with no flags._")
|
|
340
|
+
print()
|
|
341
|
+
print(
|
|
342
|
+
"_Run `whycode why <path>` for the full Risk Card on any of the above._"
|
|
343
|
+
)
|
|
344
|
+
if threshold is not None and any(c.score.value >= threshold for c in cards):
|
|
345
|
+
raise typer.Exit(1)
|
|
346
|
+
return
|
|
347
|
+
|
|
308
348
|
scope = "staged for commit" if staged else f"changed vs {actual_base}"
|
|
309
349
|
console.print(f"[bold]{len(files)} file(s) {scope}[/bold]")
|
|
310
350
|
if not flagged:
|
|
@@ -806,6 +846,131 @@ def _install_template(
|
|
|
806
846
|
return f"[green]wrote:[/green] {rel_label}"
|
|
807
847
|
|
|
808
848
|
|
|
849
|
+
_MCP_SNIPPET = ''' {
|
|
850
|
+
"mcpServers": {
|
|
851
|
+
"whycode": {"command": "whycode", "args": ["mcp"]}
|
|
852
|
+
}
|
|
853
|
+
}'''
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
@app.command()
|
|
857
|
+
def tour(
|
|
858
|
+
repo: Path = typer.Option(Path("."), "--repo", help="Path inside the repo."),
|
|
859
|
+
) -> None:
|
|
860
|
+
"""First-run walkthrough: highlights + top risky files + MCP setup snippet.
|
|
861
|
+
|
|
862
|
+
The single command to run after installing WhyCode. Skips straight to
|
|
863
|
+
the most concrete things in the repo (verbatim invariants and
|
|
864
|
+
incident-flagged commits) and ends with the one snippet you'll need to
|
|
865
|
+
wire WhyCode into an MCP-aware editor.
|
|
866
|
+
"""
|
|
867
|
+
try:
|
|
868
|
+
repo_root = gf.discover_repo_root(repo.resolve())
|
|
869
|
+
except gf.GitError as exc:
|
|
870
|
+
err.print(f"[red]error:[/red] {exc}")
|
|
871
|
+
raise typer.Exit(2) from exc
|
|
872
|
+
|
|
873
|
+
console.print("[bold]Welcome to WhyCode.[/bold]")
|
|
874
|
+
console.print(f"[dim]Reading the history of {repo_root.name}…[/dim]\n")
|
|
875
|
+
|
|
876
|
+
# Section 1 — invariants and incidents (cheap; one git log call).
|
|
877
|
+
with console.status("Looking for stated decisions…", spinner="dots"):
|
|
878
|
+
commits = gf.all_commits(repo_root, max_count=2000)
|
|
879
|
+
if not commits:
|
|
880
|
+
console.print("[yellow]This repo has no commits yet — nothing to learn from.[/yellow]")
|
|
881
|
+
return
|
|
882
|
+
|
|
883
|
+
inv_pairs = gf.extract_invariant_quotes(commits)
|
|
884
|
+
sha_to_commit = {c.sha: c for c in commits}
|
|
885
|
+
seen_lines: dict[str, str] = {}
|
|
886
|
+
for sha, line in inv_pairs:
|
|
887
|
+
seen_lines.setdefault(line, sha)
|
|
888
|
+
invariants_top = [
|
|
889
|
+
(line, sha_to_commit[sha])
|
|
890
|
+
for line, sha in seen_lines.items()
|
|
891
|
+
if sha in sha_to_commit
|
|
892
|
+
][:3]
|
|
893
|
+
incidents_top = gf.find_incidents(commits)[:3]
|
|
894
|
+
|
|
895
|
+
if invariants_top or incidents_top:
|
|
896
|
+
console.print("[bold yellow]Decisions and incidents[/bold yellow]")
|
|
897
|
+
for line, c in invariants_top:
|
|
898
|
+
console.print(f" [italic]{line}[/italic]")
|
|
899
|
+
console.print(
|
|
900
|
+
f" [dim]{c.sha[:7]} {c.authored_at.date()} {c.author_name}[/dim]\n"
|
|
901
|
+
)
|
|
902
|
+
for c in incidents_top:
|
|
903
|
+
subj = c.subject if len(c.subject) <= 70 else c.subject[:69] + "…"
|
|
904
|
+
console.print(f" [red]{subj}[/red]")
|
|
905
|
+
console.print(
|
|
906
|
+
f" [dim]{c.sha[:7]} {c.authored_at.date()} {c.author_name}[/dim]\n"
|
|
907
|
+
)
|
|
908
|
+
else:
|
|
909
|
+
console.print(
|
|
910
|
+
"[dim]No headline decisions or incidents in recent history.[/dim]"
|
|
911
|
+
)
|
|
912
|
+
console.print(
|
|
913
|
+
"[dim]Commit messages may be too terse — describing 'why' in commit "
|
|
914
|
+
"bodies (or using `hotfix:` / `BREAKING CHANGE:` prefixes) makes WhyCode "
|
|
915
|
+
"much more useful.[/dim]\n"
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
# Section 2 — top risky files. Slimmer scan: 100 files, depth 50 commits.
|
|
919
|
+
raw = gf.run_git(repo_root, "ls-files")
|
|
920
|
+
patterns = ign.effective_patterns(repo_root)
|
|
921
|
+
paths = [p for p in raw.splitlines() if p.strip() and not ign.is_ignored(p, patterns)][
|
|
922
|
+
:100
|
|
923
|
+
]
|
|
924
|
+
cards: list[rc.RiskCard] = []
|
|
925
|
+
if paths:
|
|
926
|
+
with console.status(
|
|
927
|
+
f"Risk-ranking {len(paths)} files (slim scan)…", spinner="dots"
|
|
928
|
+
):
|
|
929
|
+
for p in paths:
|
|
930
|
+
try:
|
|
931
|
+
card = rc.build(repo_root, p, max_commits=50)
|
|
932
|
+
except gf.GitError:
|
|
933
|
+
continue
|
|
934
|
+
useful = [s for s in card.signals if s.kind is not sig.SignalKind.NEWBORN]
|
|
935
|
+
if useful:
|
|
936
|
+
cards.append(card)
|
|
937
|
+
cards.sort(key=lambda c: -c.score.value)
|
|
938
|
+
|
|
939
|
+
if cards:
|
|
940
|
+
console.print("[bold red]Top 3 risky files[/bold red]")
|
|
941
|
+
for top in cards[:3]:
|
|
942
|
+
console.print(
|
|
943
|
+
f" [bold]{top.score.value:>3}[/bold] "
|
|
944
|
+
f"{top.score.band.value:<20} [cyan]{top.path}[/cyan]"
|
|
945
|
+
)
|
|
946
|
+
console.print(f" [dim]{top.signals[0].headline}[/dim]")
|
|
947
|
+
console.print()
|
|
948
|
+
|
|
949
|
+
# Section 3 — MCP setup snippet (vendor-neutral phrasing).
|
|
950
|
+
console.print("[bold magenta]Wire WhyCode into your AI editor[/bold magenta]")
|
|
951
|
+
console.print(
|
|
952
|
+
" WhyCode ships an MCP server. Any MCP-aware editor or assistant\n"
|
|
953
|
+
" can call it — just add this snippet to your editor's MCP config:\n"
|
|
954
|
+
)
|
|
955
|
+
console.print(_MCP_SNIPPET)
|
|
956
|
+
console.print(
|
|
957
|
+
"\n [dim](See your editor's docs for the exact config-file location.)[/dim]\n"
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
# Section 4 — what to do next.
|
|
961
|
+
console.print("[bold]Next:[/bold]")
|
|
962
|
+
if cards:
|
|
963
|
+
console.print(
|
|
964
|
+
f" [dim]·[/dim] [bold]whycode why {cards[0].path}[/bold] the full Risk Card"
|
|
965
|
+
)
|
|
966
|
+
console.print(
|
|
967
|
+
" [dim]·[/dim] [bold]whycode init[/bold] install CI + pre-commit"
|
|
968
|
+
)
|
|
969
|
+
console.print(
|
|
970
|
+
" [dim]·[/dim] [bold]whycode highlights[/bold] more invariants and incidents"
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
|
|
809
974
|
@app.command()
|
|
810
975
|
def init(
|
|
811
976
|
force: bool = typer.Option(
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Risk-rank pull requests with WhyCode.
|
|
2
|
+
#
|
|
3
|
+
# On every PR this workflow:
|
|
4
|
+
# 1. Computes the Risk Card for each changed file (vs the PR base)
|
|
5
|
+
# 2. Prints a risk-ranked table to the job log
|
|
6
|
+
# 3. Posts (or updates) a single PR comment with the same table
|
|
7
|
+
#
|
|
8
|
+
# Advisory by default — humans decide. To turn it into a hard gate that
|
|
9
|
+
# blocks merging, append `--fail-on <band>` to the diff line:
|
|
10
|
+
# handle block at HANDLE WITH CARE (score >= 75)
|
|
11
|
+
# history block at READ HISTORY FIRST (score >= 50)
|
|
12
|
+
# look stricter — block at >= 25
|
|
13
|
+
name: WhyCode
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
pull_request:
|
|
17
|
+
types: [opened, synchronize, reopened]
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
pull-requests: write # required to post / update the PR comment
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
whycode:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- name: Check out PR
|
|
28
|
+
uses: actions/checkout@v4
|
|
29
|
+
with:
|
|
30
|
+
fetch-depth: 0 # WhyCode needs full history
|
|
31
|
+
|
|
32
|
+
- name: Set up Python
|
|
33
|
+
uses: actions/setup-python@v5
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.11"
|
|
36
|
+
|
|
37
|
+
- name: Install WhyCode
|
|
38
|
+
run: pip install whycode-cli
|
|
39
|
+
|
|
40
|
+
- name: Risk-rank files in this PR (job log)
|
|
41
|
+
run: whycode diff --base origin/${{ github.base_ref }}
|
|
42
|
+
|
|
43
|
+
- name: Build PR comment body
|
|
44
|
+
run: whycode diff --base origin/${{ github.base_ref }} --markdown > whycode-comment.md
|
|
45
|
+
|
|
46
|
+
- name: Post or update the PR comment
|
|
47
|
+
env:
|
|
48
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
49
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
50
|
+
REPO: ${{ github.repository }}
|
|
51
|
+
run: |
|
|
52
|
+
set -euo pipefail
|
|
53
|
+
BODY=$(cat whycode-comment.md)
|
|
54
|
+
# Find any existing comment we previously posted (identified by the
|
|
55
|
+
# hidden HTML marker WhyCode emits at the top of the message).
|
|
56
|
+
EXISTING=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
57
|
+
--jq 'map(select(.body | contains("<!-- whycode-comment -->"))) | .[0].id // empty')
|
|
58
|
+
if [ -n "$EXISTING" ]; then
|
|
59
|
+
gh api -X PATCH "repos/${REPO}/issues/comments/${EXISTING}" \
|
|
60
|
+
-f body="$BODY" > /dev/null
|
|
61
|
+
else
|
|
62
|
+
gh api -X POST "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
63
|
+
-f body="$BODY" > /dev/null
|
|
64
|
+
fi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: whycode-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Tells you what to be afraid of before you touch a file.
|
|
5
5
|
Author: Kevin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -87,8 +87,9 @@ Requires Python 3.11+.
|
|
|
87
87
|
```bash
|
|
88
88
|
cd /path/to/your/repo
|
|
89
89
|
|
|
90
|
+
whycode tour # the one command to run first
|
|
90
91
|
whycode init # one-command setup: CI workflow + pre-commit gate
|
|
91
|
-
whycode highlights #
|
|
92
|
+
whycode highlights # repo-wide treasure map: top decisions + incidents
|
|
92
93
|
whycode why src/some/file.py # the Risk Card for one file
|
|
93
94
|
whycode why src/some/file.py -b # one-line summary (for triage / scripts)
|
|
94
95
|
whycode why src/some/file.py --at <sha> # risk as of a past commit
|
|
@@ -508,6 +508,39 @@ def test_scan_no_ignore_brings_them_back(repo, days_ago) -> None: # type: ignor
|
|
|
508
508
|
assert "CHANGELOG" in permissive_run.output or "src/app.py" in permissive_run.output
|
|
509
509
|
|
|
510
510
|
|
|
511
|
+
def test_diff_markdown_output(repo, days_ago) -> None: # type: ignore[no-untyped-def]
|
|
512
|
+
repo.commit("init", {"refund.py": "0"}, when=days_ago(120))
|
|
513
|
+
sha = repo.commit("feat: refund flow", {"refund.py": "1"}, when=days_ago(60))
|
|
514
|
+
repo.revert(sha, when=days_ago(50))
|
|
515
|
+
repo.commit(
|
|
516
|
+
"hotfix: regression",
|
|
517
|
+
{"refund.py": "2"},
|
|
518
|
+
body="incident #INC-42",
|
|
519
|
+
when=days_ago(10),
|
|
520
|
+
)
|
|
521
|
+
result = _invoke(repo.root, "diff", "--base", "HEAD~3", "--markdown")
|
|
522
|
+
assert result.exit_code == 0
|
|
523
|
+
out = result.output
|
|
524
|
+
# Hidden marker so the workflow can find-and-update its prior comment.
|
|
525
|
+
assert "<!-- whycode-comment -->" in out
|
|
526
|
+
# Markdown table syntax.
|
|
527
|
+
assert "| Score | Band |" in out
|
|
528
|
+
assert "| ----: |" in out
|
|
529
|
+
# File path appears as inline code.
|
|
530
|
+
assert "`refund.py`" in out
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def test_diff_markdown_quiet_repo(repo, days_ago) -> None: # type: ignore[no-untyped-def]
|
|
534
|
+
repo.commit("init", {"a.py": "1"}, when=days_ago(40))
|
|
535
|
+
repo.commit("docs: tweak", {"a.py": "2"}, when=days_ago(20))
|
|
536
|
+
result = _invoke(repo.root, "diff", "--base", "HEAD~1", "--markdown")
|
|
537
|
+
assert result.exit_code == 0
|
|
538
|
+
out = result.output
|
|
539
|
+
assert "<!-- whycode-comment -->" in out
|
|
540
|
+
# No flagged files → friendly note instead of an empty table.
|
|
541
|
+
assert "Nothing flagged" in out
|
|
542
|
+
|
|
543
|
+
|
|
511
544
|
def test_scan_respects_user_whycodeignore(repo, days_ago) -> None: # type: ignore[no-untyped-def]
|
|
512
545
|
(repo.root / ".whycodeignore").write_text("internal/legacy.py\n")
|
|
513
546
|
sha = repo.commit(
|
|
@@ -556,3 +589,51 @@ def test_scan_lists_top_files(repo, days_ago) -> None: # type: ignore[no-untype
|
|
|
556
589
|
result = _invoke(repo.root, "scan", "--top", "3")
|
|
557
590
|
assert result.exit_code == 0
|
|
558
591
|
assert "a.py" in result.output
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def test_tour_runs_and_emits_all_sections(repo, days_ago) -> None: # type: ignore[no-untyped-def]
|
|
595
|
+
repo.commit(
|
|
596
|
+
"compat: keep sync path",
|
|
597
|
+
{"a.py": "1"},
|
|
598
|
+
body="Do not switch to async — v1 clients break.",
|
|
599
|
+
when=days_ago(60),
|
|
600
|
+
)
|
|
601
|
+
repo.commit(
|
|
602
|
+
"hotfix: refund regression",
|
|
603
|
+
{"b.py": "1"},
|
|
604
|
+
body="See INC-447.",
|
|
605
|
+
when=days_ago(20),
|
|
606
|
+
)
|
|
607
|
+
sha = repo.commit("feat: A", {"a.py": "2"}, when=days_ago(40))
|
|
608
|
+
repo.revert(sha, when=days_ago(15))
|
|
609
|
+
result = _invoke(repo.root, "tour")
|
|
610
|
+
assert result.exit_code == 0
|
|
611
|
+
out = result.output
|
|
612
|
+
assert "Welcome to WhyCode" in out
|
|
613
|
+
assert "Decisions and incidents" in out
|
|
614
|
+
assert "Do not switch to async" in out
|
|
615
|
+
assert "hotfix: refund regression" in out
|
|
616
|
+
assert "Wire WhyCode into your AI editor" in out
|
|
617
|
+
# MCP snippet appears verbatim so users can copy-paste.
|
|
618
|
+
assert '"command": "whycode"' in out
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def test_tour_quiet_repo_explains_why(repo) -> None: # type: ignore[no-untyped-def]
|
|
622
|
+
repo.commit("init", {"a.py": "1"})
|
|
623
|
+
result = _invoke(repo.root, "tour")
|
|
624
|
+
assert result.exit_code == 0
|
|
625
|
+
out = result.output
|
|
626
|
+
# MCP section appears regardless — most useful next step.
|
|
627
|
+
assert "Wire WhyCode into your AI editor" in out
|
|
628
|
+
# And the empty-state explanation should mention why nothing fires.
|
|
629
|
+
assert "terse" in out.lower() or "no headline" in out.lower()
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def test_tour_outside_repo_errors(tmp_path) -> None: # type: ignore[no-untyped-def]
|
|
633
|
+
cwd = os.getcwd()
|
|
634
|
+
os.chdir(tmp_path)
|
|
635
|
+
try:
|
|
636
|
+
result = runner.invoke(app, ["tour"], catch_exceptions=False)
|
|
637
|
+
finally:
|
|
638
|
+
os.chdir(cwd)
|
|
639
|
+
assert result.exit_code != 0
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# Risk-rank pull requests with WhyCode.
|
|
2
|
-
#
|
|
3
|
-
# On every PR this workflow computes the Risk Card for each changed file
|
|
4
|
-
# (vs the PR base) and prints a risk-ranked table to the job log.
|
|
5
|
-
# Advisory by default — humans decide.
|
|
6
|
-
#
|
|
7
|
-
# To turn it into a hard gate that blocks merging, append `--fail-on <band>`
|
|
8
|
-
# to the `whycode diff` line below:
|
|
9
|
-
# handle block at HANDLE WITH CARE (score >= 75)
|
|
10
|
-
# history block at READ HISTORY FIRST (score >= 50)
|
|
11
|
-
# look stricter — block at >= 25
|
|
12
|
-
name: WhyCode
|
|
13
|
-
|
|
14
|
-
on:
|
|
15
|
-
pull_request:
|
|
16
|
-
types: [opened, synchronize, reopened]
|
|
17
|
-
|
|
18
|
-
permissions:
|
|
19
|
-
contents: read
|
|
20
|
-
pull-requests: read
|
|
21
|
-
|
|
22
|
-
jobs:
|
|
23
|
-
whycode:
|
|
24
|
-
runs-on: ubuntu-latest
|
|
25
|
-
steps:
|
|
26
|
-
- name: Check out PR
|
|
27
|
-
uses: actions/checkout@v4
|
|
28
|
-
with:
|
|
29
|
-
fetch-depth: 0 # WhyCode needs full history
|
|
30
|
-
|
|
31
|
-
- name: Set up Python
|
|
32
|
-
uses: actions/setup-python@v5
|
|
33
|
-
with:
|
|
34
|
-
python-version: "3.11"
|
|
35
|
-
|
|
36
|
-
- name: Install WhyCode
|
|
37
|
-
run: pip install whycode-cli
|
|
38
|
-
|
|
39
|
-
- name: Risk-rank files in this PR
|
|
40
|
-
run: whycode diff --base origin/${{ github.base_ref }}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|