Skip to content
Open
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ for _, f := range result.Findings {

See the [examples/](examples/) directory for runnable code samples.

## Portable quality graph

`qualitygraph.Build` projects a completed `sight.Result` into bounded shared
quality nodes, report-to-finding `contains` edges, and observation events.
Source diffs, paths, report text, messages, fixes, and reasoning are retained
only as SHA-256 digests. Sight remains the review source of truth; consumers
such as Hawk may journal and compose the projection.

## Provider Interface

Implement the `Provider` interface to use any LLM:
Expand Down
5 changes: 5 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ sight/
├── reviewer.go 🔄 Reviewer: parallel concern orchestration
├── options.go ⚙️ config, With* functions, presets
├── provider.go 🔌 Provider interface (consumers implement)
├── qualitygraph/ 🕸️ Privacy-safe shared quality graph projection
├── severity.go 📊 Re-exports from hawk-core-contracts/types
├── static_rules.go 🛡️ 30+ static analysis rules
├── taint_analysis.go 🔗 SSA-based taint tracking
Expand All @@ -44,6 +45,10 @@ sight/
└── output/ 📊 SARIF and terminal formatters
```

`qualitygraph.Build` is an explicit read-only boundary after review. It never
runs a review or persists graph state, and it excludes source and human-readable
review content from graph attributes.

---

## 📤 Public API
Expand Down
4 changes: 3 additions & 1 deletion go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 5 additions & 17 deletions incremental.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"strings"
"sync"
"time"

sightcontext "github.com/GrayCodeAI/sight/internal/context"
)

// IncrementalState tracks the last-reviewed commit SHA for incremental reviews.
Expand Down Expand Up @@ -230,20 +232,6 @@ func gitRoot(ctx context.Context) (string, error) {
return strings.TrimSpace(string(out)), nil
}

// validateGitRef ensures a git ref contains no dangerous characters.
func validateGitRef(ref string) error {
if ref == "" {
return fmt.Errorf("empty git ref")
}
if ref[0] == '-' {
return fmt.Errorf("git ref %q starts with dash", ref)
}
if strings.ContainsAny(ref, ";&|$`(){}[]<>!#*?\n\r\x00\\ ") {
return fmt.Errorf("git ref %q contains forbidden characters", ref)
}
return nil
}

// gitDiffRange runs `git diff base...head` with a context timeout.
func gitDiffRange(ctx context.Context, base, head string) (string, error) {
// Default 30s timeout if context has no deadline
Expand All @@ -253,15 +241,15 @@ func gitDiffRange(ctx context.Context, base, head string) (string, error) {
defer cancel()
}

if err := validateGitRef(base); err != nil {
if err := sightcontext.ValidateGitRef(base); err != nil {
return "", fmt.Errorf("invalid base ref: %w", err)
}
if err := validateGitRef(head); err != nil {
if err := sightcontext.ValidateGitRef(head); err != nil {
return "", fmt.Errorf("invalid head ref: %w", err)
}

// Try three-dot syntax first (merge-base diff)
// #nosec G204 — base and head validated by validateGitRef above
// #nosec G204 — base and head validated by sightcontext.ValidateGitRef above
out, err := exec.CommandContext(ctx, "git", "diff", base+"..."+head).Output()
if err == nil {
return string(out), nil
Expand Down
14 changes: 7 additions & 7 deletions internal/context/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ func FormatContext(contexts []FileContext) string {

// DiffBase returns the diff of the current branch against a base branch.
func DiffBase(base string) (string, error) {
if err := validateGitRef(base); err != nil {
if err := ValidateGitRef(base); err != nil {
return "", err
}
// #nosec G204 — base validated by validateGitRef
// #nosec G204 — base validated by ValidateGitRef
out, err := exec.Command("git", "diff", base+"...HEAD").Output()
if err != nil {
// #nosec G204 — base validated by validateGitRef
// #nosec G204 — base validated by ValidateGitRef
out2, err2 := exec.Command("git", "diff", base).Output()
if err2 != nil {
return "", fmt.Errorf("git diff failed: %w", err)
Expand All @@ -93,10 +93,10 @@ func DiffBase(base string) (string, error) {

// ChangedFiles returns the list of files changed relative to a base.
func ChangedFiles(base string) ([]string, error) {
if err := validateGitRef(base); err != nil {
if err := ValidateGitRef(base); err != nil {
return nil, err
}
// #nosec G204 — base validated by validateGitRef
// #nosec G204 — base validated by ValidateGitRef
out, err := exec.Command("git", "diff", "--name-only", base+"...HEAD").Output()
if err != nil {
return nil, fmt.Errorf("git diff --name-only failed: %w", err)
Expand Down Expand Up @@ -129,8 +129,8 @@ func Blame(file string, startLine, endLine int) (string, error) {
return parseBlameAuthors(string(out)), nil
}

// validateGitRef ensures a git ref (branch, tag, SHA) contains no dangerous characters.
func validateGitRef(ref string) error {
// ValidateGitRef ensures a git ref (branch, tag, SHA) contains no dangerous characters.
func ValidateGitRef(ref string) error {
if ref == "" {
return fmt.Errorf("empty git ref")
}
Expand Down
55 changes: 55 additions & 0 deletions internal/context/git_validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package context

import (
"strings"
"testing"
)

func TestValidateGitRef_ValidRefs(t *testing.T) {
t.Parallel()
valid := []string{
"main",
"feature/my-branch",
"v1.2.3",
"release/2026.01",
"abc123def456", // hex SHA
"HEAD",
"HEAD~1",
"my_branch",
"fix-123",
}
for _, ref := range valid {
if err := ValidateGitRef(ref); err != nil {
t.Errorf("ValidateGitRef(%q) = %v, want nil", ref, err)
}
}
}

func TestValidateGitRef_RejectsInvalid(t *testing.T) {
t.Parallel()
cases := []struct {
ref string
want string
}{
{"", "empty git ref"},
{"-force", "starts with dash"},
{"main;rm -rf", "forbidden characters"},
{"feat|cat /etc/passwd", "forbidden characters"},
{"branch$(cmd)", "forbidden characters"},
{"name with space", "forbidden characters"},
{"back\\slash", "forbidden characters"},
{"new\nline", "forbidden characters"},
{"wild*card", "forbidden characters"},
{"ques?tion", "forbidden characters"},
}
for _, tc := range cases {
err := ValidateGitRef(tc.ref)
if err == nil {
t.Errorf("ValidateGitRef(%q) = nil, want error containing %q", tc.ref, tc.want)
continue
}
if !strings.Contains(err.Error(), tc.want) {
t.Errorf("ValidateGitRef(%q) error = %q, want containing %q", tc.ref, err.Error(), tc.want)
}
}
}
4 changes: 0 additions & 4 deletions internal/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"path/filepath"
"sort"
"sync"
"time"
)

// Node represents a code unit in the dependency graph.
Expand Down Expand Up @@ -211,8 +210,6 @@ func (g *DependencyGraph) GetAllDependents(uri string) []string {

// Build parses a Go module and builds the dependency graph.
func (g *DependencyGraph) Build(ctx context.Context, modulePath string) error {
start := time.Now()

fset := token.NewFileSet()
packages, err := parser.ParseDir(fset, modulePath, nil, parser.ParseComments)
if err != nil {
Expand Down Expand Up @@ -245,7 +242,6 @@ func (g *DependencyGraph) Build(ctx context.Context, modulePath string) error {

wg.Wait()
close(sem)
fmt.Printf("Graph built in %v\n", time.Since(start))
return nil
}

Expand Down
2 changes: 0 additions & 2 deletions internal/hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,6 @@ func (d *Dispatcher) dispatch(hookType HookType, context string) error {
if err != nil {
return fmt.Errorf("hook %s failed: %w, output: %s", hook.Name, err, string(output))
}

fmt.Printf("Hook %s executed successfully\n", hook.Name)
}

return nil
Expand Down
155 changes: 0 additions & 155 deletions internal/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,161 +147,6 @@ func FormatJSON(findings []Finding) (string, error) {
return string(out), nil
}

// SARIF 2.1.0 output types (package-level so they can reference each other).

type outputSarifLog struct {
Version string `json:"version"`
Schema string `json:"$schema"`
Runs []outputSarifRun `json:"runs"`
}

type outputSarifRun struct {
Tool outputSarifTool `json:"tool"`
Results []outputSarifResult `json:"results"`
}

type outputSarifTool struct {
Driver outputSarifDriver `json:"driver"`
}

type outputSarifDriver struct {
Name string `json:"name"`
Version string `json:"version"`
InformationURI string `json:"informationUri,omitempty"`
Rules []outputSarifRule `json:"rules,omitempty"`
}

type outputSarifRule struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
ShortDescription outputSarifMessage `json:"shortDescription"`
}

type outputSarifResult struct {
RuleID string `json:"ruleId"`
Level string `json:"level"`
Message outputSarifMessage `json:"message"`
Locations []outputSarifLocation `json:"locations,omitempty"`
}

type outputSarifMessage struct {
Text string `json:"text"`
}

type outputSarifLocation struct {
PhysicalLocation *outputSarifPhysicalLocation `json:"physicalLocation,omitempty"`
}

type outputSarifPhysicalLocation struct {
ArtifactLocation outputSarifArtifactLocation `json:"artifactLocation"`
Region *outputSarifRegion `json:"region,omitempty"`
}

type outputSarifArtifactLocation struct {
URI string `json:"uri"`
}

type outputSarifRegion struct {
StartLine int `json:"startLine,omitempty"`
EndLine int `json:"endLine,omitempty"`
}

// FormatSARIF produces a SARIF 2.1.0 JSON report from findings.
func FormatSARIF(findings []Finding, version string) string {
if version == "" {
version = "dev"
}

severityToLevel := func(s int) string {
switch {
case s >= 4: // Critical
return "error"
case s >= 3: // High
return "error"
case s >= 2: // Medium
return "warning"
case s >= 1: // Low
return "note"
default: // Info
return "none"
}
}

ruleSet := make(map[string]bool)
var rules []outputSarifRule
for _, f := range findings {
id := f.Concern
if id == "" {
id = "unknown"
}
if ruleSet[id] {
continue
}
ruleSet[id] = true
rules = append(rules, outputSarifRule{
ID: id,
Name: id,
ShortDescription: outputSarifMessage{Text: id + " check"},
})
}

var results []outputSarifResult
for _, f := range findings {
id := f.Concern
if id == "" {
id = "unknown"
}
msg := f.Message
if f.Fix != "" {
msg += "\n\nFix: " + f.Fix
}
r := outputSarifResult{
RuleID: id,
Level: severityToLevel(f.Severity),
Message: outputSarifMessage{Text: msg},
}
if f.File != "" {
region := &outputSarifRegion{}
if f.Line > 0 {
region.StartLine = f.Line
}
if f.EndLine > 0 && f.EndLine != f.Line {
region.EndLine = f.EndLine
}
if region.StartLine == 0 {
region = nil
}
r.Locations = append(r.Locations, outputSarifLocation{
PhysicalLocation: &outputSarifPhysicalLocation{
ArtifactLocation: outputSarifArtifactLocation{URI: f.File},
Region: region,
},
})
}
results = append(results, r)
}

log := outputSarifLog{
Version: "2.1.0",
Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
Runs: []outputSarifRun{{
Tool: outputSarifTool{Driver: outputSarifDriver{
Name: "sight",
Version: version,
InformationURI: "https://github.com/GrayCodeAI/sight",
Rules: rules,
}},
Results: results,
}},
}

data, err := json.MarshalIndent(log, "", " ")
if err != nil {
return `{"error": "failed to generate SARIF"}`
}
return string(data)
}

// FormatGitHubReview formats all findings as a single GitHub PR review body.
func FormatGitHubReview(findings []Finding) string {
if len(findings) == 0 {
Expand Down
Loading
Loading