Skip to content

feat: adding dawgrun#24

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

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

Conversation

@seanjSO
Copy link
Copy Markdown
Contributor

@seanjSO seanjSO commented Jan 28, 2026

Summary by CodeRabbit

  • New Features
    • New interactive REPL tool for working with DAWGS-compatible Postgres graph databases
    • Parse, translate, and execute Cypher queries against live database backends
    • Query results displayed in table or JSON format
    • Database connection and kind mapping management
    • Persistent command history and configurable syntax highlighting

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 28, 2026

Walkthrough

This pull request introduces dawgrun, a new interactive REPL tool for working with DAWGS-compatible Postgres graphs. The implementation includes a complete REPL library (go-repl), command infrastructure for executing Cypher queries and managing database connections, and comprehensive build/documentation files.

Changes

Cohort / File(s) Summary
REPL Library Core
tools/dawgrun/pkg/go-repl/repl.go, ansi.go, stdinreader.go, handler.go
Terminal REPL implementation with history persistence, cursor management, status widgets, and tab completion; ANSI escape sequence utilities; stdin byte buffering; and handler interface definitions.
REPL Examples & Tests
tools/dawgrun/pkg/go-repl/examples/basic_repl.go, shell_wrapper.go, repl_popover_test.go
Example REPL applications and test suite for completion popover modes, cursor queries, and line truncation.
REPL Library Configuration
tools/dawgrun/pkg/go-repl/go.mod, Makefile, LICENSE, README.md, .gitignore
Module manifest, build recipes, licensing, documentation, and git ignore rules for the REPL library.
Command Infrastructure
tools/dawgrun/pkg/commands/types.go, registry.go, completion.go, helpers.go
Core types (CommandContext, CommandDesc, CommandOutput, Scope), command registry, command name completion, and utility functions for query parsing, line indentation, and syntax highlighting.
Database & Query Commands
tools/dawgrun/pkg/commands/db.go, cypher.go
Database connection management, kind-map loading/lookup, and Cypher query operations (parse, translate, explain, execute with table/JSON formatting).
REPL Command Handlers
tools/dawgrun/pkg/commands/help.go, runtime.go
Help command with aligned overview and detailed help per command; quit and runtime tracing commands.
Stub Implementations
tools/dawgrun/pkg/stubs/kindmapper.go
In-memory kind-to-ID mapper satisfying pgsql.KindMapper interface.
REPL Entrypoint
tools/dawgrun/cmd/dawgrun/main.go
Main REPL entry point with ASCII art banner, history persistence, status widget, prompt rendering, and command completion/dispatch logic.
Build & Configuration
tools/dawgrun/justfile, go.mod, .gitignore, cmd/dawgrun/art.txt, cmd/dawgrun/banner.txt, README.md, ../../../go.mod
Build automation (vet, build variants with/without custom DAWGS, debug), module manifest, git ignore rules, ASCII assets, comprehensive documentation, and root module tool/replace directives.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant REPL
    participant Handler
    participant CommandRegistry
    participant Command
    participant Output

    User->>REPL: Start dawgrun
    REPL->>Handler: Render prompt + status widget
    REPL->>User: Display banner and prompt

    loop User Input Loop
        User->>REPL: Enter command (e.g., "cypher parse ...")
        REPL->>Handler: Extract command prefix for completion
        Handler->>CommandRegistry: SortedCommandNames()
        CommandRegistry-->>Handler: Matching command names
        REPL->>User: Show completion candidates

        User->>REPL: Confirm command input
        REPL->>Handler: Parse and lowercase command token
        Handler->>CommandRegistry: Look up command descriptor
        CommandRegistry-->>Handler: CommandDesc with function
        REPL->>Command: Execute with CommandContext
        
        Command->>Output: Write formatted results
        Output->>User: Display command output
    end

    User->>REPL: "quit" command
    REPL->>Command: Execute quit handler
    Command->>REPL: Quit()
    REPL->>User: Exit REPL
Loading
sequenceDiagram
    participant User
    participant REPL as REPL:<br/>cypher cmd
    participant Parser as Cypher<br/>Parser
    participant Translator as SQL<br/>Translator
    participant KindMapper
    participant Database
    participant Formatter as Result<br/>Formatter

    User->>REPL: "cypher execute query ..."
    REPL->>Parser: Parse Cypher into AST
    Parser-->>REPL: RegularQuery AST

    alt With Connection & Kind Mapping
        REPL->>KindMapper: Load kind mappings
        KindMapper->>Database: Fetch kinds from active connection
        Database-->>KindMapper: Kind list
        KindMapper-->>REPL: ID↔Kind mappings
    end

    REPL->>Translator: Translate Cypher to SQL (with kind IDs)
    Translator-->>REPL: PostgreSQL SQL

    REPL->>Database: Execute SQL query
    Database-->>REPL: Result rows

    REPL->>Formatter: Format rows as table/JSON
    Formatter-->>User: Display formatted results
Loading
sequenceDiagram
    participant User
    participant REPL as REPL:<br/>db cmd
    participant DB as DB<br/>Commands
    participant DAWGS as DAWGS<br/>Postgres Pool
    participant Backend as Postgres<br/>Backend
    participant Scope as Command<br/>Scope

    User->>REPL: "db open postgres://..."
    REPL->>DB: openPGDBCmd()
    DB->>DAWGS: Create Postgres pool with connection string
    DAWGS->>Backend: Open connection
    Backend-->>DAWGS: Connection established
    DAWGS-->>DB: Graph connection
    DB->>Scope: Register connection in scope state
    DB-->>User: Confirmation message

    User->>REPL: "db kinds <connection_name>"
    REPL->>DB: getPGDBKinds()
    DB->>DB: loadKindMap(connection_name)
    DB->>Backend: Fetch current kind list
    Backend-->>DB: Kind list with IDs
    DB->>Scope: Store kind map (ID↔Kind)
    DB-->>User: Display kind mappings

    User->>REPL: "db lookup-kind <kind_name>"
    REPL->>DB: lookupKindCmd()
    DB->>Scope: Retrieve cached kind map
    Scope-->>DB: ID↔Kind mappings
    DB->>DB: Map kind_name → ID
    DB-->>User: Display ID for kind
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A REPL hops into the warren so bright,
With commands for Cypher and queries in flight,
History persists through the terminal night,
Kind mappers and traces make everything right,
Go-repl's foundation shines clear and delight!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request has no description provided by the author, failing to include required sections such as motivation, type of change, testing details, and other template elements. Add a comprehensive description following the template: explain the dawgrun feature's purpose and motivation, select the appropriate type of change (New feature / enhancement), document testing performed, and confirm the checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.29% 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' clearly identifies the main change as the addition of a new tool/feature called dawgrun, which is directly supported by the comprehensive set of new files introducing the complete dawgrun REPL tool.
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

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

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

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.

Comment on lines +61 to +63
if err := handler.r.Loop(); err != nil {
slog.Error("repl encountered error: %w", slog.String("error", err.Error()))
}
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

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and check the actual code
cat -n tools/dawgrun/cmd/dawgrun/main.go | sed -n '55,70p'

Repository: SpecterOps/DAWGS

Length of output: 510


🌐 Web query:

Go slog.Error API formatting message string %w format specifier

💡 Result:

In Go's log/slog package, there is no slog.Error type or direct API for formatting error message strings using the %w format specifier. The Logger.Error method (and others like Info, Warn) takes a message string followed by varargs (args ...any), which are key-value pairs processed into slog.Attr. Errors passed as args (e.g., slog.Any("err", err)) are specially handled: if an Attr's Value is of type error, it is formatted as a string by calling err.Error, losing any wrapped error details from %w (fmt.Errorf "msg: %w", cause). slog does not use fmt.Sprintf or %w verb for processing args; instead, it pairs them as keys/values without fmt-style formatting. To preserve error wrapping: - Use slog.Any("err", err) and implement slog.LogValuer on custom errors to output structured details (msg, cause, stack). - Use HandlerOptions.ReplaceAttr to detect error Attrs (KindAny with error value) and replace with slog.GroupValue containing err.Error and errors.Unwrap(err), or stack traces. Examples from official docs and community: logger.Error("failed", slog.Any("err", err)) // Outputs err.Error string only. For richer output, custom ReplaceAttr: func replaceAttr(groups []string, a slog.Attr) slog.Attr { if v, ok := a.Value.Any.(error); ok { return slog.Group("err", slog.String("msg", v.Error), slog.Any("cause", errors.Unwrap(v))) } return a } handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: replaceAttr}) This synthesizes stdlib error wrapping (%w) with slog structured logging. No built-in %w support in slog message formatting.

Citations:


Fix the structured log message.

slog.Error does not apply fmt formatting to the message string, so %w is logged literally. Pass the error as an attribute instead.

🪵 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))
 	}
📝 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
if err := handler.r.Loop(); err != nil {
slog.Error("repl encountered error: %w", slog.String("error", err.Error()))
}
if err := handler.r.Loop(); err != nil {
slog.Error("repl encountered error", slog.Any("error", err))
}
🤖 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 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.

Comment thread tools/dawgrun/go.mod
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgtype v1.14.4 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect
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

🧩 Analysis chain

🌐 Web query:

github.com/jackc/pgx/v5 GHSA-j88v-2chj-qfwx first patched version

💡 Result:

The first patched version of github.com/jackc/pgx/v5 for GHSA-j88v-2chj-qfwx is v5.9.2.

Citations:


🏁 Script executed:

#!/bin/bash
# Confirm the pgx version actually resolved and identify the first patched release.
gh api graphql -f query='
{
  securityVulnerabilities(first: 10, ecosystem: GO, package: "github.com/jackc/pgx/v5") {
    nodes {
      advisory { ghsaId summary severity publishedAt }
      vulnerableVersionRange
      firstPatchedVersion { identifier }
    }
  }
}'

Repository: SpecterOps/DAWGS

Length of output: 1158


Critical: github.com/jackc/pgx/v5 v5.8.0 has a critical memory-safety vulnerability requiring immediate patching.

pgx/v5 v5.8.0 is vulnerable to GHSA-9jj7-4m8r-rfcm (CRITICAL memory-safety issue, first patched in v5.9.0) and GHSA-j88v-2chj-qfwx (LOW-severity SQL injection via placeholder confusion, first patched in v5.9.2). Since dawgrun executes arbitrary Cypher translated to parameterized PostgreSQL statements, both advisories are relevant. Although pgx/v5 is declared as indirect (transitively via github.com/specterops/dawgs), the dependency is pinned here and can be bumped independently.

Bump to pgx/v5 v5.9.2 to resolve both vulnerabilities. Coordinate with the dawgs repository if its dependency graph requires the older version.

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

In `@tools/dawgrun/go.mod` at 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.

Comment on lines +187 to +190
var value string
for result.Next() {
graph.ScanNextResult(result, &value)
fmt.Fprintf(ctx.output, " %s\n", value)
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm the signature and current call sites for graph.ScanNextResult.
rg -nP -C3 '\bfunc\s+ScanNextResult\b|\bScanNextResult\s*\('

Repository: SpecterOps/DAWGS

Length of output: 1858


Handle scan failures while reading EXPLAIN rows.

Line 189 discards the error from graph.ScanNextResult, allowing malformed or unexpected result rows to be silently rendered as stale or empty output.

Proposed fix
 				var value string
 				for result.Next() {
-					graph.ScanNextResult(result, &value)
+					if err := graph.ScanNextResult(result, &value); err != nil {
+						return fmt.Errorf("could not scan EXPLAIN row: %w", err)
+					}
 					fmt.Fprintf(ctx.output, "  %s\n", value)
 				}
📝 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
var value string
for result.Next() {
graph.ScanNextResult(result, &value)
fmt.Fprintf(ctx.output, " %s\n", value)
var value string
for result.Next() {
if err := graph.ScanNextResult(result, &value); err != nil {
return fmt.Errorf("could not scan EXPLAIN row: %w", err)
}
fmt.Fprintf(ctx.output, " %s\n", value)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/dawgrun/pkg/commands/cypher.go` around lines 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.

Comment on lines +252 to +315
var (
outputColumns []string
outputTable table.Writer
jsonRows []map[string]any
)

if outputFormat == queryCypherOutputFormatTable {
outputTable = table.NewWriter()
style := table.StyleRounded
style.Options.SeparateRows = true
style.Size.WidthMax = cypherResultTableWidth()
outputTable.SetStyle(style)
}

rowCount := 0
for result.Next() {
values := result.Values()

if rowCount == 0 {
outputColumns = buildCypherResultColumns(result.Keys(), len(values))

if outputFormat == queryCypherOutputFormatTable {
outputTable.AppendHeader(buildCypherResultHeader(outputColumns))
outputTable.SetColumnConfigs(buildCypherResultColumnConfigs(len(outputColumns), cypherResultTableWidth()))
}
}

switch outputFormat {
case queryCypherOutputFormatTable:
outputTable.AppendRow(buildCypherResultRow(values))

case queryCypherOutputFormatJSON:
jsonRows = append(jsonRows, buildCypherResultJSONRow(outputColumns, values))
}

rowCount++
}

if err := result.Error(); err != nil {
return fmt.Errorf("error fetching query rows: %w", err)
}

if rowCount == 0 {
if outputFormat == queryCypherOutputFormatJSON {
fmt.Fprint(ctx.output, "[]\n")
return nil
}

fmt.Fprint(ctx.output, "(0 rows)\n")
return nil
}

switch outputFormat {
case queryCypherOutputFormatTable:
fmt.Fprint(ctx.output, outputTable.Render())
fmt.Fprintf(ctx.output, "\n(%d rows)\n", rowCount)

case queryCypherOutputFormatJSON:
formatted, err := json.MarshalIndent(jsonRows, "", " ")
if err != nil {
return fmt.Errorf("could not format query results as json: %w", err)
}

fmt.Fprintf(ctx.output, "%s\n", formatted)
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

Avoid buffering the full JSON result set.

jsonRows grows without bound for --format json; a large query can exhaust memory before anything is written. Stream the JSON array row-by-row instead.

♻️ Proposed streaming direction
-					jsonRows      []map[string]any
+					jsonRows      []map[string]any // remove this by streaming JSON rows directly

A robust fix would write [ before iteration for JSON mode, marshal each row as it is fetched, emit commas between rows, and close with ] after iteration.

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

In `@tools/dawgrun/pkg/commands/cypher.go` around lines 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.

Comment on lines +231 to +251
func (r *Repl) notifySizeChange() {
getSize := func() (int, int) {
w, h, err := term.GetSize(0)
if err != nil {
panic(err)
}

return w, h
}

r.width, r.height = getSize()

go func() {
for {
<-time.After(SIZE_POLLING_INTERVAL)

newW, newH := getSize()

r.resize(newW, newH)
}
}()
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Locate resize/render paths reached from the polling goroutine and main dispatch path.
rg -nP -C3 '\bnotifySizeChange\b|\bresize\s*\(|\bdispatch\s*\(|\bforce\s*\(|\bwriteStatus\s*\('

Repository: SpecterOps/DAWGS

Length of output: 12801


🏁 Script executed:

#!/bin/bash
# Check the Repl struct definition for mutex fields
grep -n "type Repl struct" tools/dawgrun/pkg/go-repl/repl.go
# Then read the struct definition
sed -n '/^type Repl struct/,/^}/p' tools/dawgrun/pkg/go-repl/repl.go | head -60

Repository: SpecterOps/DAWGS

Length of output: 895


🏁 Script executed:

#!/bin/bash
# Search for sync.Mutex or sync.RWMutex in the file to check for synchronization
rg -n "sync\.(Mutex|RWMutex)" tools/dawgrun/pkg/go-repl/repl.go

Repository: SpecterOps/DAWGS

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check if writeStatus uses any locking or if there's a write queue mechanism
sed -n '1690,1750p' tools/dawgrun/pkg/go-repl/repl.go

Repository: SpecterOps/DAWGS

Length of output: 1313


Serialize resize handling with the input loop.

The goroutine started in notifySizeChange() mutates REPL state (r.width, r.height, r.buffer, r.bufferPos) via resize() and force(), and writes to the terminal via writeStatus() concurrently with the main loop's dispatch() path, which does the same. The Repl struct has no mutex protecting this shared state. This causes data races and concurrent terminal writes that can garble rendering.

Send resize events through a channel to the main loop, or guard all shared state mutations and terminal I/O with a mutex.

🤖 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, 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.

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 on lines +1719 to +1722
left, right := r.statusFields()
if len(left) > w-len(right) {
left = left[0 : w-len(right)]
}
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

🧩 Analysis chain

🏁 Script executed:

# First, let's verify the file exists and read the relevant lines with context
head -n 1730 tools/dawgrun/pkg/go-repl/repl.go | tail -n 20

Repository: SpecterOps/DAWGS

Length of output: 449


🏁 Script executed:

# Let's also search for the statusFields() function to understand what it returns
rg -A 10 "func.*statusFields" tools/dawgrun/pkg/go-repl/repl.go

Repository: SpecterOps/DAWGS

Length of output: 399


🏁 Script executed:

# Find where 'w' (terminal width) is defined/set
rg -B 10 "left, right := r.statusFields()" tools/dawgrun/pkg/go-repl/repl.go

Repository: SpecterOps/DAWGS

Length of output: 255


🏁 Script executed:

# Let's see where 'w' is defined in this context - need more context around this code
rg -B 30 "left, right := r.statusFields()" tools/dawgrun/pkg/go-repl/repl.go | head -40

Repository: SpecterOps/DAWGS

Length of output: 667


🏁 Script executed:

# Let's also see more context after the code to understand the full logic
rg -A 20 "left, right := r.statusFields()" tools/dawgrun/pkg/go-repl/repl.go | head -30

Repository: SpecterOps/DAWGS

Length of output: 518


🏁 Script executed:

# Find the complete function that contains this code
ast-grep --pattern 'func ($_ *$_) $_(){ $$$left, right := r.statusFields()$$$ }'

Repository: SpecterOps/DAWGS

Length of output: 42


Clamp status fields before slicing to prevent panic with negative indices.

If right is longer than the terminal width w, then w-len(right) becomes negative, causing left[0:w-len(right)] to panic with "slice bounds out of range". This can occur when custom status widgets or default widgets return strings longer than the terminal width. Additionally, the subsequent loop for i := 0; i < w-len(left)-len(right); i++ can also produce negative iteration counts when both fields exceed available width.

🛡️ 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]
+		}
+
+		leftWidth := w - len(right)
+		if leftWidth < 0 {
+			leftWidth = 0
+		}
+		if len(left) > leftWidth {
+			left = left[:leftWidth]
 		}
🤖 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 - 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.

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.

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