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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
//! The per-commit stats footer: `Tokens-In`, `Tokens-Out`, `Savings`, `Model`,
//! and the `Tokens-Window` line that scopes the counts when the transcript fold
//! read only a tail window (#7074).
//!
//! Why: the attribution footer Claude Code writes says a session produced the
//! commit but nothing about what it cost, and the owner's observation on #7074
//! was exactly that — "I don't see the token counts/model in the commit
//! messages". These four values answer it at the one place a reader is already
//! looking. They are **git trailers**, not prose, so `git interpret-trailers`
//! and `trusty-git-analytics`' existing attribution collector can read them
//! back without a parser of their own.
//!
//! What: [`render_trailers`] turns a [`CommitStats`] into the trailer lines,
//! and [`append_trailers`] puts them into a commit message where git will
//! recognise them. Both are pure functions of their arguments — every read of
//! the ledger, the transcript, and the model store happens in the caller
//! (`tm commit-trailers`), so the placement rules below are unit-testable with
//! no filesystem at all.
//!
//! Three placement facts, each verified against git 2.54:
//!
//! - **The trailer block must be the message's last paragraph, and its FIRST
//! line must itself be a trailer.** The existing footer's first line
//! (`🤖🤖🤖 Generated with trusty-mpm — …`) is not trailer-shaped, so
//! appending our keys to that same paragraph makes git parse none of them —
//! including the `Claude-Session:` line already there. The block therefore
//! goes in its own paragraph, after a blank line.
//! - **A trailing comment block is not the end of the message.** `git commit`
//! hands a `prepare-commit-msg` hook a file whose tail is `#`-prefixed help
//! text; text appended after it parses as part of that comment run. The
//! insert goes after the last real content line instead.
//! - **`git commit --verbose` appends a diff after a scissors line.** Anything
//! below that line is discarded, so the scissors bound the insert.
//!
//! Whether a message gets a block at all depends on git's commit SOURCE, not
//! only on its text: [`stamp_policy`] maps the source to skip, restamp, or
//! stamp, and [`strip_trailers`] removes a block a reused message carried in
//! from another session (#7249).
//!
//! Test: the inline suite in `commit_trailers_tests.rs` —
//! `render_omits_absent_fields`, `render_is_none_when_nothing_is_known`,
//! `git_interpret_trailers_parses_the_appended_block`,
//! `append_goes_above_a_trailing_comment_block`,
//! `append_stays_above_the_scissors_line`, `append_is_idempotent`.
/// Trailer key carrying the session's input tokens.
///
/// Test: `git_interpret_trailers_parses_the_appended_block`.
pub const TRAILER_TOKENS_IN: &str = "Tokens-In";
/// Trailer key carrying the session's output tokens.
///
/// Test: `git_interpret_trailers_parses_the_appended_block`.
pub const TRAILER_TOKENS_OUT: &str = "Tokens-Out";
/// Trailer key stating that the token counts cover a tail window, not the
/// whole session.
///
/// Why: the transcript fold is capped so a commit never waits on a huge read,
/// and the store behind it holds only the transcript's path — there are no
/// running per-session totals that would make a tail read stand for the
/// session. When the cap cuts, `Tokens-In` and `Tokens-Out` describe the window
/// and a reader has to be told so. It is a separate key rather than a suffix on
/// the two numbers, so both stay plain integers for anything parsing them back.
/// What: present only when the fold was truncated, naming the window's size.
/// Test: `render_states_the_window_when_the_fold_was_truncated`,
/// `render_omits_the_window_when_the_fold_read_everything`.
pub const TRAILER_TOKENS_WINDOW: &str = "Tokens-Window";
/// Trailer key carrying the savings percentage (#7179's percent form).
///
/// Test: `git_interpret_trailers_parses_the_appended_block`.
pub const TRAILER_SAVINGS: &str = "Savings";
/// Trailer key carrying the harness model id the session ran under.
///
/// Test: `git_interpret_trailers_parses_the_appended_block`.
pub const TRAILER_MODEL: &str = "Model";
/// The line `git commit --verbose` puts above the diff it appends.
///
/// Why: everything from this line down is discarded by git's cleanup, so a
/// trailer inserted below it never reaches the commit object.
/// Test: `append_stays_above_the_scissors_line`.
const SCISSORS: &str = "# ------------------------ >8 ------------------------";
/// The five keys this footer owns, in render order.
///
/// Why: [`strip_trailers`] has to recognise a block it wrote in an earlier
/// session, and recognising it by key is the only thing available — the values
/// are exactly what changed.
/// Test: `strip_removes_an_inherited_block`.
const STATS_KEYS: = ;
/// What to do with a commit message, given how git says the commit was made.
///
/// Why: git tells a `prepare-commit-msg` hook its second argument — the commit
/// SOURCE — and the footer means something different for each (#7249). Tokens
/// are a property of one session's work on one message; a source that assembles
/// a message from other commits, or carries one in from another session, has no
/// such figure to state.
/// What: three answers, one per source class. See [`stamp_policy`].
/// Test: `stamp_policy_skips_merge_and_squash`,
/// `stamp_policy_restamps_a_reused_message`, `stamp_policy_stamps_a_new_message`.
/// Which policy git's commit-source argument earns.
///
/// Why (#7249): the hook used to ignore the argument, so a merge or squash got
/// the committing session's token counts stamped onto a message assembled from
/// other people's commits, and a cherry-pick kept the ORIGINAL commit's counts —
/// [`already_stamped`] read the inherited block as this session's own work and
/// left it in place. The four rules below are what the sources actually mean:
///
/// - `merge` — git built the message from the merge's parents. The tokens of
/// whoever ran `git merge` describe none of it. **Skip.**
/// - `squash` — the message is assembled by git or GitHub from a branch's
/// commits, each with its own figures. **Skip.**
/// - `commit` — the message comes from an existing commit: a cherry-pick, a
/// revert, `--amend`, `-c`/`-C`. The new commit is this session's work, so any
/// inherited block is stale and misattributed. **Restamp** — strip what came
/// in, then stamp this session's figures. With no current session there is
/// nothing to stamp, and the strip still happens: no block at all is correct,
/// another session's block is not.
/// - `message` (`-m`/`-F`), `template`, and the empty source git passes for an
/// ordinary editor commit — the message is this commit's own. **Stamp.**
///
/// What: an unrecognised source stamps, matching the empty-source case, because
/// a source git adds later is far likelier to be message-shaped than
/// merge-shaped, and a wrong stamp is recoverable where a silently skipped one
/// is invisible.
/// Test: `stamp_policy_skips_merge_and_squash`,
/// `stamp_policy_restamps_a_reused_message`, `stamp_policy_stamps_a_new_message`.
/// Remove a stats footer this code wrote, wherever the message came from.
///
/// Why (#7249): a cherry-picked or amended message arrives carrying the ORIGINAL
/// commit's figures. Those numbers are not wrong about anything the new commit
/// did — they describe a different session's work — so they are removed rather
/// than left beside a second block.
/// What: only the message's LAST content paragraph is considered, and only when
/// every one of its lines is one of [`STATS_KEYS`] — that is the exact shape
/// [`append_trailers`] writes, and confining the match to it keeps a `Model:`
/// line in someone's prose out of scope. The blank line that separated the block
/// from the paragraph above goes with it. A trailing comment block and anything
/// below a `--verbose` scissors line are left where they are. A message with no
/// such block is returned unchanged.
/// Test: `strip_removes_an_inherited_block`,
/// `strip_leaves_a_message_that_has_none`,
/// `strip_keeps_a_trailing_comment_block`,
/// `strip_leaves_a_paragraph_that_is_not_only_trailers`.
/// Is `line` one of this footer's own trailer lines?
///
/// Test: `strip_removes_an_inherited_block`.
/// What one commit's stats footer states, each field independently optional.
///
/// Why: the four values come from three different stores and any of them can be
/// absent — a session whose status bar has not rendered has no model record, a
/// session with no savings row has no percentage. An absent field is omitted
/// from the footer rather than rendered as a zero, for the same reason the
/// `💸` segment omits itself: a stated zero is a measurement that was never
/// made.
/// What: plain `Option`s; [`render_trailers`] renders exactly the present ones.
/// Test: `render_omits_absent_fields`, `render_is_none_when_nothing_is_known`.
/// Render the present fields as trailer lines, in a fixed order.
///
/// Why: a fixed order makes the footer diffable across commits, and rendering
/// only present fields is what keeps an unknown value from becoming a stated
/// zero.
/// What: `Tokens-In`, `Tokens-Out`, `Tokens-Window`, `Savings`, `Model`, one
/// `Key: value` line each, newline-separated with no trailing newline. The
/// window line follows the counts it scopes and appears only when there is a
/// count to scope. A model id carrying a newline (which would break the block
/// into two paragraphs) is rejected rather than sanitised — no store writes
/// one, and silently rewriting an id would make a wrong value look right.
/// `None` when nothing is known.
/// Test: `render_omits_absent_fields`, `render_is_none_when_nothing_is_known`,
/// `render_rejects_a_multiline_model_id`,
/// `render_states_the_window_when_the_fold_was_truncated`,
/// `render_omits_the_window_when_the_fold_read_everything`.
/// Describe the tail window the token counts were folded from.
///
/// Why: a commit footer is read by people, and `8388608` states the same fact
/// far worse than `8 MiB`. The byte form is kept for a cap that is not a whole
/// number of MiB, so the label never rounds a size into a lie.
/// What: `last <n> MiB of a larger transcript`, or the byte count when the size
/// is not a whole multiple of a MiB.
/// Test: `render_states_the_window_when_the_fold_was_truncated`,
/// `window_label_falls_back_to_bytes_off_a_mib_boundary`.
/// Put `trailers` into `message` where git will parse them as a trailer block.
///
/// Why/What: see the module doc for the three placement facts. The insert lands
/// after the last content line above the scissors, separated by one blank line,
/// so the block is its own final paragraph of the cleaned-up message. Already
/// carrying a `Tokens-In:` line, `message` is returned unchanged — `git commit
/// --amend` re-runs `prepare-commit-msg` over a message the hook already
/// stamped, and a second block would be the one git then parses.
/// Test: `git_interpret_trailers_parses_the_appended_block`,
/// `append_goes_above_a_trailing_comment_block`,
/// `append_stays_above_the_scissors_line`, `append_is_idempotent`.
/// Does `message` already carry a stats footer?
///
/// Why: this is the idempotency guard WITHIN one commit source — `git commit
/// --amend` and a retried hook both re-run over a message this session already
/// stamped, and a second block would be the one git parses. It cannot tell that
/// block apart from one a cherry-pick carried in from another session, which is
/// why the `commit` source strips before it stamps rather than asking here
/// (#7249, [`stamp_policy`]).
/// Test: `append_is_idempotent`.
/// Is `line` real message content, rather than a git comment or blank?
///
/// Test: `append_goes_above_a_trailing_comment_block`.