@@ -52,21 +52,22 @@ type rawBlameLine struct {
5252}
5353
5454type attributionLine struct {
55- LineNumber int `json:"line_number"`
56- Authorship attributionAuthorship `json:"authorship"`
57- Tag string `json:"tag"`
58- CommitSHA string `json:"commit_sha,omitempty"`
59- ShortCommitSHA string `json:"short_commit_sha,omitempty"`
60- Author string `json:"author,omitempty"`
61- AuthorTime * time.Time `json:"author_time,omitempty"`
62- CheckpointID string `json:"checkpoint_id,omitempty"`
63- SessionID string `json:"session_id,omitempty"`
64- Agent string `json:"agent,omitempty"`
65- Model string `json:"model,omitempty"`
66- Prompt string `json:"prompt,omitempty"`
67- Intent string `json:"intent,omitempty"`
68- MetadataMissing bool `json:"metadata_missing,omitempty"`
69- SessionFallback bool `json:"session_fallback,omitempty"`
55+ LineNumber int `json:"line_number"`
56+ Authorship attributionAuthorship `json:"authorship"`
57+ Tag string `json:"tag"`
58+ CommitSHA string `json:"commit_sha,omitempty"`
59+ ShortCommitSHA string `json:"short_commit_sha,omitempty"`
60+ Author string `json:"author,omitempty"`
61+ AuthorTime * time.Time `json:"author_time,omitempty"`
62+ CheckpointID string `json:"checkpoint_id,omitempty"`
63+ SessionID string `json:"session_id,omitempty"`
64+ Agent string `json:"agent,omitempty"`
65+ Model string `json:"model,omitempty"`
66+ Prompt string `json:"prompt,omitempty"`
67+ Intent string `json:"intent,omitempty"`
68+ MetadataMissing bool `json:"metadata_missing,omitempty"`
69+ MetadataMissingReason string `json:"metadata_missing_reason,omitempty"`
70+ SessionFallback bool `json:"session_fallback,omitempty"`
7071 // PromptSessionLevel is set when Prompt is the session's overall/seed prompt
7172 // (e.g. an attach/trail ReviewPrompt) rather than a prompt recorded for this
7273 // specific checkpoint. `why` labels these differently and points at
@@ -84,15 +85,16 @@ type attributionLine struct {
8485// deduplicated per-file checkpoint map — so attributionCandidate aliases it
8586// rather than duplicating the fields.
8687type attributionCheckpointContext struct {
87- CheckpointID string `json:"checkpoint_id"`
88- SessionID string `json:"session_id,omitempty"`
89- Agent string `json:"agent,omitempty"`
90- Model string `json:"model,omitempty"`
91- Prompt string `json:"prompt,omitempty"`
92- Intent string `json:"intent,omitempty"`
93- FilesTouched []string `json:"files_touched,omitempty"`
94- MetadataMissing bool `json:"metadata_missing,omitempty"`
95- Mixed bool `json:"mixed,omitempty"`
88+ CheckpointID string `json:"checkpoint_id"`
89+ SessionID string `json:"session_id,omitempty"`
90+ Agent string `json:"agent,omitempty"`
91+ Model string `json:"model,omitempty"`
92+ Prompt string `json:"prompt,omitempty"`
93+ Intent string `json:"intent,omitempty"`
94+ FilesTouched []string `json:"files_touched,omitempty"`
95+ MetadataMissing bool `json:"metadata_missing,omitempty"`
96+ MetadataMissingReason string `json:"metadata_missing_reason,omitempty"`
97+ Mixed bool `json:"mixed,omitempty"`
9698 // SessionFallback is set when the file is not in any resolved session's
9799 // recorded paths (e.g. it was renamed after the checkpoint) and the
98100 // agent/prompt shown is a best-effort guess from the checkpoint's first
@@ -255,7 +257,9 @@ func runAttributionWhy(ctx context.Context, w io.Writer, target string, opts att
255257 line , hasLine = n , true
256258 }
257259
258- result , err := resolveFileAttribution (ctx , file , false )
260+ // entire why is explanation-focused: when local metadata is missing it
261+ // should attempt the same remote enrichment path as checkpoint explain.
262+ result , err := resolveFileAttribution (ctx , file , true )
259263 if err != nil {
260264 return err
261265 }
@@ -278,13 +282,6 @@ func runAttributionWhy(ctx context.Context, w io.Writer, target string, opts att
278282 if selected == nil {
279283 return fmt .Errorf ("line %d is outside %s" , line , result .File )
280284 }
281- if selected .MetadataMissing && selected .CheckpointID != "" {
282- if err := enrichAttributionLineWithFetch (ctx , result .File , selected , result .Checkpoints ); err != nil {
283- // Remote metadata enrichment is best-effort; the trailer-level
284- // explanation is still useful and should remain available.
285- selected .MetadataMissing = true
286- }
287- }
288285
289286 if opts .JSON {
290287 payload := struct {
@@ -334,8 +331,9 @@ func resolveFileAttribution(ctx context.Context, file string, fetchOnMiss bool)
334331 for _ , candidate := range line .Candidates {
335332 if candidate .MetadataMissing {
336333 result .Checkpoints [candidate .CheckpointID ] = attributionCheckpointContext {
337- CheckpointID : candidate .CheckpointID ,
338- MetadataMissing : true ,
334+ CheckpointID : candidate .CheckpointID ,
335+ MetadataMissing : true ,
336+ MetadataMissingReason : candidate .MetadataMissingReason ,
339337 }
340338 continue
341339 }
@@ -450,12 +448,15 @@ func (r *attributionResolver) readCheckpointContext(cpID id.CheckpointID, file s
450448 ctx := attributionCheckpointContext {CheckpointID : cpID .String ()}
451449 summary , err := readAttributionCheckpointSummary (r .ctx , r .store , cpID )
452450 if err != nil && r .fetchOnMiss {
453- if fetched , fetchErr := r .fetchCheckpointContext (cpID , file ); fetchErr == nil {
451+ fetched , fetchErr := r .fetchCheckpointContext (cpID , file )
452+ if fetchErr == nil {
454453 return fetched
455454 }
455+ err = fmt .Errorf ("%w (remote refresh failed: %v)" , err , fetchErr )
456456 }
457457 if err != nil {
458458 ctx .MetadataMissing = true
459+ ctx .MetadataMissingReason = metadataMissingReason (r .ctx , cpID .String (), err )
459460 return ctx
460461 }
461462
@@ -547,33 +548,15 @@ func readAttributionCheckpointSummary(ctx context.Context, reader attributionChe
547548 return summary , nil
548549}
549550
550- func enrichAttributionLineWithFetch (ctx context.Context , file string , line * attributionLine , checkpoints map [string ]attributionCheckpointContext ) error {
551- if line == nil || len (line .Candidates ) == 0 {
552- return nil
551+ func metadataMissingReason (ctx context.Context , checkpointID string , cause error ) string {
552+ reason := "checkpoint metadata was not found locally"
553+ if cause != nil {
554+ reason = fmt .Sprintf ("%s (%v)" , reason , cause )
553555 }
554- resolver , err := newAttributionResolver (ctx , true )
555- if err != nil {
556- return err
557- }
558- defer resolver .Close ()
559-
560- candidates := make ([]attributionCandidate , 0 , len (line .Candidates ))
561- for _ , candidate := range line .Candidates {
562- cpID , idErr := id .NewCheckpointID (candidate .CheckpointID )
563- if idErr != nil {
564- candidates = append (candidates , candidate )
565- continue
566- }
567- cpCtx := resolver .checkpointContext (cpID , file )
568- checkpoints [cpCtx .CheckpointID ] = cpCtx
569- candidates = append (candidates , cpCtx )
556+ if checkpointID == "" {
557+ return fmt .Sprintf ("%s. Run: %s." , reason , suggestCheckpointFetchCommand (ctx ))
570558 }
571- preferred := preferredAttributionCandidate (candidates , file )
572- applyPreferredToLine (line , preferred )
573- line .Candidates = candidates
574- line .Authorship = authorshipForPreferred (preferred )
575- line .Tag = attributionTag (line .Authorship )
576- return nil
559+ return fmt .Sprintf ("%s. Run: %s. Then re-run entire checkpoint explain %s." , reason , suggestCheckpointFetchCommand (ctx ), checkpointID )
577560}
578561
579562func (r * attributionResolver ) fetchCheckpointContext (cpID id.CheckpointID , file string ) (attributionCheckpointContext , error ) {
@@ -1137,7 +1120,11 @@ func renderAttributionLineWhy(w io.Writer, file string, line attributionLine) {
11371120 fmt .Fprintf (w , " %s %q\n " , sty .render (sty .bold , "Intent:" ), stringutil .TruncateRunes (stringutil .CollapseWhitespace (line .Intent ), 160 , "..." ))
11381121 }
11391122 if line .MetadataMissing {
1140- fmt .Fprintf (w , " %s\n " , sty .render (sty .yellow , "Checkpoint metadata was not found locally; showing trailer-level attribution only." ))
1123+ message := "Checkpoint metadata was not found locally; showing trailer-level attribution only."
1124+ if line .MetadataMissingReason != "" {
1125+ message = line .MetadataMissingReason
1126+ }
1127+ fmt .Fprintf (w , " %s\n " , sty .render (sty .yellow , message ))
11411128 }
11421129 if line .SessionFallback {
11431130 fmt .Fprintf (w , " %s\n " , sty .render (sty .yellow , "This file is not in the checkpoint session's recorded paths (it may have been renamed); the agent and prompt shown are a best-effort guess, not necessarily the session that produced this line." ))
@@ -1201,6 +1188,13 @@ func renderAttributionFileWhy(w io.Writer, result *fileAttributionResult) {
12011188 if ctx .Prompt != "" {
12021189 fmt .Fprintf (w , " %s %q" , sty .render (sty .dim , "·" ), stringutil .TruncateRunes (stringutil .CollapseWhitespace (ctx .Prompt ), 90 , "..." ))
12031190 }
1191+ if ctx .MetadataMissing {
1192+ message := "Checkpoint metadata was not found locally."
1193+ if ctx .MetadataMissingReason != "" {
1194+ message = ctx .MetadataMissingReason
1195+ }
1196+ fmt .Fprintf (w , "\n %s %s" , sty .render (sty .yellow , "metadata missing:" ), message )
1197+ }
12041198 fmt .Fprintln (w )
12051199 }
12061200 fmt .Fprintf (w , "\n %s\n \n " , sty .render (sty .dim , "Tip: entire why " + result .File + ":<line> shows the prompt behind a specific line." ))
@@ -1301,6 +1295,7 @@ func applyPreferredToLine(line *attributionLine, preferred *attributionCandidate
13011295 line .Prompt = preferred .Prompt
13021296 line .Intent = preferred .Intent
13031297 line .MetadataMissing = preferred .MetadataMissing
1298+ line .MetadataMissingReason = preferred .MetadataMissingReason
13041299 line .SessionFallback = preferred .SessionFallback
13051300 line .PromptSessionLevel = preferred .PromptSessionLevel
13061301}
0 commit comments