Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .riskcheck.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# .riskcheck.yaml — riskcheck configuration
# Place this file at your project root.
# All fields are optional; defaults are used for unset values.

signals:
large_change:
threshold: 10 # Number of files changed
weight: 10
high_insertions:
threshold: 200 # Number of added lines
weight: 10
high_deletions:
threshold: 200 # Number of deleted lines
weight: 5
hotspot:
since: "90 days ago"
threshold: 5 # Number of changes in the period
weight: 10
no_test_change:
weight: 15
security_module:
weight: 20
paths:
- auth/
- security/
- crypto/
- permission/
- acl/
- token/
- jwt/
core_module:
weight: 20
paths:
- config/
- billing/
- payment/
- database/
- migration/
- infra/
- api/

# Override test file patterns (replaces defaults if set)
# test_patterns:
# - "*_test.go"
# - "*.test.ts"
# - "*.spec.ts"
24 changes: 10 additions & 14 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,22 @@ Temporary, task-scoped specs derived from the master specs.

## Current Focus

**Step2: Practical Level** (see `specs/roadmap.md`)
**Step3: Team Customization** (see `specs/roadmap.md`)

### Tasks

- [x] Git log integration (`git log --name-only`)
- [x] S-4: hotspot signal (90 days, 5+ changes)
- [x] S-5: no_test_change signal (aggregate)
- [x] S-6: core_module signal (file-scoped)
- [x] S-7: security_module signal (file-scoped, priority over core)
- [x] Analyzer priority dedup (security > core)
- [x] Scorer per-file risk (file-scoped weights / 40.0)
- [x] Formatter update (files[] in JSON/text)
- [x] CLI exit code (non-zero for medium+)
- [x] Config loading (`internal/config`, `.riskcheck.yaml`)
- [x] Apply config to existing signals (thresholds, weights, paths, test patterns)
- [x] S-8: high_complexity signal (`--complexity-file`)
- [x] S-9: low_coverage signal (`--coverage-file`)
- [x] CLI flags (`--config`, `--complexity-file`, `--coverage-file`)
- [x] Sample `.riskcheck.yaml.example`

### Exit Criteria

- Hotspot files detected from git history
- Missing test changes flagged
- JSON output includes `files[]` with per-file risk and signals
- Exit code reflects risk level
- `.riskcheck.yaml` overrides default behavior
- External tool output can feed into scoring
- All existing tests still pass

## Tech Stack

Expand Down
134 changes: 118 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,69 @@ riskcheck --base origin/main | claude -p "Review the high-risk areas"

- Analyze `git diff` and calculate a risk score (0-100) for your changes
- Explain **why** the change is risky with concrete reasons
- Per-file risk scoring — know exactly which files need attention
- Output structured JSON — ready to pipe into AI tools or CI pipelines
- Customizable via `.riskcheck.yaml` for team-specific rules

## Quick Start

```bash
go install github.com/hidetzu/riskcheck@latest
riskcheck init # Generate .riskcheck.yaml (optional)
riskcheck --base origin/main
```

## Signals

riskcheck detects the following risk signals:

| Signal | Description | Default Weight |
|--------|-------------|---------------|
| large_change | Too many files changed | +10 |
| high_insertions | Too many lines added | +10 |
| high_deletions | Too many lines deleted | +5 |
| hotspot | Frequently changed files (last 90 days) | +10 |
| no_test_change | Production code changed without test updates | +15 |
| security_module | Security-related paths modified (auth, crypto, etc.) | +20 |
| core_module | Core business logic paths modified (config, payment, etc.) | +20 |
| high_complexity | High cyclomatic complexity (external input) | +15 |
| low_coverage | Low test coverage (external input) | +10 |

## Output Examples

### JSON (default)

```bash
$ riskcheck --base origin/main
{
"score": 35,
"level": "low",
"score": 55,
"level": "medium",
"summary": {
"files_changed": 3,
"insertions": 45,
"deletions": 12
"files_changed": 5,
"insertions": 120,
"deletions": 30
},
"reasons": [
"high number of insertions"
"security module modified (src/auth/login.go)",
"no test updates for changed files",
"hotspot file touched (src/auth/login.go changed 8 times in last 90 days ago)"
],
"files": [
{
"path": "src/auth/login.go",
"risk": 0.75,
"signals": ["hotspot", "security_module"]
},
{
"path": "src/config/app.go",
"risk": 0.5,
"signals": ["core_module"]
},
{
"path": "src/handler/home.go",
"risk": 0,
"signals": []
}
]
}
```
Expand All @@ -49,14 +87,20 @@ $ riskcheck --base origin/main

```bash
$ riskcheck --base origin/main --format text
Risk Score: 35 / 100 (low)
Risk Score: 55 / 100 (medium)

Files changed: 3
Insertions: 45
Deletions: 12
Files changed: 5
Insertions: 120
Deletions: 30

Reasons:
- high number of insertions
- security module modified (src/auth/login.go)
- no test updates for changed files
- hotspot file touched (src/auth/login.go changed 8 times in last 90 days ago)

High-risk files:
0.75 src/auth/login.go [hotspot, security_module]
0.50 src/config/app.go [core_module]
```

## Usage
Expand All @@ -65,11 +109,69 @@ Reasons:
riskcheck [flags]

Flags:
--base Comparison base (default: "origin/main")
--target Comparison target (default: "." working tree)
--format Output format: json, text (default: "json")
-h, --help Help
-v, --version Version
--base Comparison base (default: "origin/main")
--target Comparison target (default: "." working tree)
--format Output format: json, text (default: "json")
--config Config file path (default: ".riskcheck.yaml")
--complexity-file Path to complexity JSON file (optional)
--coverage-file Path to coverage JSON file (optional)
-h, --help Help
-v, --version Version
```

### Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Low risk (score 0-39) |
| 1 | Medium or high risk (score 40-100) |
| 2 | Error |

## Configuration

Generate a config file with `riskcheck init`, then customize:

```yaml
signals:
security_module:
weight: 25
paths:
- auth/
- secrets/
- oauth/
core_module:
paths:
- payments/
- orders/
hotspot:
since: "30 days ago"
threshold: 3

test_patterns:
- "*_test.go"
- "*_spec.rb"
```

## External Tool Integration

Feed complexity or coverage data from external tools:

```bash
# Complexity (e.g., from gocyclo, lizard)
riskcheck --base origin/main --complexity-file complexity.json

# Coverage (e.g., from go test -coverprofile, JaCoCo)
riskcheck --base origin/main --coverage-file coverage.json
```

Input JSON format:

```json
// complexity.json
[{"path": "src/main.go", "complexity": 15}]

// coverage.json
[{"path": "src/main.go", "coverage": 45.2}]
```

## AI Integration
Expand Down
84 changes: 84 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package cmd

import (
"errors"
"fmt"
"io/fs"
"os"

"github.com/spf13/cobra"
)

const defaultConfig = `# .riskcheck.yaml — riskcheck configuration
# All fields are optional; defaults are used for unset values.

signals:
large_change:
threshold: 10 # Number of files changed
weight: 10
high_insertions:
threshold: 200 # Number of added lines
weight: 10
high_deletions:
threshold: 200 # Number of deleted lines
weight: 5
hotspot:
since: "90 days ago"
threshold: 5 # Number of changes in the period
weight: 10
no_test_change:
weight: 15
security_module:
weight: 20
paths:
- auth/
- security/
- crypto/
- permission/
- acl/
- token/
- jwt/
core_module:
weight: 20
paths:
- config/
- billing/
- payment/
- database/
- migration/
- infra/
- api/

# Override test file patterns (replaces defaults if set)
# test_patterns:
# - "*_test.go"
# - "*.test.ts"
# - "*.spec.ts"
`

var initCmd = &cobra.Command{
Use: "init",
Short: "Generate a .riskcheck.yaml config file",
RunE: runInit,
}

func init() {
rootCmd.AddCommand(initCmd)
}

func runInit(cmd *cobra.Command, args []string) error {
const path = ".riskcheck.yaml"

if _, err := os.Stat(path); err == nil {
return fmt.Errorf("%s already exists", path)
} else if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("check config file: %w", err)
}

if err := os.WriteFile(path, []byte(defaultConfig), 0644); err != nil {
return fmt.Errorf("write config: %w", err)
}

fmt.Fprintf(os.Stderr, "Created %s\n", path)
return nil
}
Loading
Loading