Claude (review-harness setup) f3ee4722a8 Phase A + B (MVP) — local review harness
Implements the MVP cutline from the planning artifact:
- Phase A: skeleton + CLI dispatch + provider interface + stub model doctor
- Phase B: scanner + git probe + 12 static analyzers + reporters + pipeline
- Phase B fixtures: clean-repo, insecure-repo, degraded-repo

12 static analyzers per PROMPT.md "Suggested Static Checks For MVP":
hardcoded_paths, shell_execution, raw_sql_interpolation, broad_cors,
secret_patterns, large_files, todo_comments, missing_tests,
env_file_committed, unsafe_file_io, exposed_mutation_endpoint,
hardcoded_local_ip.

Acceptance gates passing:
- B1 (intake produces accurate counts) ✓
- B2 (insecure fixture fires ≥8 distinct check_ids — actually 11/12) ✓
- B3 (clean fixture produces 0 confirmed findings — no false positives) ✓
- B4 (scrum mode produces all 6 required markdown + JSON reports) ✓
- B5 (receipts.json marks degraded phases honestly) ✓
- F  (self-review on this repo runs without crashing) ✓ — exit 66 (degraded
  because Phase C LLM review is hardcoded skipped)

Phases C (LLM review), D (validation cross-check), E (memory + diff +
rules subcommands) deferred per the cutline. The MVP delivers the
evidence-first path; LLM is purely additive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:56:02 -05:00

150 lines
4.8 KiB
Go

// Package config loads the two YAML profiles documented in
// configs/{model,review}-profile.example.yaml. Both are optional —
// callers that don't pass --review-profile or --model-profile get
// DefaultReviewProfile / DefaultModelProfile.
//
// Defaults reflect the 2026-04-30 small-model-pipeline tier bump:
// local model is qwen3.5:latest, not qwen2.5-coder. The example
// YAML in configs/ still says qwen2.5-coder per PROMPT.md as
// originally authored — operators who copy the example get that;
// operators who skip the file get the current default.
package config
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
// ModelProfile mirrors configs/model-profile.example.yaml.
type ModelProfile struct {
Provider string `yaml:"provider"`
BaseURL string `yaml:"base_url"`
Model string `yaml:"model"`
FallbackModel string `yaml:"fallback_model"`
TimeoutSeconds int `yaml:"timeout_seconds"`
Temperature float64 `yaml:"temperature"`
}
// ReviewProfile mirrors configs/review-profile.example.yaml.
// Toggles disable analyzers without code changes — review-profile
// drives noisy-check tuning per repo.
type ReviewProfile struct {
ProjectName string `yaml:"project_name"`
Mode string `yaml:"mode"`
SeverityThresholds struct {
FailOnCritical bool `yaml:"fail_on_critical"`
FailOnHigh bool `yaml:"fail_on_high"`
} `yaml:"severity_thresholds"`
StaticChecks struct {
HardcodedPaths bool `yaml:"hardcoded_paths"`
RawSQLInterpolation bool `yaml:"raw_sql_interpolation"`
ShellExecution bool `yaml:"shell_execution"`
BroadCORS bool `yaml:"broad_cors"`
SecretPatterns bool `yaml:"secret_patterns"`
LargeFiles bool `yaml:"large_files"`
TODOComments bool `yaml:"todo_comments"`
MissingTests bool `yaml:"missing_tests"`
} `yaml:"static_checks"`
Limits struct {
LargeFileLines int `yaml:"large_file_lines"`
MaxFileBytes int `yaml:"max_file_bytes"`
MaxLLMChunkChars int `yaml:"max_llm_chunk_chars"`
} `yaml:"limits"`
Reports struct {
OutputDir string `yaml:"output_dir"`
Markdown bool `yaml:"markdown"`
JSONReceipts bool `yaml:"json_receipts"`
} `yaml:"reports"`
Memory struct {
Enabled bool `yaml:"enabled"`
Path string `yaml:"path"`
AppendOnly bool `yaml:"append_only"`
} `yaml:"memory"`
}
// DefaultModelProfile reflects the current Lakehouse-Go local-tier
// default (qwen3.5:latest), not the qwen2.5-coder example file.
// PROMPT.md was authored 2026-04-29; tier bump landed 2026-04-30.
func DefaultModelProfile() ModelProfile {
return ModelProfile{
Provider: "ollama",
BaseURL: "http://localhost:11434",
Model: "qwen3.5:latest",
FallbackModel: "qwen3:latest",
TimeoutSeconds: 120,
Temperature: 0.1,
}
}
// DefaultReviewProfile turns every static check ON by default.
// review-profile.yaml in the target repo is how operators tune.
func DefaultReviewProfile() ReviewProfile {
p := ReviewProfile{
ProjectName: "review-harness",
Mode: "local-first",
}
p.SeverityThresholds.FailOnCritical = true
p.SeverityThresholds.FailOnHigh = false
p.StaticChecks.HardcodedPaths = true
p.StaticChecks.RawSQLInterpolation = true
p.StaticChecks.ShellExecution = true
p.StaticChecks.BroadCORS = true
p.StaticChecks.SecretPatterns = true
p.StaticChecks.LargeFiles = true
p.StaticChecks.TODOComments = true
p.StaticChecks.MissingTests = true
p.Limits.LargeFileLines = 800
p.Limits.MaxFileBytes = 1_000_000
p.Limits.MaxLLMChunkChars = 12000
p.Reports.OutputDir = "reports/latest"
p.Reports.Markdown = true
p.Reports.JSONReceipts = true
p.Memory.Enabled = true
p.Memory.Path = ".memory"
p.Memory.AppendOnly = true
return p
}
// LoadModelProfile reads YAML from path. Empty path returns defaults
// without error — operators don't need a profile to run.
func LoadModelProfile(path string) (ModelProfile, error) {
out := DefaultModelProfile()
if path == "" {
return out, nil
}
b, err := os.ReadFile(path)
if err != nil {
return out, fmt.Errorf("read model profile %s: %w", path, err)
}
if err := yaml.Unmarshal(b, &out); err != nil {
return out, fmt.Errorf("parse model profile %s: %w", path, err)
}
return out, nil
}
// LoadReviewProfile reads YAML from path. Empty path returns defaults.
// Partial files merge into defaults — unspecified fields stay at
// default values (yaml.v3 preserves pre-existing values for fields
// not present in the YAML).
func LoadReviewProfile(path string) (ReviewProfile, error) {
out := DefaultReviewProfile()
if path == "" {
return out, nil
}
b, err := os.ReadFile(path)
if err != nil {
return out, fmt.Errorf("read review profile %s: %w", path, err)
}
if err := yaml.Unmarshal(b, &out); err != nil {
return out, fmt.Errorf("parse review profile %s: %w", path, err)
}
return out, nil
}