@heyquilt/seam 0.27.0 → 0.130.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.
- package/bin/seam.js +117 -17
- package/lib/quilts/agent-tokens.js +148 -0
- package/lib/quilts/archiver.js +241 -0
- package/lib/quilts/attest.js +212 -0
- package/lib/quilts/bisect-run.js +3 -2
- package/lib/quilts/block.js +497 -55
- package/lib/quilts/cas.js +78 -0
- package/lib/quilts/cbor.js +2 -0
- package/lib/quilts/claims.js +409 -0
- package/lib/quilts/cli.js +611 -69
- package/lib/quilts/client.js +305 -19
- package/lib/quilts/conflict-format.js +266 -0
- package/lib/quilts/daemon.js +174 -0
- package/lib/quilts/describe.js +281 -38
- package/lib/quilts/dict.js +34 -0
- package/lib/quilts/exit-archive.js +184 -0
- package/lib/quilts/export-format.js +52 -0
- package/lib/quilts/export-git.js +27 -3
- package/lib/quilts/export-lfs.js +52 -0
- package/lib/quilts/export-sign.js +49 -0
- package/lib/quilts/fastcdc.js +186 -0
- package/lib/quilts/hash.js +17 -2
- package/lib/quilts/ignored.js +46 -0
- package/lib/quilts/import-git.js +54 -2
- package/lib/quilts/journal.js +105 -0
- package/lib/quilts/limits.js +197 -0
- package/lib/quilts/linepolicy.js +109 -0
- package/lib/quilts/pack.js +146 -18
- package/lib/quilts/persona.js +70 -0
- package/lib/quilts/records.js +7 -3
- package/lib/quilts/recover.js +37 -0
- package/lib/quilts/runner.js +79 -12
- package/lib/quilts/sealed.js +231 -0
- package/lib/quilts/secrets.js +86 -0
- package/lib/quilts/signing.js +7 -3
- package/lib/quilts/tarball.js +107 -0
- package/lib/quilts/textdiff.js +7 -5
- package/lib/quilts/tools.js +401 -83
- package/lib/quilts/tree.js +351 -0
- package/lib/quilts/want.js +51 -0
- package/lib/quilts/webhook.js +116 -0
- package/package.json +3 -2
package/lib/quilts/describe.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
: [
|
|
26
|
-
if (
|
|
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
|
-
|
|
32
|
-
if (
|
|
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.
|
|
35
|
-
return [...d.files.map((f) => ` ${f.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
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}` : ''}`).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,73 @@ 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')
|
|
288
480
|
}
|
|
289
481
|
|
|
290
|
-
|
|
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
|
+
export const describeOrgTeams = (r) => r.teams.length ? r.teams.map((t) => `${t.name}: ${t.members.join(', ')}`).join('\n') : `${r.org} has no teams.`
|
|
501
|
+
|
|
502
|
+
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>.'
|
|
503
|
+
|
|
504
|
+
export const describeTeams = (r) => r.teams.length ? r.teams.map((t) => `${t.name}: ${t.members.join(', ')}`).join('\n') : `${r.quilt} has no teams.`
|
|
505
|
+
|
|
506
|
+
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' : ''}).`
|
|
507
|
+
|
|
508
|
+
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
509
|
|
|
292
510
|
/** An import record (14) in a few lines. */
|
|
293
511
|
export function describeImportRecord (r) {
|
|
294
512
|
const lines = (r.lines || []).map((l) => ` ${l.line}: ${l.patches} patches, git ${l.tip_sha ? l.tip_sha.slice(0, 12) : '(none)'}`)
|
|
295
|
-
|
|
513
|
+
const asked = r.state === 'awaiting_authors' && r.proposed_authors
|
|
514
|
+
? [' 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'}`)]
|
|
515
|
+
: []
|
|
516
|
+
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')
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** One block, in a line: `<id> sq:<tail> sparse, live, at <commit>, src, docs (holder on machine)`. */
|
|
520
|
+
export function describeBlock (b) {
|
|
521
|
+
const where = b.paths.length ? b.paths.join(', ') : 'the whole quilt'
|
|
522
|
+
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})`
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function describeBlocks (r) {
|
|
526
|
+
return r.blocks.length ? r.blocks.map(describeBlock).join('\n') : `No open blocks on ${r.quilt}.`
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** GET /public-reads as lines: `<when> <addr> <n> reads, <bytes> bytes` (11.10). */
|
|
530
|
+
export function describePublicReads (r) {
|
|
531
|
+
if (!r.reads.length) return `${r.quilt}: no public reads in the last 90 days.`
|
|
532
|
+
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')
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** What block.rebuild did (05 "Rebuilt, never repaired"). */
|
|
536
|
+
export function describeRebuild (r) {
|
|
537
|
+
if (!r.finished) return `Nothing to rebuild: the folder is whole${r.changes ? ` (${r.changes} uncommitted change(s), all yours)` : ''}.`
|
|
538
|
+
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
539
|
}
|
|
@@ -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)
|