feat(#137): add doc-map input for path-scoped doc injection #138
@@ -1,5 +1,4 @@
|
||||
// Package review provides doc-map parsing and doc injection for path-scoped
|
||||
// design document context in AI code reviews.
|
||||
// doc-map parsing and doc injection for path-scoped design document context in AI code reviews.
|
||||
|
|
||||
package review
|
||||
|
||||
import (
|
||||
@@ -106,7 +105,7 @@ func mappingMatches(patterns, files []string) bool {
|
||||
|
||||
// globMatch matches a path against a glob pattern that may contain **.
|
||||
// It supports:
|
||||
// - Standard path.Match patterns (*, ?, [range])
|
||||
// - filepath.Match patterns (*, ?, [range])
|
||||
// - ** as a path segment that matches zero or more segments
|
||||
// - Trailing /** to match a directory and all its contents
|
||||
//
|
||||
@@ -246,9 +245,13 @@ type docEntry struct {
|
||||
// If the path is a directory, all .md files under it are returned.
|
||||
|
sonnet-review-bot
commented
[MINOR] In **[MINOR]** In `loadDocEntries`, when `GetAllFilesInPath` returns an error (rather than empty results), the code silently falls through to try `GetFileContent`. The plan's decision explicitly states 'If GetAllFilesInPath returns an error, try GetFileContent', which this does — but the directory error is swallowed without logging. If both calls fail, only the file error is returned. Consider logging a debug message for the directory error before the fallback, which would help diagnose unexpected behavior.
|
||||
// If it's a file, a single entry is returned.
|
||||
func loadDocEntries(ctx context.Context, fetcher DocFetcher, owner, repo, docPath string) ([]docEntry, error) {
|
||||
if err := validateDocPath(docPath); err != nil {
|
||||
return nil, fmt.Errorf("doc path %q rejected: %w", docPath, err)
|
||||
}
|
||||
|
||||
// Try directory expansion first.
|
||||
files, err := fetcher.GetAllFilesInPath(ctx, owner, repo, docPath)
|
||||
if err == nil && len(files) > 0 {
|
||||
files, dirErr := fetcher.GetAllFilesInPath(ctx, owner, repo, docPath)
|
||||
|
sonnet-review-bot
commented
[NIT] **[NIT]** `sortDocEntries` uses an insertion sort described as "doc lists are small". This is fine, but Go's `sort.Slice` or `slices.SortFunc` (Go 1.21+) would be more idiomatic and readable with no meaningful performance difference at these sizes. The comment justifying insertion sort is unnecessary ceremony for a standard sort.
|
||||
if dirErr == nil && len(files) > 0 {
|
||||
// Filter for .md files only.
|
||||
var entries []docEntry
|
||||
for path, content := range files {
|
||||
@@ -261,6 +264,11 @@ func loadDocEntries(ctx context.Context, fetcher DocFetcher, owner, repo, docPat
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// Directory expansion returned nothing; log and fall through to single-file fetch.
|
||||
if dirErr != nil {
|
||||
slog.Debug("doc-map: directory expansion failed, trying as single file", "path", docPath, "error", dirErr)
|
||||
}
|
||||
|
||||
// Try as a single file.
|
||||
content, fileErr := fetcher.GetFileContent(ctx, owner, repo, docPath)
|
||||
if fileErr != nil {
|
||||
@@ -290,8 +298,29 @@ func readFileBytes(path string) ([]byte, error) {
|
||||
return os.ReadFile(path)
|
||||
}
|
||||
|
||||
// validateDocPath rejects doc paths that could cause path traversal via the
|
||||
// VCS API (absolute paths, any ".." segment). Defense-in-depth: the VCS API
|
||||
|
sonnet-review-bot
commented
[MINOR] sortDocEntries uses an insertion sort implemented manually. The standard library's sort.Slice would be idiomatic and more readable: **[MINOR]** sortDocEntries uses an insertion sort implemented manually. The standard library's sort.Slice would be idiomatic and more readable: `sort.Slice(entries, func(i, j int) bool { return entries[i].path < entries[j].path })`. The comment says "doc lists are small" which justifies the O(n²) complexity, but the standard library sort is both clearer and handles all sizes correctly. This is a NIT-level style issue per project conventions.
|
||||
// should already scope paths to the repo, but we validate locally to avoid
|
||||
// any quirk in backend path handling.
|
||||
func validateDocPath(p string) error {
|
||||
if filepath.IsAbs(p) {
|
||||
return fmt.Errorf("absolute paths not allowed")
|
||||
}
|
||||
for _, segment := range strings.Split(p, "/") {
|
||||
if segment == ".." {
|
||||
return fmt.Errorf("path traversal ('..' segment) not allowed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// truncateUTF8 truncates s to at most maxBytes without splitting multi-byte
|
||||
// UTF-8 characters. Returns a valid UTF-8 string of at most maxBytes bytes.
|
||||
//
|
||||
// Note: an identical implementation exists in budget/budget.go. The two
|
||||
// packages are intentionally separate (review does not import budget), so
|
||||
// the duplication is accepted rather than introducing a shared internal
|
||||
// package for a single small function.
|
||||
func truncateUTF8(s string, maxBytes int) string {
|
||||
if len(s) <= maxBytes {
|
||||
return s
|
||||
|
||||
[NIT] Comment for globMatch mentions 'Standard path.Match patterns' while code uses filepath.Match. Consider aligning the comment to say 'filepath.Match patterns' for accuracy.
[NIT] The package doc comment is a single-line imperative description rather than the conventional
// Package review ...format documented in the Go patterns (documentation.md pattern #3). The comment reads// doc-map parsing and doc injection for path-scoped design document context in AI code reviews.— but this is a file-level comment, not a package comment. The review package presumably has a package comment elsewhere; this file comment is fine as a file-level description, though it's atypical (most Go files don't have pre-package file comments).