Module: Spill::Briefs
- Defined in:
- lib/spill/briefs.rb
Overview
Builds one fact block per repo for the narrator: explicit, unambiguous statements derived from the report structure rather than rendered output, so the small on-device model can't misread "opened" as "merged" or turn a file count into invented work. Repos whose only activity is a dirty tree or an unpushed branch carry nothing worth summarizing and get no block at all — the model never sees them.
Constant Summary collapse
- FINISHED_VERBS =
{ pr_merged: "Merged PR", pr_opened: "Opened PR", pr_opened_and_merged: "Opened and merged PR", review: "Reviewed PR", issue_closed: "Closed issue", commented: "Commented on" }.freeze
- MAX_BODY_CHARS =
The on-device model has a small context window; a block that outgrows this budget keeps the facts at the end of the list — the newest commits and all the GitHub facts, which follow them — and says how many earlier ones were cut.
140- MAX_BLOCK_CHARS =
3000
Class Method Summary collapse
- .block_for(finished_facts, doing_facts) ⇒ Object
-
.body_suffix(commit) ⇒ Object
The commit body carries the why behind the subject, but only its first sentence, and only when that sentence is short: dense multi-clause bodies measurably degrade the small model's summaries (it drifts into "fixed a bunch of stuff" or refuses), while a short body sentence is what turns a cryptic "fix" subject into a real update.
-
.build(report) ⇒ Object
Returns [ [ repo, block ], ... ] — done repos first (report order, most recent first), then repos that only appear in doing.
- .capped(facts) ⇒ Object
- .doing_facts(entry, opened_refs) ⇒ Object
-
.finished_facts(entry, open_refs) ⇒ Object
A PR that was opened in the window but is still open would appear twice — "Opened PR #N" under FINISHED and "PR #N open" under IN PROGRESS — and the model reads that pairing as a merge.
- .normalize_ref(ref) ⇒ Object
- .pr_refs(groups, key, &matcher) ⇒ Object
- .summarizable?(repo, finished, report) ⇒ Boolean
- .title_suffix(title) ⇒ Object
Class Method Details
.block_for(finished_facts, doing_facts) ⇒ Object
52 53 54 55 56 57 |
# File 'lib/spill/briefs.rb', line 52 def block_for(finished_facts, doing_facts) lines = [] lines << "FINISHED" << capped(finished_facts) if finished_facts.any? lines << "IN PROGRESS" << doing_facts if doing_facts.any? lines.flatten.join("\n") end |
.body_suffix(commit) ⇒ Object
The commit body carries the why behind the subject, but only its first sentence, and only when that sentence is short: dense multi-clause bodies measurably degrade the small model's summaries (it drifts into "fixed a bunch of stuff" or refuses), while a short body sentence is what turns a cryptic "fix" subject into a real update.
113 114 115 116 117 118 119 |
# File 'lib/spill/briefs.rb', line 113 def body_suffix(commit) body = commit.extra[:body].to_s.gsub(/\s+/, " ").strip return "" if body.empty? first_sentence = body.split(/(?<=[.!?])\s/, 2).first first_sentence.length > MAX_BODY_CHARS ? "" : " — #{first_sentence}" end |
.build(report) ⇒ Object
Returns [ [ repo, block ], ... ] — done repos first (report order, most recent first), then repos that only appear in doing.
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/spill/briefs.rb', line 29 def build(report) open_refs = pr_refs(report.doing, :items) { |event| event.kind == :pr_open } opened_refs = pr_refs(report.done, :github) { |event| event.kind == :pr_opened } finished = {} report.done.each { |entry| finished[entry[:repo]] = finished_facts(entry, open_refs) } in_progress = {} report.doing.each { |entry| in_progress[entry[:repo]] = doing_facts(entry, opened_refs) } ordered = finished.keys + (in_progress.keys - finished.keys) ordered.filter_map do |repo| block = block_for(finished[repo] || [], in_progress[repo] || []) [ repo, block ] if summarizable?(repo, finished, report) end end |
.capped(facts) ⇒ Object
59 60 61 62 63 64 65 |
# File 'lib/spill/briefs.rb', line 59 def capped(facts) budget = MAX_BLOCK_CHARS kept = facts.reverse.take_while { |fact| (budget -= fact.length + 1) >= 0 }.reverse kept = facts.last(1) if kept.empty? cut = facts.size - kept.size cut.positive? ? [ "(and #{cut} earlier items not listed)" ] + kept : kept end |
.doing_facts(entry, opened_refs) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/spill/briefs.rb', line 83 def doing_facts(entry, opened_refs) entry[:items].filter_map do |event| case event.kind when :pr_open ref = normalize_ref(event.ref) if opened_refs[entry[:repo]]&.include?(ref) "Opened PR #{ref}, awaiting merge#{title_suffix(event.title)}" else "PR #{ref} still open, not merged yet#{title_suffix(event.title)}" end when :dirty_tree "Uncommitted local changes in #{event.extra[:files]} file(s)" when :branch_wip count = event.extra[:no_upstream] ? event.extra[:unpushed] : event.extra[:ahead] "Branch #{event.ref}: #{count} commit(s) not pushed yet" end end end |
.finished_facts(entry, open_refs) ⇒ Object
A PR that was opened in the window but is still open would appear twice — "Opened PR #N" under FINISHED and "PR #N open" under IN PROGRESS — and the model reads that pairing as a merge. State it once, in IN PROGRESS, as "Opened PR #N, awaiting merge".
71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/spill/briefs.rb', line 71 def finished_facts(entry, open_refs) commits = entry[:branches].flat_map do |branch| branch[:commits].map { |commit| "Commit: #{commit.title}#{body_suffix(commit)}" } end github = entry[:github].filter_map do |event| next if event.kind == :pr_opened && open_refs[entry[:repo]]&.include?(normalize_ref(event.ref)) "#{FINISHED_VERBS.fetch(event.kind)} #{normalize_ref(event.ref)}#{title_suffix(event.title)}" end commits + github end |
.normalize_ref(ref) ⇒ Object
121 122 123 |
# File 'lib/spill/briefs.rb', line 121 def normalize_ref(ref) ref.to_s.delete_prefix("#").prepend("#") end |
.pr_refs(groups, key, &matcher) ⇒ Object
102 103 104 105 106 |
# File 'lib/spill/briefs.rb', line 102 def pr_refs(groups, key, &matcher) groups.to_h do |entry| [ entry[:repo], entry[key].select(&matcher).map { |event| normalize_ref(event.ref) } ] end end |
.summarizable?(repo, finished, report) ⇒ Boolean
45 46 47 48 49 50 |
# File 'lib/spill/briefs.rb', line 45 def summarizable?(repo, finished, report) return true if finished[repo]&.any? entry = report.doing.find { |e| e[:repo] == repo } entry ? entry[:items].any? { |event| event.kind == :pr_open } : false end |
.title_suffix(title) ⇒ Object
125 126 127 |
# File 'lib/spill/briefs.rb', line 125 def title_suffix(title) title.to_s.empty? ? "" : ": #{title}" end |