1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
//! PlanIndex + turn grouping (opens_turn segmentation) + content flattening.
use *;
/// An index of ExitPlanMode `tool_use_id → planFilePath` built from a session's records
/// (§4.2.4). A tool-use rejection-with-message ([`Record::plan_rejection_message`])
/// resolves the rejected `tool_use_id` through this index to surface a `[plan: <path>]`
/// pointer so a consuming LLM can go Read the plan. Built once per session via
/// [`PlanIndex::from_records`]; cheap (one `BTreeMap` of the few ExitPlanMode calls).
/// `"s"` for plural counts, `""` for exactly one - for the `N question(s)` label.
pub
/// Group records (in file order) into TURNS, returning one `Vec<usize>` of record
/// indices per turn - the outer index IS the 0-based turn index (genuine-user order).
///
/// The single source of truth for turn delimiting (§6.4), shared by `search`'s
/// exchange reconstruction and `files`'s mutation attribution so the two never drift:
///
/// - A turn opens on a boundary record (`is_genuine`); every record after it, up to the
/// next boundary, belongs to that turn (a non-boundary `tool_result`-carrier, an
/// `isMeta` pseudo-turn, and a compaction summary are turn MEMBERS, never delimiters).
/// - Records before the first boundary (rare: leading tool noise) seed turn 0 so they
/// are never lost. When such a synthetic lead exists AND a real user turn follows, the
/// lead is folded into the first real turn so indices stay 0-based on boundary
/// openers. With NO boundary at all, the orphans are a standalone turn 0.
///
/// `is_genuine` is a closure (rather than calling [`Record::opens_turn`] directly) only
/// so callers can test the grouping over lightweight bool fixtures; in production it is
/// always [`Record::opens_turn`] - which opens on a genuine human message, an answered
/// AskUserQuestion (the answer is the user's message, §4.4), OR a tool-use
/// rejection-with-message (§4.2.4). An AUQ answer / plan rejection becoming a turn
/// boundary is the sanctioned correct behavior change (a previously-MISSED genuine user
/// message); interrupts / `<local-command-stdout>` / `<command-name>` wrappers, formerly
/// spurious boundaries, are excluded by `is_genuine_user` (regression fixes).
///
/// NOTE: this raw grouper trusts file order and knows nothing of the SURVIVAL AXIS. The
/// production surfaces use [`group_turn_indices_chained`], which drops every record
/// Claude Code's own conversation chain no longer reaches (§6.4.1). This bare form stays
/// for the lightweight bool-fixture tests and any caller that has no `Record`.
// Production now routes through `group_turn_indices_chained`, so in the bin build this bare
// generic is reached only from `#[cfg(test)]` - kept as the documented base primitive +
// bool-fixture test entry (same retained-shape rationale as the `#[allow(dead_code)]` on
// `Record`).
/// [`group_turn_indices`] under the SURVIVAL AXIS ([`Chain`]): a turn opens only on a
/// record the conversation chain still reaches, and a record it does not reach belongs to
/// no numbered turn at all - neither as a boundary nor as a member. That covers what the
/// three former heuristics covered (a recalled draft, a compaction re-anchor's replay
/// copy, the history above a compaction cut) plus what none of them could see: a turn the
/// operator rewound past, and which child of a branch point the conversation continued
/// from. Abandoned records stay searchable, addressable and disclosed; they are simply not
/// numbered.
///
/// CAUTION - this entry point builds the chain from whatever records the caller hands it,
/// and a caller whose prefilter dropped the `attachment` and `system` lines hands it a
/// DAG with holes. A chain that cannot see those lines cannot be trusted to say a whole
/// BRANCH is abandoned, so this form drops only what the pre-0.12.0 rule dropped: the
/// superseded OPENER itself. Every other record stays a member of the turn above it, and
/// a turn-keyed consumer (`files --by timeline`) keeps its row.
///
/// Every production surface now hands the grouper a WHOLE DAG - `search`/`show`/`stats`
/// splice [`crate::parse::spine_record`] rows into their own record list and call
/// [`group_turn_indices_chained`]; `verbatim`/`files`/`recover`/`image` go through
/// [`crate::model::ChainView`], which does the same splice in their own index space. So
/// this conservative form has NO caller left outside the unit tests; it is kept as the
/// documented middle rung (a chain built from a hole-punched DAG must not be trusted to
/// call a whole branch abandoned) and as the entry those tests exercise.
/// [`group_turn_indices_deduped`] with the chain already built - the path `search` takes,
/// because its scan splices the structural spine rows of the lines its prefilter drops
/// into the chain's view and must not pay for a second build.
/// Shared engine for [`group_turn_indices`] and [`group_turn_indices_deduped`]. Every index
/// in `skip` is omitted entirely (`continue`) - neither a turn boundary nor a member - which
/// is how superseded drafts are dropped. With an empty `skip` the behaviour is identical to
/// the original file-order grouper.
pub
/// Flatten a `Content` to a single normalized line of its textual parts.
/// `string` → itself; `blocks` → all `text` blocks joined (other block types,
/// which never co-occur with a genuine user `text` block, are ignored).
pub
/// Extract the textual payload of a `tool_result` block's `content` (§4.5). The
/// content is raw `serde_json::Value`: a bare string, OR an array of
/// `{type:"text",text}` / `{type:"image"}` / `{type:"tool_reference",tool_name}`
/// objects. We concatenate every `text` field found and, for `tool_reference`,
/// surface the `tool_name` (so a regex like `ToolSearch` still matches). Anything
/// else (images, unknown shapes) contributes nothing. Whitespace is NOT normalized
/// here - callers that excerpt do their own normalization; matchers want the raw
/// text. Returns an owned `String` (possibly empty).
/// Scrape the inline persisted-output pointer (§4.6 fallback): the line
/// `Full output saved to: <ABSOLUTE_PATH>` inside a `<persisted-output>` block.
/// Returns the trimmed path, or `None` if the marker is absent.
pub