Skip to content

Commit a238323

Browse files
committed
Unify detail flag and add traversal depth controls
1 parent 8810280 commit a238323

File tree

9 files changed

+204
-43
lines changed

9 files changed

+204
-43
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Fibers are minimal by default. All fields except title are optional.
2424

2525
**Status is opt-in.** `felt add "title"` creates a statusless fiber. `felt add "title" -s open` enters tracking. `felt edit <id> -s active` enters tracking. `felt ls` only shows tracked fibers.
2626

27-
**Progressive disclosure.** `felt show <id> -d compact` shows metadata + outcome without body. Levels: title, compact, summary, full (default). `felt upstream/downstream <id> -d compact` renders each connected fiber at that depth.
27+
**Progressive disclosure.** `felt show <id> -d compact` shows metadata + outcome without body. Levels: title, compact, summary, full (default). `felt upstream/downstream <id> -d compact` renders each connected fiber at that detail level.
2828

2929
**Backward compat:** `close-reason` reads as `outcome`, `kind` reads as a tag. Both migrate on next write.
3030

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ felt show <id> -d compact # see outcome without full body
7777

7878
### Progressive Disclosure
7979

80-
`felt show` supports depth levels via `--depth` / `-d`:
80+
`felt show` supports detail levels via `--detail` / `-d`:
8181

8282
| Level | What you see |
8383
|---|---|

cmd/graph.go

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import (
88
)
99

1010
var (
11-
graphFormat string
12-
upDownDepth string
11+
graphFormat string
12+
upDownDetail string
13+
traversalAll bool
1314
)
1415

1516
var graphCmd = &cobra.Command{
@@ -54,27 +55,43 @@ var graphCmd = &cobra.Command{
5455
var upstreamCmd = &cobra.Command{
5556
Use: "upstream <id>",
5657
Short: "Show what a felt depends on",
57-
Long: `Lists all transitive dependencies of a felt. Use --depth to control detail per item.`,
58-
Args: cobra.ExactArgs(1),
58+
Long: `Lists dependencies of a felt.
59+
60+
By default shows direct dependencies only (depth 1).
61+
Use --all for the full transitive closure (all ancestors).
62+
Use -d/--detail to control detail level per item.`,
63+
Args: cobra.ExactArgs(1),
5964
RunE: func(cmd *cobra.Command, args []string) error {
65+
depth := 1
66+
if traversalAll {
67+
depth = 0
68+
}
6069
return runTraversal(args[0], traversalConfig{
61-
getRelated: func(g *felt.Graph, id string) []string { return g.GetUpstream(id) },
62-
edgeLabel: func(g *felt.Graph, fiberID, relatedID string) string { return edgeLabelInGraph(g, relatedID, fiberID) },
63-
emptyMsg: "No dependencies",
70+
getRelated: func(g *felt.Graph, id string) []string { return g.GetUpstreamN(id, depth) },
71+
edgeLabel: func(g *felt.Graph, fiberID, relatedID string) string { return edgeLabelInGraph(g, relatedID, fiberID) },
72+
emptyMsg: "No dependencies",
6473
})
6574
},
6675
}
6776

6877
var downstreamCmd = &cobra.Command{
6978
Use: "downstream <id>",
7079
Short: "Show what depends on a felt",
71-
Long: `Lists all felts that transitively depend on this one. Use --depth to control detail per item.`,
72-
Args: cobra.ExactArgs(1),
80+
Long: `Lists felts that depend on this one.
81+
82+
By default shows direct dependents only (depth 1).
83+
Use --all for the full transitive closure (all descendants).
84+
Use -d/--detail to control detail level per item.`,
85+
Args: cobra.ExactArgs(1),
7386
RunE: func(cmd *cobra.Command, args []string) error {
87+
depth := 1
88+
if traversalAll {
89+
depth = 0
90+
}
7491
return runTraversal(args[0], traversalConfig{
75-
getRelated: func(g *felt.Graph, id string) []string { return g.GetDownstream(id) },
76-
edgeLabel: func(g *felt.Graph, fiberID, relatedID string) string { return edgeLabelInGraph(g, fiberID, relatedID) },
77-
emptyMsg: "Nothing depends on this",
92+
getRelated: func(g *felt.Graph, id string) []string { return g.GetDownstreamN(id, depth) },
93+
edgeLabel: func(g *felt.Graph, fiberID, relatedID string) string { return edgeLabelInGraph(g, fiberID, relatedID) },
94+
emptyMsg: "Nothing depends on this",
7895
})
7996
},
8097
}
@@ -93,8 +110,8 @@ func runTraversal(fiberArg string, cfg traversalConfig) error {
93110
return fmt.Errorf("not in a felt repository")
94111
}
95112

96-
if upDownDepth != "" {
97-
if err := validateDepth(upDownDepth); err != nil {
113+
if upDownDetail != "" {
114+
if err := validateDepth(upDownDetail); err != nil {
98115
return err
99116
}
100117
}
@@ -129,12 +146,12 @@ func runTraversal(fiberArg string, cfg traversalConfig) error {
129146
}
130147

131148
// Depth-aware rendering
132-
if upDownDepth != "" {
149+
if upDownDetail != "" {
133150
for i, id := range related {
134151
dep := g.Nodes[id]
135152
if dep != nil {
136-
fmt.Print(renderFelt(dep, g, upDownDepth))
137-
if upDownDepth != DepthTitle && i < len(related)-1 {
153+
fmt.Print(renderFelt(dep, g, upDownDetail))
154+
if upDownDetail != DepthTitle && i < len(related)-1 {
138155
fmt.Println()
139156
}
140157
}
@@ -261,6 +278,8 @@ func init() {
261278
rootCmd.AddCommand(checkCmd)
262279

263280
graphCmd.Flags().StringVarP(&graphFormat, "format", "f", "mermaid", "Output format (mermaid, dot, text)")
264-
upstreamCmd.Flags().StringVarP(&upDownDepth, "depth", "d", "", "Detail level per item (title, compact, summary, full)")
265-
downstreamCmd.Flags().StringVarP(&upDownDepth, "depth", "d", "", "Detail level per item (title, compact, summary, full)")
281+
upstreamCmd.Flags().StringVarP(&upDownDetail, "detail", "d", "", "Detail level per item (title, compact, summary, full)")
282+
downstreamCmd.Flags().StringVarP(&upDownDetail, "detail", "d", "", "Detail level per item (title, compact, summary, full)")
283+
upstreamCmd.Flags().BoolVar(&traversalAll, "all", false, "Traverse full transitive closure (all ancestors)")
284+
downstreamCmd.Flags().BoolVar(&traversalAll, "all", false, "Traverse full transitive closure (all descendants)")
266285
}

cmd/hook.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,16 @@ felt show <id> -d compact # metadata + outcome only
223223
felt ls # tracked fibers (open/active)
224224
felt ls -t tapestry: # any filter widens to all statuses
225225
felt ls -s closed "query" # explicit -s overrides; -e exact, -r regex
226-
felt upstream/downstream <id> # DAG traversal
227-
-d title|compact|summary|full # depth for show, ls, upstream, downstream
228-
Also: link, unlink, tag, untag, tree, ready, rm
226+
felt upstream <id> # direct dependencies
227+
felt upstream <id> -d summary # with summaries
228+
felt upstream <id> --all # full transitive closure
229+
felt downstream <id> # direct dependents
230+
felt downstream <id> -d summary # with summaries
231+
felt downstream <id> --all # full transitive closure
232+
Also: link, unlink, tag, untag, path, tree, ready, rm
229233
` + "```" + `
230234
Statuses: · untracked, ○ open, ◐ active, ● closed
231-
Depth: title < compact < summary < full (default). Summary shows the **lede** — the first paragraph of the body. Write it to stand alone.
235+
Detail: title < compact < summary < full (default). Summary shows the **lede** — the first paragraph of the body. Write it to stand alone.
232236
To patch body text (not replace), edit .felt/<id>.md directly.
233237
234238
`

cmd/integration_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,31 @@ func TestIntegration(t *testing.T) {
153153
if !strings.Contains(out, "second fiber") {
154154
t.Fatalf("downstream: expected child fiber, got: %s", out)
155155
}
156+
fiber3ID := strings.TrimSpace(mustFelt(t, dir, "add", "third fiber", "-s", "open"))
157+
if fiber3ID == "" {
158+
t.Fatal("add: expected fiber3 ID")
159+
}
160+
mustFelt(t, dir, "link", fiber3ID, fiber2ID)
161+
162+
// Traversal defaults to direct neighbors only.
163+
out = mustFelt(t, dir, "downstream", fiberID)
164+
if strings.Contains(out, "third fiber") {
165+
t.Fatalf("downstream default: expected direct dependents only, got: %s", out)
166+
}
167+
// --all includes transitive dependents.
168+
out = mustFelt(t, dir, "downstream", fiberID, "--all")
169+
if !strings.Contains(out, "second fiber") || !strings.Contains(out, "third fiber") {
170+
t.Fatalf("downstream --all: expected transitive closure, got: %s", out)
171+
}
172+
173+
out = mustFelt(t, dir, "upstream", fiber3ID)
174+
if strings.Contains(out, "test fiber") || !strings.Contains(out, "second fiber") {
175+
t.Fatalf("upstream default: expected direct dependencies only, got: %s", out)
176+
}
177+
out = mustFelt(t, dir, "upstream", fiber3ID, "--all")
178+
if !strings.Contains(out, "second fiber") || !strings.Contains(out, "test fiber") {
179+
t.Fatalf("upstream --all: expected transitive closure, got: %s", out)
180+
}
156181

157182
// tag and untag
158183
mustFelt(t, dir, "tag", fiber2ID, "testlabel")
@@ -166,6 +191,7 @@ func TestIntegration(t *testing.T) {
166191
mustFelt(t, dir, "ready")
167192

168193
// rm --force
194+
mustFelt(t, dir, "rm", "--force", fiber3ID)
169195
mustFelt(t, dir, "rm", "--force", fiber2ID)
170196
lsOut := mustFelt(t, dir, "ls", "-s", "all")
171197
if strings.Contains(lsOut, "second fiber") {

cmd/show.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import (
99

1010
var (
1111
showBodyOnly bool
12-
showDepth string
12+
showDetail string
1313
)
1414

1515
var showCmd = &cobra.Command{
1616
Use: "show <id>",
1717
Short: "Show details of a felt",
18-
Long: `Displays details of a felt at the requested depth level.
18+
Long: `Displays details of a felt at the requested detail level.
1919
20-
Depth levels control progressive disclosure:
20+
Detail levels control progressive disclosure:
2121
title Title and tags only
2222
compact Structured overview: metadata, outcome, dependency IDs
2323
summary Compact + lede paragraph + dependency titles
@@ -29,11 +29,11 @@ Depth levels control progressive disclosure:
2929
return fmt.Errorf("not in a felt repository")
3030
}
3131

32-
depth := showDepth
33-
if depth == "" {
34-
depth = DepthFull
32+
detail := showDetail
33+
if detail == "" {
34+
detail = DepthFull
3535
}
36-
if err := validateDepth(depth); err != nil {
36+
if err := validateDepth(detail); err != nil {
3737
return err
3838
}
3939

@@ -60,13 +60,13 @@ Depth levels control progressive disclosure:
6060
}
6161
graph := felt.BuildGraph(felts)
6262

63-
fmt.Print(renderFelt(f, graph, depth))
63+
fmt.Print(renderFelt(f, graph, detail))
6464
return nil
6565
},
6666
}
6767

6868
func init() {
6969
rootCmd.AddCommand(showCmd)
7070
showCmd.Flags().BoolVarP(&showBodyOnly, "body", "b", false, "Output only the body (for piping)")
71-
showCmd.Flags().StringVarP(&showDepth, "depth", "d", "", "Detail level (title, compact, summary, full)")
71+
showCmd.Flags().StringVarP(&showDetail, "detail", "d", "", "Detail level (title, compact, summary, full)")
7272
}

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ felt show <id> -d compact # see outcome without full body
6262

6363
### Progressive Disclosure
6464

65-
`felt show` supports depth levels via `--depth` / `-d`:
65+
`felt show` supports detail levels via `--detail` / `-d`:
6666

6767
| Level | What you see |
6868
|---|---|

internal/felt/graph.go

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,35 +40,59 @@ func BuildGraph(felts []*Felt) *Graph {
4040
// GetUpstream returns all transitive dependencies of the given ID.
4141
// Uses BFS for level-order traversal.
4242
func (g *Graph) GetUpstream(id string) []string {
43-
return g.bfs(id, g.Upstream)
43+
return g.bfs(id, g.Upstream, 0)
4444
}
4545

4646
// GetDownstream returns all nodes that transitively depend on the given ID.
4747
// Uses BFS for level-order traversal.
4848
func (g *Graph) GetDownstream(id string) []string {
49-
return g.bfs(id, g.Downstream)
49+
return g.bfs(id, g.Downstream, 0)
50+
}
51+
52+
// GetUpstreamN returns dependencies up to maxLevels deep.
53+
// maxLevels=1 returns direct dependencies only.
54+
// maxLevels=0 returns the full transitive closure (same as GetUpstream).
55+
func (g *Graph) GetUpstreamN(id string, maxLevels int) []string {
56+
return g.bfs(id, g.Upstream, maxLevels)
57+
}
58+
59+
// GetDownstreamN returns dependents up to maxLevels deep.
60+
// maxLevels=1 returns direct dependents only.
61+
// maxLevels=0 returns the full transitive closure (same as GetDownstream).
62+
func (g *Graph) GetDownstreamN(id string, maxLevels int) []string {
63+
return g.bfs(id, g.Downstream, maxLevels)
5064
}
5165

5266
// bfs performs breadth-first traversal from start using the given adjacency map.
53-
func (g *Graph) bfs(start string, adj map[string]Dependencies) []string {
67+
// maxLevels controls how many levels to traverse (0 = unlimited).
68+
func (g *Graph) bfs(start string, adj map[string]Dependencies, maxLevels int) []string {
69+
type entry struct {
70+
id string
71+
level int
72+
}
5473
visited := make(map[string]bool)
5574
var result []string
56-
queue := []string{start}
75+
queue := []entry{{id: start, level: 0}}
5776
visited[start] = true
5877

5978
for len(queue) > 0 {
6079
current := queue[0]
6180
queue = queue[1:]
6281

6382
// Don't include start in result
64-
if current != start {
65-
result = append(result, current)
83+
if current.id != start {
84+
result = append(result, current.id)
85+
}
86+
87+
// Stop expanding if we've reached the max depth
88+
if maxLevels > 0 && current.level >= maxLevels {
89+
continue
6690
}
6791

68-
for _, dep := range adj[current] {
92+
for _, dep := range adj[current.id] {
6993
if !visited[dep.ID] {
7094
visited[dep.ID] = true
71-
queue = append(queue, dep.ID)
95+
queue = append(queue, entry{id: dep.ID, level: current.level + 1})
7296
}
7397
}
7498
}

0 commit comments

Comments
 (0)