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>
150 lines
4.8 KiB
Go
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
|
|
}
|