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 <virgil@lethean.io>
This commit is contained in:
Claude 2026-03-31 08:23:49 +01:00
parent 294892c83b
commit a10e30d9db
No known key found for this signature in database
GPG key ID: AF404715446AEB41
5 changed files with 153 additions and 109 deletions

View file

@ -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.

View file

@ -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
}

12
html.go
View file

@ -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())

153
parser.go
View file

@ -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] + "..."
}

View file

@ -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)
}