Skip to content

Headless crawl paths are not exportable#1721

Open
Mzack9999 wants to merge 2 commits into
devfrom
headless-path-export
Open

Headless crawl paths are not exportable#1721
Mzack9999 wants to merge 2 commits into
devfrom
headless-path-export

Conversation

@Mzack9999

@Mzack9999 Mzack9999 commented Jul 17, 2026

Copy link
Copy Markdown
Member

Fixes #1715

Pure headless builds a crawl graph but does not expose a replayable path from the entrypoint to each location. This adds a Path type, PathFromRoot / AllPathsFromRoot, Paths() on the crawler, and paths.json when diagnostics are on.

Summary by CodeRabbit

  • New Features

    • Added crawl path discovery, including entrypoints, targets, navigation steps, and path lengths.
    • Added access to all replayable paths generated during a crawl.
    • Added optional JSON export of discovered paths to the diagnostics output as paths.json.
    • Added consistent path cloning and serialization, including empty step lists.
  • Tests

    • Added coverage for path discovery, traversal, cloning, lengths, and JSON output.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Headless crawling now represents replayable navigation paths, derives them from crawl graphs, exposes them through Crawler.Paths(), and exports them as paths.json when diagnostics are enabled.

Headless crawl paths

Layer / File(s) Summary
Path model and serialization
pkg/engine/headless/types/path.go, pkg/engine/headless/types/path_test.go
Adds Path with ordered actions, cloning, length calculation, and JSON serialization that emits empty steps as [].
Graph root and path traversal
pkg/engine/headless/graph/graph.go, pkg/engine/headless/graph/graph_test.go
Adds root detection and methods for deriving paths to individual or all graph vertices, with coverage for action ordering and root paths.
Crawler access and diagnostics export
pkg/engine/headless/crawler/crawler.go, pkg/engine/headless/crawler/paths_test.go
Adds Crawler.Paths() and writes collected paths to paths.json during diagnostic cleanup, with crawler-level tests.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Crawler
  participant CrawlGraph
  participant Path
  participant Diagnostics
  Crawler->>CrawlGraph: AllPathsFromRoot()
  CrawlGraph->>Path: build paths from root
  Path-->>CrawlGraph: ordered navigation paths
  CrawlGraph-->>Crawler: path collection
  Crawler->>Diagnostics: marshal and write paths.json
Loading

Poem

I’m a rabbit with routes in a neat little chain,
From root through the graph, hopping over each lane.
Actions line up, and the paths now shine,
In a JSON burrow, tidy and fine.
Hop, hop—rewalks can follow the sign!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and matches the main change: making headless crawl paths exportable.
Linked Issues check ✅ Passed The PR implements the requested Path type, graph traversal methods, crawler exposure, and diagnostics export.
Out of Scope Changes check ✅ Passed The changes stay focused on path export support and associated tests, with no obvious unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 headless-path-export

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/engine/headless/types/path.go (1)

41-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prevent nil panics and avoid duplicating struct fields.

If json.Marshal is invoked on a nil *Path, it will panic when accessing p.Steps. Additionally, mirroring the Path struct inside the custom marshaller risks omitting new fields added in the future.

You can handle both issues robustly by adding a nil check and using a type alias. Embedding a type alias allows you to inherit all original fields and tags, while shadowing Steps to guarantee the JSON output is an empty array instead of being dropped by omitempty.

♻️ Proposed refactor
 // MarshalJSON ensures nil Steps serialize as [] not null.
 func (p *Path) MarshalJSON() ([]byte, error) {
+	if p == nil {
+		return []byte("null"), nil
+	}
 	steps := p.Steps
 	if steps == nil {
 		steps = []*Action{}
 	}
+	type Alias Path
 	return json.Marshal(&struct {
-		EntryID   string    `json:"entry_id"`
-		TargetID  string    `json:"target_id"`
-		TargetURL string    `json:"target_url,omitempty"`
+		*Alias
 		Steps     []*Action `json:"steps"`
 	}{
-		EntryID:   p.EntryID,
-		TargetID:  p.TargetID,
-		TargetURL: p.TargetURL,
+		Alias: (*Alias)(p),
 		Steps:     steps,
 	})
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/engine/headless/types/path.go` around lines 41 - 58, Update
Path.MarshalJSON to return the JSON null representation when the receiver is nil
before dereferencing it. Replace the duplicated anonymous struct with a type
alias of Path, embedding that alias alongside a shadowed Steps field so all Path
fields and tags are preserved while nil Steps serialize as an empty array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/engine/headless/graph/graph.go`:
- Around line 190-215: Update AllPathsFromRoot to construct each path inline
using the already resolved entryID and vertex data, rather than calling
PathFromRoot for every vertex. Reuse the existing ShortestPath logic while
preserving the current root, about:blank, and error-skipping behavior, and avoid
repeatedly invoking RootID or fetching the vertex list during path construction.

---

Nitpick comments:
In `@pkg/engine/headless/types/path.go`:
- Around line 41-58: Update Path.MarshalJSON to return the JSON null
representation when the receiver is nil before dereferencing it. Replace the
duplicated anonymous struct with a type alias of Path, embedding that alias
alongside a shadowed Steps field so all Path fields and tags are preserved while
nil Steps serialize as an empty array.
🪄 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: 189bef6f-71e4-43ce-961a-7c1688ce9e3b

📥 Commits

Reviewing files that changed from the base of the PR and between ba15ac0 and 27dceba.

📒 Files selected for processing (6)
  • pkg/engine/headless/crawler/crawler.go
  • pkg/engine/headless/crawler/paths_test.go
  • pkg/engine/headless/graph/graph.go
  • pkg/engine/headless/graph/graph_test.go
  • pkg/engine/headless/types/path.go
  • pkg/engine/headless/types/path_test.go

Comment on lines +190 to +215
// AllPathsFromRoot returns a path from the entrypoint to every non-entry vertex.
func (g *CrawlGraph) AllPathsFromRoot() ([]*types.Path, error) {
entryID, err := g.RootID()
if err != nil {
return nil, err
}
var paths []*types.Path
for _, id := range g.GetVertices() {
if id == entryID {
continue
}
ps, err := g.GetPageState(id)
if err != nil {
continue
}
if ps.URL == "about:blank" {
continue
}
p, err := g.PathFromRoot(id)
if err != nil {
continue
}
paths = append(paths, p)
}
return paths, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Inline path construction to prevent O(V^2) allocations.

Calling PathFromRoot(id) recalculates RootID() for every vertex. Since RootID() invokes GetVertices() (which fetches the graph's AdjacencyMap and allocates slices), iterating over all vertices results in O(V^2) operations and allocations.

Since AllPathsFromRoot already skips the root vertex and resolves the target's PageState, you can inline ShortestPath to eliminate the redundant overhead.

⚡ Proposed performance optimization
 // AllPathsFromRoot returns a path from the entrypoint to every non-entry vertex.
 func (g *CrawlGraph) AllPathsFromRoot() ([]*types.Path, error) {
 	entryID, err := g.RootID()
 	if err != nil {
 		return nil, err
 	}
 	var paths []*types.Path
 	for _, id := range g.GetVertices() {
 		if id == entryID {
 			continue
 		}
 		ps, err := g.GetPageState(id)
 		if err != nil {
 			continue
 		}
 		if ps.URL == "about:blank" {
 			continue
 		}
-		p, err := g.PathFromRoot(id)
+		steps, err := g.ShortestPath(entryID, id)
 		if err != nil {
 			continue
 		}
-		paths = append(paths, p)
+		paths = append(paths, &types.Path{
+			EntryID:   entryID,
+			TargetID:  id,
+			TargetURL: ps.URL,
+			Steps:     steps,
+		})
 	}
 	return paths, 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
// AllPathsFromRoot returns a path from the entrypoint to every non-entry vertex.
func (g *CrawlGraph) AllPathsFromRoot() ([]*types.Path, error) {
entryID, err := g.RootID()
if err != nil {
return nil, err
}
var paths []*types.Path
for _, id := range g.GetVertices() {
if id == entryID {
continue
}
ps, err := g.GetPageState(id)
if err != nil {
continue
}
if ps.URL == "about:blank" {
continue
}
p, err := g.PathFromRoot(id)
if err != nil {
continue
}
paths = append(paths, p)
}
return paths, nil
}
// AllPathsFromRoot returns a path from the entrypoint to every non-entry vertex.
func (g *CrawlGraph) AllPathsFromRoot() ([]*types.Path, error) {
entryID, err := g.RootID()
if err != nil {
return nil, err
}
var paths []*types.Path
for _, id := range g.GetVertices() {
if id == entryID {
continue
}
ps, err := g.GetPageState(id)
if err != nil {
continue
}
if ps.URL == "about:blank" {
continue
}
steps, err := g.ShortestPath(entryID, id)
if err != nil {
continue
}
paths = append(paths, &types.Path{
EntryID: entryID,
TargetID: id,
TargetURL: ps.URL,
Steps: steps,
})
}
return paths, nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/engine/headless/graph/graph.go` around lines 190 - 215, Update
AllPathsFromRoot to construct each path inline using the already resolved
entryID and vertex data, rather than calling PathFromRoot for every vertex.
Reuse the existing ShortestPath logic while preserving the current root,
about:blank, and error-skipping behavior, and avoid repeatedly invoking RootID
or fetching the vertex list during path construction.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Headless crawl paths are not exportable

1 participant