feat: Milestone 2 — Validation Layer (Issues #58, #59, #60)#121
Conversation
…ration (#58, #59, #60) Milestone 2 validation layer: - Add RuntimeCheck (build binary, start app, probe HTTP routes) with full test coverage including timeout, route probing, and process cleanup - Add PostGenEngine for lightweight post-generation validation (go.mod, templates, migrations) that skips compilation since sqlc hasn't run yet - Add FullEngine/ValidateFull with RuntimeCheck for thorough validation - Integrate --skip-validation flag into gen resource/view/schema and auth - Add ValidationOutput structs to MCP server with structured JSON results - Update MCP gen handlers to run validation explicitly (avoid double-run) - Update help text and usage docs for --skip-validation - Update existing tests with --skip-validation where compilation isn't expected Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: Milestone 2 — Validation Layer (#58, #59, #60)Good overall direction. The layered engine design (PostGen / Default / Full) is clean and the RuntimeCheck is well-structured. A few issues worth addressing before merge. Bug: Validation in
|
There was a problem hiding this comment.
Pull request overview
Adds a post-generation validation layer to lvt (structural checks by default, optional full/runtime checks), exposes validation controls via a --skip-validation flag on generation commands, and returns structured validation results in MCP tool responses.
Changes:
- Introduces
RuntimeCheckand expands the validation engine withPostGenEngine/ValidatePostGenandFullEngine/ValidateFull. - Adds
--skip-validationtogen resource/view/schema/auth, updates CLI help/usage, and runs post-gen structural validation by default. - Extends MCP server tool outputs to include structured
validationresults and updates tests accordingly.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
main.go |
Updates CLI usage text to document --skip-validation. |
internal/validation/validation.go |
Adds PostGen/Full engines and convenience validation entrypoints. |
internal/validation/runtime.go |
Implements runtime validation by building and probing a running app subprocess. |
internal/validation/runtime_test.go |
Adds runtime validation test coverage (build failures, timeouts, routes, cleanup, cancellation). |
commands/gen.go |
Parses --skip-validation and runs post-gen structural validation after generation. |
commands/auth.go |
Adds --skip-validation handling and invokes post-gen validation for auth generation. |
commands/help.go |
Documents --skip-validation under gen subcommand help. |
commands/mcp_server.go |
Adds ValidationOutput types and includes structured validation in MCP gen tool outputs. |
commands/gen_validation_test.go |
Adds integration tests for validation behavior + MCP validation JSON output. |
commands/mcp_tools_test.go |
Updates MCP-related tests to pass --skip-validation to avoid double validation. |
commands/auth_test.go |
Updates auth integration tests to pass --skip-validation where needed. |
| return NewEngine( | ||
| WithCheck(&GoModCheck{}), | ||
| WithCheck(&TemplateCheck{}), | ||
| WithCheck(&MigrationCheck{}), | ||
| WithCheck(&CompilationCheck{}), | ||
| WithCheck(&RuntimeCheck{}), | ||
| ) |
There was a problem hiding this comment.
FullEngine duplicates the DefaultEngine check list plus RuntimeCheck. This can drift if DefaultEngine is updated (new checks, changed ordering). Consider constructing FullEngine from DefaultEngine (or a shared slice of default checks) and then appending RuntimeCheck to avoid divergence.
| return NewEngine( | |
| WithCheck(&GoModCheck{}), | |
| WithCheck(&TemplateCheck{}), | |
| WithCheck(&MigrationCheck{}), | |
| WithCheck(&CompilationCheck{}), | |
| WithCheck(&RuntimeCheck{}), | |
| ) | |
| e := DefaultEngine() | |
| e.checks = append(e.checks, &RuntimeCheck{}) | |
| return e |
| cmd := exec.CommandContext(appCtx, binaryPath) | ||
| cmd.Dir = appPath | ||
| cmd.Env = append(envWithGOWORKOff(), fmt.Sprintf("PORT=%d", port)) | ||
| cmd.Stdout = nil | ||
| cmd.Stderr = nil |
There was a problem hiding this comment.
RuntimeCheck discards the subprocess stdout/stderr (Stdout/Stderr are nil), which makes startup failures hard to debug and undermines the goal of reporting useful runtime error details. Consider capturing stdout/stderr (or at least the tail) and including it in the validation issues when the app exits early or fails to become ready.
| func runMCPValidation(appPath string) *ValidationOutput { | ||
| result := validation.ValidatePostGen(context.Background(), appPath) |
There was a problem hiding this comment.
runMCPValidation hard-codes context.Background(), so MCP tool callers cannot cancel/timeout validation via the request context. Consider taking a context.Context parameter (from the tool handler) and passing it through to ValidatePostGen so validation is abortable when the client disconnects or cancels.
| func runMCPValidation(appPath string) *ValidationOutput { | |
| result := validation.ValidatePostGen(context.Background(), appPath) | |
| func runMCPValidation(ctx context.Context, appPath string) *ValidationOutput { | |
| result := validation.ValidatePostGen(ctx, appPath) |
| return nil, GenViewOutput{ | ||
| Success: true, | ||
| Message: fmt.Sprintf("Successfully generated %s view", input.Name), | ||
| Files: files, | ||
| Success: validationResult.Valid, | ||
| Message: fmt.Sprintf("Successfully generated %s view", input.Name), | ||
| Files: files, | ||
| Validation: validationResult, |
There was a problem hiding this comment.
GenViewOutput sets Success based on validationResult.Valid, but Message always says "Successfully generated..." even when Success=false. MCP clients will likely treat Message as authoritative; adjust the message (or include a validation summary) when validation fails.
| } | ||
|
|
||
| // Run validation explicitly for structured results | ||
| wd, _ := os.Getwd() |
There was a problem hiding this comment.
os.Getwd() errors are ignored here; if it fails, validation may run against an empty/incorrect path and return confusing results. Please handle the error and return a failed tool response if the working directory can’t be determined.
| wd, _ := os.Getwd() | |
| wd, err := os.Getwd() | |
| if err != nil { | |
| return nil, GenResourceOutput{ | |
| Success: false, | |
| Message: fmt.Sprintf("Failed to determine working directory for validation: %v", err), | |
| }, nil | |
| } |
| // TestGenResource_WithValidation verifies that validation runs after generation | ||
| // in a fully valid app (with go.mod + main.go so compilation succeeds). | ||
| func TestGenResource_WithValidation(t *testing.T) { | ||
| tmpDir, cleanup := setupMCPTestDir(t) | ||
| defer cleanup() |
There was a problem hiding this comment.
This test comment says validation includes compilation, but Gen() now runs post-generation structural validation (ValidatePostGen), which explicitly skips compilation. Please update the comment to reflect what is actually being validated to avoid misleading future test maintenance.
| // TestGenView_WithValidation verifies that validation runs after view generation. | ||
| func TestGenView_WithValidation(t *testing.T) { | ||
| tmpDir, cleanup := setupMCPTestDir(t) | ||
| defer cleanup() | ||
|
|
There was a problem hiding this comment.
This test comment implies "validation" here is compilation-based, but the gen command’s default post-generation validation is structural only (no compilation). Please adjust the comment wording so the test intent matches the behavior.
| return nil, GenResourceOutput{ | ||
| Success: true, | ||
| Message: fmt.Sprintf("Successfully generated %s resource with %d fields", input.Name, len(input.Fields)), | ||
| Files: files, | ||
| Success: validationResult.Valid, | ||
| Message: fmt.Sprintf("Successfully generated %s resource with %d fields", input.Name, len(input.Fields)), | ||
| Files: files, | ||
| Validation: validationResult, |
There was a problem hiding this comment.
GenResourceOutput sets Success based on validationResult.Valid, but Message always says "Successfully generated..." even when Success=false. This is misleading for MCP consumers; consider emitting a different message when validation fails (e.g., "Generated, but validation failed") and/or summarizing the validation errors.
| return nil, GenAuthOutput{ | ||
| Success: true, | ||
| Message: "Successfully generated authentication system", | ||
| Success: validationResult.Valid, | ||
| Message: "Successfully generated authentication system", | ||
| Validation: validationResult, |
There was a problem hiding this comment.
GenAuthOutput sets Success based on validationResult.Valid, but Message is always "Successfully generated authentication system" even when Success=false. Consider changing Message when validation fails so MCP consumers aren’t told the operation succeeded when it didn’t validate.
| // Post-generation validation | ||
| if !skipValidation { | ||
| return runPostGenValidation(basePath) | ||
| } |
There was a problem hiding this comment.
Post-generation validation runs after printing "✅ ... generated successfully". If validation fails, users will see a success message immediately followed by a validation failure, which is confusing. Consider running validation before printing the success banner, or changing the banner/message to reflect "generated, then validated" only on success.
- Fix gofmt formatting in mcp_server.go (CI failure) - Handle os.Getwd() errors in MCP handlers instead of silently ignoring - Fix Success/Message contradiction: Success reflects generation success, Message adjusts when validation finds issues - Pass context.Context through runMCPValidation for cancellation support - Compose FullEngine from DefaultEngine to avoid check list drift - Capture subprocess stdout/stderr in RuntimeCheck for better diagnostics - Detect early process exit in waitForReady instead of waiting full timeout - Run validation before success banner in gen.go to avoid contradictory output - Fix misleading test comments about compilation (now structural validation) - Document TOCTOU race in getFreePort - Fix idiomatic bool check (decoded.Valid != false → decoded.Valid) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: Milestone 2 — Validation LayerGood design overall. The three-tier engine (PostGen / Default / Full) is clean and well-documented, and the RuntimeCheck lifecycle management is solid. A few issues to address, including a correction to the existing review. Correction to previous reviewauth.go validation IS post-generation, not pre-generation. The previous review claims validation in
The comment "Post-generation validation (before interactive prompts)" is accurate. mcp_server.go does handle os.Getwd() errors. The previous review claims Bug: Deadlock in RuntimeCheck when process exits early
defer func() {
appCancel()
_ = cmd.Process.Kill()
<-procDone // blocks forever — channel already drained above
}()The goroutine that called The existing test suite does not exercise this path — all test apps either sleep forever ( Suggested fix: track whether consumed := false
ready, earlyExit := waitForReadyOrExit(ctx, baseURL, timeout, procDone)
if earlyExit {
consumed = true
}
// ...
defer func() {
appCancel()
_ = cmd.Process.Kill()
if !consumed {
<-procDone
}
}()Inconsistency: auth.go returns immediately on validation failure; gen.go defers the errorIn var validationErr error
if !skipValidation {
validationErr = runPostGenValidation(basePath)
}
// print banner, list files...
return validationErr // error returned last, after all outputIn if !skipValidation {
if err := runPostGenValidation(wd); err != nil {
return err // returns immediately, skips interactive prompts
}
}
// interactive resource protection prompts...Skipping the interactive prompts when validation fails may be intentional — if auth code has structural issues, asking about resource protection is probably premature. But the UX difference is undocumented: Real issues confirmed from the previous reviewTOCTOU race in
Minor
Flag parsing is inconsistent across commands — GenResource parses inline; GenView/GenSchema prepend a separate filter loop; Auth uses a switch case. All correct, but a shared helper would make future flag additions uniform. What is solid
Summary: The deadlock in RuntimeCheck is the only blocking issue — not hit by the current test suite but will manifest when a generated app binary exits before becoming ready. The auth.go behavior difference is worth a comment. The two main bug reports in the previous review are based on misreadings of the actual code. |
When the subprocess exits before becoming ready, waitForReadyOrExit drains the procDone channel. The deferred cleanup then tried to read from the same channel, causing a deadlock. Track consumption state to skip the redundant receive. Also document the auth.go validation behavior difference vs gen.go. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: feat: Milestone 2 — Validation LayerOverall this is solid work — the design is clean, the intent is well-documented, and the test coverage for the new Bugs / Correctness1. // commands/mcp_server.go
type ValidationOutput struct {
Valid bool `json:"valid"`
ErrorCount int `json:"error_count"`
WarnCount int `json:"warning_count"` // ← mismatch
...
}The Go field is 2. // internal/validation/runtime.go
func trimOutput(output []byte) string {
s := string(output)
if len(s) > 500 {
s = s[:500] + "..." // ← slicing by byte, not rune
}
return s
}
Design / Consistency3. // internal/validation/validation.go
func FullEngine() *Engine {
e := DefaultEngine()
e.checks = append(e.checks, &RuntimeCheck{}) // ← direct field mutation
return e
}Every other engine-construction path uses 4. Duplicated 5. return nil, GenResourceOutput{
Success: true, // generation succeeded...
Message: msg, // ...but "validation found issues"
Validation: validationResult,
}, nilThe intent (generation succeeded, validation is advisory) is reasonable and the message communicates it, but MCP clients that only check Test Reliability6. ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
c := &RuntimeCheck{StartupTimeout: 5 * time.Second}
result := c.Run(ctx, dir)
if result.Valid {
t.Error("expected invalid due to context cancellation")
}With the context already cancelled, 7. if err := os.Chdir(appDir); err != nil { ... }This affects the entire test process. If Minor / Nits8. 9. 10. Acknowledged TOCTOU in What's Working Well
The structural issues (items 1–4) are worth fixing before merge. The rest are improvements that could land in a follow-up. |
- Rename WarnCount → WarningCount to match JSON tag (warning_count) - Fix trimOutput to use rune-aware slicing for UTF-8 safety - Use WithCheck() pattern in FullEngine instead of direct field mutation - Add doc comments on Success field clarifying it reflects generation, not validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: Milestone 2 — Validation LayerOverall this is well-designed and well-tested. The Issues1.
func runPostGenValidation(basePath string) error {
fmt.Println("Running validation...")
result := validation.ValidatePostGen(context.Background(), basePath)If the user Ctrl+C during 2. Context cancellation produces a misleading error message
When The message says "build failed" when the build never ran. A cheap fix is a guard at the top of if ctx.Err() != nil {
result.AddError("runtime check: cancelled: "+ctx.Err().Error(), "", 0)
return result
}The test 3. Missing validation test for
4.
Minor Notes5. Flag parsing duplication The func parseSkipValidation(args []string) (skip bool, remaining []string)would keep this DRY. Not a blocker, but worth doing before more flags are added. 6.
func FullEngine() *Engine {
e := DefaultEngine()
WithCheck(&RuntimeCheck{})(e)
return e
}
7. Hardcoded binary name
binaryPath := filepath.Join(appPath, "lvt-runtime-check")Fine for current use (sequential calls, separate temp dirs in tests). If What's well done
|
- Add early context cancellation guard in RuntimeCheck.Run() to avoid misleading "build failed: " message when context is already cancelled - Add TestGenSchema_WithValidation for test parity with resource/view - Add TODO comment for context propagation in runPostGenValidation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: Milestone 2 — Validation LayerFour reviews already exist on this PR. Rather than repeat what's been said, this review focuses on a new finding, two mid-execution edge cases that weren't fully explored, and a correction to inaccurate claims in the earlier reviews. Inaccuracies in previous reviews (corrections)Several claims in the earlier review comments do not match the actual diff:
New finding: panic when
|
GenView and GenSchema checked len(args) before filtering out --skip-validation, so `lvt gen view --skip-validation` would pass the initial check then panic on args[0] after filtering left an empty slice. Move flag parsing before positional arg validation. Also improve RuntimeCheck error message on mid-execution context cancellation to show "cancelled" instead of misleading timeout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: Milestone 2 — Validation LayerOverall this is a solid, well-structured PR. The design decisions are clearly documented, the test coverage is good, and the incremental commit history shows careful iteration. Below are the issues I found, ordered by severity. Bugs / Correctness1. In both // commands/gen.go ~L151, ~L205
_ = skipValidation // used later ← remove this lineThis looks like it was added during development to satisfy the compiler before the validation block was written, and was never removed. It should be deleted. 2. Binary naming conflict in The build output is hardcoded to // Instead of:
binaryPath := filepath.Join(appPath, "lvt-runtime-check")
// Consider:
f, err := os.CreateTemp("", "lvt-runtime-check-*")
if err != nil { ... }
binaryPath := f.Name()
f.Close()The temp file would also avoid placing the binary inside the app dir, which matters if the app has a file watcher (see point 5). 3. Early process exit can be misreported as timeout in If the process exits exactly as the deadline elapses, the outer // After the loop, do a final non-blocking drain:
select {
case <-procDone:
return false, true
default:
}
return false, falseCode Quality4. func FullEngine() *Engine {
e := DefaultEngine()
WithCheck(&RuntimeCheck{})(e) // unusual: returns a func, immediately calls it
return e
}This works correctly but is an unusual pattern. The more readable alternative is to use 5. Binary placed inside app directory
6. // internal/validation/runtime_test.go
ln, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)This is Linux/Unix-specific. The ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
t.Errorf("port %d still in use after runtime check cleanup", port)
return
}
ln.Close()Design / Architecture7.
8. Context not propagated to The TODO comment is already there, which is appreciated. Just flagging it as a real usability concern: 9. MCP clients only get structural validation, not compilation
// runMCPValidation runs structural validation (post-gen) and converts to MCP output.
// Uses PostGen engine since generated code may not compile until sqlc generate is run.
// For full compilation checks, use validation.Validate() directly.
func runMCPValidation(ctx context.Context, appPath string) *ValidationOutput {(The last sentence is new.) Minor / Nits
Test Coverage: Positive Highlights
Summary: The design is sound and well-documented. The two most actionable fixes before merge are removing the 🤖 Generated with Claude Code |
Remove unnecessary `_ = skipValidation` blank assignments that were left from development. Add doc note to runMCPValidation about using validation.Validate() for full compilation checks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: Milestone 2 — Validation LayerOverall this is a solid, well-thought-out feature. The layered engine design ( Bugs1.
// Current (unchanged) — missing --skip-validation
args := []string{"schema", input.Table}Since
This needs 2.
The function name suggests trimming; it should call 3. Binary name collision under concurrent use The runtime check binary is always placed at Consider placing the binary in Design / Quality4. The defer func() {
appCancel()
_ = cmd.Process.Kill()
select {
case <-procDone:
default:
<-procDone
}
}()Actually — the simplest fix: just drain unconditionally in the defer and remove the bool flag. 5.
6. The TODO comment acknowledges this: // TODO: accept context.Context so Ctrl+C propagates to validation.
// Structural checks are fast today so context.Background() is acceptable.
func runPostGenValidation(basePath string) error {
result := validation.ValidatePostGen(context.Background(), basePath)The callers ( Test Coverage7. Tests use
8. The test calls Minor
Summary
The schema handler issue (#1) and binary collision (#3) should be addressed before merge. The rest are improvements worth tracking. 🤖 Generated with Claude Code |
…tput - Add --skip-validation + explicit validation to registerGenSchemaTools, matching the resource/view/auth MCP handler pattern. Previously the schema handler would print validation text to stdout (corrupting MCP JSON-RPC framing) and not return structured validation results. - Add strings.TrimSpace to trimOutput for cleaner error messages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: Milestone 2 — Validation Layer (final pass)Seven review comments have already been posted on this PR, several with inaccuracies that compounded across responses. This is a clean-slate review based on reading the actual diff. What the previous reviews got wrong (clearing the record)The following claims in prior comments do not match the code:
The PR is in better shape than the review history implies. Genuine issues1. Mid-execution context cancellation produces a misleading error message
When the context is cancelled during the startup poll (after instead of the correct cancellation message. The early-exit guard at the top of if !ready {
var msg string
switch {
case ctx.Err() != nil: // ← already there, handles pre-build cancel
msg = "runtime check: cancelled: " + ctx.Err().Error()
case earlyExit:
msg = "runtime check: app exited before becoming ready"
default:
msg = fmt.Sprintf("runtime check: app did not start within %s", timeout)
}The 2.
Multiple tests call if err := os.Chdir(appDir); err != nil {
t.Fatalf("Failed to change to app dir: %v", err)
}
// no defer os.Chdir(original)
original, _ := os.Getwd()
defer os.Chdir(original)
if err := os.Chdir(appDir); err != nil { ... }Go 1.24's 3.
4. func FullEngine() *Engine {
e := DefaultEngine()
WithCheck(&RuntimeCheck{})(e) // returns a func, then immediately calls it
return e
}This is correct (Go closures, same package), but asymmetric with What's working well
Summary: The |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: Milestone 2 — Validation Layer (#58, #59, #60)The layered engine design (PostGen / Default / Full) is clean and the RuntimeCheck is well-structured with solid process lifecycle management. A few things to address before merge. Correction to existing review: auth.go placement is correctThe previous review flagged validation in // line 102 — generation happens here
if err := generator.GenerateAuth(wd, genConfig); err != nil {
return fmt.Errorf("failed to generate auth: %w", err)
}
// ...
// line 118 — validation runs AFTER generation
if !skipValidation {
if err := runPostGenValidation(wd); err != nil { ...The placement is intentional and correct. Similarly, the Bug:
|
Summary
RuntimeCheckthat builds the app binary, starts it as a subprocess, and probes HTTP routes. IncludesFullEngine()/ValidateFull()andPostGenEngine()/ValidatePostGen()convenience functions.--skip-validationflag intogen resource,gen view,gen schema, andgen authcommands. Post-generation validation runs structural checks (go.mod, templates, migrations) automatically.ValidationOutputstructs to MCP server. Gen handlers pass--skip-validationto avoid double-validation and return structured JSON validation results.Key Design Decisions
sqlc generateruns. PostGenEngine checks structural things only; full compilation is available viaValidate()/ValidateFull().--skip-validationtoGen()then callrunMCPValidation()to get structured JSON output instead of text.FullEngine()orWithCheck(&RuntimeCheck{}).Files Changed
internal/validation/runtime.gointernal/validation/runtime_test.gointernal/validation/validation.gocommands/gen.gocommands/auth.gocommands/help.gomain.gocommands/mcp_server.gocommands/gen_validation_test.gocommands/mcp_tools_test.gocommands/auth_test.goTest plan
go test ./internal/validation/...— 37 tests pass (6 new RuntimeCheck + 31 existing)go test ./commands/...— all commands tests pass including new validation testsgo test ./...— full suite passes (all 30 packages green)lvt gen resource posts title:stringshows validation outputlvt gen resource posts title:string --skip-validationskips validation"validation": {"valid": true, ...}field🤖 Generated with Claude Code