This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
// matches expected_evidence, outputs pass/fail report.
|
||||
//
|
||||
// Usage: go run scripts/verification_executor.go [--dry-run] [--task T-Q2-1.1]
|
||||
//go:build llm_script
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -21,28 +23,36 @@ import (
|
||||
)
|
||||
|
||||
type Verification struct {
|
||||
Mode string
|
||||
Command string
|
||||
Mode string
|
||||
Command string
|
||||
ExpectedEvidence string
|
||||
TimeoutSeconds int
|
||||
TimeoutSeconds int
|
||||
EvidenceGrade string
|
||||
TaskType string
|
||||
}
|
||||
|
||||
type TaskResult struct {
|
||||
TaskID string
|
||||
TaskName string
|
||||
Verified bool
|
||||
Command string
|
||||
ExitCode int
|
||||
Stdout string
|
||||
Stderr string
|
||||
Error string
|
||||
Reason string
|
||||
TaskID string
|
||||
TaskName string
|
||||
Verified bool
|
||||
Command string
|
||||
ExitCode int
|
||||
Stdout string
|
||||
Stderr string
|
||||
StdoutSummary string
|
||||
StderrSummary string
|
||||
Error string
|
||||
Reason string
|
||||
EvidenceGrade string
|
||||
TaskType string
|
||||
}
|
||||
|
||||
func main() {
|
||||
dryRun := flag.Bool("dry-run", false, "print commands without executing")
|
||||
taskFilter := flag.String("task", "", "filter by task ID (e.g. T-Q2-1.1)")
|
||||
tasksPathFlag := flag.String("tasks", "", "path to TASKS.md")
|
||||
statusFilter := flag.String("status", "all", "filter by normalized status: all|completed|in_progress|planned|paused|unknown")
|
||||
completedOnly := flag.Bool("completed-only", false, "shortcut for --status completed")
|
||||
flag.Parse()
|
||||
|
||||
tasksPath := resolveTasksPath(*tasksPathFlag)
|
||||
@@ -65,8 +75,18 @@ func main() {
|
||||
tasks = filtered
|
||||
}
|
||||
|
||||
effectiveStatus := *statusFilter
|
||||
if *completedOnly {
|
||||
effectiveStatus = "completed"
|
||||
}
|
||||
tasks, err = filterTasksByStatus(tasks, effectiveStatus)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "filter tasks: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("=== Verification Report (%s) ===\n", time.Now().Format("2006-01-02 15:04"))
|
||||
fmt.Printf("Tasks checked: %d | Dry-run: %v | TASKS: %s\n\n", len(tasks), *dryRun, tasksPath)
|
||||
fmt.Printf("Tasks checked: %d | Dry-run: %v | Status: %s | TASKS: %s\n\n", len(tasks), *dryRun, effectiveStatus, tasksPath)
|
||||
|
||||
var passed, failed int
|
||||
var results []TaskResult
|
||||
@@ -87,17 +107,24 @@ func main() {
|
||||
icon = "❌"
|
||||
}
|
||||
fmt.Printf("%s [%s] %s\n", icon, r.TaskID, r.TaskName)
|
||||
if r.Command != "" {
|
||||
fmt.Printf(" cmd: %s\n", r.Command)
|
||||
}
|
||||
if r.EvidenceGrade != "" || r.TaskType != "" {
|
||||
fmt.Printf(" grade: %s | type: %s\n", r.EvidenceGrade, r.TaskType)
|
||||
}
|
||||
if r.StderrSummary != "" {
|
||||
fmt.Printf(" stderr: %s\n", r.StderrSummary)
|
||||
}
|
||||
if r.StdoutSummary != "" && (!r.Verified || r.Reason != "" || r.Error != "") {
|
||||
fmt.Printf(" stdout: %s\n", r.StdoutSummary)
|
||||
}
|
||||
if r.Error != "" {
|
||||
fmt.Printf(" ERROR: %s\n", r.Error)
|
||||
} else {
|
||||
if r.Command != "" {
|
||||
fmt.Printf(" cmd: %s\n", r.Command)
|
||||
}
|
||||
if r.ExitCode != 0 && r.Stdout != "" {
|
||||
fmt.Printf(" output: %s\n", strings.TrimSpace(r.Stdout))
|
||||
} else if r.Reason != "" {
|
||||
fmt.Printf(" reason: %s\n", r.Reason)
|
||||
}
|
||||
} else if r.ExitCode != 0 && r.Stdout != "" {
|
||||
fmt.Printf(" output: %s\n", strings.TrimSpace(r.Stdout))
|
||||
} else if r.Reason != "" {
|
||||
fmt.Printf(" reason: %s\n", r.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,28 +135,40 @@ func main() {
|
||||
}
|
||||
|
||||
func resolveTasksPath(flagValue string) string {
|
||||
envValue := os.Getenv("TASKS_PATH")
|
||||
wd := ""
|
||||
if currentWD, err := os.Getwd(); err == nil {
|
||||
wd = currentWD
|
||||
}
|
||||
sourceDir := ""
|
||||
if _, sourcePath, _, ok := runtime.Caller(0); ok {
|
||||
sourceDir = filepath.Dir(sourcePath)
|
||||
}
|
||||
return resolveTasksPathWithContext(flagValue, envValue, wd, sourceDir, "/home/long/.openclaw/workspace/TASKS.md")
|
||||
}
|
||||
|
||||
func resolveTasksPathWithContext(flagValue, envValue, wd, sourceDir, globalTasksPath string) string {
|
||||
candidates := []string{}
|
||||
if flagValue != "" {
|
||||
candidates = append(candidates, flagValue)
|
||||
}
|
||||
if envValue := os.Getenv("TASKS_PATH"); envValue != "" {
|
||||
if envValue != "" {
|
||||
candidates = append(candidates, envValue)
|
||||
}
|
||||
|
||||
if wd, err := os.Getwd(); err == nil {
|
||||
if wd != "" {
|
||||
candidates = append(candidates,
|
||||
filepath.Join(wd, "TASKS.md"),
|
||||
filepath.Join(wd, "..", "TASKS.md"),
|
||||
)
|
||||
}
|
||||
|
||||
if _, sourcePath, _, ok := runtime.Caller(0); ok {
|
||||
scriptDir := filepath.Dir(sourcePath)
|
||||
candidates = append(candidates, filepath.Join(scriptDir, "..", "TASKS.md"))
|
||||
defaultProjectTasks := ""
|
||||
if sourceDir != "" {
|
||||
defaultProjectTasks = filepath.Join(sourceDir, "..", "TASKS.md")
|
||||
candidates = append(candidates, defaultProjectTasks)
|
||||
}
|
||||
|
||||
candidates = append(candidates, "/home/long/.openclaw/workspace/TASKS.md")
|
||||
|
||||
seen := map[string]struct{}{}
|
||||
for _, candidate := range candidates {
|
||||
if candidate == "" {
|
||||
@@ -148,16 +187,26 @@ func resolveTasksPath(flagValue string) string {
|
||||
if flagValue != "" {
|
||||
return filepath.Clean(flagValue)
|
||||
}
|
||||
if envValue := os.Getenv("TASKS_PATH"); envValue != "" {
|
||||
if envValue != "" {
|
||||
return filepath.Clean(envValue)
|
||||
}
|
||||
return "/home/long/.openclaw/workspace/TASKS.md"
|
||||
if defaultProjectTasks != "" {
|
||||
return filepath.Clean(defaultProjectTasks)
|
||||
}
|
||||
if wd != "" {
|
||||
return filepath.Clean(filepath.Join(wd, "TASKS.md"))
|
||||
}
|
||||
if globalTasksPath != "" {
|
||||
return filepath.Clean(globalTasksPath)
|
||||
}
|
||||
return "TASKS.md"
|
||||
}
|
||||
|
||||
type taskEntry struct {
|
||||
ID string
|
||||
Name string
|
||||
Verification Verification
|
||||
ID string
|
||||
Name string
|
||||
Status string
|
||||
Verification Verification
|
||||
HasVerification bool
|
||||
}
|
||||
|
||||
@@ -176,7 +225,7 @@ func parseTasks(f *os.File) []taskEntry {
|
||||
if currentTask != nil {
|
||||
tasks = append(tasks, *currentTask)
|
||||
}
|
||||
currentTask = &taskEntry{ID: m[1], Name: m[2]}
|
||||
currentTask = &taskEntry{ID: m[1], Name: m[2], Status: normalizeStatusFromText(line)}
|
||||
inVerification = false
|
||||
continue
|
||||
}
|
||||
@@ -193,6 +242,10 @@ func parseTasks(f *os.File) []taskEntry {
|
||||
}
|
||||
|
||||
if !inVerification {
|
||||
statusRe := regexp.MustCompile(`^\s*-\s+\*\*状态\*\*:(.+)$`)
|
||||
if m := statusRe.FindStringSubmatch(line); m != nil {
|
||||
currentTask.Status = normalizeStatusFromText(m[1])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -216,6 +269,18 @@ func parseTasks(f *os.File) []taskEntry {
|
||||
continue
|
||||
}
|
||||
|
||||
evidenceGradeRe := regexp.MustCompile(`^\s+- evidence_grade:\s+` + "`" + `([^` + "`" + `]+)` + "`")
|
||||
if m := evidenceGradeRe.FindStringSubmatch(line); m != nil {
|
||||
currentTask.Verification.EvidenceGrade = m[1]
|
||||
continue
|
||||
}
|
||||
|
||||
taskTypeRe := regexp.MustCompile(`^\s+- task_type:\s+` + "`" + `([^` + "`" + `]+)` + "`")
|
||||
if m := taskTypeRe.FindStringSubmatch(line); m != nil {
|
||||
currentTask.Verification.TaskType = m[1]
|
||||
continue
|
||||
}
|
||||
|
||||
timeoutRe := regexp.MustCompile(`^\s+- timeout_seconds:\s+(\d+)`)
|
||||
if m := timeoutRe.FindStringSubmatch(line); m != nil {
|
||||
fmt.Sscanf(m[1], "%d", ¤tTask.Verification.TimeoutSeconds)
|
||||
@@ -244,6 +309,18 @@ func verifyTask(t taskEntry, dryRun bool) TaskResult {
|
||||
return r
|
||||
}
|
||||
|
||||
t.Verification.Mode = strings.TrimSpace(t.Verification.Mode)
|
||||
t.Verification.TaskType = normalizeTaskType(t.Verification.TaskType)
|
||||
t.Verification.EvidenceGrade = normalizeEvidenceGrade(t.Verification.Mode, t.Verification.EvidenceGrade)
|
||||
r.TaskType = t.Verification.TaskType
|
||||
r.EvidenceGrade = t.Verification.EvidenceGrade
|
||||
|
||||
if validationErr := validateVerification(t.Verification); validationErr != "" {
|
||||
r.Verified = false
|
||||
r.Reason = validationErr
|
||||
return r
|
||||
}
|
||||
|
||||
if t.Verification.Command == "" {
|
||||
r.Reason = "verification.command is empty"
|
||||
r.Verified = false
|
||||
@@ -283,6 +360,8 @@ func verifyTask(t taskEntry, dryRun bool) TaskResult {
|
||||
|
||||
r.Stdout = stdout.String()
|
||||
r.Stderr = stderr.String()
|
||||
r.StdoutSummary = summarizeOutput(r.Stdout)
|
||||
r.StderrSummary = summarizeOutput(r.Stderr)
|
||||
|
||||
if r.ExitCode != 0 && t.Verification.Mode == "test_pass" {
|
||||
r.Verified = false
|
||||
@@ -325,3 +404,134 @@ func verifyTask(t taskEntry, dryRun bool) TaskResult {
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func normalizeEvidenceGrade(mode, explicit string) string {
|
||||
if explicit = strings.TrimSpace(explicit); explicit != "" {
|
||||
return explicit
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(mode) {
|
||||
case "test_pass":
|
||||
return "runtime-verified"
|
||||
case "artifact_present":
|
||||
return "artifact-present"
|
||||
case "semantic":
|
||||
return "doc-claimed"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeTaskType(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return "unspecified"
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func normalizeStatusFromText(raw string) string {
|
||||
lower := strings.ToLower(strings.TrimSpace(raw))
|
||||
switch {
|
||||
case strings.Contains(raw, "✅") || strings.Contains(raw, "完成"):
|
||||
return "completed"
|
||||
case strings.Contains(raw, "🟡") || strings.Contains(raw, "进行中"):
|
||||
return "in_progress"
|
||||
case strings.Contains(raw, "🔶") || strings.Contains(raw, "🔴") || strings.Contains(raw, "待启动") || strings.Contains(raw, "未开始"):
|
||||
return "planned"
|
||||
case strings.Contains(raw, "⏸️") || strings.Contains(raw, "待规划") || strings.Contains(raw, "暂停"):
|
||||
return "paused"
|
||||
case lower == "":
|
||||
return "unknown"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func filterTasksByStatus(tasks []taskEntry, filter string) ([]taskEntry, error) {
|
||||
filter = strings.TrimSpace(filter)
|
||||
if filter == "" {
|
||||
filter = "all"
|
||||
}
|
||||
|
||||
valid := map[string]struct{}{
|
||||
"all": {},
|
||||
"completed": {},
|
||||
"in_progress": {},
|
||||
"planned": {},
|
||||
"paused": {},
|
||||
"unknown": {},
|
||||
}
|
||||
if _, ok := valid[filter]; !ok {
|
||||
return nil, fmt.Errorf("unsupported status filter: %s", filter)
|
||||
}
|
||||
if filter == "all" {
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
filtered := make([]taskEntry, 0, len(tasks))
|
||||
for _, t := range tasks {
|
||||
status := t.Status
|
||||
if status == "" {
|
||||
status = "unknown"
|
||||
}
|
||||
if status == filter {
|
||||
filtered = append(filtered, t)
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func summarizeOutput(raw string) string {
|
||||
cleaned := strings.TrimSpace(raw)
|
||||
if cleaned == "" {
|
||||
return ""
|
||||
}
|
||||
cleaned = strings.Join(strings.Fields(cleaned), " ")
|
||||
const limit = 220
|
||||
if len(cleaned) <= limit {
|
||||
return cleaned
|
||||
}
|
||||
return cleaned[:limit] + "..."
|
||||
}
|
||||
|
||||
func validateVerification(v Verification) string {
|
||||
validModes := map[string]struct{}{
|
||||
"test_pass": {},
|
||||
"artifact_present": {},
|
||||
"semantic": {},
|
||||
}
|
||||
if _, ok := validModes[v.Mode]; !ok {
|
||||
return fmt.Sprintf("unsupported verification mode: %s", v.Mode)
|
||||
}
|
||||
|
||||
validGrades := map[string]struct{}{
|
||||
"runtime-verified": {},
|
||||
"artifact-present": {},
|
||||
"doc-claimed": {},
|
||||
}
|
||||
if v.EvidenceGrade != "" {
|
||||
if _, ok := validGrades[v.EvidenceGrade]; !ok {
|
||||
return fmt.Sprintf("unsupported evidence grade: %s", v.EvidenceGrade)
|
||||
}
|
||||
}
|
||||
|
||||
validTaskTypes := map[string]struct{}{
|
||||
"unspecified": {},
|
||||
"code": {},
|
||||
"automation": {},
|
||||
"documentation": {},
|
||||
"configuration": {},
|
||||
"data": {},
|
||||
"analysis": {},
|
||||
}
|
||||
if _, ok := validTaskTypes[v.TaskType]; !ok {
|
||||
return fmt.Sprintf("unsupported task type: %s", v.TaskType)
|
||||
}
|
||||
|
||||
if (v.TaskType == "code" || v.TaskType == "automation") && v.Mode == "semantic" {
|
||||
return fmt.Sprintf("semantic-only verification is not allowed for %s tasks", v.TaskType)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user