Skip to content

feat: adding dawgrun#24

Open
seanjSO wants to merge 14 commits intomainfrom
seanj/dawgrun
Open

feat: adding dawgrun#24
seanjSO wants to merge 14 commits intomainfrom
seanj/dawgrun

Conversation

@seanjSO
Copy link
Copy Markdown
Contributor

@seanjSO seanjSO commented Jan 28, 2026

Summary by CodeRabbit

  • New Features
    • Introduced dawgrun, an interactive REPL for querying and exploring DAWGS-compatible Postgres graphs.
    • Added support for Cypher query parsing, translation to SQL, and execution with table and JSON output formats.
    • Implemented database connection management, kind mapping introspection, and EXPLAIN query analysis.
    • Included runtime tracing for performance insights and persistent command history with syntax highlighting.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 28, 2026

Walkthrough

Introduces dawgrun, a new interactive REPL tool for querying DAWGS-compatible PostgreSQL graphs. Includes a complete terminal REPL library, command registry for Cypher parsing/translation/execution, Postgres connection management with kind mapping, and comprehensive build automation.

Changes

Cohort / File(s) Summary
Module Configuration
go.mod, tools/dawgrun/go.mod, tools/dawgrun/pkg/go-repl/go.mod
Adds module declarations for dawgrun and embedded go-repl library, with dependency management and local path replacements.
CLI Entry Point
tools/dawgrun/cmd/dawgrun/main.go, tools/dawgrun/cmd/dawgrun/art.txt, tools/dawgrun/cmd/dawgrun/banner.txt
Implements REPL entry point with banner display, prompt/command parsing, command dispatch, history management, and status widget integration.
Command System
tools/dawgrun/pkg/commands/types.go, tools/dawgrun/pkg/commands/registry.go, tools/dawgrun/pkg/commands/help.go, tools/dawgrun/pkg/commands/cypher.go, tools/dawgrun/pkg/commands/db.go, tools/dawgrun/pkg/commands/runtime.go, tools/dawgrun/pkg/commands/helpers.go
Defines command infrastructure (types, registry, context, output handling) and implements Cypher parsing/translation/execution, Postgres connection/kind-map management, help system, and runtime tracing commands.
REPL Library
tools/dawgrun/pkg/go-repl/repl.go, tools/dawgrun/pkg/go-repl/handler.go, tools/dawgrun/pkg/go-repl/ansi.go, tools/dawgrun/pkg/go-repl/stdinreader.go, tools/dawgrun/pkg/go-repl/repl_popover_test.go
Implements complete terminal REPL with raw mode, history traversal, reverse search, tab completion, status bar, and terminal control via ANSI escape sequences.
REPL Documentation & Tests
tools/dawgrun/pkg/go-repl/README.md, tools/dawgrun/pkg/go-repl/LICENSE, tools/dawgrun/pkg/go-repl/justfile, tools/dawgrun/pkg/go-repl/.gitignore
Provides REPL library documentation, public domain license, test automation, and build artifacts exclusion.
Utilities & Stubs
tools/dawgrun/pkg/stubs/kindmapper.go
Implements in-memory PostgreSQL kind mapping with ID↔kind lookup and caching.
Build & Configuration
tools/dawgrun/justfile, tools/dawgrun/.gitignore
Defines build tasks (vet, build, clean, debug) with support for local/upstream DAWGS configurations.
Documentation
tools/dawgrun/README.md, tools/dawgrun/docs/RFC.md
Comprehensive user guide for dawgrun REPL operations, commands, and configuration; draft RFC documenting design, constraints, and roadmap.

Sequence Diagram

sequenceDiagram
    participant User
    participant REPL as REPL Loop
    participant CmdReg as Command<br/>Registry
    participant CmdExec as Command<br/>Handler
    participant Cypher as Cypher<br/>Parser
    participant Trans as SQL<br/>Translator
    participant DB as Database<br/>Connection
    participant Result as Result<br/>Formatter

    User->>REPL: Input query
    REPL->>REPL: Parse input (shlex)
    REPL->>CmdReg: Lookup command
    CmdReg-->>REPL: Command descriptor
    REPL->>CmdExec: Execute command
    
    alt Query Execution (queryCypherCmd)
        CmdExec->>CmdExec: Get named connection
        CmdExec->>Cypher: Parse Cypher query
        Cypher-->>CmdExec: AST
        CmdExec->>Trans: Translate to SQL
        Trans-->>CmdExec: SQL string
        CmdExec->>DB: Execute in transaction
        DB-->>CmdExec: Result rows
        CmdExec->>Result: Format (table/JSON)
        Result-->>CmdExec: Formatted output
    else Cypher Translation (translateToPsqlCmd)
        CmdExec->>Cypher: Parse Cypher
        Cypher-->>CmdExec: AST
        CmdExec->>CmdExec: Load kind map (if -conn)
        CmdExec->>Trans: Translate with kinds
        Trans-->>CmdExec: SQL AST
        CmdExec->>Result: Format SQL
        Result-->>CmdExec: Formatted string
    else Explain (explainAsPsqlCmd)
        CmdExec->>CmdExec: Load kinds from connection
        CmdExec->>Cypher: Parse Cypher
        CmdExec->>Trans: Translate to SQL
        CmdExec->>DB: Run EXPLAIN in transaction
        DB-->>CmdExec: EXPLAIN results
    end
    
    CmdExec-->>REPL: Output + warnings
    REPL->>REPL: Append to history
    REPL-->>User: Display result
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~80 minutes

Poem

🐰 A REPL springs forth in midnight's glow,
With Cypher queries set to dance and flow—
Kind maps and traces spark the database way,
While history remembers all we say.
Terminal tamed, completion near—
Postgres whispers, crystal clear!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is entirely empty, failing to include any of the required template sections such as description, type of change, testing details, or the pre-merge checklist items. Add a comprehensive PR description following the provided template: explain the feature, select applicable change type, document testing approach, and confirm checklist items completion.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: adding dawgrun' accurately summarizes the main change—introducing a new REPL tool called dawgrun with command infrastructure, while remaining concise and clear.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch seanj/dawgrun
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch seanj/dawgrun

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@seanjSO seanjSO self-assigned this Jan 28, 2026
@seanjSO seanjSO added the enhancement New feature or request label Jan 28, 2026
@zinic
Copy link
Copy Markdown
Contributor

zinic commented Feb 3, 2026

I see this is in draft but do we want to pull this in any time soon?

@seanjSO
Copy link
Copy Markdown
Contributor Author

seanjSO commented Feb 3, 2026

@zinic for sure! Dillon has me working on an RFC right now to get in front of the eng council to solidify the whole thing, so Very Soon™

@seanjSO seanjSO force-pushed the seanj/dawgrun branch 2 times, most recently from f50705c to 589cc4e Compare April 22, 2026 16:54
@seanjSO seanjSO marked this pull request as ready for review April 23, 2026 00:26
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🧹 Nitpick comments (6)
tools/dawgrun/pkg/stubs/kindmapper.go (1)

76-90: Minor: GetKindByID / GetIDByKind aren't part of pgsql.KindMapper and use an awkward inverted-if idiom.

Two small notes:

  1. Per the interface in cypher/models/pgsql/model.go:15-18, only MapKinds and AssertKinds are required. GetKindByID/GetIDByKind are exported helpers specific to this stub — fine, but worth a doc comment clarifying they exist for the lookup-kind / lookup-kind-id REPL commands (tools/dawgrun/pkg/commands/db.go).
  2. The if _, ok := …; !ok { return err } else { return val } pattern inverts the happy path. Idiomatic Go flips this:
♻️ Proposed refactor
 func (k *DumbKindMapper) GetKindByID(id int16) (graph.Kind, error) {
-	if kind, ok := k.idToKind[id]; !ok {
-		return nil, fmt.Errorf("%w: %d", ErrNoSuchKindID, id)
-	} else {
-		return kind, nil
-	}
+	kind, ok := k.idToKind[id]
+	if !ok {
+		return nil, fmt.Errorf("%w: %d", ErrNoSuchKindID, id)
+	}
+	return kind, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/stubs/kindmapper.go` around lines 76 - 90, GetKindByID and
GetIDByKind are exported helper methods on DumbKindMapper used by the REPL but
lack doc comments and currently use an inverted-if idiom; add a short doc
comment to each method explaining they are helpers used by the lookup-kind /
lookup-kind-id REPL commands and refactor both methods (GetKindByID and
GetIDByKind) to the idiomatic Go form that checks the positive/happy path first
(e.g., if kind, ok := k.idToKind[id]; ok { return kind, nil } return nil,
fmt.Errorf(...)) and similarly for kindToID to return the found value
immediately and only construct the error on the failure path.
tools/dawgrun/pkg/go-repl/go.mod (1)

1-8: Vendored upstream module manifest — acceptable as-is.

This mirrors the upstream github.com/openengineer/go-repl manifest (go 1.15, Unlicense) and is used solely via the replace in tools/dawgrun/go.mod. No changes required here; however, since the library is now effectively a fork living in-tree, consider whether you want to bump go 1.15 to something closer to the toolchain used by tools/dawgrun (1.25.4) to avoid confusing IDE/tooling behavior when editing these files directly. Optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/go.mod` around lines 1 - 8, The vendored go.mod
currently declares "module github.com/openengineer/go-repl" with "go 1.15";
leave it as-is if you want an exact upstream mirror, or update the Go version
directive from "go 1.15" to the toolchain version you use (e.g., "go 1.25" or
"go 1.25.4") to avoid IDE/tooling confusion when editing this in-tree—edit the
go directive in the go.mod (the "go 1.15" token) accordingly while keeping the
module name and require block unchanged.
tools/dawgrun/README.md (1)

37-180: Optional: normalize code-block style to fenced.

markdownlint-cli2 flags mixed indented (MD046) and unlabeled fenced blocks (MD040) throughout. Not functionally important, but rendering engines (GitHub vs some doc viewers) handle indented blocks inconsistently, and command lines get no syntax highlighting. Consider converting all the dawgrun > … blocks to ```text (or ```shell where appropriate) and tagging lines 1 and 66 with a language hint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/README.md` around lines 37 - 180, Normalize the README's
example command blocks that are currently indented (the many "    dawgrun > ..."
examples) to fenced code blocks using a language hint (use ```text for generic
command output and ```shell where appropriate) so markdownlint MD046/MD040
warnings are resolved; convert each indented block into a fenced block and
ensure the two prominent example blocks (the top usage example and the
Parse/Translate examples) include an explicit language tag to enable consistent
rendering and optional highlighting.
tools/dawgrun/pkg/go-repl/LICENSE (1)

1-24: LGTM — Unlicense preserved verbatim from upstream.

Since this is a fork of github.com/openengineer/go-repl, consider adding a small NOTICE or top-of-file comment in one of the .go files recording the upstream commit you forked from; helps future maintainers when rebasing patches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/LICENSE` around lines 1 - 24, Add a short
provenance note (e.g., a NOTICE or top-of-file comment) recording the upstream
repo and commit you forked from (github.com/openengineer/go-repl at <commit>)
alongside the existing LICENSE; place it as a comment above the package
declaration in the primary Go source (e.g., above the package line in the main
.go file or the package where the REPL is implemented) so future maintainers can
find the upstream commit easily while preserving the Unlicense verbatim in
LICENSE.
tools/dawgrun/pkg/commands/registry.go (1)

22-24: Avoid returning the mutable registry map.

Registry() exposes the live package map, so any caller can delete or replace commands globally. Return a clone unless external mutation is an intentional plugin API.

Defensive-copy option
 package commands
+
+import "maps"
@@
 func Registry() map[string]CommandDesc {
-	return cmdRegistry
+	return maps.Clone(cmdRegistry)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/commands/registry.go` around lines 22 - 24, Registry()
currently returns the live cmdRegistry map which allows callers to mutate
package state; change Registry to return a defensive copy: create a new
map[string]CommandDesc, iterate over cmdRegistry and copy each entry into the
new map, and return that new map instead of cmdRegistry (if CommandDesc is a
pointer or contains mutable fields, also clone or copy its value before
inserting to avoid exposing internal mutability).
tools/dawgrun/justfile (1)

6-12: Keep build from mutating the working tree.

Because build depends on vet, every build/debug run can rewrite go.mod, go.sum, and formatted source via go mod tidy / go fmt. Consider splitting this into an explicit check/fix recipe and making build only compile.

♻️ Proposed recipe split
-vet:
+fix:
     go mod tidy
     go fmt ./... 
+
+vet:
     go vet ./...
 
-build *BUILDARGS: vet
+build *BUILDARGS:
     go build -o dawgrun {{ BUILDARGS }} ./cmd/...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/justfile` around lines 6 - 12, The current justfile makes the
build recipe depend on vet, which runs go mod tidy and go fmt and thus mutates
go.mod/go.sum and source; split responsibilities by creating separate recipes
(e.g., check and fix) where check runs go vet/go list checks and fix runs go mod
tidy and go fmt, then change the build recipe (build *BUILDARGS) to depend only
on compilation (remove the vet dependency) so running build won’t rewrite files;
update references to the existing vet recipe name or replace it with the new
check/fix recipes as appropriate to preserve CI/lint flows and to keep build
purely compiling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@go.mod`:
- Around line 39-41: The go.mod contains a tool directive for
github.com/specterops/dawgs/tools/dawgrun/cmd/dawgrun but is missing the
required module entry; add a require statement for
github.com/specterops/dawgs/tools/dawgrun/cmd/dawgrun v0.0.0 alongside the
existing replace and tool lines so the module is included in the module graph
and resolves for go mod tidy and other operations.

In `@tools/dawgrun/cmd/dawgrun/main.go`:
- Around line 61-63: The log call in the handler.r.Loop error branch is using
fmt-style "%w" which slog won't expand; change the call to pass a plain message
and the error as an attribute instead (e.g., replace slog.Error("repl
encountered error: %w", slog.String("error", err.Error())) with slog.Error("repl
encountered error", slog.String("error", err.Error())) or slog.Error("repl
encountered error", slog.Any("error", err)) at the handler.r.Loop error handling
site so the error is recorded as a structured attribute.

In `@tools/dawgrun/go.mod`:
- Line 36: Update the pinned indirect dependency for github.com/jackc/pgx/v5 in
go.mod from v5.8.0 to v5.9.2 to address the CRITICAL GHSA-9jj7-4m8r-rfcm and the
SQL-injection GHSA-j88v-2chj-qfwx; after changing the version string for the
module declaration github.com/jackc/pgx/v5, run go mod tidy to refresh the
module graph, run the test suite and any CI checks, and if the transitive
dependency originates from github.com/specterops/dawgs coordinate with that repo
to ensure no incompatible pin remains.

In `@tools/dawgrun/pkg/commands/cypher.go`:
- Around line 252-315: The JSON path currently accumulates all rows in jsonRows
causing unbounded memory use; change it to stream the JSON array directly to
ctx.output: before iterating with result.Next() write a '[' when outputFormat ==
queryCypherOutputFormatJSON, then inside the loop call
buildCypherResultJSONRow(values) and marshal that single row (or use
json.Encoder with SetIndent) and write it immediately, prefixing with a comma
for rowCount>0; after the loop write ']' and a newline (and handle the zero-row
case to emit "[]\n" as before). Remove the jsonRows slice usage and ensure the
existing result.Error() check and rowCount logic around the loop still apply so
errors and "(0 rows)"/count printing behave correctly.
- Around line 187-190: The loop currently discards the error returned by
graph.ScanNextResult when iterating result.Next(); change it to capture the
error (e.g., err := graph.ScanNextResult(result, &value)) and handle it instead
of ignoring it: if err is non-nil, write a concise error message to the command
output or error stream (using ctx.output or an appropriate logger) and skip the
malformed row (continue) or break out if unrecoverable, otherwise print the
value as before; update the block around result.Next(), graph.ScanNextResult,
value, and ctx.output to implement this error check and handling.

In `@tools/dawgrun/pkg/commands/db.go`:
- Around line 21-42: The command handler currently takes only fields[1] as
connStr (losing tokens) and prints the full connection string (exposing
secrets); instead, build the connection string by joining all remaining fields
(e.g., connStr := strings.Join(fields[1:], " ")), use that joined connStr when
calling dawgsPg.NewPool and dawgs.Open, and stop echoing the connection string
to output—only report the connection name (use fmt.Fprintf(ctx.output, "Opened
connection '%s'\n", name)) while storing the joined connStr-backed query in
ctx.scope.connections[name].

In `@tools/dawgrun/pkg/commands/helpers.go`:
- Around line 26-30: The error path in highlightText currently echoes the full
source `text` which may expose sensitive or huge payloads; update the error
returned from the `quick.Highlight` failure (inside function highlightText) to
omit the raw `text` and instead include contextual details such as the `lexer`
and `style` parameters (and optionally the text length) in the fmt.Errorf
message so callers get useful debug info without leaking the source content.

In `@tools/dawgrun/pkg/commands/runtime.go`:
- Around line 68-82: The stop branch incorrectly sets state["run"] = true after
calling trace.Stop() and closing traceFile, causing subsequent "runtime-trace
start" to think tracing is still running; update the stop handling in the
runtime command (the case "stop" block that calls trace.Stop(), inspects
state["tracefile"] and closes traceFile) to set state["run"] = false (and keep
state["tracefile"] = nil) so the runtime trace state correctly reflects that
tracing is stopped.
- Around line 54-61: The created trace file (traceFile from os.Create) is left
open if trace.Start(traceFile) returns an error; modify the runtime.go code
around the os.Create and trace.Start calls so that if trace.Start(...) fails you
close traceFile before returning (e.g., call traceFile.Close() on error), and
consider establishing a single defer to close traceFile only when appropriate
(or close on the error path) to avoid leaking the file descriptor; locate the
os.Create(...) assignment to traceFile and the trace.Start(traceFile) call to
implement this change.

In `@tools/dawgrun/pkg/commands/types.go`:
- Around line 47-50: The Scope struct's maps (connections and connKindMaps) are
accessed concurrently (e.g., NumConns reads connections) and must be protected
to avoid concurrent read/write panics; add a sync.RWMutex (or sync.Mutex) field
to Scope and update all direct accesses and mutating code paths to use the mutex
(use RLock/RUnlock for readers like NumConns and Lock/Unlock for writers that
add/remove entries), or expose new locked accessor/mutator methods on Scope
(e.g., GetNumConns, AddConnection, RemoveConnection, GetKindMap) and refactor
callers to use those methods so all reads/writes of connections and connKindMaps
are synchronized.
- Around line 108-115: The current CommandOutput.WriteHighlightedWithStyle logs
the entire raw text on highlight failure (via slog.String("text", text)); change
this so the raw content is not emitted to logs: in the error branch keep the
error and any safe metadata (e.g., slog.String("error", err.Error()) and
optionally slog.Int("text_len", len(text)) or a short checksum), remove or
replace slog.String("text", text), and still fall back to
co.outputBuilder.WriteString(text). Update the error logging call in
WriteHighlightedWithStyle (and any related paths that call highlightText) to
avoid including the full `text` value.

In `@tools/dawgrun/pkg/go-repl/ansi.go`:
- Line 8: The ANSI helper functions in ansi.go are unconditional and break on
Windows; gate them by platform: add a build tag (e.g., //go:build !windows) to
ansi.go (package go-repl) and provide a companion ansi_windows.go that
implements the same exported helper function names as no-ops/Windows-safe
variants, or alternatively wrap ANSI use with a runtime.GOOS == "windows" check
and fallback to non-ANSI behavior; ensure the function names/signatures remain
identical so callers compile on all platforms and update package docs to state
Unix-only if you choose not to implement Windows support.

In `@tools/dawgrun/pkg/go-repl/examples/basic_repl.go`:
- Around line 24-33: The two example files both define func main (basic_repl.go
using MyHandler with h.r = repl.NewRepl(h) and calling h.r.Loop()) and
shell_wrapper.go which causes a redeclared main error; resolve by separating
examples into distinct packages or adding build tags: either move basic_repl.go
and shell_wrapper.go into their own subdirectories (each with package main and
its own main) or add top-of-file build tags (e.g., //go:build example_basic_repl
and //go:build example_shell_wrapper) so only the intended example is built;
update imports/comments as needed so MyHandler, repl.NewRepl and Loop remain
reachable in each example.

In `@tools/dawgrun/pkg/go-repl/Makefile`:
- Around line 1-15: The Makefile's command targets all, test, and
test-shell_wrapper should be declared .PHONY and a clean target added to remove
generated example binaries; update the file to add a .PHONY line that lists all,
test, and test-shell_wrapper (and any other non-file targets you add), and add a
clean target that removes built binaries produced by the pattern rule (e.g.,
delete ./examples/basic_repl and ./examples/shell_wrapper or use a wildcard like
./examples/* for generated executables), keeping the existing pattern rule
./examples/% unchanged.

In `@tools/dawgrun/pkg/go-repl/repl.go`:
- Around line 1557-1570: cleanAndAddToBuffer currently drops every byte >=127
which corrupts non-ASCII input; update it to be rune-aware by decoding msg as
runes (e.g. for _, rr := range string(msg)) instead of iterating bytes, replace
tabs with a space, and keep non-control runes (use unicode.IsControl(rr) to
filter out control characters rather than filtering by byte value), then
re-encode the kept runes to UTF-8 bytes and call
r.addBytesToBuffer(filteredBytes); reference cleanAndAddToBuffer, msg and
r.addBytesToBuffer when making the change and add the unicode import as needed.
- Around line 180-183: The slice truncation in repl.go currently keeps the
oldest entries (r.history = r.history[0:r.historyMaxLines]) and discards recent
commands; change the truncation to keep the newest entries by slicing the tail
of r.history (use r.history[len(r.history)-r.historyMaxLines:] when
len(r.history) > r.historyMaxLines), and add a guard for non-positive
r.historyMaxLines to avoid panics; update the logic around r.history and
r.historyMaxLines accordingly.
- Around line 231-251: The goroutine in notifySizeChange currently mutates Repl
fields and calls resize()/force()/writeStatus() concurrently with dispatch(),
causing races; change this to send size events to the main input loop instead:
add a resize channel (e.g., resizeCh chan struct{} or chan struct{w,h int}) on
the Repl struct, have notifySizeChange only detect size changes and send a
message on resizeCh (do not call resize()/force()/writeStatus() or mutate
r.width/r.height there), and modify the main loop (where dispatch() runs) to
select on resizeCh and perform r.resize(newW,newH) and any subsequent
force()/writeStatus() there so all shared-state mutations and terminal I/O
happen serialized in the main loop.
- Around line 1719-1722: The slice and loop use negative widths when right is
longer than terminal width w; before doing left = left[0 : w-len(right)] and the
for loop, clamp the available width to a non-negative value (e.g., avail := w -
len(right); if avail < 0 { avail = 0 }) and use that clamped avail for slicing
and for-loop bounds so left is never sliced with a negative index and the loop
never iterates with a negative count; update the code around r.statusFields(),
the left/right handling, and the for i := 0; ... loop to use this clamped avail
variable.

In `@tools/dawgrun/pkg/go-repl/stdinreader.go`:
- Around line 64-90: The goroutine that reads bytes from r.reader has three
issues: protect r.lastTime reads and writes with r.lock (i.e., wrap accesses to
r.lastTime in the same mutex used in start()), protect r.reader
mutations/readers by performing the nil assignment under r.lock or convert
r.reader to an atomic.Pointer and use atomic loads/stores so the main loop and
goroutine cannot race, and replace panic(err) on ReadByte with graceful handling
(treat io.EOF as clean shutdown and send other errors back via an error channel
or the existing bytes channel instead of panicking); update the goroutine around
ReadByte, the stopNow logic, and the place that checks r.reader (the tight loop
calling r.reader.read()) to use the same synchronization approach.

In `@tools/dawgrun/pkg/stubs/kindmapper.go`:
- Around line 45-51: MapperFromKindMap currently seeds DumbKindMapper.lastID to
-1 and stores kindMap by reference, which causes ID collisions and mutates the
caller's map when AssertKinds assigns new IDs; fix by making a defensive copy of
the incoming KindMap for idToKind, build kindToID from that copy (using
kindMap.Invert() or re-computing), and initialize lastID to the maximum key
present in the copied idToKind (or -1 if empty) so subsequent AssertKinds will
allocate new IDs above the existing range without overwriting existing entries.

---

Nitpick comments:
In `@tools/dawgrun/justfile`:
- Around line 6-12: The current justfile makes the build recipe depend on vet,
which runs go mod tidy and go fmt and thus mutates go.mod/go.sum and source;
split responsibilities by creating separate recipes (e.g., check and fix) where
check runs go vet/go list checks and fix runs go mod tidy and go fmt, then
change the build recipe (build *BUILDARGS) to depend only on compilation (remove
the vet dependency) so running build won’t rewrite files; update references to
the existing vet recipe name or replace it with the new check/fix recipes as
appropriate to preserve CI/lint flows and to keep build purely compiling.

In `@tools/dawgrun/pkg/commands/registry.go`:
- Around line 22-24: Registry() currently returns the live cmdRegistry map which
allows callers to mutate package state; change Registry to return a defensive
copy: create a new map[string]CommandDesc, iterate over cmdRegistry and copy
each entry into the new map, and return that new map instead of cmdRegistry (if
CommandDesc is a pointer or contains mutable fields, also clone or copy its
value before inserting to avoid exposing internal mutability).

In `@tools/dawgrun/pkg/go-repl/go.mod`:
- Around line 1-8: The vendored go.mod currently declares "module
github.com/openengineer/go-repl" with "go 1.15"; leave it as-is if you want an
exact upstream mirror, or update the Go version directive from "go 1.15" to the
toolchain version you use (e.g., "go 1.25" or "go 1.25.4") to avoid IDE/tooling
confusion when editing this in-tree—edit the go directive in the go.mod (the "go
1.15" token) accordingly while keeping the module name and require block
unchanged.

In `@tools/dawgrun/pkg/go-repl/LICENSE`:
- Around line 1-24: Add a short provenance note (e.g., a NOTICE or top-of-file
comment) recording the upstream repo and commit you forked from
(github.com/openengineer/go-repl at <commit>) alongside the existing LICENSE;
place it as a comment above the package declaration in the primary Go source
(e.g., above the package line in the main .go file or the package where the REPL
is implemented) so future maintainers can find the upstream commit easily while
preserving the Unlicense verbatim in LICENSE.

In `@tools/dawgrun/pkg/stubs/kindmapper.go`:
- Around line 76-90: GetKindByID and GetIDByKind are exported helper methods on
DumbKindMapper used by the REPL but lack doc comments and currently use an
inverted-if idiom; add a short doc comment to each method explaining they are
helpers used by the lookup-kind / lookup-kind-id REPL commands and refactor both
methods (GetKindByID and GetIDByKind) to the idiomatic Go form that checks the
positive/happy path first (e.g., if kind, ok := k.idToKind[id]; ok { return
kind, nil } return nil, fmt.Errorf(...)) and similarly for kindToID to return
the found value immediately and only construct the error on the failure path.

In `@tools/dawgrun/README.md`:
- Around line 37-180: Normalize the README's example command blocks that are
currently indented (the many "    dawgrun > ..." examples) to fenced code blocks
using a language hint (use ```text for generic command output and ```shell where
appropriate) so markdownlint MD046/MD040 warnings are resolved; convert each
indented block into a fenced block and ensure the two prominent example blocks
(the top usage example and the Parse/Translate examples) include an explicit
language tag to enable consistent rendering and optional highlighting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2bbc0c2b-3199-4d67-b0c9-01c090c51615

📥 Commits

Reviewing files that changed from the base of the PR and between dbdd47f and 07cee93.

⛔ Files ignored due to path filters (3)
  • go.work is excluded by !**/*.work
  • go.work.sum is excluded by !**/*.sum
  • tools/dawgrun/go.sum is excluded by !**/*.sum
📒 Files selected for processing (29)
  • go.mod
  • tools/dawgrun/.gitignore
  • tools/dawgrun/README.md
  • tools/dawgrun/cmd/dawgrun/art.txt
  • tools/dawgrun/cmd/dawgrun/banner.txt
  • tools/dawgrun/cmd/dawgrun/main.go
  • tools/dawgrun/go.mod
  • tools/dawgrun/justfile
  • tools/dawgrun/pkg/commands/completion.go
  • tools/dawgrun/pkg/commands/cypher.go
  • tools/dawgrun/pkg/commands/db.go
  • tools/dawgrun/pkg/commands/help.go
  • tools/dawgrun/pkg/commands/helpers.go
  • tools/dawgrun/pkg/commands/registry.go
  • tools/dawgrun/pkg/commands/runtime.go
  • tools/dawgrun/pkg/commands/types.go
  • tools/dawgrun/pkg/go-repl/.gitignore
  • tools/dawgrun/pkg/go-repl/LICENSE
  • tools/dawgrun/pkg/go-repl/Makefile
  • tools/dawgrun/pkg/go-repl/README.md
  • tools/dawgrun/pkg/go-repl/ansi.go
  • tools/dawgrun/pkg/go-repl/examples/basic_repl.go
  • tools/dawgrun/pkg/go-repl/examples/shell_wrapper.go
  • tools/dawgrun/pkg/go-repl/go.mod
  • tools/dawgrun/pkg/go-repl/handler.go
  • tools/dawgrun/pkg/go-repl/repl.go
  • tools/dawgrun/pkg/go-repl/repl_popover_test.go
  • tools/dawgrun/pkg/go-repl/stdinreader.go
  • tools/dawgrun/pkg/stubs/kindmapper.go

Comment thread go.mod Outdated
Comment on lines +39 to +41
replace github.com/specterops/dawgs/tools/dawgrun/cmd/dawgrun => ./tools/dawgrun/cmd/dawgrun

tool github.com/specterops/dawgs/tools/dawgrun/cmd/dawgrun
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Nested go.mod files under tools/dawgrun:"
fd -a '^go\.mod$' tools/dawgrun -x sh -c 'printf "\n== %s ==\n" "$1"; sed -n "1,40p" "$1"' sh {}

echo
echo "Root go.mod dawgrun tool wiring:"
rg -n -C3 'github\.com/specterops/dawgs/tools/dawgrun/cmd/dawgrun|^tool\b|^replace\b|^require\b' go.mod

Repository: SpecterOps/DAWGS

Length of output: 2806


🌐 Web query:

In Go 1.25, does a go.mod tool directive for a package in a replaced local nested module require a corresponding require directive?

💡 Result:

Yes, in Go 1.25, a go.mod tool directive for a package in a replaced local nested module requires a corresponding require directive.

Citations:


Add a require directive for the dawgrun tool module.

The tool directive on line 41 requires a corresponding require statement to properly add the module to the module graph. In Go 1.25, add:

require github.com/specterops/dawgs/tools/dawgrun/cmd/dawgrun v0.0.0

This directive is needed alongside the replace to ensure the tool module is resolvable when go mod tidy or other module operations are run.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@go.mod` around lines 39 - 41, The go.mod contains a tool directive for
github.com/specterops/dawgs/tools/dawgrun/cmd/dawgrun but is missing the
required module entry; add a require statement for
github.com/specterops/dawgs/tools/dawgrun/cmd/dawgrun v0.0.0 alongside the
existing replace and tool lines so the module is included in the module graph
and resolves for go mod tidy and other operations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right before this branch gets merged, the replace directive should go away anyway. It's only there for local development and testing

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seanjSO, understood — thanks for the context! Since the replace directive is a temporary local-dev convenience that will be dropped before merge, my concern about the missing require directive is effectively addressed as part of that pre-merge cleanup. Just a heads-up: once the replace is removed, the tool directive will need the module to be resolvable via a proper require entry pointing to a real published version. That would be the right time to add it.


✏️ Learnings added
Learnt from: seanjSO
Repo: SpecterOps/DAWGS PR: 24
File: go.mod:39-41
Timestamp: 2026-04-27T14:41:22.654Z
Learning: In the SpecterOps/DAWGS repository (go.mod), the `replace github.com/specterops/dawgs/tools/dawgrun/cmd/dawgrun => ./tools/dawgrun/cmd/dawgrun` directive is intentionally temporary — used only for local development and testing. It will be removed before the branch is merged. The corresponding `tool` directive will eventually need a proper `require` entry referencing a published version once the `replace` is dropped.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread tools/dawgrun/cmd/dawgrun/main.go
Comment thread tools/dawgrun/go.mod
Comment thread tools/dawgrun/pkg/commands/cypher.go
Comment thread tools/dawgrun/pkg/commands/cypher.go Outdated
Comment thread tools/dawgrun/pkg/go-repl/repl.go
Comment on lines +1557 to +1570
func (r *Repl) cleanAndAddToBuffer(msg []byte) {
// remove bad chars
// XXX: what about unicode?
filtered := make([]byte, 0)

for _, c := range msg {
if c == '\t' {
filtered = append(filtered, ' ')
} else if c >= 32 && c < 127 {
filtered = append(filtered, c)
}
}

r.addBytesToBuffer(filtered)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not silently drop non-ASCII query input.

cleanAndAddToBuffer removes every byte >= 127, so pasted Cypher containing Unicode labels, properties, or string literals is corrupted before execution.

Do you want me to draft a rune-aware input/rendering change so Unicode is preserved correctly?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/repl.go` around lines 1557 - 1570,
cleanAndAddToBuffer currently drops every byte >=127 which corrupts non-ASCII
input; update it to be rune-aware by decoding msg as runes (e.g. for _, rr :=
range string(msg)) instead of iterating bytes, replace tabs with a space, and
keep non-control runes (use unicode.IsControl(rr) to filter out control
characters rather than filtering by byte value), then re-encode the kept runes
to UTF-8 bytes and call r.addBytesToBuffer(filteredBytes); reference
cleanAndAddToBuffer, msg and r.addBytesToBuffer when making the change and add
the unicode import as needed.

Comment thread tools/dawgrun/pkg/go-repl/repl.go
Comment on lines +64 to +90
go func() {
for {
b, err := r.reader.ReadByte()
if err != nil {
panic(err)
}

stopNow := false
if b == 13 && time.Now().After(r.lastTime.Add(MACHINE_INTERVAL)) {
// it is unlikely that a carriage return followed by some text is pasted into the terminal, so we can use this as a queu to quit
stopNow = true
}

r.lastTime = time.Now()

r.lock.Lock()

r.buffer = append(r.buffer, b)

r.lock.Unlock()

if stopNow {
r.reader = nil
return
}
}
}()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Data race on r.lastTime and r.reader, plus panic on EOF.

Three concerns in the stdin read goroutine:

  1. r.lastTime race. start() reads r.lastTime.Add(MACHINE_INTERVAL) at line 42 under r.lock, but the reader goroutine writes r.lastTime = time.Now() at line 77 without holding the lock (and also reads it at line 72 unlocked). go test -race will flag this; under load it can misgroup escape sequences.
  2. r.reader race. Line 86 sets r.reader = nil from the goroutine while the main event loop checks r.reader != nil at line 57 (see repl.go:1779-1809 calling r.reader.read() in a tight loop). Assign under the same lock or make the field an atomic.Pointer[bufio.Reader].
  3. panic(err) on ReadByte error (line 68). This triggers on normal stdin closure (piped input, Ctrl-D in some configurations, closed PTY) and takes down the REPL with a stack trace. At minimum, treat io.EOF as a clean shutdown and forward other errors through the bytes channel or a dedicated error channel.

Since this file is a fork living in-tree (Unlicense), you can patch it directly.

🐛 Proposed fix sketch
 func (r *_StdinReader) read() {
-	if r.reader != nil {
-		return
-	}
-
-	r.reader = bufio.NewReader(os.Stdin)
-	r.lastTime = time.Now()
+	r.lock.Lock()
+	if r.reader != nil {
+		r.lock.Unlock()
+		return
+	}
+	r.reader = bufio.NewReader(os.Stdin)
+	r.lastTime = time.Now()
+	r.lock.Unlock()
 
 	go func() {
 		for {
 			b, err := r.reader.ReadByte()
 			if err != nil {
-				panic(err)
+				if errors.Is(err, io.EOF) {
+					r.lock.Lock()
+					r.reader = nil
+					r.lock.Unlock()
+					return
+				}
+				panic(err) // or surface via an error channel
 			}
 
-			stopNow := false
-			if b == 13 && time.Now().After(r.lastTime.Add(MACHINE_INTERVAL)) {
-				stopNow = true
-			}
-
-			r.lastTime = time.Now()
-
 			r.lock.Lock()
-
+			stopNow := b == 13 && time.Now().After(r.lastTime.Add(MACHINE_INTERVAL))
+			r.lastTime = time.Now()
 			r.buffer = append(r.buffer, b)
-
-			r.lock.Unlock()
-
 			if stopNow {
 				r.reader = nil
+				r.lock.Unlock()
 				return
 			}
+			r.lock.Unlock()
 		}
 	}()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/stdinreader.go` around lines 64 - 90, The goroutine
that reads bytes from r.reader has three issues: protect r.lastTime reads and
writes with r.lock (i.e., wrap accesses to r.lastTime in the same mutex used in
start()), protect r.reader mutations/readers by performing the nil assignment
under r.lock or convert r.reader to an atomic.Pointer and use atomic
loads/stores so the main loop and goroutine cannot race, and replace panic(err)
on ReadByte with graceful handling (treat io.EOF as clean shutdown and send
other errors back via an error channel or the existing bytes channel instead of
panicking); update the goroutine around ReadByte, the stopNow logic, and the
place that checks r.reader (the tight loop calling r.reader.read()) to use the
same synchronization approach.

Comment on lines +45 to +51
func MapperFromKindMap(kindMap KindMap) *DumbKindMapper {
return &DumbKindMapper{
idToKind: kindMap,
kindToID: kindMap.Invert(),
lastID: -1,
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: MapperFromKindMap seeds lastID at -1, causing guaranteed ID collisions on any subsequent AssertKinds call.

If the incoming kindMap contains, say, {0: User, 1: Group, 2: Domain}, lastID stays at -1, so the first previously-unseen kind passed to AssertKinds is assigned newID = 0, which silently overwrites idToKind[0] (User) and breaks every subsequent GetKindByID(0). Additionally, because idToKind is stored by reference, this mutation is visible to the caller that supplied kindMap.

Initialize lastID to the maximum ID present and copy the map defensively:

🐛 Proposed fix
 func MapperFromKindMap(kindMap KindMap) *DumbKindMapper {
-	return &DumbKindMapper{
-		idToKind: kindMap,
-		kindToID: kindMap.Invert(),
-		lastID:   -1,
-	}
+	idToKind := make(KindMap, len(kindMap))
+	lastID := int16(-1)
+	for id, kind := range kindMap {
+		idToKind[id] = kind
+		if id > lastID {
+			lastID = id
+		}
+	}
+	return &DumbKindMapper{
+		idToKind: idToKind,
+		kindToID: idToKind.Invert(),
+		lastID:   lastID,
+	}
 }

Per tools/dawgrun/README.md, load-db-kinds / lookup-kind flows construct the mapper from a DB-sourced kind map and then lazily discover more kinds — exactly the path that triggers this bug.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func MapperFromKindMap(kindMap KindMap) *DumbKindMapper {
return &DumbKindMapper{
idToKind: kindMap,
kindToID: kindMap.Invert(),
lastID: -1,
}
}
func MapperFromKindMap(kindMap KindMap) *DumbKindMapper {
idToKind := make(KindMap, len(kindMap))
lastID := int16(-1)
for id, kind := range kindMap {
idToKind[id] = kind
if id > lastID {
lastID = id
}
}
return &DumbKindMapper{
idToKind: idToKind,
kindToID: idToKind.Invert(),
lastID: lastID,
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/stubs/kindmapper.go` around lines 45 - 51,
MapperFromKindMap currently seeds DumbKindMapper.lastID to -1 and stores kindMap
by reference, which causes ID collisions and mutates the caller's map when
AssertKinds assigns new IDs; fix by making a defensive copy of the incoming
KindMap for idToKind, build kindToID from that copy (using kindMap.Invert() or
re-computing), and initialize lastID to the maximum key present in the copied
idToKind (or -1 if empty) so subsequent AssertKinds will allocate new IDs above
the existing range without overwriting existing entries.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (9)
tools/dawgrun/pkg/commands/runtime.go (2)

54-61: ⚠️ Potential issue | 🟠 Major

Close traceFile when trace.Start fails.

On Line 59-Line 60, traceFile stays open if tracing fails to start, leaking the descriptor.

🐛 Proposed fix
 				traceFile, err := os.Create(traceOut)
 				if err != nil {
 					return fmt.Errorf("error creating tracefile: %w", err)
 				}
 
 				if err := trace.Start(traceFile); err != nil {
+					_ = traceFile.Close()
 					return fmt.Errorf("could not start tracing: %w", err)
 				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/commands/runtime.go` around lines 54 - 61, When creating
traceFile and calling trace.Start(traceFile) in runtime.go, ensure traceFile is
closed if trace.Start returns an error to avoid leaking the file descriptor;
update the block around traceFile and trace.Start to call traceFile.Close() (or
defer a conditional close) before returning the fmt.Errorf when trace.Start
fails, referencing the traceFile variable and the trace.Start(...) call so the
file is always closed on error.

68-82: ⚠️ Potential issue | 🟠 Major

stop leaves runtime-trace marked as running.

After stopping tracing, Line 81 sets state["run"] = true; this makes later start calls incorrectly fail as “already enabled.”

🐛 Proposed fix
 				traceFile, ok := state["tracefile"].(*os.File)
 				if !ok {
 					return fmt.Errorf("could not get open tracing file")
 				}
 
-				traceFile.Close()
+				if err := traceFile.Close(); err != nil {
+					state["run"] = false
+					state["tracefile"] = nil
+					return fmt.Errorf("could not close tracing file: %w", err)
+				}
 
-				state["run"] = true
+				state["run"] = false
 				state["tracefile"] = nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/commands/runtime.go` around lines 68 - 82, In the "stop"
case handling (the switch branch labeled case "stop") the code sets state["run"]
= true after stopping tracing which incorrectly leaves the runtime marked as
running; change that assignment to state["run"] = false and keep
state["tracefile"] = nil after calling trace.Stop() and traceFile.Close() so
subsequent calls to the start path correctly detect tracing is stopped (refer to
symbols: case "stop", trace.Stop(), traceFile.Close(), state["run"],
state["tracefile"]).
tools/dawgrun/pkg/go-repl/stdinreader.go (1)

56-87: ⚠️ Potential issue | 🔴 Critical

Synchronize shared reader state and handle stdin EOF without panic.

r.reader/r.lastTime are accessed both with and without r.lock (Line 57, Line 72, Line 77, Line 86), which introduces races. Also, panic(err) on Line 68 will crash the REPL on normal stdin closure paths.

🐛 Proposed fix
 import (
 	"bufio"
+	"errors"
+	"io"
 	"os"
 	"sync"
 	"time"
 )
@@
 func (r *_StdinReader) read() {
-	if r.reader != nil {
+	r.lock.Lock()
+	if r.reader != nil {
+		r.lock.Unlock()
 		return
 	}
 
-	r.reader = bufio.NewReader(os.Stdin)
+	reader := bufio.NewReader(os.Stdin)
+	r.reader = reader
 	r.lastTime = time.Now()
+	r.lock.Unlock()
 
-	go func() {
+	go func(reader *bufio.Reader) {
 		for {
-			b, err := r.reader.ReadByte()
+			b, err := reader.ReadByte()
 			if err != nil {
-				panic(err)
+				r.lock.Lock()
+				r.reader = nil
+				r.lock.Unlock()
+				if errors.Is(err, io.EOF) {
+					return
+				}
+				return
 			}
 
-			stopNow := false
-			if b == 13 && time.Now().After(r.lastTime.Add(MACHINE_INTERVAL)) {
-				// it is unlikely that a carriage return followed by some text is pasted into the terminal, so we can use this as a queu to quit
-				stopNow = true
-			}
-
-			r.lastTime = time.Now()
-
 			r.lock.Lock()
-
+			stopNow := b == 13 && time.Now().After(r.lastTime.Add(MACHINE_INTERVAL))
+			r.lastTime = time.Now()
 			r.buffer = append(r.buffer, b)
-
-			r.lock.Unlock()
-
 			if stopNow {
 				r.reader = nil
+				r.lock.Unlock()
 				return
 			}
+			r.lock.Unlock()
 		}
-	}()
+	}(reader)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/stdinreader.go` around lines 56 - 87, The
_StdinReader.read goroutine currently races on r.reader and r.lastTime and
panics on normal stdin EOF; fix by guarding all accesses and mutations of
r.reader and r.lastTime with r.lock (e.g., reading r.reader at start,
checking/updating r.lastTime before/after ReadByte, and setting r.reader = nil
inside the same lock) and by replacing panic(err) with proper EOF/error handling
(treat io.EOF as a normal stop: clear reader under lock and return; for other
errors log/propagate instead of panicking). Ensure the stopNow logic that checks
MACHINE_INTERVAL still reads r.lastTime under r.lock so the comparison is
race-free and the goroutine exits cleanly.
tools/dawgrun/pkg/commands/db.go (1)

26-42: ⚠️ Potential issue | 🟠 Major

Preserve full connection string input and stop printing secrets.

Line 27 only takes fields[1], which drops remaining tokens. Also, Line 38 and Line 41 expose the raw connection string (potential credentials) in output/errors.

🐛 Proposed fix
 import (
 	"fmt"
 	"strconv"
+	"strings"
@@
 			name := fields[0]
-			connStr := fields[1]
+			connStr := strings.Join(fields[1:], " ")
 			connPool, err := dawgsPg.NewPool(connStr)
 			if err != nil {
 				return fmt.Errorf("error opening connection pool: %w", err)
 			}
@@
 			})
 			if err != nil {
-				return fmt.Errorf("error opening database connection '%s': %w", connStr, err)
+				return fmt.Errorf("error opening database connection %q: %w", name, err)
 			}
 
-			fmt.Fprintf(ctx.output, "Opened connection '%s': %s\n", name, connStr)
+			fmt.Fprintf(ctx.output, "Opened connection '%s'\n", name)
 			ctx.scope.connections[name] = query
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/commands/db.go` around lines 26 - 42, The code slices the
connection string with fields[1] (losing tokens) and prints the raw connection
string in logs/errors; fix by reassembling the full connection string from
remaining tokens (e.g., connStr := strings.Join(fields[1:], " ")) before calling
dawgsPg.NewPool and dawgs.Open, and avoid emitting secrets: change
fmt.Fprintf(ctx.output, ...) to only show the connection name (or a masked
version) and update error returns from dawgs.Open/dawgsPg.NewPool to reference
the connection name (or masked value) instead of including connStr; keep storing
the query in ctx.scope.connections[name] as before.
tools/dawgrun/cmd/dawgrun/main.go (1)

61-63: ⚠️ Potential issue | 🟡 Minor

Use structured slog error fields; %w in the message is not interpolated.

At Line 62, %w is treated as literal text in slog.Error, so the log message is misleading.

🪵 Proposed fix
 if err := handler.r.Loop(); err != nil {
-		slog.Error("repl encountered error: %w", slog.String("error", err.Error()))
+		slog.Error("repl encountered error", slog.Any("error", err))
 }
In Go's standard library `log/slog`, does `Logger.Error("msg: %w", ...)` interpolate `%w`, or is the message treated as a literal string?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/cmd/dawgrun/main.go` around lines 61 - 63, The log call using
slog.Error with a format string ("repl encountered error: %w") treats the
message as literal and does not interpolate %w; update the handler.r.Loop error
logging to use structured slog fields instead—call slog.Error with a plain
message like "repl encountered error" and pass the error as a field (e.g.,
slog.String("error", err.Error()) or slog.Any("error", err)) so the log contains
the actual error; locate the call around handler.r.Loop() and replace the
current slog.Error invocation accordingly.
tools/dawgrun/pkg/go-repl/repl.go (4)

180-183: ⚠️ Potential issue | 🟡 Minor

Keep newest history entries when enforcing the limit.

This truncation keeps the oldest lines and drops the most recent ones.

🧾 Proposed fix
 // truncate history buffer before save.
 if len(r.history) > r.historyMaxLines {
-	r.history = r.history[0:r.historyMaxLines]
+	r.history = r.history[len(r.history)-r.historyMaxLines:]
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/repl.go` around lines 180 - 183, The current
truncation keeps the oldest entries and discards the newest; change the logic in
the block that trims r.history to retain the most recent r.historyMaxLines
entries instead of the first ones by reassigning r.history to the slice
containing the last r.historyMaxLines elements (i.e., start at
len(r.history)-r.historyMaxLines and take to the end), making sure to guard
against negative indices when r.historyMaxLines is larger than len(r.history).

1719-1731: ⚠️ Potential issue | 🔴 Critical

Clamp available status width before slicing.

At Line 1721, w-len(right) can be negative, causing a slice-bounds panic.

🛡️ Proposed fix
 left, right := r.statusFields()
-if len(left) > w-len(right) {
-	left = left[0 : w-len(right)]
+if len(right) > w {
+	right = right[:w]
+}
+avail := w - len(right)
+if avail < 0 {
+	avail = 0
+}
+if len(left) > avail {
+	left = left[:avail]
 }
@@
-for i := 0; i < w-len(left)-len(right); i++ {
+for i := 0; i < w-len(left)-len(right); i++ {
 	fmt.Print(" ")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/repl.go` around lines 1719 - 1731, Clamp the
available status width before slicing to avoid a negative index panic: compute
avail := w - len(right) and if avail < 0 set avail = 0, then use avail when
truncating left (in the block that calls r.statusFields() and does left =
left[0:...]) and when iterating to print padding spaces; update the uses around
r.statusFields(), left/right, highlight() and the padding loop to reference
avail instead of w-len(right).

231-251: ⚠️ Potential issue | 🟠 Major

Serialize resize handling with the input loop.

notifySizeChange() mutates REPL state and triggers rendering concurrently with dispatch(), which risks races and garbled terminal output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/repl.go` around lines 231 - 251, notifySizeChange
currently polls and directly mutates r.width/r.height and calls r.resize from a
background goroutine, causing concurrent access with dispatch(); change it to
serialize resize handling through the REPL's main input/dispatch loop instead:
keep getSize, but replace the background goroutine's direct r.resize call with
sending a resize event (width,height) into the same event/channel that
dispatch() processes (or invoke a dispatch-safe enqueue method on Repl), and let
the main loop perform r.resize and state mutation so all rendering/state updates
are single-threaded.

1557-1570: ⚠️ Potential issue | 🟠 Major

Do not drop non-ASCII input in buffer cleaning.

The byte-range filter strips Unicode characters, which corrupts user input before evaluation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/repl.go` around lines 1557 - 1570, The current
Repl.cleanAndAddToBuffer(byte[]) drops all non-ASCII bytes; change it to
preserve Unicode by decoding msg as runes (e.g., convert to string and range
over runes) so you handle UTF-8 properly, replace '\t' with a space, and keep
all runes >= 0x20 (and not DEL/control) instead of filtering by byte values;
then re-encode the cleaned runes to bytes and call r.addBytesToBuffer(filtered)
so multi-byte UTF-8 characters are not lost (update the logic inside
Repl.cleanAndAddToBuffer and continue using r.addBytesToBuffer).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/dawgrun/pkg/go-repl/examples/basic_repl.go`:
- Around line 43-44: The comment above MyHandler.Eval is stale: it states there
are two return values but Eval(buffer string) returns a single string; update
the comment to accurately describe the single return (e.g., "returns stdout
output as a string" or remove the "first/second return value" phrasing) so it
correctly documents MyHandler.Eval's signature and behavior.

In `@tools/dawgrun/pkg/go-repl/examples/shell_wrapper.go`:
- Around line 75-77: The call to repl.NewRepl in ShellWrapper (h.r =
repl.NewRepl(h)) passes only the Handler argument but NewRepl requires two
parameters (handler Handler, opts *Options); update the invocation to provide
the second argument (either nil if defaults are acceptable or an Options value
such as &repl.Options{...}) so change the call to repl.NewRepl(h, nil) or
repl.NewRepl(h, &repl.Options{...}) and ensure the Options type from the repl
package is used.

In `@tools/dawgrun/pkg/go-repl/repl.go`:
- Around line 171-173: The saveHistory function can panic if r.historyFile is
nil; update saveHistory (the method on Repl that calls r.historyFile.Truncate
and subsequent operations) to early-return (no-op) when r.historyFile == nil so
it doesn't attempt to Truncate or write; locate the references to
r.historyFile.Truncate(...) and the following write/seek logic in saveHistory
and wrap them in a nil-check (or return nil) to safely handle the case where no
history file is configured.

---

Duplicate comments:
In `@tools/dawgrun/cmd/dawgrun/main.go`:
- Around line 61-63: The log call using slog.Error with a format string ("repl
encountered error: %w") treats the message as literal and does not interpolate
%w; update the handler.r.Loop error logging to use structured slog fields
instead—call slog.Error with a plain message like "repl encountered error" and
pass the error as a field (e.g., slog.String("error", err.Error()) or
slog.Any("error", err)) so the log contains the actual error; locate the call
around handler.r.Loop() and replace the current slog.Error invocation
accordingly.

In `@tools/dawgrun/pkg/commands/db.go`:
- Around line 26-42: The code slices the connection string with fields[1]
(losing tokens) and prints the raw connection string in logs/errors; fix by
reassembling the full connection string from remaining tokens (e.g., connStr :=
strings.Join(fields[1:], " ")) before calling dawgsPg.NewPool and dawgs.Open,
and avoid emitting secrets: change fmt.Fprintf(ctx.output, ...) to only show the
connection name (or a masked version) and update error returns from
dawgs.Open/dawgsPg.NewPool to reference the connection name (or masked value)
instead of including connStr; keep storing the query in
ctx.scope.connections[name] as before.

In `@tools/dawgrun/pkg/commands/runtime.go`:
- Around line 54-61: When creating traceFile and calling trace.Start(traceFile)
in runtime.go, ensure traceFile is closed if trace.Start returns an error to
avoid leaking the file descriptor; update the block around traceFile and
trace.Start to call traceFile.Close() (or defer a conditional close) before
returning the fmt.Errorf when trace.Start fails, referencing the traceFile
variable and the trace.Start(...) call so the file is always closed on error.
- Around line 68-82: In the "stop" case handling (the switch branch labeled case
"stop") the code sets state["run"] = true after stopping tracing which
incorrectly leaves the runtime marked as running; change that assignment to
state["run"] = false and keep state["tracefile"] = nil after calling
trace.Stop() and traceFile.Close() so subsequent calls to the start path
correctly detect tracing is stopped (refer to symbols: case "stop",
trace.Stop(), traceFile.Close(), state["run"], state["tracefile"]).

In `@tools/dawgrun/pkg/go-repl/repl.go`:
- Around line 180-183: The current truncation keeps the oldest entries and
discards the newest; change the logic in the block that trims r.history to
retain the most recent r.historyMaxLines entries instead of the first ones by
reassigning r.history to the slice containing the last r.historyMaxLines
elements (i.e., start at len(r.history)-r.historyMaxLines and take to the end),
making sure to guard against negative indices when r.historyMaxLines is larger
than len(r.history).
- Around line 1719-1731: Clamp the available status width before slicing to
avoid a negative index panic: compute avail := w - len(right) and if avail < 0
set avail = 0, then use avail when truncating left (in the block that calls
r.statusFields() and does left = left[0:...]) and when iterating to print
padding spaces; update the uses around r.statusFields(), left/right, highlight()
and the padding loop to reference avail instead of w-len(right).
- Around line 231-251: notifySizeChange currently polls and directly mutates
r.width/r.height and calls r.resize from a background goroutine, causing
concurrent access with dispatch(); change it to serialize resize handling
through the REPL's main input/dispatch loop instead: keep getSize, but replace
the background goroutine's direct r.resize call with sending a resize event
(width,height) into the same event/channel that dispatch() processes (or invoke
a dispatch-safe enqueue method on Repl), and let the main loop perform r.resize
and state mutation so all rendering/state updates are single-threaded.
- Around line 1557-1570: The current Repl.cleanAndAddToBuffer(byte[]) drops all
non-ASCII bytes; change it to preserve Unicode by decoding msg as runes (e.g.,
convert to string and range over runes) so you handle UTF-8 properly, replace
'\t' with a space, and keep all runes >= 0x20 (and not DEL/control) instead of
filtering by byte values; then re-encode the cleaned runes to bytes and call
r.addBytesToBuffer(filtered) so multi-byte UTF-8 characters are not lost (update
the logic inside Repl.cleanAndAddToBuffer and continue using
r.addBytesToBuffer).

In `@tools/dawgrun/pkg/go-repl/stdinreader.go`:
- Around line 56-87: The _StdinReader.read goroutine currently races on r.reader
and r.lastTime and panics on normal stdin EOF; fix by guarding all accesses and
mutations of r.reader and r.lastTime with r.lock (e.g., reading r.reader at
start, checking/updating r.lastTime before/after ReadByte, and setting r.reader
= nil inside the same lock) and by replacing panic(err) with proper EOF/error
handling (treat io.EOF as a normal stop: clear reader under lock and return; for
other errors log/propagate instead of panicking). Ensure the stopNow logic that
checks MACHINE_INTERVAL still reads r.lastTime under r.lock so the comparison is
race-free and the goroutine exits cleanly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 56c92ae0-889b-4f39-ab7f-63a27e8afa65

📥 Commits

Reviewing files that changed from the base of the PR and between 07cee93 and b6b7251.

⛔ Files ignored due to path filters (4)
  • go.sum is excluded by !**/*.sum
  • go.work is excluded by !**/*.work
  • go.work.sum is excluded by !**/*.sum
  • tools/dawgrun/go.sum is excluded by !**/*.sum
📒 Files selected for processing (29)
  • go.mod
  • tools/dawgrun/.gitignore
  • tools/dawgrun/README.md
  • tools/dawgrun/cmd/dawgrun/art.txt
  • tools/dawgrun/cmd/dawgrun/banner.txt
  • tools/dawgrun/cmd/dawgrun/main.go
  • tools/dawgrun/go.mod
  • tools/dawgrun/justfile
  • tools/dawgrun/pkg/commands/completion.go
  • tools/dawgrun/pkg/commands/cypher.go
  • tools/dawgrun/pkg/commands/db.go
  • tools/dawgrun/pkg/commands/help.go
  • tools/dawgrun/pkg/commands/helpers.go
  • tools/dawgrun/pkg/commands/registry.go
  • tools/dawgrun/pkg/commands/runtime.go
  • tools/dawgrun/pkg/commands/types.go
  • tools/dawgrun/pkg/go-repl/.gitignore
  • tools/dawgrun/pkg/go-repl/LICENSE
  • tools/dawgrun/pkg/go-repl/Makefile
  • tools/dawgrun/pkg/go-repl/README.md
  • tools/dawgrun/pkg/go-repl/ansi.go
  • tools/dawgrun/pkg/go-repl/examples/basic_repl.go
  • tools/dawgrun/pkg/go-repl/examples/shell_wrapper.go
  • tools/dawgrun/pkg/go-repl/go.mod
  • tools/dawgrun/pkg/go-repl/handler.go
  • tools/dawgrun/pkg/go-repl/repl.go
  • tools/dawgrun/pkg/go-repl/repl_popover_test.go
  • tools/dawgrun/pkg/go-repl/stdinreader.go
  • tools/dawgrun/pkg/stubs/kindmapper.go
✅ Files skipped from review due to trivial changes (12)
  • tools/dawgrun/cmd/dawgrun/art.txt
  • tools/dawgrun/.gitignore
  • tools/dawgrun/cmd/dawgrun/banner.txt
  • tools/dawgrun/pkg/go-repl/LICENSE
  • tools/dawgrun/pkg/commands/completion.go
  • tools/dawgrun/pkg/go-repl/go.mod
  • tools/dawgrun/go.mod
  • tools/dawgrun/pkg/go-repl/README.md
  • tools/dawgrun/pkg/commands/helpers.go
  • tools/dawgrun/pkg/go-repl/Makefile
  • tools/dawgrun/pkg/commands/types.go
  • tools/dawgrun/pkg/stubs/kindmapper.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • tools/dawgrun/pkg/go-repl/.gitignore
  • tools/dawgrun/pkg/go-repl/handler.go
  • tools/dawgrun/README.md
  • tools/dawgrun/pkg/commands/help.go
  • tools/dawgrun/pkg/commands/cypher.go

Comment thread tools/dawgrun/pkg/go-repl/examples/basic_repl.go Outdated
Comment thread tools/dawgrun/pkg/go-repl/examples/shell_wrapper.go Outdated
Comment thread tools/dawgrun/pkg/go-repl/repl.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
tools/dawgrun/pkg/go-repl/stdinreader.go (1)

37-40: ⚡ Quick win

Use a ticker instead of allocating a timer every millisecond.

time.After creates a new timer on every pass through this always-on 1ms loop. A time.Ticker gives the same cadence without the continuous timer churn.

♻️ Suggested change
 func (r *_StdinReader) start() {
 	go func() {
+		ticker := time.NewTicker(MACHINE_INTERVAL)
+		defer ticker.Stop()
+
 		for {
-			<-time.After(MACHINE_INTERVAL)
+			<-ticker.C
 
 			r.lock.Lock()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/stdinreader.go` around lines 37 - 40, Replace the
per-iteration time.After call with a single time.Ticker created outside the for
loop: create ticker := time.NewTicker(MACHINE_INTERVAL) before the for { ... }
loop, use <-ticker.C inside the loop instead of <-time.After(...), and ensure
you call ticker.Stop() (e.g., via defer) to avoid leaking the ticker; update the
anonymous goroutine in stdinreader.go accordingly.
tools/dawgrun/pkg/go-repl/repl.go (3)

1366-1377: 💤 Low value

Consider logging saveHistory() errors on quit.

Line 1368 calls saveHistory() but discards any error. If history fails to save (disk full, permissions, etc.), the user receives no feedback.

💡 Optional improvement
 func (r *Repl) quit() {
 	r.clearAfterPrompt()
-	r.saveHistory()
+	if err := r.saveHistory(); err != nil {
+		fmt.Fprintf(os.Stderr, "\r\nWarning: could not save history: %v\r\n", err)
+	}
 
 	fmt.Print("\n\r")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/repl.go` around lines 1366 - 1377, Repl.quit
currently ignores errors from saveHistory(); change it to capture the returned
error (err := r.saveHistory()) and, if non-nil, log it to stderr or the existing
logger (e.g., fmt.Fprintf(os.Stderr, "failed to save history: %v\n", err) or
r.logger.Printf(...)) before proceeding to UnmakeRaw and os.Exit(0); update the
function Repl.quit to check and report saveHistory() errors so users see
failures (disk/permission) when quitting.

1827-1836: 💤 Low value

Unreachable return statement after infinite loop.

Line 1835 (return nil) is dead code since the for loop on line 1827 has no break path—it only exits via quit() calling os.Exit(0).

Consider either removing the unreachable return or restructuring to allow graceful shutdown without os.Exit().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/repl.go` around lines 1827 - 1836, The infinite
for-loop that calls r.reader.read() and r.dispatch(bts) makes the final "return
nil" unreachable; either remove that dead return or restructure the loop to
support graceful shutdown without os.Exit. Modify the loop in repl.go (the block
using r.reader.read(), r.reader.bytes, and r.dispatch) to select on the bytes
channel plus a quit/ctx channel (or expose r.quit used by quit()/os.Exit) so you
can break and return nil cleanly, or simply delete the unreachable "return nil"
if you intend process termination via os.Exit().

246-252: 💤 Low value

Consider graceful handling when stdin is not a TTY.

term.GetSize(0) will return an error if stdin is not a terminal (e.g., piped input, CI environments). The current panic (line 248) provides no recovery path.

While a REPL inherently requires a TTY, returning a sensible default or error message would improve robustness.

🛡️ Suggested improvement
 	getSize := func() (int, int) {
 		w, h, err := term.GetSize(0)
 		if err != nil {
-			panic(err)
+			// Fallback to common terminal size if not a TTY
+			return 80, 24
 		}
 
 		return w, h
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/repl.go` around lines 246 - 252, term.GetSize(0)
can fail when stdin is not a TTY; instead of panicking, handle the error
gracefully: in the block that calls term.GetSize(0) (currently panicking on err)
either return sensible default dimensions (e.g., 80x24) and emit a non-fatal
warning (using the existing logger or fmt.Fprintf(os.Stderr, ...)), or change
the function signature to return an error and propagate it upstream; update
callers accordingly. Ensure you reference term.GetSize(0) and replace the
panic(err) with the chosen graceful fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/dawgrun/pkg/commands/db.go`:
- Around line 36-53: The current flow closes existingConn from
ctx.scope.connections[name] before calling dawgs.Open, which can leave a closed
handle and stale connKindMaps entry if the new open fails; change the logic to
call dawgs.Open first and only if it returns no error then warn/close the
existingConn and replace it via ctx.scope.AddConnection(name, querier), also
ensure any connKindMaps[name] update/migration happens atomically with the
replacement so on failure the original connection and connKindMaps remain intact
(use local variables querier and err to stage the new connection before mutating
ctx.scope or connKindMaps).

In `@tools/dawgrun/pkg/go-repl/repl.go`:
- Around line 1849-1853: Repl.UnmakeRaw currently calls r.onEnd() unguarded
which can panic if r.onEnd is nil; modify UnmakeRaw so it first checks if
r.onEnd != nil before invoking it and then set r.onEnd = nil (ensuring
idempotence if called multiple times). Locate the UnmakeRaw method on the Repl
type and add a nil-check around the call to r.onEnd to safely handle cases where
MakeRaw was never called, UnmakeRaw is called twice, or quit() already cleared
it.
- Around line 1707-1714: Replace the panic in the status-rendering branch that
checks tot and cur (the block using variables tot and cur) with a graceful
fallback: instead of panic("unexpected") return a user-friendly status like
fmt.Sprintf("%d matches", tot) and optionally emit a non-fatal debug/warning log
(e.g., via log.Printf or the package logger) indicating the unexpected state;
this avoids crashing the REPL if tot > 0 but cur == -1 while preserving visible
status output.

---

Nitpick comments:
In `@tools/dawgrun/pkg/go-repl/repl.go`:
- Around line 1366-1377: Repl.quit currently ignores errors from saveHistory();
change it to capture the returned error (err := r.saveHistory()) and, if
non-nil, log it to stderr or the existing logger (e.g., fmt.Fprintf(os.Stderr,
"failed to save history: %v\n", err) or r.logger.Printf(...)) before proceeding
to UnmakeRaw and os.Exit(0); update the function Repl.quit to check and report
saveHistory() errors so users see failures (disk/permission) when quitting.
- Around line 1827-1836: The infinite for-loop that calls r.reader.read() and
r.dispatch(bts) makes the final "return nil" unreachable; either remove that
dead return or restructure the loop to support graceful shutdown without
os.Exit. Modify the loop in repl.go (the block using r.reader.read(),
r.reader.bytes, and r.dispatch) to select on the bytes channel plus a quit/ctx
channel (or expose r.quit used by quit()/os.Exit) so you can break and return
nil cleanly, or simply delete the unreachable "return nil" if you intend process
termination via os.Exit().
- Around line 246-252: term.GetSize(0) can fail when stdin is not a TTY; instead
of panicking, handle the error gracefully: in the block that calls
term.GetSize(0) (currently panicking on err) either return sensible default
dimensions (e.g., 80x24) and emit a non-fatal warning (using the existing logger
or fmt.Fprintf(os.Stderr, ...)), or change the function signature to return an
error and propagate it upstream; update callers accordingly. Ensure you
reference term.GetSize(0) and replace the panic(err) with the chosen graceful
fallback.

In `@tools/dawgrun/pkg/go-repl/stdinreader.go`:
- Around line 37-40: Replace the per-iteration time.After call with a single
time.Ticker created outside the for loop: create ticker :=
time.NewTicker(MACHINE_INTERVAL) before the for { ... } loop, use <-ticker.C
inside the loop instead of <-time.After(...), and ensure you call ticker.Stop()
(e.g., via defer) to avoid leaking the ticker; update the anonymous goroutine in
stdinreader.go accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3a5d4d0f-fc42-412f-86f9-582ec24e6cd9

📥 Commits

Reviewing files that changed from the base of the PR and between b6b7251 and 40be087.

📒 Files selected for processing (12)
  • tools/dawgrun/cmd/dawgrun/main.go
  • tools/dawgrun/docs/RFC.md
  • tools/dawgrun/go.mod
  • tools/dawgrun/pkg/commands/cypher.go
  • tools/dawgrun/pkg/commands/db.go
  • tools/dawgrun/pkg/commands/helpers.go
  • tools/dawgrun/pkg/commands/registry.go
  • tools/dawgrun/pkg/commands/runtime.go
  • tools/dawgrun/pkg/commands/types.go
  • tools/dawgrun/pkg/go-repl/justfile
  • tools/dawgrun/pkg/go-repl/repl.go
  • tools/dawgrun/pkg/go-repl/stdinreader.go
✅ Files skipped from review due to trivial changes (3)
  • tools/dawgrun/pkg/go-repl/justfile
  • tools/dawgrun/pkg/commands/runtime.go
  • tools/dawgrun/pkg/commands/helpers.go

Comment on lines +36 to +53
if existingConn, ok := ctx.scope.connections[name]; ok {
// Warn+close existing connection before overwriting it
ctx.output.Warnf("Discarding previous connection for '%s'", name)
if err := existingConn.Close(ctx); err != nil {
return fmt.Errorf("could not close previous connection '%s' for overwriting: %w", name, err)
}
}

querier, err := dawgs.Open(ctx, "pg", dawgs.Config{
ConnectionString: connStr,
Pool: connPool,
})
if err != nil {
return fmt.Errorf("error opening database connection '%s': %w", connStr, err)
}

fmt.Fprintf(ctx.output, "Opened connection '%s'\n", name)
ctx.scope.AddConnection(name, querier)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t tear down the current connection until the replacement is ready.

This overwrite path closes the existing connection before dawgs.Open succeeds. If the new open fails, ctx.scope.connections[name] still points at a closed handle, and the old connKindMaps[name] entry is left behind for later lookup commands.

🧩 Suggested direction
-			if existingConn, ok := ctx.scope.connections[name]; ok {
-				// Warn+close existing connection before overwriting it
-				ctx.output.Warnf("Discarding previous connection for '%s'", name)
-				if err := existingConn.Close(ctx); err != nil {
-					return fmt.Errorf("could not close previous connection '%s' for overwriting: %w", name, err)
-				}
-			}
+			var existingConn graph.Database
+			if current, ok := ctx.scope.connections[name]; ok {
+				existingConn = current
+			}
@@
 			querier, err := dawgs.Open(ctx, "pg", dawgs.Config{
 				ConnectionString: connStr,
 				Pool:             connPool,
 			})
 			if err != nil {
 				return fmt.Errorf("error opening database connection '%s': %w", connStr, err)
 			}
+
+			if existingConn != nil {
+				ctx.output.Warnf("Discarding previous connection for '%s'", name)
+				if err := existingConn.Close(ctx); err != nil {
+					_ = querier.Close(ctx)
+					return fmt.Errorf("could not close previous connection '%s' for overwriting: %w", name, err)
+				}
+			}
+
+			delete(ctx.scope.connKindMaps, name)
 
 			fmt.Fprintf(ctx.output, "Opened connection '%s'\n", name)
 			ctx.scope.AddConnection(name, querier)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/commands/db.go` around lines 36 - 53, The current flow
closes existingConn from ctx.scope.connections[name] before calling dawgs.Open,
which can leave a closed handle and stale connKindMaps entry if the new open
fails; change the logic to call dawgs.Open first and only if it returns no error
then warn/close the existingConn and replace it via
ctx.scope.AddConnection(name, querier), also ensure any connKindMaps[name]
update/migration happens atomically with the replacement so on failure the
original connection and connKindMaps remain intact (use local variables querier
and err to stage the new connection before mutating ctx.scope or connKindMaps).

Comment on lines +1707 to +1714
if tot == 0 {
return "No matches"
} else if cur != -1 {
return fmt.Sprintf("%d/%d matches", cur+1, tot)
} else {
panic("unexpected")
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace panic with graceful fallback for edge cases.

panic("unexpected") on line 1712 will crash the REPL if tot > 0 but cur == -1. While current control flow should prevent this, the condition could be reached through:

  • Race conditions (resize goroutine vs main loop)
  • Future code modifications breaking the invariant

A REPL should not panic on rendering the status bar.

🛡️ Proposed fix
 	if tot == 0 {
 		return "No matches"
 	} else if cur != -1 {
 		return fmt.Sprintf("%d/%d matches", cur+1, tot)
 	} else {
-		panic("unexpected")
+		return fmt.Sprintf("%d matches", tot)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/repl.go` around lines 1707 - 1714, Replace the
panic in the status-rendering branch that checks tot and cur (the block using
variables tot and cur) with a graceful fallback: instead of panic("unexpected")
return a user-friendly status like fmt.Sprintf("%d matches", tot) and optionally
emit a non-fatal debug/warning log (e.g., via log.Printf or the package logger)
indicating the unexpected state; this avoids crashing the REPL if tot > 0 but
cur == -1 while preserving visible status output.

Comment on lines +1849 to +1853
func (r *Repl) UnmakeRaw() {
r.onEnd()

r.onEnd = nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against nil onEnd to prevent panic.

UnmakeRaw() will panic on line 1850 if r.onEnd is nil. This can happen if:

  • Called before MakeRaw() is invoked
  • Called twice in succession
  • Called after quit() (which calls UnmakeRaw() via r.onEnd)
🛡️ Proposed fix
 func (r *Repl) UnmakeRaw() {
+	if r.onEnd == nil {
+		return
+	}
+
 	r.onEnd()
 
 	r.onEnd = nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (r *Repl) UnmakeRaw() {
r.onEnd()
r.onEnd = nil
}
func (r *Repl) UnmakeRaw() {
if r.onEnd == nil {
return
}
r.onEnd()
r.onEnd = nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/go-repl/repl.go` around lines 1849 - 1853, Repl.UnmakeRaw
currently calls r.onEnd() unguarded which can panic if r.onEnd is nil; modify
UnmakeRaw so it first checks if r.onEnd != nil before invoking it and then set
r.onEnd = nil (ensuring idempotence if called multiple times). Locate the
UnmakeRaw method on the Repl type and add a nil-check around the call to r.onEnd
to safely handle cases where MakeRaw was never called, UnmakeRaw is called
twice, or quit() already cleared it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants