whycode-cli 0.2.4__py3-none-any.whl → 0.2.6__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.
whycode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """WhyCode — tells you what to be afraid of before touching a file."""
2
2
 
3
- __version__ = "0.2.4"
3
+ __version__ = "0.2.6"
whycode/cli.py CHANGED
@@ -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(
@@ -1,11 +1,12 @@
1
1
  # Risk-rank pull requests with WhyCode.
2
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.
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
6
7
  #
7
- # To turn it into a hard gate that blocks merging, append `--fail-on <band>`
8
- # to the `whycode diff` line below:
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:
9
10
  # handle block at HANDLE WITH CARE (score >= 75)
10
11
  # history block at READ HISTORY FIRST (score >= 50)
11
12
  # look stricter — block at >= 25
@@ -17,7 +18,7 @@ on:
17
18
 
18
19
  permissions:
19
20
  contents: read
20
- pull-requests: read
21
+ pull-requests: write # required to post / update the PR comment
21
22
 
22
23
  jobs:
23
24
  whycode:
@@ -36,5 +37,28 @@ jobs:
36
37
  - name: Install WhyCode
37
38
  run: pip install whycode-cli
38
39
 
39
- - name: Risk-rank files in this PR
40
+ - name: Risk-rank files in this PR (job log)
40
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.4
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 # first-run treasure map: top decisions + incidents
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
@@ -1,6 +1,6 @@
1
- whycode/__init__.py,sha256=vahOy3X-a02yX10f7cBtuylMKDlCKJQyvZVggnCiQqw,96
1
+ whycode/__init__.py,sha256=PX9ljfWyjwwJEA1_I-kk34Qfj-9N3WRnXy1zQ6i6t-M,96
2
2
  whycode/__main__.py,sha256=dqAk6746YpuM-FTIH4TBOULegGc5WweojiZjce0VYgQ,105
3
- whycode/cli.py,sha256=jrX3GaMmZbNjnXzQtCViXMB6C3F-zwKKoMPtsISwgT4,30917
3
+ whycode/cli.py,sha256=JTufemrXaq-3ySNG-xfPZ0f5UhbtThiD4TXWSxE5qZ4,37365
4
4
  whycode/git_facts.py,sha256=VozSt59dWhUcDQ2qyDA2Bfa6AWvfBmIaQKP1DAYUpPM,17820
5
5
  whycode/ignore.py,sha256=sdRO_0HSedm8aO69CSGl-zQrUVX5MEg9QGcAJWwAvP4,3021
6
6
  whycode/mcp_server.py,sha256=56csOHSP90Zk59-_Puvk4WTSlCJ6xQAm-K10b_qmyAQ,7105
@@ -9,11 +9,11 @@ whycode/scorer.py,sha256=4pBejunfxzYhGUzMeL8uGEMQzC6DWiqwcTeMdo3eras,1444
9
9
  whycode/signals.py,sha256=14KziRolXvhmOnMnluXpPPInoBRO5uDu0tm024EYik0,13066
10
10
  whycode/suppressions.py,sha256=1lKSs-kCgpnJbcxozcgiSP8ZAfjEDMHXuM3sw4FaY78,3836
11
11
  whycode/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- whycode/templates/github-workflow.yml,sha256=yy87tbYKCexNYFso4e4OxGAdIIYOLn2cVxEt-FzP2oo,1095
12
+ whycode/templates/github-workflow.yml,sha256=LAfHMDG2TkAwi4vCNinHk-4zOt-mCWErBpmpaqlW5oA,2251
13
13
  whycode/templates/pre-commit,sha256=IhU11CvoDwqRAAsvHwUo-BwaNbdgy1cpXc54Z_phrmQ,316
14
- whycode_cli-0.2.4.dist-info/licenses/LICENSE,sha256=U6LN5qg5kJXSJf7KFPm9KJhmiGn3qK_GsTVWXdt1DFA,1062
15
- whycode_cli-0.2.4.dist-info/METADATA,sha256=ESACr8PI_DsPCZl0_LQ4fD0EhpsK1FEKgUrdwv3HDV4,9260
16
- whycode_cli-0.2.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
17
- whycode_cli-0.2.4.dist-info/entry_points.txt,sha256=xrNWc4CQn3ZhQFJxsGIPiTqpN19K4pRpgaj6qGaEzSQ,44
18
- whycode_cli-0.2.4.dist-info/top_level.txt,sha256=6yIL5rxW-4DbARHQYrPlGQVqKddZ88sjvmNosDh1w3A,8
19
- whycode_cli-0.2.4.dist-info/RECORD,,
14
+ whycode_cli-0.2.6.dist-info/licenses/LICENSE,sha256=U6LN5qg5kJXSJf7KFPm9KJhmiGn3qK_GsTVWXdt1DFA,1062
15
+ whycode_cli-0.2.6.dist-info/METADATA,sha256=zp9iSlF6ymPkl2om4iaza9CshWA_aHyETjFs7MbPJIg,9327
16
+ whycode_cli-0.2.6.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
17
+ whycode_cli-0.2.6.dist-info/entry_points.txt,sha256=xrNWc4CQn3ZhQFJxsGIPiTqpN19K4pRpgaj6qGaEzSQ,44
18
+ whycode_cli-0.2.6.dist-info/top_level.txt,sha256=6yIL5rxW-4DbARHQYrPlGQVqKddZ88sjvmNosDh1w3A,8
19
+ whycode_cli-0.2.6.dist-info/RECORD,,