@heyquilt/seam 0.27.0 → 0.131.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/bin/seam.js +118 -17
  2. package/lib/quilts/agent-tokens.js +148 -0
  3. package/lib/quilts/archiver.js +241 -0
  4. package/lib/quilts/attest.js +212 -0
  5. package/lib/quilts/bisect-run.js +3 -2
  6. package/lib/quilts/block.js +497 -55
  7. package/lib/quilts/cas.js +78 -0
  8. package/lib/quilts/cbor.js +2 -0
  9. package/lib/quilts/claims.js +409 -0
  10. package/lib/quilts/cli.js +612 -69
  11. package/lib/quilts/client.js +306 -19
  12. package/lib/quilts/conflict-format.js +266 -0
  13. package/lib/quilts/daemon.js +174 -0
  14. package/lib/quilts/describe.js +296 -38
  15. package/lib/quilts/dict.js +34 -0
  16. package/lib/quilts/exit-archive.js +184 -0
  17. package/lib/quilts/export-format.js +52 -0
  18. package/lib/quilts/export-git.js +27 -3
  19. package/lib/quilts/export-lfs.js +52 -0
  20. package/lib/quilts/export-sign.js +49 -0
  21. package/lib/quilts/fastcdc.js +186 -0
  22. package/lib/quilts/hash.js +17 -2
  23. package/lib/quilts/ignored.js +46 -0
  24. package/lib/quilts/import-git.js +54 -2
  25. package/lib/quilts/journal.js +105 -0
  26. package/lib/quilts/limits.js +197 -0
  27. package/lib/quilts/linepolicy.js +109 -0
  28. package/lib/quilts/pack.js +146 -18
  29. package/lib/quilts/persona.js +70 -0
  30. package/lib/quilts/records.js +7 -3
  31. package/lib/quilts/recover.js +37 -0
  32. package/lib/quilts/runner.js +79 -12
  33. package/lib/quilts/sealed.js +231 -0
  34. package/lib/quilts/secrets.js +86 -0
  35. package/lib/quilts/signing.js +7 -3
  36. package/lib/quilts/tarball.js +107 -0
  37. package/lib/quilts/textdiff.js +7 -5
  38. package/lib/quilts/tools.js +404 -83
  39. package/lib/quilts/tree.js +351 -0
  40. package/lib/quilts/want.js +51 -0
  41. package/lib/quilts/webhook.js +116 -0
  42. package/package.json +3 -2
@@ -14,27 +14,41 @@ export function describeSquare (s) {
14
14
  if (s.behind_by) bits.push(`${s.behind_by} patch${s.behind_by === 1 ? '' : 'es'} behind ${s.line}`)
15
15
  if (s.conflicted_paths) bits.push(`${s.conflicted_paths} conflicted path${s.conflicted_paths === 1 ? '' : 's'}`)
16
16
  }
17
- bits.push(`held by ${s.holder}`)
17
+ bits.push(`held by ${s.holder_name || s.holder}`)
18
18
  return bits.join(' · ')
19
19
  }
20
20
 
21
- export function describeLog (r, { oneline = false } = {}) {
22
- if (!r.commits.length) return 'No commits.'
21
+ export function describeLog (r, { oneline = false, graph = false } = {}) {
22
+ const more = r.cursor ? `(more: seam log … --cursor ${r.cursor})` : r.more ? (oneline ? '…' : '(more: seam log -n <count>)') : null
23
+ if (r.groups) {
24
+ // group_by: git shortlog -s, with lines added and removed.
25
+ if (!r.groups.length) return 'No commits.'
26
+ const w = Math.max(...r.groups.map((g) => String(g.commits).length))
27
+ return [...r.groups.map((g) => `${String(g.commits).padStart(w + 2)} ${g.key == null ? '(none)' : g.name ? `${g.name} <${g.key}>` : g.key} +${g.added} -${g.removed}`), ...(more ? [`${r.partial ? '(partial) ' : ''}${more}`] : [])].join('\n')
28
+ }
29
+ if (!r.commits.length) return more ? `No commits on this page.\n${more}` : r.existed === false ? 'No commits: this path was never here.' : 'No commits.'
30
+ const mark = (c) => graph ? (c.parents.length > 1 ? '* ' : c.hidden ? 'x ' : '* ') : ''
31
+ const merged = (c) => (c.parent_squares || []).map((s) => `${graph ? '|\\ ' : ''}merged ${s.square_id ? `square ${s.square_id.slice(0, 8)}${s.title ? ` "${s.title}"` : ''}` : s.commit.slice(0, 12)}`)
23
32
  const lines = r.commits.map((c) => oneline
24
- ? `${c.short} ${c.patch ? `[${c.patch.line}:${c.patch.patch_no}] ` : ''}${first(c.message)}${r.follow && c.path && c.path !== r.follow ? ` (${c.path})` : ''}${c.signature ? ` (signature: ${c.signature.status})` : ''}`
25
- : [`commit ${c.id}${c.patch ? ` (${patchName(c.patch)})` : ''}`, `Square: ${c.square}`, ...(r.follow && c.path && c.path !== r.follow ? [`Path: ${c.path}`] : []), `Author: ${c.author.name} <${c.author.member_id}>${c.author.tool ? ` via ${c.author.tool}` : ''}`, ...(c.signature ? [describeSignature(c.signature)] : []), `Date: ${when(c.authored_at)}`, '', ...String(c.message || '').split('\n').map((l) => ` ${l}`), ''].join('\n'))
26
- if (r.more) lines.push(oneline ? '…' : '(more: seam log -n <count>)')
33
+ ? [`${mark(c)}${c.short} ${c.patch ? `[${c.patch.line}:${c.patch.patch_no}] ` : ''}${first(c.message)}${r.follow && c.path && c.path !== r.follow ? ` (${c.path})` : ''}${c.hidden ? ` (hidden: replaced by ${c.predecessor_of.slice(0, 12)})` : ''}${c.signature ? ` (signature: ${c.signature.status})` : ''}`, ...merged(c)].join('\n')
34
+ : [`${mark(c)}commit ${c.id}${c.patch ? ` (${patchName(c.patch)})` : ''}${c.hidden ? ` (hidden: replaced by ${c.predecessor_of.slice(0, 12)})` : ''}`, ...(c.parents.length > 1 ? [`Merge: ${c.parents.map((p) => p.slice(0, 12)).join(' ')}`, ...merged(c)] : []), `Square: ${c.square}`, ...(r.follow && c.path && c.path !== r.follow ? [`Path: ${c.path}`] : []), `Author: ${c.author.name} <${c.author.member_id}>${c.author.tool ? ` via ${c.author.tool}` : ''}${c.on_behalf_of ? ` for ${c.on_behalf_of}` : ''}`, ...(c.task_id ? [`Task: ${c.task_id}`] : []), ...(c.signature ? [describeSignature(c.signature)] : []), `Date: ${when(c.authored_at)}`, '', ...String(c.message || '').split('\n').map((l) => ` ${l}`), ''].join('\n'))
35
+ if (more) lines.push(`${r.partial ? '(partial) ' : ''}${more}`)
36
+ if (r.last_present !== undefined) lines.push(`This path is gone: it was last here in patch ${r.last_present}.`)
27
37
  return lines.join('\n')
28
38
  }
29
39
 
30
40
  export function describeDiff (d, { format = 'unified' } = {}) {
31
- if (!d.files.length) return 'No differences.'
32
- if (format === 'names') return d.files.map((f) => `${f.status[0].toUpperCase()}\t${f.path}`).join('\n')
41
+ const more = d.cursor ? `${format === 'unified' ? '' : '\n'}(more files: seam diff … --cursor ${d.cursor})` : ''
42
+ if (d.rename_limit_hit) d = { ...d, note: '(too many files to look for renames: only exact renames are shown)' }
43
+ if (!d.files.length) return 'No differences.' + more
44
+ const name = (f) => f.old_path ? `${f.old_path} => ${f.path}` : f.path
45
+ const tail = (d.note ? `\n${d.note}` : '') + more
46
+ if (format === 'names') return d.files.map((f) => f.old_path ? `R${Math.round((f.similarity ?? 1) * 100)}\t${f.old_path}\t${f.path}` : `${f.status[0].toUpperCase()}\t${f.path}`).join('\n') + tail
33
47
  if (format === 'stat') {
34
- const w = Math.max(...d.files.map((f) => f.path.length))
35
- return [...d.files.map((f) => ` ${f.path.padEnd(w)} | ${f.binary ? 'Bin' : `${(f.add || 0) + (f.del || 0)} ${'+'.repeat(Math.min(f.add || 0, 40))}${'-'.repeat(Math.min(f.del || 0, 40))}`}`), ` ${d.files.length} file${d.files.length === 1 ? '' : 's'} changed, ${d.add || 0} insertions(+), ${d.del || 0} deletions(-)`].join('\n')
48
+ const w = Math.max(...d.files.map((f) => name(f).length))
49
+ return [...d.files.map((f) => ` ${name(f).padEnd(w)} | ${f.binary ? 'Bin' : `${(f.add || 0) + (f.del || 0)} ${'+'.repeat(Math.min(f.add || 0, 40))}${'-'.repeat(Math.min(f.del || 0, 40))}`}`), ` ${d.files.length} file${d.files.length === 1 ? '' : 's'} changed, ${d.add || 0} insertions(+), ${d.del || 0} deletions(-)`].join('\n') + tail
36
50
  }
37
- return d.files.map((f) => f.binary ? `Binary file ${f.path} ${f.status}\n` : f.patch || `${f.path} ${f.status}\n`).join('')
51
+ return d.files.map((f) => f.binary ? `Binary file ${name(f)} ${f.status}\n` : f.patch || `${f.path} ${f.status}\n`).join('') + tail
38
52
  }
39
53
 
40
54
  export function describeShow (r, { format = 'unified' } = {}) {
@@ -46,11 +60,20 @@ export function describeShow (r, { format = 'unified' } = {}) {
46
60
  head.push(`Date: ${when(c.authored_at)}`)
47
61
  if (c.predecessor) head.push(`Rewrites: ${c.predecessor.slice(0, 12)}`)
48
62
  if (c.conflicted) head.push(`Conflicted paths in this commit: ${c.conflicted}`)
63
+ if (r.class) head.push(`Class: ${r.class.class}${r.class.flags.length ? ` (${r.class.flags.join(', ')})` : ''}`)
64
+ const a = r.annotations
65
+ if (a) {
66
+ if (a.merged) head.push(`Merged: ${a.merged.line}:${a.merged.patch_no}${a.merged_by ? ` by ${a.merged_by}` : ''}`)
67
+ if (a.hidden) head.push('Hidden: rewritten since (amend or rebase)')
68
+ if (a.checks.length) head.push(`Checks: ${a.checks.map((c) => `${c.name} ${c.verdict || c.state}`).join(', ')}`)
69
+ for (const p of a.picked_to) head.push(`Picked: ${describePick(p)}`)
70
+ }
49
71
  head.push('', ...String(c.message || '').split('\n').map((l) => ` ${l}`), '')
50
72
  return head.join('\n') + (r.diff ? '\n' + describeDiff(r.diff, { format }) : '')
51
73
  }
52
74
 
53
75
  export function describeBlame (r) {
76
+ if (r.reverse) return r.lines.map((l) => `${`${r.reverse.line}:${l.last_patch}`.padEnd(12)} ${String(l.line).padStart(4)}) ${l.text}`).join('\n')
54
77
  const w = Math.max(...r.lines.map((l) => (l.author || '').length), 4)
55
78
  return r.lines.map((l) => `${(l.short || '?').slice(0, 10)} ${l.patch ? `${l.patch.line}:${l.patch.patch_no}`.padEnd(9) : (l.square || '').padEnd(9)} (${(l.author || '?').padEnd(w)} ${when(l.at).slice(0, 10)} ${String(l.line).padStart(4)}) ${l.text}`).join('\n')
56
79
  }
@@ -67,14 +90,26 @@ export function describeSquareStatus (s, { prefix = '' } = {}) {
67
90
  const wc = s.would_conflict || []
68
91
  if (wc.length) lines.push(`Would conflict if merged now: ${wc.map((w) => `${w.path}${w.patch_no ? ` (with patch ${w.patch_no} by ${w.author})` : ''}`).join(', ')}. Rebase (seam rebase) to settle it in your square first.`)
69
92
  else if (under.length) lines.push('Would merge cleanly now.')
93
+ for (const c of s.claims_overlap || []) lines.push(`Claimed by ${c.holder} in ${c.square}${c.task_id ? ` (task ${c.task_id})` : ''}: ${c.pattern}${c.note ? ` (${c.note})` : ''}, over ${[...c.paths, ...(c.your_claims || []).map((p) => `your claim ${p}`)].join(', ')}. Ask them to hand it over or work on other files.`)
94
+ lines.push(...describeWarnings(s.warnings))
70
95
  return lines.join('\n')
71
96
  }
72
97
 
98
+ /** 07's symbol warnings, one line each: what changed under the square's code. They never refuse a merge. */
99
+ export function describeWarnings (warnings = []) {
100
+ const said = (warnings || []).filter((w) => w.kind === 'symbol_overlap' || w.kind === 'caller_of_changed').map((w) => w.kind === 'symbol_overlap'
101
+ ? ` ${w.symbol} in ${w.path}: you changed it, and patch ${w.with.patch_no} by ${w.with.author} changed its ${w.change === 'removed' ? 'existence (removed it)' : w.change}`
102
+ : ` ${w.symbol}, which ${w.path} now calls: patch ${w.with.patch_no} by ${w.with.author} changed its ${w.change === 'removed' ? 'existence (removed it)' : w.change} in ${w.defined_in}`)
103
+ return said.length ? ['Changed under your code (a warning, not a refusal: check it still fits):', ...said] : []
104
+ }
105
+
73
106
  export function describeStatus (st) {
74
107
  const lines = []
75
108
  if (st.square) lines.push(describeSquareStatus(st.square, { prefix: 'On square ' }))
109
+ else if (st.on_line) lines.push(`On ${st.line} at ${st.at ? st.at.slice(0, 12) : '(empty)'}: your next commit opens a square for it (seam pull brings ${st.line}'s new patches in).`)
76
110
  else lines.push(`A read-only checkout of ${st.at ? st.at.slice(0, 12) : 'an empty line'}`)
77
111
  lines.push(`Block: ${st.paths.map((p) => p || '/').join(', ')} at ${st.at ? st.at.slice(0, 12) : '(empty)'}`)
112
+ if (st.square_closed) lines.push(`This square is ${st.square_closed}: seam pull moves the folder to ${st.square.line}'s tip.`)
78
113
  if (st.stale) lines.push('The square moved on since this folder last looked: run seam checkout ' + st.square.short + ' after committing.')
79
114
  if (st.conflicted.length) lines.push(`Conflicted (edit and commit without the markers, or seam resolve --ours|--theirs): ${st.conflicted.join(', ')}`)
80
115
  if (!st.changes.length) lines.push('Nothing changed since the last commit.')
@@ -82,6 +117,11 @@ export function describeStatus (st) {
82
117
  return lines.join('\n')
83
118
  }
84
119
 
120
+ /** "Opened sq:… "title" on main for this change." when a commit in a clone opened its square. */
121
+ export function describeOpened (r) {
122
+ return r.opened ? `Opened ${r.opened.short} "${r.opened.title}" on ${r.opened.line} for this change.\n` : ''
123
+ }
124
+
85
125
  export function describeWrite (r, verb = 'Done') {
86
126
  const c = r.result
87
127
  const bits = [`${verb}${c && c.id ? ` ${c.short}` : ''}${c && c.message ? ` "${first(c.message)}"` : ''}`]
@@ -89,19 +129,30 @@ export function describeWrite (r, verb = 'Done') {
89
129
  if (r.conflicted) bits.push(`${r.conflicted} conflicted path${r.conflicted === 1 ? '' : 's'}: ${(r.terms || []).map((t) => `${t.path} (${t.kind})`).join(', ')}`)
90
130
  const reused = (r.combined || []).filter((c) => c.how === 'reuse')
91
131
  if (reused.length) bits.push(`Settled the way it was settled before: ${reused.map((c) => `${c.path} (by ${c.by})`).join(', ')}`)
132
+ for (const w of r.warnings || []) bits.push(`Warning: ${w.path}${w.line ? `:${w.line}` : ''} looks like a secret (${w.rule}, ${w.id})${w.local ? ', found on this computer' : ''}`)
92
133
  return bits.join('\n')
93
134
  }
94
135
 
136
+ /** TODO 0.9: the patch took the square's title, and its commits said otherwise. */
137
+ function titledNote (m) {
138
+ return m && m.titled_by_square ? ' Its title is the square\'s, with your commit messages below it; seam merge -m "…" names a patch yourself.' : ''
139
+ }
140
+
95
141
  export function describeMerge (r) {
96
- if (r.dry_run) return r.ok ? `Would merge as patch ${r.would_merge.line}:${r.would_merge.patch_no}${r.would_merge.fast ? ' (no file touched on both sides)' : ''}.` : `Would be refused: ${r.refused.detail}.`
142
+ const seal = r.would_seal ? ` The live work in ${r.would_seal.join(', ')} would be committed first.` : ''
143
+ if (r.dry_run) return r.ok ? `Would merge as patch ${r.would_merge.line}:${r.would_merge.patch_no}${r.would_merge.subject ? ` "${r.would_merge.subject}"` : ''}${r.would_merge.fast ? ' (no file touched on both sides)' : ''}.${seal}${titledNote(r.would_merge)}` : `Would be refused: ${r.refused.detail}.${seal}`
144
+ const sealed = r.sealed ? `Committed the square's live work first (${r.sealed.paths.join(', ')}) as ${r.sealed.commit.slice(0, 12)}.` : null
97
145
  if (r.ok) {
98
- const lines = [`Merged as ${patchName(r.merged)} (${r.merged.commit.slice(0, 12)})${r.retries ? ` after ${r.retries} lost race${r.retries === 1 ? '' : 's'}` : ''}.`]
146
+ const lines = [`Merged as ${patchName(r.merged)}${r.merged.subject ? ` "${r.merged.subject}"` : ''} (${r.merged.commit.slice(0, 12)})${r.retries ? ` after ${r.retries} lost race${r.retries === 1 ? '' : 's'}` : ''}.${titledNote(r.merged)}`]
147
+ if (sealed) lines.unshift(sealed)
99
148
  const combined = (r.combined || []).filter((c) => c.how !== 'lines' || true)
100
149
  if (combined.length) lines.push(`Combined with changes already on ${r.merged.line}: ${combined.map((c) => c.how === 'reuse' ? `${c.path} (settled as ${c.by} settled it before)` : `${c.path} (${c.how})`).join(', ')}.`)
150
+ lines.push(...describeWarnings(r.warnings))
101
151
  for (const k of r.restacked || []) lines.push(k.error ? `${k.square} is stacked on it and could not be moved (${k.error}): its holder rebases it.` : `Moved ${k.square}, stacked on it, onto ${r.merged.line}${k.conflicted ? ` with ${k.conflicted} conflicted path${k.conflicted === 1 ? '' : 's'}` : ''}.`)
102
152
  return lines.join('\n')
103
153
  }
104
154
  const lines = [`Not merged: ${r.refused ? r.refused.detail : r.error}.`]
155
+ if (sealed) lines.unshift(sealed)
105
156
  if (r.terms && r.terms.length) {
106
157
  lines.push('Conflicts, now kept in the square:')
107
158
  for (const t of r.terms) lines.push(` ${t.path} (${t.kind})${t.sides ? `: ${t.sides.map((s) => s.by).join(' vs ')}` : ''}${t.suggestion ? ' (a word-level merge is offered: seam resolve --suggestion, or choose suggestion)' : ''}`)
@@ -113,16 +164,32 @@ export function describeMerge (r) {
113
164
  /** Events as one line each (12's inbox line: kind, who, square, paths, patch, would_conflict). */
114
165
  export function describeEvents (r) {
115
166
  if (!r.events.length) return `No new events (cursor ${r.after}).`
116
- const line = (e) => {
117
- const p = e.payload || {}
118
- if (e.kind === 'patch.merged') return `#${e.event_seq} patch.merged: patch ${e.patch_no} by ${e.by.member_id} "${first(p.message || '')}" changed ${e.paths.join(', ')} in your square ${e.square_short}${p.would_conflict ? `; would conflict in ${p.would_conflict_paths.join(', ')}` : '; merges cleanly'}`
119
- if (e.kind === 'conflict.added') return `#${e.event_seq} conflict.added: ${p.path} (${p.kind}) in ${e.square_short}${p.caused_by && p.caused_by.patch_no ? `, against patch ${p.caused_by.patch_no}` : ''}; settle it and merge again`
120
- if (e.kind === 'check.cancelled') return `#${e.event_seq} check.cancelled: stop ${p.check} (${p.tier}) on ${p.commit.slice(0, 12)}, run_key ${p.run_key}: ${p.superseded_by} replaces it`
121
- if (e.kind === 'check.requested') return `#${e.event_seq} check.requested: run ${p.check} (${p.tier}) on ${p.commit.slice(0, 12)}${e.square_short ? ` in ${e.square_short}` : ` at ${e.line}:${p.patch_no}`}; report with run_key ${p.run_key}`
122
- if (e.kind === 'square.handoff') return `#${e.event_seq} square.handoff: ${e.square_short} from ${p.from} to ${p.to}${p.note ? `: ${p.note}` : ''}`
123
- return `#${e.event_seq} ${e.kind}`
124
- }
125
- return [...r.events.map(line), `(next: after=${r.after})`].join('\n')
167
+ return [...r.events.map(eventLine), `(next: after=${r.after})`].join('\n')
168
+ }
169
+
170
+ /** One event as its inbox line (12): the same line in seam events and in every tool answer. */
171
+ export function eventLine (e) {
172
+ const p = e.payload || {}
173
+ if (e.kind === 'patch.merged') return `#${e.event_seq} patch.merged: patch ${e.patch_no} by ${e.by.member_id} "${first(p.message || '')}" ${e.paths.length ? `changed ${e.paths.join(', ')}` : `changed ${(p.symbols || []).join(', ')}, which`} in your square ${e.square_short}${!e.paths.length ? ' calls' : ''}${p.would_conflict ? `; would conflict in ${p.would_conflict_paths.join(', ')}` : '; merges cleanly'}`
174
+ if (e.kind === 'watch.triggered') return `#${e.event_seq} watch.triggered: patch ${e.patch_no} by ${e.by.member_id} "${first(p.message || '')}" (${p.class}, ${p.stat ? `+${p.stat.add} -${p.stat.del}` : ''}) changed ${e.paths.join(', ')}${p.note ? ` (${p.note})` : ''}`
175
+ if (e.kind === 'check.verdict') return `#${e.event_seq} check.verdict: ${p.check} is ${p.verdict} on ${p.tier === 'post_merge' ? `${e.line} after patch ${p.patch_no}` : e.square_short} (${String(p.commit || '').slice(0, 12)}) from ${e.by.member_id}${p.log_url ? `: ${p.log_url}` : ''}`
176
+ if (e.kind === 'export.state') return `#${e.event_seq} export.state: the export to ${p.target} is ${p.state}${p.remote_message ? `: ${first(p.remote_message)}` : ''}`
177
+ if (e.kind === 'claim.granted') return `#${e.event_seq} claim.granted: ${p.pattern} is yours in ${e.square_short}, from ${p.from}${p.note ? `: "${first(p.note)}"` : ''}`
178
+ if (e.kind === 'claim.refused') return `#${e.event_seq} claim.refused: ${p.path} is claimed by ${p.holder} in ${p.holder_square}${p.note ? ` (${first(p.note)})` : ''}; ${p.queue_position ? `you are ${p.queue_position} in its queue` : 'ask for it with seam claim request, or work on other files'}`
179
+ if (e.kind === 'claim.expired') return `#${e.event_seq} claim.expired: your claim on ${p.pattern} in ${e.square_short} ran out and was released; claim it again if you still need it`
180
+ if (e.kind === 'claim.handoff') return `#${e.event_seq} claim.handoff: ${p.pattern} went from ${p.from} to ${p.to}${p.note ? `: "${first(p.note)}"` : ''}`
181
+ if (e.kind === 'review.comment') return `#${e.event_seq} review.comment: ${e.by.member_id} on ${e.square_short}${p.path ? ` at ${p.path}${p.anchor != null ? `:${p.anchor}` : ''}` : ''}: "${first(p.text || '')}"`
182
+ if (e.kind === 'review.requested') return `#${e.event_seq} review.requested: ${e.by.member_id} asked for a review of ${e.square_short}${p.text ? `: "${first(p.text)}"` : ''}${p.needs?.length ? ` (needs ${p.needs.length} thing${p.needs.length === 1 ? '' : 's'}: seam review ${e.square_short})` : ''}`
183
+ if (e.kind === 'review.stale') return `#${e.event_seq} review.stale: your approval of ${e.square_short} no longer covers its head (${p.stale_why === 'paths' ? 'it now changes paths outside your scope' : p.interdiff_summary ? `${p.interdiff_summary.class} change in ${p.interdiff_summary.files} file${p.interdiff_summary.files === 1 ? '' : 's'} since you looked` : 'it changed since you looked'}): seam review interdiff ${e.square_short}`
184
+ if (e.kind === 'review.decision') return `#${e.event_seq} review.decision: ${e.by.member_id} ${p.decision === 'approve' ? 'approved' : 'asked for changes on'} ${e.square_short}${p.text ? `: "${first(p.text)}"` : ''}`
185
+ if (e.kind === 'policy.needed') return `#${e.event_seq} policy.needed: ${e.square_short} into ${p.line} needs ${(p.needs || []).map((n) => n.what).join('; ')}`
186
+ if (e.kind === 'check.culprit') return `#${e.event_seq} check.culprit: ${p.check} broke at patch ${p.patch_no}${p.reverted ? '' : ', not reverted (3 reverts this hour): fix it or revert it'}`
187
+ if (e.kind === 'conflict.added') return `#${e.event_seq} conflict.added: ${p.path} (${p.kind}) in ${e.square_short}${p.caused_by && p.caused_by.patch_no ? `, against patch ${p.caused_by.patch_no}` : ''}; ${p.escalated ? `an AI tried twice: ${p.resolver} settles it now (stop working on this file)` : `settle it and merge again${p.attempts_left != null ? ` (attempt ${p.attempt}; ${p.attempts_left} left before it goes to a person)` : ''}`}`
188
+ if (e.kind === 'check.cancelled') return `#${e.event_seq} check.cancelled: stop ${p.check} (${p.tier}) on ${p.commit.slice(0, 12)}, run_key ${p.run_key}: ${p.superseded_by} replaces it`
189
+ if (e.kind === 'check.requested') return `#${e.event_seq} check.requested: run ${p.check} (${p.tier}) on ${p.commit.slice(0, 12)}${e.square_short ? ` in ${e.square_short}` : ` at ${e.line}:${p.patch_no}`}; report with run_key ${p.run_key}`
190
+ if (e.kind === 'square.restacked') return `#${e.event_seq} square.restacked: ${e.square_short} moved onto ${String(p.new_head).slice(0, 12)}${p.parent ? ` after ${p.parent}` : ''}${p.conflicted ? `, ${p.conflicted} conflict${p.conflicted === 1 ? '' : 's'} to resolve` : ''}`
191
+ if (e.kind === 'square.handoff') return `#${e.event_seq} square.handoff: ${e.square_short} from ${p.from} to ${p.to}${p.note ? `: ${p.note}` : ''}`
192
+ return `#${e.event_seq} ${e.kind}`
126
193
  }
127
194
 
128
195
  /** One tag per line: name, commit, patch, and "release" when it is one. */
@@ -159,7 +226,13 @@ export function describeClean (r, again) {
159
226
 
160
227
  export function describeLive (r) {
161
228
  if (!r.changes.length) return `No uncommitted work shared for ${r.square}: ${r.square}:live is its head.`
162
- return [`Uncommitted work in ${r.square}${r.by ? ` (shared by ${r.by})` : ''}, read as ${r.square}:live:`, ...r.changes.map((c) => ` ${c.status.padEnd(9)} ${c.path}`)].join('\n')
229
+ return [`Uncommitted work in ${r.square}${r.by ? ` (shared by ${r.by})` : ''}, read as ${r.square}:live:`, ...r.changes.map((c) => ` ${c.status.padEnd(9)} ${c.path}`), ...(r.hidden ? [` (and ${r.hidden} you may not read)`] : [])].join('\n')
230
+ }
231
+
232
+ /** GET /live/join: whether a member may load the square's live work whole. */
233
+ export function describeLiveJoin (r) {
234
+ if (r.may) return `${r.member} may load the live work of ${r.square} (${r.as}).`
235
+ return `${r.member} may not load the live work of ${r.square}: ${r.unreadable} shared path(s) it may not read. Commit or move them, or make it a writer.`
163
236
  }
164
237
 
165
238
  export function describeLiveSet (r) {
@@ -168,10 +241,42 @@ export function describeLiveSet (r) {
168
241
 
169
242
  const at = (c) => `${c.short || String(c.id || c.commit).slice(0, 12)}${c.patch ? ` (${c.patch.line}:${c.patch.patch_no})` : ''}`
170
243
 
244
+ /** The stack at a revision (10): one line per key, with where it was set and how. */
245
+ export function describeStack (r) {
246
+ const keys = Object.keys(r.stack || {}).sort()
247
+ const head = r.patch ? `${r.patch.line}:${r.patch.patch_no} (${r.commit.slice(0, 12)})` : r.commit.slice(0, 12)
248
+ if (!keys.length) return `${head}: no stack.`
249
+ return [head, ...keys.map((k) => { const v = r.stack[k]; return ` ${k} = ${v.value} (${v.how} at ${typeof v.set_at === 'number' ? `patch ${v.set_at}` : v.set_at})` })].join('\n')
250
+ }
251
+
252
+ /** The stack keys that changed between two revisions (diff --stack). */
253
+ export function describeStackDiff (r) {
254
+ const name = (s) => s.patch ? `${s.patch.line}:${s.patch.patch_no}` : s.commit.slice(0, 12)
255
+ if (!r.changed.length) return `No stack key changed between ${name(r.from)} and ${name(r.to)}.`
256
+ return r.changed.map((c) => `${c.key}: ${c.from ?? '(none)'} → ${c.to ?? '(none)'}`).join('\n')
257
+ }
258
+
171
259
  export function describeMergeBase (r) {
172
260
  return r.commit ? `${r.commit}${r.patch ? ` (${r.patch.line}:${r.patch.patch_no})` : ''}` : 'No common history.'
173
261
  }
174
262
 
263
+ export function describeAncestor (r, a, b) {
264
+ if (!r.ancestor) return `${a} is not in ${b}'s history${r.partial ? ' (as far as Seam looked)' : ''}.`
265
+ return `${a} is in ${b}'s history${r.first_parent ? `, ${r.distance === 0 ? 'the same commit' : `${r.distance} first parent${r.distance === 1 ? '' : 's'} down`}` : ', through a merge'}.`
266
+ }
267
+
268
+ /** A cherry-pick or revert of a commit, in a line: where it went. */
269
+ const describePick = (p) => `${p.reason} in ${sqShort(p.square)}${p.title ? ` "${p.title}"` : ''}: ${p.line ? `merged as ${p.line}:${p.patch_no}` : p.state}`
270
+ const sqShort = (id) => `sq:${String(id).replace(/-/g, '').slice(-8)}`
271
+
272
+ export function describeContains (r) {
273
+ const out = []
274
+ out.push(r.lines.length ? 'On lines:' : 'On no line yet.')
275
+ for (const l of r.lines) out.push(` ${l.line}${l.via ? ` (as patch ${l.via.line}:${l.via.patch_no})` : ''}`)
276
+ if (r.picked_to.length) { out.push('Carried elsewhere:'); for (const p of r.picked_to) out.push(` ${describePick(p)}`) }
277
+ return out.join('\n')
278
+ }
279
+
175
280
  export function describeGrep (r, { filesOnly = false } = {}) {
176
281
  if (!r.matches.length) return 'No matches.'
177
282
  const lines = filesOnly ? r.files : r.matches.map((m) => `${m.quilt ? m.quilt + ':' : ''}${m.path}:${m.line}:${m.text}`)
@@ -189,32 +294,77 @@ export function describeBisect (r) {
189
294
  return `Test ${at(r.next)} ${first(r.next.message)}: ${r.remaining} commit(s) left, about ${r.steps} step(s). Mark it good, bad or skip and ask again.`
190
295
  }
191
296
 
297
+ /** Code search results, one per line: path (kind), path:line: text, or path:line symbol name (kind). */
298
+ export function describeSearch (r) {
299
+ if (!r.results.length) return 'No results.'
300
+ const lines = r.results.map((x) => {
301
+ const where = `${x.quilt && r.results.some((y) => y.quilt !== x.quilt) ? x.quilt + ':' : ''}${x.path}`
302
+ if (x.kind === 'path') return `${where} (path)`
303
+ if (x.kind === 'symbol') return `${where}:${x.line} ${x.symbol.kind} ${x.symbol.name}`
304
+ return `${where}:${x.line}: ${x.text}`
305
+ })
306
+ lines.push(`${r.counts.files} file${r.counts.files === 1 ? '' : 's'}, ${r.counts.matches} match${r.counts.matches === 1 ? '' : 'es'}${r.cursor || r.more ? ' (more: raise --max-count or ask with the cursor)' : ''}`)
307
+ return lines.join('\n')
308
+ }
309
+
310
+ /** A member's watches, one per line. */
311
+ export function describeWatches (ws) {
312
+ if (!ws.length) return 'No watches.'
313
+ return ws.map((w) => `${w.id} ${w.paths.join(', ')}${w.classes ? ` classes ${w.classes.join(',')}` : ''}${w.min_lines != null ? ` over ${w.min_lines} lines` : ''}${w.lines ? ` on ${w.lines.join(',')}` : ''}${w.note ? ` (${w.note})` : ''}`).join('\n')
314
+ }
315
+
192
316
  /** A list of claims, one per line. */
193
317
  export function describeClaims (claims) {
194
318
  if (!claims.length) return 'No claims.'
195
- return claims.map((c) => `${c.pattern} ${c.square} (${c.line}) ${c.holder}${c.note ? `: ${c.note}` : ''}`).join('\n')
319
+ return claims.map((c) => `${c.pattern} ${c.square} (${c.line}) ${c.holder_name || c.holder}${c.auto ? ' (auto)' : ''}${c.owner ? ' (owner)' : ''}${c.note ? `: ${c.note}` : ''}${c.expires_at ? ` (until ${new Date(c.expires_at).toISOString()})` : ''}` +
320
+ (c.queue || []).map((r, i) => `\n ${i + 1}. ${r.by} in ${r.square}: ${r.title}${r.description ? ` (${r.description})` : ''} [request ${r.id}]`).join('')).join('\n')
321
+ }
322
+
323
+ export function describeHandoff (r) {
324
+ return `Handed ${r.claim.pattern} from ${r.from.square} to ${r.claim.holder} in ${r.claim.square} (their request: ${r.request.title}).`
325
+ }
326
+
327
+ export function describeFinished (r) {
328
+ if (!r.released.length && !r.handed.length) return 'No auto-claims to let go of; claims made on purpose stay until seam unclaim or the square ends.'
329
+ return [...(r.released.length ? [`Let go of ${r.released.map((c) => c.pattern).join(', ')}.`] : []), ...r.handed.map(describeHandoff)].join('\n')
196
330
  }
197
331
 
198
332
  export function describeCheck (r) {
199
333
  const lines = [`${r.line}: merge window ${r.window} an hour${r.red.length ? ` (red: ${r.red.join(', ')})` : ''}.`]
200
334
  if (r.advisory) lines.push('Your verdict is advisory: you are not one of the check\'s runners, so it changes nothing.')
335
+ if (r.bisect && r.bisect.bisect) lines.push(`${r.bisect.joined ? 'A bisect of it is already open' : 'Bisecting'}: patches ${r.bisect.bisect.range[0] + 1} to ${r.bisect.bisect.range[1]}, round ${r.bisect.bisect.round}${r.bisect.waiting ? ` (${r.bisect.waiting} run${r.bisect.waiting === 1 ? '' : 's'} to answer)` : ''}.`)
336
+ else if (r.bisect && r.bisect.flaky) lines.push('The bisect found it flaky: nothing is reverted.')
337
+ else if (r.bisect && r.bisect.culprit) lines.push(r.bisect.reverted === false ? `Culprit: patch ${r.line}:${r.bisect.culprit}, not reverted (3 reverts this hour): its holder and the admins are told.` : `Culprit: patch ${r.line}:${r.bisect.culprit}, reverted by the system.`)
201
338
  const v = r.revert
202
- if (v && v.reverted) lines.push(`Reverted patch ${r.line}:${v.patch_no} as patch ${r.line}:${v.revert_patch_no}.${v.fix_square ? ` The change is again in square ${v.fix_square.short}, held by ${v.fix_square.holder}: fix it there, commit, merge.` : ''}`)
339
+ if (v && v.reverted) lines.push(`Reverted patch ${r.line}:${v.patch_no} as patch ${r.line}:${v.revert_patch_no}.${v.fix_square ? ` The change is again in square ${v.fix_square.short}, held by ${v.fix_square.holder_name || v.fix_square.holder}: fix it there, commit, merge.` : ''}`)
203
340
  else if (v) lines.push(`Did not revert patch ${r.line}:${v.patch_no}: ${(v.refused && (v.refused.detail || v.refused.code)) || 'refused'}.${v.square ? ` The revert waits in square ${v.square.short}.` : ''}`)
204
341
  return lines.join('\n')
205
342
  }
206
343
 
344
+ /** Where a check's post-merge pipeline stands (seam check pipeline). */
345
+ export function describePipeline (p) {
346
+ const lines = [p.bisect ? `${p.check} on ${p.line}: bisecting patches ${p.bisect.range[0] + 1} to ${p.bisect.range[1]} (round ${p.bisect.round}, ${p.bisect.phase === 'confirm' ? 'confirming the culprit' : 'searching'}).` : `${p.check} on ${p.line}: no bisect open.`]
347
+ if (p.last) lines.push(p.last.stopped ? 'The last bisect was stopped by an admin: nothing was reverted.' : p.last.flaky ? 'The last bisect found the check flaky: nothing was reverted.' : `The last bisect found patch ${p.line}:${p.last.culprit}.`)
348
+ lines.push(`${p.reverts_last_hour} of 3 system reverts used this hour.`)
349
+ return lines.join('\n')
350
+ }
351
+
207
352
  /** Remembered resolutions, one per line. */
208
353
  export function describeResolutions (rows) {
209
354
  if (!rows.length) return 'No remembered resolutions.'
210
- return rows.map((r) => `${r.short} ${r.path} ${r.answer === 'delete' ? 'deleted' : `kept ${r.blob.slice(0, 12)}`} (${r.how}, ${r.by})`).join('\n')
355
+ return rows.map((r) => `${r.short} ${r.path}${r.kind === 'hunk' ? ' (one region)' : ''} ${r.answer === 'delete' ? 'deleted' : `kept ${r.blob.slice(0, 12)}`} (${r.how}, ${r.by})`).join('\n')
211
356
  }
212
357
 
213
358
  /** Where a square stands in review: approvals against what its line needs. */
214
359
  function describeStanding (r) {
215
360
  const parts = [`${r.have} of ${r.need} approval${r.need === 1 ? '' : 's'}${r.approved_by.length ? ` (${r.approved_by.join(', ')})` : ''}`]
216
361
  if (r.changes_requested_by.length) parts.push(`changes asked for by ${r.changes_requested_by.join(', ')}`)
362
+ if (r.requested?.length) parts.push(`review asked of ${r.requested.map((x) => x.member + (x.reason?.startsWith('rule:') ? ` (${x.reason})` : '')).join(', ')}`)
363
+ if (r.stale?.length) parts.push(`stale approval${r.stale.length === 1 ? '' : 's'} from ${r.stale.map((x) => x.by + (x.stale_why === 'paths' ? ' (outside their scope)' : '')).join(', ')} (changed since)`)
364
+ if (r.partial?.length) parts.push(`partial approval${r.partial.length === 1 ? '' : 's'} from ${r.partial.map((x) => `${x.by} (${x.scope.join(', ')})`).join(', ')}`)
217
365
  for (const c of r.checks || []) if (c.required || c.status !== 'missing') parts.push(`check ${c.name} ${c.status}`)
366
+ const need = (n) => n.rule === 'approvals' ? `${n.missing} approval${n.missing === 1 ? '' : 's'}` : n.rule === 'changes_requested' ? `changes answered for ${n.who.join(', ')}` : n.rule === 'check' ? `check ${n.check} (${n.status})` : `${n.missing} approval${n.missing === 1 ? '' : 's'} from ${[].concat(n.from).join(' or ')} for ${n.paths.slice(0, 3).join(', ')}${n.paths.length > 3 ? ', …' : ''}`
367
+ if (r.needs?.length) parts.push(`needs: ${r.needs.map(need).join('; ')}`)
218
368
  parts.push(r.ready ? 'ready to merge' : 'not ready to merge')
219
369
  return parts.join('; ')
220
370
  }
@@ -222,22 +372,35 @@ function describeStanding (r) {
222
372
  /** A square's reviews, oldest first, then where it stands. */
223
373
  export function describeReviews (r) {
224
374
  const rows = r.reviews.map((x) => {
225
- const what = { approve: 'approved', request_changes: 'asked for changes', dismiss: 'dismissed their verdict', comment: 'commented' }[x.kind]
375
+ const what = { approve: 'approved', request_changes: 'asked for changes', dismiss: 'dismissed their verdict', comment: 'commented', request: `asked ${(x.asked || []).join(', ')} for a review` }[x.kind]
226
376
  const where = x.path ? ` on ${x.path}${x.line ? `:${x.line}` : ''}` : ''
227
- return `${x.author} ${what}${where}${x.outdated ? ' (an earlier commit)' : ''}${x.body ? `: ${x.body}` : ''}`
377
+ const sug = x.suggestion ? `\n suggests for ${x.suggestion.path}:${x.suggestion.from_line}${x.suggestion.to_line > x.suggestion.from_line ? `-${x.suggestion.to_line}` : ''}${x.suggestion.text ? `:\n ${x.suggestion.text.replace(/\n$/, '').split('\n').join('\n ')}` : ' removing those lines'}\n (seam review apply ${x.id})` : ''
378
+ return `${x.author} ${what}${where}${x.outdated ? ' (an earlier commit)' : ''}${x.body ? `: ${x.body}` : ''}${sug}`
228
379
  })
229
- return `${r.square} on ${r.line}: ${describeStanding(r)}${rows.length ? '\n' + rows.join('\n') : '\nNo reviews yet.'}`
380
+ const seen = r.since_seen ? `\nsince you looked: ${r.since_seen.same ? 'the same change' : `${r.since_seen.class}, ${r.since_seen.files.length} file${r.since_seen.files.length === 1 ? '' : 's'} (${r.since_seen.files.slice(0, 5).map((f) => f.path).join(', ')})`} (seam review interdiff)` : ''
381
+ return `${r.square} on ${r.line}: ${describeStanding(r)}${seen}${rows.length ? '\n' + rows.join('\n') : '\nNo reviews yet.'}`
382
+ }
383
+
384
+ /** What changed in a square since a head (13 "What changed since I looked"). */
385
+ export function describeInterdiff (r) {
386
+ const since = String(r.since).slice(0, 12)
387
+ if (r.same) return `${r.square}: the same change as at ${since}${r.from_base?.length ? ` (only the line under it moved: ${r.from_base.length} path${r.from_base.length === 1 ? '' : 's'})` : ''}.`
388
+ const head = `${r.square}: ${r.class} since ${since}, ${r.files.length} file${r.files.length === 1 ? '' : 's'}`
389
+ const rows = r.files.map((f) => ` ${f.op} ${f.path} (${f.class})`)
390
+ const base = r.from_base?.length ? [` and ${r.from_base.length} path${r.from_base.length === 1 ? '' : 's'} from the line under it, not the square's`] : []
391
+ return [head, ...rows, ...base].join('\n')
230
392
  }
231
393
 
232
394
  /** One review just given. */
233
395
  export function describeReviewed (r) {
234
396
  const x = r.review
235
- const what = { approve: 'Approved', request_changes: 'Asked for changes on', dismiss: 'Dismissed your verdict on', comment: 'Commented on' }[x.kind]
397
+ const what = { approve: 'Approved', request_changes: 'Asked for changes on', dismiss: 'Dismissed your verdict on', comment: 'Commented on', request: `Asked ${(x.asked || []).join(', ')} to review` }[x.kind]
236
398
  return `${what} ${x.square}${x.path ? ` at ${x.path}${x.line ? `:${x.line}` : ''}` : ''}. ${describeStanding(r)}.`
237
399
  }
238
400
 
239
401
  /** What a line needs to take a merge. */
240
402
  export function describePolicy (p) {
403
+ if (p.pending) return `Proposed as version ${p.pending.version} of ${p.line}'s policy; not live yet: ${p.pending.how}.`
241
404
  const parts = []
242
405
  if (p.approvals) parts.push(`${p.approvals} approval${p.approvals === 1 ? '' : 's'} of a square's latest commit from someone other than its holder`)
243
406
  if (p.checks?.length) parts.push(`check${p.checks.length === 1 ? '' : 's'} ${p.checks.join(', ')} green on it`)
@@ -246,9 +409,30 @@ export function describePolicy (p) {
246
409
  const tiers = Object.entries(p.tiers || {}).map(([c, ts]) => `${c} ${ts.length ? ts.join(' and ') : 'never'}`)
247
410
  const tg = (tiers.length ? ` Tiers: ${tiers.join('; ')}.` : '') +
248
411
  (p.tests?.length ? ` Tests: ${p.tests.join(', ')} (a pre-merge run names the ones a change reaches).` : '') +
249
- (p.ci_config?.length ? ` CI config: ${p.ci_config.join(', ')} (a change there runs everything).` : '')
250
- return (parts.length ? `${p.line} needs ${parts.join(' and ')} to take a merge.` : `${p.line} takes merges without approvals or checks.`) + runs + tg
251
- }
412
+ (p.ci_config?.length ? ` CI config: ${p.ci_config.join(', ')} (a change there runs everything).` : '') +
413
+ (p.only && Object.keys(p.only).length ? ` Only when their paths change: ${Object.entries(p.only).map(([c, g]) => `${c} ${g.join(' ')}`).join('; ')}.` : '') +
414
+ (p.human_only?.length ? ` Human-only: ${p.human_only.join(', ')} (agents read these and never change them).` : '') +
415
+ (p.pre_merge_checks ? ' Pre-merge checks: on (the checks must be green on the tree that merges: a square the line moved past on its own paths rebases and runs them again).' : '')
416
+ const rules = p.rules?.length ? `\nRules by path and class${p.mode === 'evaluate' ? ' (reporting only)' : ''}: ${p.rules.map((r) => `${r.id}: ${[r.match.paths && r.match.paths.join(', '), r.match.classes && r.match.classes.join('/'), r.match.flags && r.match.flags.join('/')].filter(Boolean).join(' ') || 'every change'} needs ${r.needs.approvals} from ${Array.isArray(r.needs.from) ? r.needs.from.join(' or ') : r.needs.from}${r.needs.checks ? ` and ${r.needs.checks.join(', ')} green` : ''}`).join('; ')}.` : ''
417
+ const combine = (p.auto_combine && p.auto_combine !== 'whitespace' ? ` Merges combine clashing changes up to ${p.auto_combine} only.` : '') +
418
+ (p.batch ? ` Post-merge checks run every ${p.batch.patches} patches or ${p.batch.seconds} s.` : '') + (p.bisect_width ? ` A red check is bisected ${p.bisect_width} runs a round.` : '') +
419
+ (p.needs_stack?.length ? ` Every merge carries stack ${p.needs_stack.join(', ')}.` : '') +
420
+ (p.stack_defaults ? ` A merge sets ${Object.entries(p.stack_defaults).map(([k, v]) => `${k} to ${v}`).join(', ')}.` : '') +
421
+ (p.monotonic_stack?.length ? ` No merge lowers stack ${p.monotonic_stack.join(', ')}.` : '') +
422
+ (p.migrations_in_order ? ' Migrations merge in order.' : '')
423
+ return (parts.length ? `${p.line} needs ${parts.join(' and ')} to take a merge.` : `${p.line} takes merges without approvals or checks.`) + runs + tg + rules + combine
424
+ }
425
+
426
+ /** Where a square stands against its line's rules by path and class (13). */
427
+ export function describeSquarePolicy (p) {
428
+ const files = p.files.map((f) => ` ${f.class.padEnd(12)} ${f.path}${f.flags.length ? ` (${f.flags.join(', ')})` : ''}`)
429
+ const needs = p.needs.map((n) => ` ${n.have >= n.approvals ? 'met ' : 'NEED'} ${n.approvals} from ${Array.isArray(n.from) ? n.from.join(' or ') : n.from} (has ${n.have}) for ${n.paths.slice(0, 4).join(', ')}${n.paths.length > 4 ? ', …' : ''} [rule ${n.rules.join(', ')}]`)
430
+ const checks = p.checks.map((c) => ` check ${c.name}: ${c.status}`)
431
+ return [`${p.square} on ${p.line}: ${p.class} change, ${p.ready ? 'ready to merge' : 'not ready'}${p.mode === 'evaluate' ? ' (rules only report)' : ''}.`, 'Files:', ...files, ...(needs.length ? ['Needs:', ...needs] : ['No rule asks anything of it.']), ...checks].join('\n')
432
+ }
433
+
434
+ /** A line's policy versions, newest first. */
435
+ export const describePolicyVersions = (r) => r.versions.length ? r.versions.map((v) => `${v.version === r.active ? '*' : ' '} ${String(v.version).padStart(3)} ${v.state.padEnd(8)} by ${v.proposed_by}${v.decided_by ? `, ${v.state === 'rejected' ? 'rejected' : 'approved'} by ${v.decided_by}${v.confirmed_with ? ` (signed ${v.confirmed_with})` : ''}` : ''} approvals ${v.body.approvals}${v.body.checks?.length ? `, checks ${v.body.checks.join(', ')}` : ''}${v.body.policy?.rules?.length ? `, ${v.body.policy.rules.length} rules` : ''}${v.body.human_only?.length ? `, human-only ${v.body.human_only.join(', ')}` : ''}`).join('\n') : `${r.line} has no policy versions yet.`
252
436
 
253
437
  /** A run's affected set in a few words. */
254
438
  const affectedWords = (a) => !a ? '' : a.confidence === 'all' ? ' runs all' : ` runs ${a.tests.length} test${a.tests.length === 1 ? '' : 's'}`
@@ -283,14 +467,88 @@ export function describeKeys (r) {
283
467
  }
284
468
 
285
469
  export function describeTokens (r) {
286
- if (!r.tokens.length) return `${r.quilt} has no tokens. Make one with seam token new <name> [--level read|write].`
287
- return r.tokens.map((t) => `${t.id} ${t.name} ${t.level}${t.revoked_at ? ` (revoked ${when(t.revoked_at)})` : ` made ${when(t.created_at)} by ${t.created_by}`}`).join('\n')
470
+ if (!r.tokens.length) return `${r.quilt} has no tokens. Make one with seam token new <name> [--level read|write] [--days 1-365].`
471
+ return r.tokens.map((t) => `${t.id} ${t.name} ${t.level}${t.revoked_at ? ` (revoked ${when(t.revoked_at)})` : ` made ${when(t.created_at)} by ${t.created_by}${t.expires_at ? `, expires ${when(t.expires_at)}` : ''}`}`).join('\n')
472
+ }
473
+
474
+ /** A protections table (11.2), one row a line in the order it is read: the last match decides. */
475
+ export function describeProtections (r) {
476
+ const who = (p) => p.id ? `${p.kind === 'member' ? '' : `${p.kind}:`}${p.id}` : p.kind
477
+ const rows = r.rows.map((x) => `${String(x.n).padStart(3)} ${x.exclude ? 'exclude' : 'grant '} ${x.level.padEnd(5)} ${(x.path || "''").padEnd(24)} ${who(x.principal)}${x.note ? ` # ${x.note}` : ''}`)
478
+ const teams = (r.teams || []).map((t) => `team ${t.name}: ${t.members.join(', ')}`)
479
+ return [r.default ? `${r.quilt} uses the default protections: each member role keeps what it allows.` : `${r.quilt}'s protections (the last row that matches decides):`, ...rows, ...(teams.length ? ['', ...teams] : [])].join('\n')
480
+ }
481
+
482
+ /** A redaction done (11.8). */
483
+ export const describeRedacted = (r) => `Redacted blob ${r.blob.slice(0, 12)} at seq ${r.seq} (${r.reason}). It was at ${r.paths.length ? r.paths.join(', ') : 'no path in this quilt'}; ${r.packs_rewritten} pack${r.packs_rewritten === 1 ? '' : 's'} written again without it${r.squares_cleaned.length ? `; live work cleaned in ${r.squares_cleaned.join(', ')}` : ''}. Every read now gets a tombstone, and the same bytes are refused if anyone adds them again. History and ids are unchanged.`
484
+
485
+ export const describeMetrics = (r) => [
486
+ ...Object.entries(r.metrics).map(([k, v]) => `${k} ${v}`),
487
+ r.breaches.length ? 'Past their threshold:' : 'Nothing is past its threshold.',
488
+ ...r.breaches.map((b) => ` ${b.monitor}: ${b.metric} ${b.value} (${b.above != null ? `over ${b.above}` : `under ${b.below}`}): ${b.why}`)
489
+ ].join('\n')
490
+ export const describeRedactions = (r) => r.redactions.length ? r.redactions.map((x) => `${x.blob.slice(0, 12)} seq ${x.seq} by ${x.by} ${x.reason}`).join('\n') : `${r.quilt} has no redactions.`
491
+
492
+ /** An org: its members by role, and its quilts. */
493
+ export function describeOrg (o) {
494
+ const members = (o.members || []).map((m) => ` ${m.role.padEnd(6)} ${m.name ? `${m.name} ` : ''}${m.member_id}`)
495
+ const quilts = (o.quilts || []).map((q) => ` ${q.slug} ${q.id}`)
496
+ const teams = (o.teams || []).map((t) => ` ${t.name}: ${t.members.join(', ')}`)
497
+ return [`${o.slug}${o.name && o.name !== o.slug ? ` (${o.name})` : ''}${o.role ? `: you are ${o.role === 'admin' ? 'an' : 'a'} ${o.role}` : ''}`, ...(members.length ? ['Members:', ...members] : []), ...(teams.length ? ['Teams (org_team:<name> in its quilts\' protections):', ...teams] : []), ...(quilts.length ? ['Quilts:', ...quilts] : o.members ? ['No quilts yet.'] : [])].join('\n')
498
+ }
499
+
500
+ /** An org's log (19.4), newest first: when, who, what. */
501
+ export function describeOrgLog (r) {
502
+ const what = (x) => {
503
+ const d = x.data || {}
504
+ if (x.kind === 'created') return `made the org ${d.slug}`
505
+ if (x.kind === 'member') return d.role ? `${d.was ? `made ${d.member} ${d.role === 'admin' || d.role === 'owner' ? 'an' : 'a'} ${d.role} (was ${d.was})` : `added ${d.member} as ${d.role === 'member' ? 'a' : 'an'} ${d.role}`}` : `removed ${d.member} (was ${d.was})`
506
+ if (x.kind === 'team') return d.members?.length ? `set the team ${d.name}: ${d.members.join(', ')}` : `removed the team ${d.name}`
507
+ if (x.kind === 'quilt_in') return `moved the quilt ${d.slug} in (from ${d.from})`
508
+ if (x.kind === 'quilt_out') return `moved the quilt ${d.slug} out (to ${d.to})`
509
+ return `${x.kind} ${JSON.stringify(d)}`
510
+ }
511
+ if (!r.log.length) return `${r.org}'s log is empty.`
512
+ return [...r.log.map((x) => `${x.id.padStart(6)} ${new Date(x.at).toISOString().slice(0, 16).replace('T', ' ')} ${x.actor} ${what(x)}`), ...(r.next ? [`More: seam org log ${r.org} --before ${r.next}`] : [])].join('\n')
288
513
  }
289
514
 
290
- export const describeNewToken = (t) => `Made token ${t.name} (${t.level} on ${t.quilt}, id ${t.id}). Its secret, shown only this once:\n\n ${t.token}\n\nSet it as SEAM_TOKEN (or send it as Authorization: Bearer <token>).`
515
+ export const describeOrgTeams = (r) => r.teams.length ? r.teams.map((t) => `${t.name}: ${t.members.join(', ')}`).join('\n') : `${r.org} has no teams.`
516
+
517
+ export const describeOrgs = (r) => r.orgs.length ? r.orgs.map((o) => `${o.slug} ${o.role}`).join('\n') : 'You are in no orgs. Make one with seam org new <slug>.'
518
+
519
+ export const describeTeams = (r) => r.teams.length ? r.teams.map((t) => `${t.name}: ${t.members.join(', ')}`).join('\n') : `${r.quilt} has no teams.`
520
+
521
+ export const describeAccess = (r) => `You hold ${r.level} at ${r.path || 'the whole quilt'} (role ${r.role}${r.teams.length ? `, teams ${r.teams.join(', ')}` : ''}; at most ${r.max} anywhere${r.default ? ', default protections' : ''}).`
522
+
523
+ export const describeNewToken = (t) => `Made token ${t.name} (${t.level} on ${t.quilt}, id ${t.id}${t.expires_at ? `, expires ${when(t.expires_at)}` : ''}). Its secret, shown only this once:\n\n ${t.token}\n\nSet it as SEAM_TOKEN (or send it as Authorization: Bearer <token>).`
291
524
 
292
525
  /** An import record (14) in a few lines. */
293
526
  export function describeImportRecord (r) {
294
527
  const lines = (r.lines || []).map((l) => ` ${l.line}: ${l.patches} patches, git ${l.tip_sha ? l.tip_sha.slice(0, 12) : '(none)'}`)
295
- return [`Import ${r.id} of ${r.source?.url}: ${r.state}${r.follow ? `, following every ${r.interval_s} s` : ''}${r.error ? ` (${r.error})` : ''}`, ...lines].join('\n')
528
+ const asked = r.state === 'awaiting_authors' && r.proposed_authors
529
+ ? [' Authors (answer with seam import authors <id> --authors <file>; an email left out is a stub member):', ...r.proposed_authors.map((a) => ` ${a.email} (${a.name}, ${a.commits} commits) → ${a.member_id || 'stub'}`)]
530
+ : []
531
+ return [`Import ${r.id} of ${r.source?.url}: ${r.state}${r.follow ? `, following every ${r.interval_s} s` : ''}${r.error ? ` (${r.error})` : ''}`, ...lines, ...asked].join('\n')
532
+ }
533
+
534
+ /** One block, in a line: `<id> sq:<tail> sparse, live, at <commit>, src, docs (holder on machine)`. */
535
+ export function describeBlock (b) {
536
+ const where = b.paths.length ? b.paths.join(', ') : 'the whole quilt'
537
+ return `block ${b.id} on sq:${String(b.square).slice(-8)}: ${b.mode}, ${b.live ? 'live' : 'read-only'}, at ${String(b.at || '').slice(0, 12) || 'no commit yet'}, ${where} (${b.holder} on ${b.machine})`
538
+ }
539
+
540
+ export function describeBlocks (r) {
541
+ return r.blocks.length ? r.blocks.map(describeBlock).join('\n') : `No open blocks on ${r.quilt}.`
542
+ }
543
+
544
+ /** GET /public-reads as lines: `<when> <addr> <n> reads, <bytes> bytes` (11.10). */
545
+ export function describePublicReads (r) {
546
+ if (!r.reads.length) return `${r.quilt}: no public reads in the last 90 days.`
547
+ return [`${r.quilt}: public reads, newest first (10 minutes each, address hashed):`, ...r.reads.map((x) => `${new Date(x.bucket).toISOString().slice(0, 16)}Z ${x.addr} ${x.reads} read${x.reads === 1 ? '' : 's'}, ${x.bytes} bytes`)].join('\n')
548
+ }
549
+
550
+ /** What block.rebuild did (05 "Rebuilt, never repaired"). */
551
+ export function describeRebuild (r) {
552
+ if (!r.finished) return `Nothing to rebuild: the folder is whole${r.changes ? ` (${r.changes} uncommitted change(s), all yours)` : ''}.`
553
+ return `Finished a move that stopped part way: ${r.written.length} file(s) written, ${r.removed.length} removed${r.kept.length ? `; kept your edits to ${r.kept.join(', ')}` : ''}. The folder is on ${String(r.to).slice(0, 12)}.`
296
554
  }
@@ -0,0 +1,34 @@
1
+ // The built-in dictionary (plans/quilt-source-control/03, "Blob encodings": "before that, a
2
+ // built-in dictionary ..., id pinned in the server binary"). Small source files compress poorly
3
+ // alone: there is too little in one file for the compressor to find repeats. A preset dictionary
4
+ // of text common to source files in many languages gives it repeats from the first byte.
5
+ // It is a raw-content dictionary (any bytes serve, for zstd and for deflate's preset window), so
6
+ // no trainer is needed. Its bytes never change: every entry written with it names it by its flag
7
+ // (pack.js DEFLATE_DICT, ZSTD_DICT), and its id is pinned by a test. A new dictionary is a new id
8
+ // and a new flag, never an edit here.
9
+ import { idOf } from './hash.js'
10
+
11
+ const TEXT = [
12
+ // Markup, config and prose.
13
+ '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title></title>\n <link rel="stylesheet" href="style.css">\n</head>\n<body>\n <div class="container">\n <a href="https://"></a>\n </div>\n <script src="index.js"></script>\n</body>\n</html>\n',
14
+ '.container {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: space-between;\n margin: 0 auto;\n padding: 0;\n width: 100%;\n height: 100%;\n color: #fff;\n background-color: #000;\n font-size: 14px;\n border-radius: 4px;\n position: absolute;\n}\n@media (max-width: 768px) {\n}\n',
15
+ '# Title\n\n## Installation\n\n```bash\nnpm install\n```\n\n## Usage\n\n## License\n\nMIT License\n\nCopyright (c) \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\n',
16
+ '[package]\nname = ""\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\nserde = { version = "1", features = ["derive"] }\n',
17
+ 'name: CI\non:\n push:\n branches: [ main ]\n pull_request:\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - name: Install\n run: npm ci\n - name: Test\n run: npm test\n',
18
+ '{\n "name": "",\n "version": "1.0.0",\n "description": "",\n "main": "index.js",\n "type": "module",\n "scripts": {\n "build": "tsc",\n "test": "node --test",\n "start": "node index.js",\n "lint": "eslint ."\n },\n "dependencies": {\n },\n "devDependencies": {\n "typescript": "^5.0.0"\n },\n "license": "MIT"\n}\n',
19
+ // Go, Rust, Java, C.
20
+ 'package main\n\nimport (\n\t"context"\n\t"errors"\n\t"fmt"\n\t"net/http"\n\t"os"\n\t"strings"\n\t"time"\n)\n\nfunc main() {\n\tif err != nil {\n\t\treturn nil, fmt.Errorf("%w", err)\n\t}\n}\n\ntype Server struct {\n\tctx context.Context\n}\n\nfunc (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n}\n',
21
+ 'use std::collections::HashMap;\nuse std::io::{self, Read, Write};\n\n#[derive(Debug, Clone, PartialEq)]\npub struct Config {\n pub name: String,\n}\n\nimpl Config {\n pub fn new() -> Self {\n Self { name: String::new() }\n }\n}\n\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n let mut map = HashMap::new();\n match value {\n Some(x) => x,\n None => return Err(io::Error::new(io::ErrorKind::Other, "")),\n }\n Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn it_works() {\n assert_eq!(2 + 2, 4);\n }\n}\n',
22
+ 'package com.example;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\npublic class Main {\n private static final String NAME = "";\n\n public static void main(String[] args) {\n System.out.println("");\n }\n\n @Override\n public String toString() {\n return "";\n }\n}\n',
23
+ '#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nint main(int argc, char **argv) {\n for (int i = 0; i < argc; i++) {\n printf("%s\\n", argv[i]);\n }\n return 0;\n}\n',
24
+ // Python.
25
+ '#!/usr/bin/env python3\n"""Module docstring."""\nfrom __future__ import annotations\n\nimport json\nimport os\nimport sys\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Optional\n\n\nclass Config:\n def __init__(self, name: str) -> None:\n self.name = name\n\n def __repr__(self) -> str:\n return f"Config({self.name!r})"\n\n\ndef main() -> int:\n for item in items:\n if item is None:\n continue\n try:\n pass\n except Exception as e:\n raise ValueError(str(e))\n return 0\n\n\nif __name__ == "__main__":\n sys.exit(main())\n',
26
+ // JavaScript and TypeScript last: the most common, nearest the data for deflate.
27
+ "import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'\nimport type { ReactNode } from 'react'\n\ninterface Props {\n children?: ReactNode\n className?: string\n}\n\nexport type Options = {\n name: string\n value?: number\n}\n\nexport default function App({ children, className }: Props) {\n const [state, setState] = useState<string | null>(null)\n useEffect(() => {\n }, [])\n return (\n <div className={className}>\n {children}\n </div>\n )\n}\n",
28
+ "'use strict'\n\nconst fs = require('fs')\nconst path = require('path')\n\nmodule.exports = {\n}\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport assert from 'node:assert/strict'\nimport test from 'node:test'\n\n/**\n * @param {string} name\n * @returns {Promise<void>}\n */\nexport async function main (options = {}) {\n const { name, value } = options\n if (!name) throw new Error('name is required')\n for (const item of items) {\n if (item === undefined || item === null) continue\n }\n try {\n const result = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })\n if (!result.ok) return null\n return await result.json()\n } catch (err) {\n console.error(err)\n } finally {\n }\n return Object.keys(map).map((key) => ({ key, value: map[key] })).filter(Boolean).length > 0\n}\n\nexport class Store {\n constructor () {\n this.items = new Map()\n }\n\n get (id) {\n return this.items.get(id)\n }\n}\n\ntest('it works', async () => {\n assert.equal(typeof main, 'function')\n assert.deepEqual([], [])\n})\n\nexport function \nexport const \nexport default \n return \n return \n const \n const \n if (\n if (\n} else {\n } else {\n }\n}\n"
29
+ ].join('\n')
30
+
31
+ /** The built-in dictionary's bytes (under 32 KiB, so deflate's window holds all of it). */
32
+ export const BUILTIN_DICT = Buffer.from(TEXT, 'utf8')
33
+ /** Its id: the BLAKE3 of its bytes under the 'dict' context, pinned by a test. */
34
+ export const BUILTIN_DICT_ID = idOf('dict', BUILTIN_DICT)