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
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
> race-safe per-session state (mandate M7). Branch protection on `main`
> permanently relaxed to `required_approving_review_count: 0` for solo-
> maintainer workflow.
> `sin-code audit` + `sin-code ceo-audit` added (issue #180): complexity-audit
> engine in `cmd/sin-code/internal/audit/`, 48th CEO-audit gate.

---

Expand Down Expand Up @@ -175,10 +177,20 @@ SIN-CODE-CLI (this repo, cmd/sin-code)
├─ sin-code summary ← v3.13.0: session auto-summary
├─ sin-code compress ← v3.18.0: deterministic + LLM compaction (issue #172)
└─ 40 subcommands
├─ sin-code audit ← v3.18.0: repo-wide complexity audit (issue #180)
├─ sin-code ceo-audit ← v3.18.0: 48-gate CEO audit with complexity gate (issue #180)
└─ 41 subcommands

┌──────────────────────────────────────────┐
│ AUDIT LAYER (v3.18.0) │
│ • complexity — static + LLM judge (M3) │
│ • ceo-audit — 48 gates incl. complexity │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ AGENT LOOP (agentloop) │
│ PLAN → ACT → VERIFY → DONE │
│ • Permission engine (allow/ask/deny) │
Expand Down Expand Up @@ -606,6 +618,8 @@ per-skill bundles and per-profile mirrors with one regex. Profile
renders are idempotent (rerun with unchanged source = byte-identical
output), exactly as skilldist demands.
``` (v3.18.0: 41 subcommands — `debt` is issue #177, sin-debt markers; `install` is issue #170; `eval`, `evalset`, `prp`, `instinct`, `assets`, `hooks`, `rtk`, `codegraph`, `spec` follow)
Audit: audit, ceo-audit
``` (v3.18.0: 41 subcommands, up from 39 in v3.13.0)

### Hook events (verified `internal/hooks/hooks.go`, v3.5.0)

Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,20 @@ reports them; `debt check` gates them.
preserved unchanged; the new harness lives at `sin evalset` to avoid
a cobra `Use:` collision.)

### Added — Complexity Audit (issue #180)
- **`sin-code audit complexity`** — repo-wide ponytail-audit analog. Five tags
(`delete`, `stdlib`, `native`, `yagni`, `shrink`), deterministic static pass
(single-impl interfaces, single-product factories, wrapper functions,
one-export files, dead flags/config, hand-rolled stdlib), optional LLM judge
for top-N findings. Output: one-liner per finding ending with
`net: -<N> lines, -<M> deps possible.` or `Lean already. Ship.`.
- **`cmd/sin-code/internal/audit/`** — new `Auditor`, `Finding`, `Result` types;
`// sin-debt:` markers approve findings and exclude them from the net total.
- **`sin-code ceo-audit`** — new 48-gate CEO-grade audit. The 48th gate is the
complexity audit; score contribution is `+1` per 100 removable lines.
- **Docs** `docs/complexity-audit.md` and **tests** `complexity_test.go` +
`audit_cmd_test.go` with race-free coverage.

### Notes
- All loop-engineering features are opt-in and fail-safe: a nil stop-gate /
empty contract / `AllowContinuation=false` preserves exact legacy behavior.
Expand Down
156 changes: 156 additions & 0 deletions cmd/sin-code/audit_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: MIT
// Purpose: audit command — repo-wide complexity audit (ponytail-audit analog).
// Docs: docs/complexity-audit.md
package main

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/spf13/cobra"

"github.com/OpenSIN-Code/SIN-Code/cmd/sin-code/internal/audit"
)

var (
auditPath string
auditFormat string
auditTags string
auditRank string
auditSince string
auditMaxNet int
auditStrict bool
auditNoLLM bool
)

// NewAuditCmd creates `sin-code audit` with `complexity` subcommand.
func NewAuditCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "audit",
Short: "Repo-wide audits (complexity, ...)",
Long: `Run repo-wide audits. The complexity subcommand is a ponytail-audit analog:

sin-code audit complexity
sin-code audit complexity --path ./cmd/sin-code
sin-code audit complexity --format json
sin-code audit complexity --tags yagni,delete --rank lines
sin-code audit complexity --strict --max-net-lines 100`,
}

complexityCmd := &cobra.Command{
Use: "complexity [path]",
Short: "Repo-wide complexity audit — ponytail-audit analog",
Long: `Scan the whole tree for structural complexity and emit one-line findings:

<tag> <what to cut>. <replacement>. [path]

Tags: delete, stdlib, native, yagni, shrink.
End with: net: -<N> lines, -<M> deps possible. or Lean already. Ship.

// sin-debt: markers approve a finding and exclude it from the net total.`,
Args: cobra.MaximumNArgs(1),
Version: Version,
RunE: runComplexityAudit,
}
complexityCmd.Flags().StringVar(&auditPath, "path", "", "Sub-tree to audit (default: current directory)")
complexityCmd.Flags().StringVarP(&auditFormat, "format", "f", "text", "Output format: text, json, markdown")
complexityCmd.Flags().StringVar(&auditTags, "tags", "", "Comma-separated tags (default: all)")
complexityCmd.Flags().StringVar(&auditRank, "rank", "lines", "Rank by: lines, deps")
complexityCmd.Flags().StringVar(&auditSince, "since", "", "Audit only files changed since git ref (not implemented in static pass)")
complexityCmd.Flags().IntVar(&auditMaxNet, "max-net-lines", 0, "Fail if removable net-lines exceed this threshold")
complexityCmd.Flags().BoolVarP(&auditStrict, "strict", "s", false, "Exit with error if threshold exceeded")
complexityCmd.Flags().BoolVar(&auditNoLLM, "no-llm", false, "Skip LLM second pass")

cmd.AddCommand(complexityCmd)
return cmd
}

func runComplexityAudit(cmd *cobra.Command, args []string) error {
root := "."
if len(args) > 0 {
root = args[0]
}
if auditPath != "" {
root = auditPath
}
abs, err := filepath.Abs(root)
if err != nil {
return fmt.Errorf("invalid path: %w", err)
}
info, err := os.Stat(abs)
if err != nil {
return fmt.Errorf("path not found: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("path is not a directory: %s", abs)
}

var tags []string
if auditTags != "" {
for _, t := range strings.Split(auditTags, ",") {
tags = append(tags, strings.TrimSpace(t))
}
if err := audit.ValidateTags(tags); err != nil {
return err
}
}
if auditRank != "lines" && auditRank != "deps" {
return fmt.Errorf("rank must be 'lines' or 'deps'")
}
if auditFormat != "text" && auditFormat != "json" && auditFormat != "markdown" {
return fmt.Errorf("format must be text, json, or markdown")
}

opts := audit.Options{
Tags: tags,
Rank: auditRank,
SinceRef: auditSince,
MaxNet: auditMaxNet,
Strict: auditStrict,
NoLLM: auditNoLLM,
}
if auditMaxNet > 0 {
opts.Strict = true
}

res, err := audit.NewAuditor(nil).Audit(context.Background(), abs, opts)
if err != nil {
return err
}

switch auditFormat {
case "json":
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(res)
case "markdown":
fmt.Print(formatMarkdown(res))
default:
fmt.Print(audit.FormatResult(res, "text"))
}
return nil
}

func formatMarkdown(res *audit.Result) string {
var sb strings.Builder
sb.WriteString("# Complexity Audit\n\n")
if len(res.Findings) == 0 {
sb.WriteString("**" + res.Status + "**\n")
return sb.String()
}
sb.WriteString("| Tag | What to cut | Replacement | Path | Lines |\n")
sb.WriteString("|-----|-------------|-------------|------|------|\n")
for _, f := range res.Findings {
approved := ""
if f.Approved {
approved = " (approved: " + f.Approver + ")"
}
sb.WriteString(fmt.Sprintf("| %s | %s | %s | %s:%d | %d |%s\n", f.Tag, f.Problem, f.Replacement, f.Path, f.Line, f.LineCount, approved))
}
sb.WriteString("\n**" + res.Status + "**\n")
return sb.String()
}
178 changes: 178 additions & 0 deletions cmd/sin-code/audit_cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package main

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/spf13/cobra"

"github.com/OpenSIN-Code/SIN-Code/cmd/sin-code/internal/audit"
)

func TestAuditCommandComplexityJSON(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "a.go"), `package demo

// sin-debt: legacy interface
type Reader interface { Read() }

func NewReader() Reader { return &reader{} }
type reader struct{}
func (r *reader) Read() {}
`)

cmd := NewAuditCmd()
cmd.SetArgs([]string{"complexity", dir, "--format", "json", "--tags", "yagni"})
out := captureOutput(t, cmd)

var res audit.Result
if err := json.Unmarshal([]byte(out), &res); err != nil {
t.Fatalf("invalid JSON output: %v\n%s", err, out)
}
if len(res.Findings) == 0 {
t.Fatalf("expected findings, got: %s", out)
}
for _, f := range res.Findings {
if f.Tag != "yagni" {
t.Fatalf("expected only yagni tag, got %v", f)
}
}
}

func TestAuditCommandComplexityText(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "a.go"), `package demo
var VerboseFlag bool
`)

cmd := NewAuditCmd()
cmd.SetArgs([]string{"complexity", dir, "--tags", "delete"})
out := captureOutput(t, cmd)
if !strings.Contains(out, "delete:") {
t.Fatalf("expected delete finding in output:\n%s", out)
}
if !strings.Contains(out, "net:") {
t.Fatalf("expected net summary in output:\n%s", out)
}
}

func TestAuditCommandComplexityMarkdown(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "a.go"), `package demo
func Exported() {}
`)

cmd := NewAuditCmd()
cmd.SetArgs([]string{"complexity", dir, "--format", "markdown", "--tags", "shrink"})
out := captureOutput(t, cmd)
if !strings.HasPrefix(out, "# Complexity Audit") {
t.Fatalf("expected markdown header, got:\n%s", out)
}
}

func TestAuditCommandUnknownTag(t *testing.T) {
cmd := NewAuditCmd()
cmd.SetArgs([]string{"complexity", ".", "--tags", "bad"})
if err := cmd.Execute(); err == nil {
t.Fatal("expected error for unknown tag")
}
}

func TestAuditCommandInvalidRank(t *testing.T) {
cmd := NewAuditCmd()
cmd.SetArgs([]string{"complexity", ".", "--rank", "bad"})
if err := cmd.Execute(); err == nil {
t.Fatal("expected error for invalid rank")
}
}

func TestAuditCommandStrictThreshold(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "a.go"), `package demo
var VerboseFlag bool
var DebugConfig string
var OtherFlag string
`)

cmd := NewAuditCmd()
cmd.SetArgs([]string{"complexity", dir, "--tags", "delete", "--strict", "--max-net-lines", "1"})
if err := cmd.Execute(); err == nil {
t.Fatal("expected strict threshold error")
}
}

func TestCEOAUDITCommand(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "a.go"), `package demo
var VerboseFlag bool
`)

cmd := NewCEOAUDITCmd()
cmd.SetArgs([]string{dir, "--format", "json", "--tags", "delete"})
out := captureOutput(t, cmd)

if !strings.Contains(out, "complexity-audit") {
t.Fatalf("expected complexity gate in output:\n%s", out)
}
if !strings.Contains(out, "net:") {
t.Fatalf("expected net summary in output:\n%s", out)
}
}

func TestCEOAUDITCommand48Gates(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "a.go"), `package demo
`)

cmd := NewCEOAUDITCmd()
cmd.SetArgs([]string{dir, "--format", "json"})
out := captureOutput(t, cmd)

var result ceoResult
if err := json.Unmarshal([]byte(out), &result); err != nil {
t.Fatalf("invalid JSON: %v\n%s", err, out)
}
if len(result.Gates) != 48 {
t.Fatalf("expected 48 gates, got %d", len(result.Gates))
}
found := false
for _, g := range result.Gates {
if g.Name == "complexity-audit" {
found = true
}
}
if !found {
t.Fatal("complexity-audit gate not found")
}
}

func mustWrite(t *testing.T, path, content string) {
t.Helper()
_ = os.MkdirAll(filepath.Dir(path), 0755)
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}
}

func captureOutput(t *testing.T, cmd *cobra.Command) string {
t.Helper()
old := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdout = w
if err := cmd.Execute(); err != nil {
os.Stdout = old
fmt.Fprintf(old, "command error: %v\n", err)
}
w.Close()
os.Stdout = old
b := make([]byte, 65536)
n, _ := r.Read(b)
return string(b[:n])
}
Loading
Loading