Repository navigation
Expand file tree
/
Copy pathaccount.go
More file actions
551 lines (519 loc) · 22.3 KB
/
Copy pathaccount.go
File metadata and controls
551 lines (519 loc) · 22.3 KB
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
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
package exec
import (
"fmt"
"sort"
"strings"
)
// Account is a worker's own account of the work it did, in the one shape every
// reader downstream needs and none of them can reconstruct.
//
// It exists because of a measured, expensive silence. A subharness that drives
// a whole pipeline behind a process boundary used to hand back one sentence and
// a bill, and when the pipeline ended without a verdict of its own the sentence
// was "it ended without saying how it went" — a void. Three readers then had to
// act on that void: the delivery gate judged it and reacted differently every
// time, the remainder judge could not tell finished work from unstarted work
// and added children to re-investigate what was already done, and the person
// reading the record was told nothing at all. Between 48% and 57% of a run's
// cost was measured going into that re-investigation.
//
// So the account is structural rather than prose: the files the work changed
// with the kind of change and its size, the commands it issued itself, the
// checks the closing photograph ran with what each one found, and the last thing
// the worker said for itself. Nothing here is interpreted — every field is a
// measured fact no reader further down could observe for itself — and nothing
// here is worker-specific. Any subharness that owns a verifier can fill it in,
// which is why it is named for what it is rather than for who writes it first.
//
// A nil Account is the ordinary case: a worker that photographs nothing leaves
// it empty, and an empty account reads as "no claim" everywhere it is rendered.
type Account struct {
// Files is what the work changed, one row per path, merged across however
// many times the worker touched it.
Files []FileChange
// Checks is what the worker's own verifier ran, as of the last time it ran.
// It is replaced rather than appended by a repeated verification pass: a
// suite that ran four times reports the state of the fourth run, and the
// three before it are history rather than evidence.
Checks []Check
// Commands is the bounded list of shell commands the leaf ITSELF ran. It is
// distinct from Checks, which is the closing photograph's reading of the
// finished tree: no command here is evidence that anything passed because
// nothing parsed its output, and Verified does not read this field.
Commands []string
// CommandsRun is how many commands the leaf itself ran in total, including
// the earlier commands omitted from the bounded list above.
CommandsRun int
// Final is the last thing the worker said about the whole job, verbatim.
Final string
// Unread is why the finished tree could not be read, when something was
// asked of it and the answer could not be understood — a strategy that
// would not start a second time, a suite that failed to collect, a command
// killed at its ceiling before it named a check. Empty when nothing was
// attempted, and that emptiness is what tells [Account.rows] apart the two
// worlds an absent Checks list used to collapse. It is
// Verification.Unread's own sentence, carried rather than recomposed, so
// the account and the reading cannot come to say different things about the
// same failure.
Unread string
// Range is the two commits this account's change set was measured between,
// when it was measured from the repository rather than narrated by the
// worker as it went. See [Range] for why the difference is the whole point.
Range Range
// Patch is where the change set's own text lives: a path to the diff, in
// full, written under the harness's own directory for this node.
//
// It is a handle rather than the bytes because a diff is unbounded and
// every reader downstream is bounded — but it is a handle to CONTENT, which
// is the thing the file list never was. A judge asked whether the
// deliverable's account of the change is true can read the change; a method
// writer handed the goal of describing it can read it instead of inferring
// it. Empty when nothing was derived, which reads as no claim.
Patch string
// Withheld is where this change set actually IS, when it is not in the
// workspace anybody else can open: the directory of the leaf's own view and
// the branch its work sits on. Empty is the ordinary case and means the
// change set is in the shared tree.
//
// It exists because a change set has two properties that were being read as
// one. Whether work was DONE is answered by the repository — files, a range,
// a diff — and stayed true of a leaf whose delivery gate failed, because the
// leaf really had written all of it. Whether the work is WHERE ANYBODY CAN
// SEE IT is a different question with a different answer, and nothing asked
// it. A measured run finished a complete implementation inside its view,
// failed the gate, merged nothing, and ended with an empty workspace and no
// sentence anywhere naming the checkout the work was in: 67% of that run's
// spend, on files the person never saw.
Withheld string
WithheldBranch string
}
// WithheldWords is the one sentence a person is owed about work that is real,
// finished and not in their tree — where it is, and the two things they can do
// with it. Empty when nothing was withheld.
//
// It is a method rather than a stored string so there is one wording of it, and
// so the fact and the sentence cannot drift apart: everything it says is read
// off the fields above.
func (a *Account) WithheldWords() string {
if a == nil || strings.TrimSpace(a.Withheld) == "" {
return ""
}
words := fmt.Sprintf("This work is written and it is not in your workspace: %d file(s) are in %s",
len(a.Files), a.Withheld)
if branch := strings.TrimSpace(a.WithheldBranch); branch != "" {
words += ", on branch " + branch + " — `git merge " + branch + "` brings it over"
}
return words + "."
}
// Range is the span of repository history one node's work occupies: the commit
// its own change set is measured from, and the commit it is measured to.
//
// It exists because the two accounts of what a coding leaf changed were not the
// same account. One was narration — the file rows the engine published on the
// wire as each tool call finished — and it was per-PASS: a second attempt at
// the same node reported only what the second attempt touched, so a repair
// round that rewrote prose over a landed diff reported a change set of nothing.
// The other was git: a before/after read of the shared workspace, keyed by leaf
// and therefore correct across every pass. The second one is the truth, and
// this is the handle that lets it be stated rather than recomputed differently
// by each reader.
//
// Base is durable per NODE and not per pass. It is written to a git ref the
// first time the node opens a view and read back on every later one, so it
// survives a process restart with no in-memory carry at all — which is the only
// way an account of "what this node changed" can outlive the run that made it.
type Range struct {
Base, Head string
}
// Derived reports that this range was actually measured. An empty base or head
// is a range nobody could compute, and it must not read as "measured, and the
// answer was nothing".
func (r Range) Derived() bool {
return strings.TrimSpace(r.Base) != "" && strings.TrimSpace(r.Head) != ""
}
// Landed reports that this account's change set came out of the repository and
// is not empty: the work is on disk, in commits, and can be read by anyone.
//
// It is the fact the repair path turns on. A gate that fails a finished coding
// leaf for what its PROSE did not say used to buy a second run of the whole
// engine, into a tree where the change had already landed — 23 model calls and
// zero edits, because there was nothing left to do. This is how that case is
// recognised without asking a model: the substrate says the work exists.
func (a *Account) Landed() bool {
// Withheld is the third clause and it is not a detail. The other two say the
// change set was measured and is not empty; both are true of work sitting on
// a branch in a checkout nobody else can open, and a reader told that such
// work has landed goes on to write prose about a repository that does not
// hold it. Composable is exactly that reader.
return a != nil && a.Range.Derived() && len(a.Files) > 0 &&
strings.TrimSpace(a.Withheld) == ""
}
// ChangeRange names the span in one clause for a reader who is about to be shown
// its contents, so a truncated diff can still be placed in the repository's own
// history. Empty when nothing was measured.
func (a *Account) ChangeRange() string {
if a == nil || !a.Range.Derived() {
return ""
}
return "git diff " + shortCommit(a.Range.Base) + ".." + shortCommit(a.Range.Head)
}
// shortCommit is git's own abbreviation, which is what a person reading a range
// expects to see and what they can paste back into git.
func shortCommit(sha string) string {
sha = strings.TrimSpace(sha)
if len(sha) > 12 {
return sha[:12]
}
return sha
}
// FileChange is one path the work touched: what kind of change it was, and how
// large. The line counts are the worker's own, taken from the patch it applied,
// rather than a diff computed afterwards by somebody who was not there.
type FileChange struct {
Path string
Change string
Added int
Removed int
}
// The change kinds. They are the vocabulary a reader of the account sees, and
// they are deliberately the plain English words rather than git's letters: the
// account is read by a model and by a person, and neither of them should have
// to be told what "M" means.
const (
ChangeAdded = "added"
ChangeChanged = "changed"
ChangeMoved = "moved"
ChangeDeleted = "deleted"
)
// changeRank orders the kinds so a path touched several times keeps the honest
// word for what happened to it overall. A file created and then edited was
// added; a file edited and then deleted is gone, whatever was done to it on the
// way; a file moved and then edited moved.
func changeRank(change string) int {
switch change {
case ChangeDeleted:
return 4
case ChangeAdded:
return 3
case ChangeMoved:
return 2
case ChangeChanged:
return 1
}
return 0
}
// Check is one command the worker's verifier ran and what the process said.
//
// Known is the one field that is not simply the exit status: a check that came
// back red and was red in exactly the same places before the work began has not
// been broken by this work. The worker is the only thing in the system that
// photographed the repository beforehand, so it is the only thing that can say
// so, and a reader that treated a known-red suite as this work's failure is the
// measured way correct work gets thrown away.
type Check struct {
Command string
Kind string
Passed bool
Known bool
Tail string
}
// settled reports whether a check is evidence for the work rather than against
// it: it passed, or it failed in a place that was already failing.
func (c Check) settled() bool { return c.Passed || c.Known }
// Empty reports that there is nothing here to show. A nil account is empty, so
// every caller can ask without guarding first.
func (a *Account) Empty() bool {
// AN ACCOUNT EXISTS BECAUSE THERE WERE FILES OR CHECKS. Commands ride one
// that already exists and never turn an otherwise empty account into a claim.
return a == nil || (len(a.Files) == 0 && len(a.Checks) == 0 && strings.TrimSpace(a.Final) == "")
}
// Verified reports that the worker ran checks and every one of them settled.
// It is the fact the remainder judge exists to be told: a node whose suite is
// green is finished, and a child added to re-run those tests is money spent
// proving something already proved.
//
// A worker that ran no checks answers no, because "nothing was checked" and
// "everything passed" are the two answers a silence used to collapse into.
func (a *Account) Verified() bool {
if a == nil || len(a.Checks) == 0 {
return false
}
for _, check := range a.Checks {
if !check.settled() {
return false
}
}
return true
}
// Stat is the diff in three numbers: how many paths, and the lines added and
// removed across all of them.
func (a *Account) Stat() (files, added, removed int) {
if a == nil {
return 0, 0, 0
}
for _, file := range a.Files {
added += file.Added
removed += file.Removed
}
return len(a.Files), added, removed
}
// Note records one file the work touched, merging it with whatever is already
// known about that path. The line counts accumulate because each call is a
// separate edit that really did add and remove those lines; the kind takes the
// strongest word, which is what changeRank is for.
func (a *Account) Note(path, change string, added, removed int) {
if a == nil {
return
}
if path = strings.TrimSpace(path); path == "" {
return
}
for index := range a.Files {
if a.Files[index].Path != path {
continue
}
a.Files[index].Added += added
a.Files[index].Removed += removed
if changeRank(change) > changeRank(a.Files[index].Change) {
a.Files[index].Change = change
}
return
}
if change == "" {
change = ChangeChanged
}
a.Files = append(a.Files, FileChange{Path: path, Change: change, Added: added, Removed: removed})
}
// SetFiles replaces the change set outright, and it is the counterpart to Note
// rather than a convenience beside it.
//
// Note accumulates because each call is a separate edit that really did add
// those lines, and that is the right law for narration arriving on a wire. It
// is the wrong law for a derivation: a change set computed from two commits is
// already the whole answer, and merging it into whatever the narration had said
// would double every line it agrees with. So the two channels do not mix — the
// derived one, when there is one, is the account.
func (a *Account) SetFiles(files []FileChange) {
if a == nil {
return
}
a.Files = files
}
// accountFileRows and accountTailBytes bound what an account may say.
//
// They are absolute rather than a share of anybody's window, for the reason
// Outcome.Ran's bounds are: this is not the worker's own memory but a
// fixed-size record handed to somebody else — a judge, a reviser, a person
// reading a job's summary — and what bounds it is that reader's patience. A
// refactor that touched four hundred files is a real outcome; four hundred rows
// in three dependents' contexts is not, and the count still tells the truth.
const (
accountFileRows = 20
accountTailBytes = 200
)
// FileLines renders the changed files as evidence rows: the arithmetic first,
// then one row per path, then the count of whatever was left out. Empty when
// the work changed nothing, which is itself a fact and belongs to whoever is
// composing rather than being invented here.
func (a *Account) FileLines() []string {
if a == nil || len(a.Files) == 0 {
return nil
}
files := append([]FileChange(nil), a.Files...)
sort.Slice(files, func(i, j int) bool { return files[i].Path < files[j].Path })
count, added, removed := a.Stat()
lines := []string{fmt.Sprintf("%s changed, %+d %+d lines", plural(count, "file"), added, -removed)}
shown := files
if len(shown) > accountFileRows {
shown = shown[:accountFileRows]
}
for _, file := range shown {
lines = append(lines, fmt.Sprintf(" %s %s (%+d %+d)",
file.Change, file.Path, file.Added, -file.Removed))
}
if left := len(files) - len(shown); left > 0 {
lines = append(lines, fmt.Sprintf(" and %d more, named in the run's own record", left))
}
return lines
}
// CommandLines renders what the leaf itself ran, separately from the closing
// photograph's checks. The total names any earlier commands the bounded record
// left out, so its tail cannot be mistaken for the whole run.
func (a *Account) CommandLines() []string {
if a == nil || len(a.Commands) == 0 {
return nil
}
lines := make([]string, 0, len(a.Commands)+1)
for _, command := range a.Commands {
lines = append(lines, " "+command)
}
if earlier := a.CommandsRun - len(a.Commands); earlier > 0 {
lines = append(lines, fmt.Sprintf(" and %d earlier commands, in the run's own record", earlier))
}
return lines
}
// CheckLines renders the verification story as evidence rows: one line per
// command, the verdict first so a reader scanning the left edge sees what
// happened before it sees what was run.
//
// A failure that was already there before the work began says so on its own
// line rather than being dropped. Dropping it would make the account claim a
// green suite over a suite anyone can watch failing, which is the fastest way
// to teach a reader that the account cannot be trusted.
func (a *Account) CheckLines() []string {
if a == nil || len(a.Checks) == 0 {
return nil
}
lines := make([]string, 0, len(a.Checks))
for _, check := range a.Checks {
verdict := "failed"
switch {
case check.Passed:
verdict = "passed"
case check.Known:
verdict = "failed, and was already failing before this work began"
}
line := " " + verdict + ": " + check.Command
if !check.Passed && strings.TrimSpace(check.Tail) != "" {
line += " — " + snip(strings.TrimSpace(firstAccountLine(check.Tail)), accountTailBytes)
}
lines = append(lines, line)
}
return lines
}
// Lines is the whole account as evidence rows, headed so each block says what
// it is. This is what the delivery gate is handed: rows, in the same grammar as
// the run tail beside them, and never a paragraph explaining what to make of
// them — what to make of them is the gate's job and the gate's alone.
//
// The worker's own last word is included HERE and nowhere else, because the
// gate is often judging a deliverable that has been composed since — a
// synthesis, a repair pass, a parent's answer — and the sentence the worker
// signed off with may no longer be anywhere in it.
func (a *Account) Lines() []string { return a.rows(true) }
// Report is the same account as a block of text, for the leaf's own
// deliverable. It is the account and nothing else — no verdict, no advice —
// because the sentence above it already said how the run ended and this is the
// evidence for it.
//
// It leaves the worker's last word out for exactly that reason: the leaf's own
// text OPENS with it, and an account that repeated it would put the same
// paragraph twice into every context the deliverable travels through.
func (a *Account) Report() string {
lines := a.rows(false)
if len(lines) == 0 {
return ""
}
return strings.Join(lines, "\n")
}
// accountFinalBytes bounds the worker's last word where it is quoted. The whole
// of it is the deliverable itself; this is the copy that rides beside the diff.
const accountFinalBytes = 600
// noCheckWords is the one spelling of the measured fact that nothing checked
// work which changed the tree.
const noCheckWords = "no check was run on the finished tree"
// rows is the one renderer both readers share, differing only in whether the
// worker's sign-off belongs in this particular copy.
func (a *Account) rows(signoff bool) []string {
if a.Empty() {
return nil
}
var lines []string
if files := a.FileLines(); len(files) > 0 {
lines = append(lines, "What the work changed:")
lines = append(lines, files...)
}
if commands := a.CommandLines(); len(commands) > 0 {
if len(lines) > 0 {
lines = append(lines, "")
}
lines = append(lines, "What the work ran itself:")
lines = append(lines, commands...)
}
if checks := a.CheckLines(); len(checks) > 0 {
if len(lines) > 0 {
lines = append(lines, "")
}
lines = append(lines, "What the work ran to check itself, and what each one found:")
lines = append(lines, checks...)
} else if len(a.Files) > 0 {
// AN ABSENT CHECKS LIST IS TWO DIFFERENT WORLDS AND THEY GET DIFFERENT
// SENTENCES. Nothing was asked of the finished tree — no suite, no
// strategy — and that is a measured absence: the work changed the tree,
// nothing checked it, and omitting that would make the deliverable the
// same claim with the evidence removed. This is the one deliberate
// exception to the emptiness law in this file.
//
// But something asked and could not be read is an UNKNOWN, and saying
// "no check was run" of it is the failure the emptiness law exists to
// prevent, inverted — a guess wearing the words of a measurement. Worse,
// the same outcome carries Verification.Unread, and a reader gets both:
// the revision pass renders that sentence while the gate renders these
// lines, so the record would say nothing checked it beside the thing
// that checked it could not be read. Unread's own sentence is said
// here instead, once.
if len(lines) > 0 {
lines = append(lines, "")
}
words := noCheckWords
if unread := strings.TrimSpace(a.Unread); unread != "" {
words = unread
}
lines = append(lines, "What was run to check this work:", " "+words)
}
if final := strings.TrimSpace(a.Final); signoff && final != "" {
if len(lines) > 0 {
lines = append(lines, "")
}
lines = append(lines, "What the work said for itself when it finished:",
snip(final, accountFinalBytes))
}
return lines
}
// Summary is the account in one clause, for a reader with room for a clause
// rather than a block: the plan's own state view, where every node gets one
// line and thirty nodes share a budget.
func (a *Account) Summary() string {
if a.Empty() {
return ""
}
var parts []string
if count, added, removed := a.Stat(); count > 0 {
parts = append(parts, fmt.Sprintf("%s changed, %+d %+d lines",
plural(count, "file"), added, -removed))
}
if len(a.Checks) > 0 {
commands := make([]string, 0, len(a.Checks))
for _, check := range a.Checks {
commands = append(commands, check.Command)
}
if a.Verified() {
parts = append(parts, "its own checks passed: "+strings.Join(commands, ", "))
} else {
parts = append(parts, "its own checks did not pass: "+strings.Join(commands, ", "))
}
} else if len(a.Files) > 0 {
// [Account.rows]'s rule, in a clause: the measured absence is said as
// itself, and a reading that could not be read says so instead.
if unread := strings.TrimSpace(a.Unread); unread != "" {
parts = append(parts, unread)
} else {
parts = append(parts, noCheckWords)
}
}
return strings.Join(parts, "; ")
}
// plural writes a count with its noun, so no caller has to compose "1 files".
func plural(count int, noun string) string {
if count == 1 {
return "1 " + noun
}
return fmt.Sprintf("%d %ss", count, noun)
}
// firstAccountLine keeps a command's tail to its first line. A test runner's
// output is many lines and the row it goes into is one; the whole of it is in
// the run's own record for whoever needs it.
func firstAccountLine(body string) string {
line, _, _ := strings.Cut(strings.TrimSpace(body), "\n")
return line
}