From a10e30d9db8fd1db2f05aac3a9bf0b6743585d08 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 31 Mar 2026 08:23:49 +0100 Subject: [PATCH] chore(ax): pass 2 AX compliance sweep Resolve all deferred items from pass 1 and deeper violations found in this pass: rename abbreviated variables (a, e, f, s, ts, inp, inputStr, firstTS/lastTS, argv, id) to full descriptive names; add usage-example comments to all unexported helpers (parseFromReader, extractToolInput, extractResultContent, truncate, shortID, formatDuration, generateTape, extractCommand, lookupExecutable, isExecutablePath, runCommand, resultError, repeatString, containsAny, indexOf, trimQuotes); rename single-letter helper parameters to match AX principle 1. Co-Authored-By: Virgil --- analytics.go | 28 ++++----- core_helpers.go | 41 +++++++++---- html.go | 12 +++- parser.go | 153 +++++++++++++++++++++++++----------------------- video.go | 28 ++++++--- 5 files changed, 153 insertions(+), 109 deletions(-) diff --git a/analytics.go b/analytics.go index 23a576c..6473f4e 100644 --- a/analytics.go +++ b/analytics.go @@ -31,7 +31,7 @@ type SessionAnalytics struct { // Example: // analytics := session.Analyse(sess) func Analyse(sess *Session) *SessionAnalytics { - a := &SessionAnalytics{ + analytics := &SessionAnalytics{ ToolCounts: make(map[string]int), ErrorCounts: make(map[string]int), AvgLatency: make(map[string]time.Duration), @@ -39,11 +39,11 @@ func Analyse(sess *Session) *SessionAnalytics { } if sess == nil { - return a + return analytics } - a.Duration = sess.EndTime.Sub(sess.StartTime) - a.EventCount = len(sess.Events) + analytics.Duration = sess.EndTime.Sub(sess.StartTime) + analytics.EventCount = len(sess.Events) // Track totals for latency averaging type latencyAccum struct { @@ -57,23 +57,23 @@ func Analyse(sess *Session) *SessionAnalytics { for evt := range sess.EventsSeq() { // Token estimation: ~4 chars per token - a.EstimatedInputTokens += len(evt.Input) / 4 - a.EstimatedOutputTokens += len(evt.Output) / 4 + analytics.EstimatedInputTokens += len(evt.Input) / 4 + analytics.EstimatedOutputTokens += len(evt.Output) / 4 if evt.Type != "tool_use" { continue } totalToolCalls++ - a.ToolCounts[evt.Tool]++ + analytics.ToolCounts[evt.Tool]++ if !evt.Success { totalErrors++ - a.ErrorCounts[evt.Tool]++ + analytics.ErrorCounts[evt.Tool]++ } // Active time: sum of tool call durations - a.ActiveTime += evt.Duration + analytics.ActiveTime += evt.Duration // Latency tracking if _, ok := latencies[evt.Tool]; !ok { @@ -82,24 +82,24 @@ func Analyse(sess *Session) *SessionAnalytics { latencies[evt.Tool].total += evt.Duration latencies[evt.Tool].count++ - if evt.Duration > a.MaxLatency[evt.Tool] { - a.MaxLatency[evt.Tool] = evt.Duration + if evt.Duration > analytics.MaxLatency[evt.Tool] { + analytics.MaxLatency[evt.Tool] = evt.Duration } } // Compute averages for tool, accumulator := range latencies { if accumulator.count > 0 { - a.AvgLatency[tool] = accumulator.total / time.Duration(accumulator.count) + analytics.AvgLatency[tool] = accumulator.total / time.Duration(accumulator.count) } } // Success rate if totalToolCalls > 0 { - a.SuccessRate = float64(totalToolCalls-totalErrors) / float64(totalToolCalls) + analytics.SuccessRate = float64(totalToolCalls-totalErrors) / float64(totalToolCalls) } - return a + return analytics } // FormatAnalytics returns a tabular text summary suitable for CLI display. diff --git a/core_helpers.go b/core_helpers.go index 42f0c7b..8140723 100644 --- a/core_helpers.go +++ b/core_helpers.go @@ -26,6 +26,9 @@ func (m rawJSON) MarshalJSON() ([]byte, error) { return m, nil } +// resultError extracts the error from a core.Result, or constructs one if absent. +// +// err := resultError(hostFS.Write(path, content)) func resultError(result core.Result) error { if result.OK { return nil @@ -36,32 +39,44 @@ func resultError(result core.Result) error { return core.E("resultError", "unexpected core result failure", nil) } -func repeatString(s string, count int) string { - if s == "" || count <= 0 { +// repeatString returns text repeated count times. +// +// repeatString("=", 50) // "==================================================" +func repeatString(text string, count int) string { + if text == "" || count <= 0 { return "" } - return string(bytes.Repeat([]byte(s), count)) + return string(bytes.Repeat([]byte(text), count)) } -func containsAny(s, chars string) bool { +// containsAny reports whether text contains any rune in chars. +// +// containsAny("/tmp/file", `/\`) // true +func containsAny(text, chars string) bool { for _, character := range chars { - if bytes.IndexRune([]byte(s), character) >= 0 { + if bytes.IndexRune([]byte(text), character) >= 0 { return true } } return false } -func indexOf(s, substr string) int { - return bytes.Index([]byte(s), []byte(substr)) +// indexOf returns the byte offset of substr in text, or -1 if not found. +// +// indexOf("hello world", "world") // 6 +func indexOf(text, substr string) int { + return bytes.Index([]byte(text), []byte(substr)) } -func trimQuotes(s string) string { - if len(s) < 2 { - return s +// trimQuotes strips a single layer of matching double-quotes or back-ticks. +// +// trimQuotes(`"hello"`) // "hello" +func trimQuotes(text string) string { + if len(text) < 2 { + return text } - if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '`' && s[len(s)-1] == '`') { - return s[1 : len(s)-1] + if (text[0] == '"' && text[len(text)-1] == '"') || (text[0] == '`' && text[len(text)-1] == '`') { + return text[1 : len(text)-1] } - return s + return text } diff --git a/html.go b/html.go index fd7bfbc..bf13526 100644 --- a/html.go +++ b/html.go @@ -21,10 +21,10 @@ func RenderHTML(sess *Session, outputPath string) error { duration := sess.EndTime.Sub(sess.StartTime) toolCount := 0 errorCount := 0 - for e := range sess.EventsSeq() { - if e.Type == "tool_use" { + for evt := range sess.EventsSeq() { + if evt.Type == "tool_use" { toolCount++ - if !e.Success { + if !evt.Success { errorCount++ } } @@ -246,6 +246,9 @@ document.addEventListener('keydown', e => { return nil } +// shortID returns the first 8 characters of an ID for display purposes. +// +// shortID("abc123def456") // "abc123de" func shortID(id string) string { if len(id) > 8 { return id[:8] @@ -253,6 +256,9 @@ func shortID(id string) string { return id } +// formatDuration renders a duration as a compact human-readable string. +// +// formatDuration(5*time.Minute + 30*time.Second) // "5m30s" func formatDuration(d time.Duration) string { if d < time.Second { return core.Sprintf("%dms", d.Milliseconds()) diff --git a/parser.go b/parser.go index 6ac0a50..ce33e14 100644 --- a/parser.go +++ b/parser.go @@ -165,7 +165,7 @@ func ListSessionsSeq(projectsDir string) iter.Seq[Session] { continue } - s := Session{ + sess := Session{ ID: id, Path: filePath, } @@ -175,14 +175,14 @@ func ListSessionsSeq(projectsDir string) iter.Seq[Session] { if !openResult.OK { continue } - f, ok := openResult.Value.(io.ReadCloser) + fileHandle, ok := openResult.Value.(io.ReadCloser) if !ok { continue } - scanner := bufio.NewScanner(f) + scanner := bufio.NewScanner(fileHandle) scanner.Buffer(make([]byte, 1024*1024), 1024*1024) - var firstTS, lastTS string + var firstTimestamp, lastTimestamp string for scanner.Scan() { var entry rawEntry if !core.JSONUnmarshal(scanner.Bytes(), &entry).OK { @@ -191,36 +191,36 @@ func ListSessionsSeq(projectsDir string) iter.Seq[Session] { if entry.Timestamp == "" { continue } - if firstTS == "" { - firstTS = entry.Timestamp + if firstTimestamp == "" { + firstTimestamp = entry.Timestamp } - lastTS = entry.Timestamp + lastTimestamp = entry.Timestamp } - f.Close() + fileHandle.Close() - if firstTS != "" { - if t, err := time.Parse(time.RFC3339Nano, firstTS); err == nil { - s.StartTime = t + if firstTimestamp != "" { + if parsedTime, err := time.Parse(time.RFC3339Nano, firstTimestamp); err == nil { + sess.StartTime = parsedTime } } - if lastTS != "" { - if t, err := time.Parse(time.RFC3339Nano, lastTS); err == nil { - s.EndTime = t + if lastTimestamp != "" { + if parsedTime, err := time.Parse(time.RFC3339Nano, lastTimestamp); err == nil { + sess.EndTime = parsedTime } } - if s.StartTime.IsZero() { - s.StartTime = info.ModTime() + if sess.StartTime.IsZero() { + sess.StartTime = info.ModTime() } - sessions = append(sessions, s) + sessions = append(sessions, sess) } slices.SortFunc(sessions, func(i, j Session) int { return j.StartTime.Compare(i.StartTime) }) - for _, s := range sessions { - if !yield(s) { + for _, sess := range sessions { + if !yield(sess) { return } } @@ -292,16 +292,16 @@ func ParseTranscript(filePath string) (*Session, *ParseStats, error) { if !openResult.OK { return nil, nil, core.E("ParseTranscript", "open transcript", resultError(openResult)) } - f, ok := openResult.Value.(io.ReadCloser) + fileHandle, ok := openResult.Value.(io.ReadCloser) if !ok { return nil, nil, core.E("ParseTranscript", "unexpected file handle type", nil) } - defer f.Close() + defer fileHandle.Close() base := path.Base(filePath) id := core.TrimSuffix(base, ".jsonl") - sess, stats, err := parseFromReader(f, id) + sess, stats, err := parseFromReader(fileHandle, id) if sess != nil { sess.Path = filePath } @@ -325,9 +325,9 @@ func ParseTranscriptReader(r io.Reader, id string) (*Session, *ParseStats, error return sess, stats, nil } -// parseFromReader is the shared implementation for both file-based and -// reader-based parsing. It scans line-by-line using bufio.Scanner with -// an 8 MiB buffer, gracefully skipping malformed lines. +// parseFromReader scans r line-by-line and returns parsed session events. +// +// sess, stats, err := parseFromReader(f, "abc123") func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) { sess := &Session{ ID: id, @@ -375,17 +375,17 @@ func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) { continue } - ts, err := time.Parse(time.RFC3339Nano, entry.Timestamp) + entryTime, err := time.Parse(time.RFC3339Nano, entry.Timestamp) if err != nil { stats.Warnings = append(stats.Warnings, core.Sprintf("line %d: bad timestamp %q: %v", lineNum, entry.Timestamp, err)) continue } - if sess.StartTime.IsZero() && !ts.IsZero() { - sess.StartTime = ts + if sess.StartTime.IsZero() && !entryTime.IsZero() { + sess.StartTime = entryTime } - if !ts.IsZero() { - sess.EndTime = ts + if !entryTime.IsZero() { + sess.EndTime = entryTime } switch entry.Type { @@ -406,18 +406,18 @@ func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) { case "text": if text := core.Trim(block.Text); text != "" { sess.Events = append(sess.Events, Event{ - Timestamp: ts, + Timestamp: entryTime, Type: "assistant", Input: truncate(text, 500), }) } case "tool_use": - inputStr := extractToolInput(block.Name, block.Input) + toolInput := extractToolInput(block.Name, block.Input) pendingTools[block.ID] = toolUse{ - timestamp: ts, + timestamp: entryTime, tool: block.Name, - input: inputStr, + input: toolInput, } } } @@ -447,7 +447,7 @@ func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) { ToolID: block.ToolUseID, Input: pendingTool.input, Output: truncate(output, 2000), - Duration: ts.Sub(pendingTool.timestamp), + Duration: entryTime.Sub(pendingTool.timestamp), Success: !isError, } if isError { @@ -460,7 +460,7 @@ func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) { case "text": if text := core.Trim(block.Text); text != "" { sess.Events = append(sess.Events, Event{ - Timestamp: ts, + Timestamp: entryTime, Type: "user", Input: truncate(text, 500), }) @@ -492,6 +492,9 @@ func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) { return sess, stats, nil } +// extractToolInput decodes a tool's raw JSON input to a human-readable string. +// +// label := extractToolInput("Bash", raw) // "ls -la # list files" func extractToolInput(toolName string, raw rawJSON) string { if raw == nil { return "" @@ -499,64 +502,67 @@ func extractToolInput(toolName string, raw rawJSON) string { switch toolName { case "Bash": - var inp bashInput - if core.JSONUnmarshal(raw, &inp).OK { - desc := inp.Description + var input bashInput + if core.JSONUnmarshal(raw, &input).OK { + desc := input.Description if desc != "" { desc = " # " + desc } - return inp.Command + desc + return input.Command + desc } case "Read": - var inp readInput - if core.JSONUnmarshal(raw, &inp).OK { - return inp.FilePath + var input readInput + if core.JSONUnmarshal(raw, &input).OK { + return input.FilePath } case "Edit": - var inp editInput - if core.JSONUnmarshal(raw, &inp).OK { - return core.Sprintf("%s (edit)", inp.FilePath) + var input editInput + if core.JSONUnmarshal(raw, &input).OK { + return core.Sprintf("%s (edit)", input.FilePath) } case "Write": - var inp writeInput - if core.JSONUnmarshal(raw, &inp).OK { - return core.Sprintf("%s (%d bytes)", inp.FilePath, len(inp.Content)) + var input writeInput + if core.JSONUnmarshal(raw, &input).OK { + return core.Sprintf("%s (%d bytes)", input.FilePath, len(input.Content)) } case "Grep": - var inp grepInput - if core.JSONUnmarshal(raw, &inp).OK { - path := inp.Path - if path == "" { - path = "." + var input grepInput + if core.JSONUnmarshal(raw, &input).OK { + grepPath := input.Path + if grepPath == "" { + grepPath = "." } - return core.Sprintf("/%s/ in %s", inp.Pattern, path) + return core.Sprintf("/%s/ in %s", input.Pattern, grepPath) } case "Glob": - var inp globInput - if core.JSONUnmarshal(raw, &inp).OK { - return inp.Pattern + var input globInput + if core.JSONUnmarshal(raw, &input).OK { + return input.Pattern } case "Task": - var inp taskInput - if core.JSONUnmarshal(raw, &inp).OK { - desc := inp.Description + var input taskInput + if core.JSONUnmarshal(raw, &input).OK { + desc := input.Description if desc == "" { - desc = truncate(inp.Prompt, 80) + desc = truncate(input.Prompt, 80) } - return core.Sprintf("[%s] %s", inp.SubagentType, desc) + return core.Sprintf("[%s] %s", input.SubagentType, desc) } } // Fallback: show raw JSON keys - var m map[string]any - if core.JSONUnmarshal(raw, &m).OK { - parts := slices.Sorted(maps.Keys(m)) + var jsonFields map[string]any + if core.JSONUnmarshal(raw, &jsonFields).OK { + parts := slices.Sorted(maps.Keys(jsonFields)) return core.Join(", ", parts...) } return "" } +// extractResultContent coerces a tool_result content value to a plain string. +// +// text := extractResultContent(block.Content) // "total 42\n..." func extractResultContent(content any) string { switch v := content.(type) { case string: @@ -564,8 +570,8 @@ func extractResultContent(content any) string { case []any: var parts []string for _, item := range v { - if m, ok := item.(map[string]any); ok { - if text, ok := m["text"].(string); ok { + if contentMap, ok := item.(map[string]any); ok { + if text, ok := contentMap["text"].(string); ok { parts = append(parts, text) } } @@ -579,9 +585,12 @@ func extractResultContent(content any) string { return core.Sprint(content) } -func truncate(s string, max int) string { - if len(s) <= max { - return s +// truncate clips text to at most maxLen bytes, appending "..." if clipped. +// +// truncate("hello world", 5) // "hello..." +func truncate(text string, maxLen int) string { + if len(text) <= maxLen { + return text } - return s[:max] + "..." + return text[:maxLen] + "..." } diff --git a/video.go b/video.go index 4635e10..9011e22 100644 --- a/video.go +++ b/video.go @@ -40,6 +40,9 @@ func RenderMP4(sess *Session, outputPath string) error { return nil } +// generateTape produces a VHS .tape script from session events. +// +// tape := generateTape(sess, "/tmp/session.mp4") func generateTape(sess *Session, outputPath string) string { builder := core.NewBuilder() @@ -53,12 +56,12 @@ func generateTape(sess *Session, outputPath string) string { builder.WriteString("\n") // Title frame - id := sess.ID - if len(id) > 8 { - id = id[:8] + sessionID := sess.ID + if len(sessionID) > 8 { + sessionID = sessionID[:8] } builder.WriteString(core.Sprintf("Type \"# Session %s | %s\"\n", - id, sess.StartTime.Format("2006-01-02 15:04"))) + sessionID, sess.StartTime.Format("2006-01-02 15:04"))) builder.WriteString("Enter\n") builder.WriteString("Sleep 2s\n") builder.WriteString("\n") @@ -121,14 +124,19 @@ func generateTape(sess *Session, outputPath string) string { return builder.String() } +// extractCommand strips the description suffix from a Bash input string. +// +// extractCommand("ls -la # list files") // "ls -la" func extractCommand(input string) string { - // Remove description suffix (after " # ") if index := indexOf(input, " # "); index > 0 { return input[:index] } return input } +// lookupExecutable searches PATH for an executable with the given name. +// +// lookupExecutable("vhs") // "/usr/local/bin/vhs" func lookupExecutable(name string) string { if name == "" { return "" @@ -152,6 +160,9 @@ func lookupExecutable(name string) string { return "" } +// isExecutablePath reports whether filePath is a regular executable file. +// +// isExecutablePath("/usr/bin/vhs") // true func isExecutablePath(filePath string) bool { statResult := hostFS.Stat(filePath) if !statResult.OK { @@ -164,14 +175,17 @@ func isExecutablePath(filePath string) bool { return info.Mode()&0111 != 0 } +// runCommand executes command with args, inheriting stdio, and waits for exit. +// +// err := runCommand("/usr/local/bin/vhs", "/tmp/session.tape") func runCommand(command string, args ...string) error { - argv := append([]string{command}, args...) + arguments := append([]string{command}, args...) procAttr := &syscall.ProcAttr{ Env: syscall.Environ(), Files: []uintptr{0, 1, 2}, } - pid, err := syscall.ForkExec(command, argv, procAttr) + pid, err := syscall.ForkExec(command, arguments, procAttr) if err != nil { return core.E("runCommand", "fork exec command", err) }