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>
This commit is contained in:
co-authored by
Claude Opus 4.7
commit
f3ee4722a8
@@ -0,0 +1,149 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user