heyquilt 0.3.121 → 0.3.138

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 (129) hide show
  1. package/RELEASES.md +292 -17
  2. package/bin/quilt.js +72 -5
  3. package/desktop/main.js +16 -2
  4. package/package.json +3 -1
  5. package/src/agent-task-workflow.js +2 -2
  6. package/src/agents/common.js +13 -0
  7. package/src/ai-vm.js +9 -1
  8. package/src/api/activity.js +5 -0
  9. package/src/api/memory-store.js +21 -10
  10. package/src/api/notify.js +134 -0
  11. package/src/api/routes/browsers.js +29 -0
  12. package/src/api/routes/github.js +0 -13
  13. package/src/api/routes/relay.js +26 -3
  14. package/src/api/routes/seam-connect.js +103 -0
  15. package/src/api/routes/session-migrate.js +39 -0
  16. package/src/api/routes/sessions.js +73 -1
  17. package/src/api/server.js +46 -12
  18. package/src/api/supabase-store.js +53 -4
  19. package/src/autowork.js +182 -16
  20. package/src/connection.js +39 -9
  21. package/src/control.js +139 -13
  22. package/src/force-date.js +27 -0
  23. package/src/fsutil.js +13 -3
  24. package/src/gitstate.js +38 -2
  25. package/src/history.js +25 -1
  26. package/src/mcp.js +93 -7
  27. package/src/merge3.js +11 -3
  28. package/src/now-summary.js +113 -0
  29. package/src/pathrules.js +12 -0
  30. package/src/protocol.js +10 -2
  31. package/src/quilt-default.js +84 -0
  32. package/src/quilt-history.js +106 -0
  33. package/src/quilt-remote.js +78 -0
  34. package/src/relay-commit.js +5 -0
  35. package/src/relay-lease.js +6 -2
  36. package/src/relay-mcp.js +172 -8
  37. package/src/relay-outbound.js +1 -1
  38. package/src/relay-prune.js +4 -1
  39. package/src/release-words.js +2 -2
  40. package/src/runner.js +20 -7
  41. package/src/seam-access.js +97 -0
  42. package/src/seam-claims.js +53 -0
  43. package/src/seam-url.js +15 -0
  44. package/src/server.js +1767 -33
  45. package/src/session-follow.js +515 -0
  46. package/src/session-layout3.js +466 -0
  47. package/src/session-migrate.js +123 -0
  48. package/src/session-quilt.js +145 -0
  49. package/src/session-seam.js +284 -0
  50. package/src/session.js +899 -46
  51. package/src/source-live/bring-in.js +283 -0
  52. package/src/source-live/live-block.js +507 -35
  53. package/src/source-live/live-square.js +17 -6
  54. package/src/status.js +5 -0
  55. package/src/ui/agent-guide.js +1 -1
  56. package/src/ui/app.css +85 -20
  57. package/src/ui/app.js +4 -1
  58. package/src/ui/chat.js +9 -3
  59. package/src/ui/cloud-ai-model.js +13 -0
  60. package/src/ui/cloud-ai.js +10 -2
  61. package/src/ui/common.js +6 -2
  62. package/src/ui/fileview.js +20 -7
  63. package/src/ui/force-words.js +19 -0
  64. package/src/ui/git-rule.js +9 -0
  65. package/src/ui/home.js +0 -147
  66. package/src/ui/loom-model.js +15 -1
  67. package/src/ui/quiltview.js +213 -0
  68. package/src/ui/seam-model.js +251 -0
  69. package/src/ui/session.js +413 -31
  70. package/src/ui/voice-call.js +83 -14
  71. package/src/ui/voice-model.js +39 -0
  72. package/src/ui/voice.js +167 -17
  73. package/src/ui-server.js +80 -28
  74. package/src/update-check.js +25 -1
  75. package/src/voice.js +13 -6
  76. package/src/git.js +0 -147
  77. package/src/quilts/AGENTS.md +0 -1
  78. package/src/quilts/CLAUDE.md +0 -1
  79. package/src/quilts/archiver.js +0 -175
  80. package/src/quilts/autorevert.js +0 -49
  81. package/src/quilts/bisect-run.js +0 -31
  82. package/src/quilts/blame.js +0 -103
  83. package/src/quilts/block.js +0 -347
  84. package/src/quilts/bucket.js +0 -25
  85. package/src/quilts/cbor.js +0 -92
  86. package/src/quilts/claims.js +0 -144
  87. package/src/quilts/cli.js +0 -449
  88. package/src/quilts/client.js +0 -128
  89. package/src/quilts/conflict-format.js +0 -190
  90. package/src/quilts/describe.js +0 -266
  91. package/src/quilts/dogfood.js +0 -82
  92. package/src/quilts/errors.js +0 -8
  93. package/src/quilts/events.js +0 -160
  94. package/src/quilts/export-git.js +0 -130
  95. package/src/quilts/export.js +0 -81
  96. package/src/quilts/fastmerge.js +0 -41
  97. package/src/quilts/hash.js +0 -44
  98. package/src/quilts/history.js +0 -266
  99. package/src/quilts/ids.js +0 -12
  100. package/src/quilts/import-git.js +0 -291
  101. package/src/quilts/import.js +0 -134
  102. package/src/quilts/index.js +0 -72
  103. package/src/quilts/limits.js +0 -67
  104. package/src/quilts/lines.js +0 -49
  105. package/src/quilts/live.js +0 -50
  106. package/src/quilts/merge-smart.js +0 -114
  107. package/src/quilts/merge.js +0 -486
  108. package/src/quilts/objects.js +0 -144
  109. package/src/quilts/pack.js +0 -118
  110. package/src/quilts/records.js +0 -211
  111. package/src/quilts/resolutions.js +0 -69
  112. package/src/quilts/reviews.js +0 -195
  113. package/src/quilts/routes.js +0 -126
  114. package/src/quilts/service.js +0 -923
  115. package/src/quilts/setup.js +0 -34
  116. package/src/quilts/signing.js +0 -149
  117. package/src/quilts/squares.js +0 -239
  118. package/src/quilts/store-memory.js +0 -683
  119. package/src/quilts/store-rpc.js +0 -79
  120. package/src/quilts/store-supabase.js +0 -13
  121. package/src/quilts/tags.js +0 -64
  122. package/src/quilts/textdiff.js +0 -96
  123. package/src/quilts/tokens.js +0 -66
  124. package/src/quilts/tools.js +0 -375
  125. package/src/quilts/tree.js +0 -351
  126. package/src/quilts/webhook.js +0 -116
  127. package/src/quilts/window.js +0 -119
  128. package/src/quilts/xdiff.js +0 -435
  129. package/src/ui/branches.js +0 -111
@@ -1,160 +0,0 @@
1
- // Events (plans/quilt-source-control/12): never written in the commit or merge transaction. The
2
- // events job reads the quilt's log after its cursor, derives each event and its recipients, and
3
- // writes them with the cursor moved in one transaction (store.addEvents); then it POSTs each one
4
- // to the recipients' webhooks, signed and retried by seam's own webhook.js. Members read theirs
5
- // over HTTP (GET /v1/quilts/:q/events?after=), the CLI (`seam events`) and the MCP tool seam_events, so an agent in any tool is woken the same way.
6
- import { QuiltError, squareShort } from './service.js'
7
- import { makeSubscription, deliverWebhook, RETRY_DELAYS_MS } from './webhook.js'
8
-
9
- export const QUILT_EVENT_KINDS = ['patch.merged', 'patch.reverted', 'conflict.added', 'square.handoff']
10
- const MAX_DIFF_PATHS = 200
11
-
12
- export function eventOps (svc, sq, { allowLocalWebhooks = false, fetch, delays = RETRY_DELAYS_MS, log = () => {} } = {}) {
13
- const { store, now, quiltFor, getCommit, getTerm, conflictsAt, snapshot, reader } = svc.internals
14
- const running = new Map() // quilt → the job in flight, so one process runs one job per quilt at a time
15
- const sending = new Map() // quilt + member → delivery chain, so a member's POSTs keep their order
16
-
17
- const by = (c) => ({ member_id: c.author.member_id, kind: c.author.kind, name: c.author.name, ...(c.author.tool ? { tool: c.author.tool } : {}) })
18
-
19
- async function changedPaths (qid, s) {
20
- if (!s.head) return []
21
- return (await reader(qid).diff(await snapshot(qid, s.base), await snapshot(qid, s.head))).map((e) => e.path)
22
- }
23
-
24
- /** The events one log row causes. */
25
- async function derive (qid, row) {
26
- const out = []
27
- if (row.kind === 'merge' && row.payload?.reverts) {
28
- // patch.reverted: the reverted patch's holder (and whom they acted for) learns it is gone (07 step 4).
29
- const c = await getCommit(qid, row.commit)
30
- const gone = await store.patch(qid, row.line, Number(row.payload.reverts))
31
- const orig = gone && gone.square ? await store.getSquare(gone.square) : null
32
- const oc = gone ? await getCommit(qid, gone.commit).catch(() => null) : null
33
- const to = [orig && !orig.import ? orig.holder : null, oc && oc.on_behalf_of].filter(Boolean)
34
- if (to.length) {
35
- out.push({
36
- kind: 'patch.reverted', at: row.at, cause_seq: row.seq, square: orig.id, line: row.line, patch_no: Number(row.payload.reverts), by: by(c), paths: c.changes.map((ch) => ch.path).slice(0, MAX_DIFF_PATHS),
37
- payload: { patch_no: Number(row.payload.reverts), revert_patch_no: row.patch_no, commit: c.id, message: c.message },
38
- recipients: [...new Set(to)]
39
- })
40
- }
41
- }
42
- if (row.kind === 'merge') {
43
- // patch.merged: to the holder and writers of every other open square on the line that changed a path the patch changed.
44
- const c = await getCommit(qid, row.commit)
45
- const touched = new Set(c.changes.map((ch) => ch.path))
46
- for (const s of await store.squares(qid, { state: 'open' })) {
47
- if (s.id === row.square || s.line !== row.line) continue
48
- const overlap = (await changedPaths(qid, s)).filter((p) => touched.has(p))
49
- if (!overlap.length) continue
50
- const st = await sq.statusOf(qid, s)
51
- const wc = (st.would_conflict || []).map((w) => w.path)
52
- out.push({
53
- kind: 'patch.merged', at: row.at, cause_seq: row.seq, square: s.id, line: row.line, patch_no: row.patch_no, by: by(c), paths: overlap,
54
- ...(c.task_id ? { task: { id: c.task_id } } : {}),
55
- payload: { patch_no: row.patch_no, commit: c.id, message: c.message, author: c.author.member_id, ...(c.task_id ? { task: c.task_id } : {}), stat: c.changes.slice(0, MAX_DIFF_PATHS).map((ch) => ({ path: ch.path, op: ch.op })), would_conflict: wc.length > 0, would_conflict_paths: wc },
56
- recipients: [s.holder, ...(s.writers || [])]
57
- })
58
- }
59
- } else if (row.kind === 'commit') {
60
- // conflict.added: a term at a path that did not hold that term before this commit.
61
- const c = await getCommit(qid, row.commit)
62
- const terms = c.changes.filter((ch) => ch.op === 'conflict')
63
- if (!terms.length) return out
64
- const prev = c.predecessor || c.parents[0]
65
- const before = new Map(prev ? (await conflictsAt(qid, prev)).map((e) => [e.path, e.term]) : [])
66
- const s = await store.getSquare(row.square)
67
- const cause = row.payload?.rebased_onto ? { commit: row.payload.rebased_onto, ...((await getCommit(qid, row.payload.rebased_onto).catch(() => null))?.patch || {}) } : row.payload?.merged_from ? { commit: row.payload.merged_from } : row.payload?.folded_from ? { square: row.payload.folded_from } : null
68
- for (const ch of terms) {
69
- if (before.get(ch.path) === ch.term) continue
70
- const t = await getTerm(qid, ch.term)
71
- out.push({
72
- kind: 'conflict.added', at: row.at, cause_seq: row.seq, square: row.square, line: s && s.line, by: by(c), paths: [ch.path],
73
- payload: { path: ch.path, kind: t.kind, term: ch.term, commit: c.id, sides: t.adds.map((a) => ({ by: a.by || null, commit: a.from_commit || null, ...(a.from_square ? { square: squareShort(a.from_square) } : {}) })), ...(cause ? { caused_by: cause } : {}), resolver: s ? s.holder : null },
74
- recipients: s ? [s.holder] : []
75
- })
76
- }
77
- } else if (row.kind === 'square' && row.payload?.op === 'handoff') {
78
- out.push({
79
- kind: 'square.handoff', at: row.at, cause_seq: row.seq, square: row.square, by: { member_id: row.actor },
80
- payload: { from: row.payload.from, to: row.payload.to, ...(row.payload.context ? { note: row.payload.context } : {}) },
81
- recipients: [row.payload.from, row.payload.to]
82
- })
83
- }
84
- return out
85
- }
86
-
87
- /** Runs the events job for a quilt until it has read the whole log; returns the new events. */
88
- function pump (qid) {
89
- const prev = running.get(qid) || Promise.resolve()
90
- const job = prev.catch(() => {}).then(async () => {
91
- const made = []
92
- for (;;) {
93
- const cursor = await store.eventCursor(qid)
94
- const rows = await store.logRows(qid, { after: cursor, limit: 200 })
95
- if (!rows.length) return made
96
- const events = []
97
- for (const r of rows) events.push(...await derive(qid, r))
98
- try {
99
- made.push(...await store.addEvents({ quilt_id: qid, events, expect_cursor: cursor, cursor: rows.at(-1).seq }))
100
- } catch (err) { if (err.code !== 'cursor_moved') throw err }
101
- }
102
- })
103
- running.set(qid, job)
104
- job.finally(() => { if (running.get(qid) === job) running.delete(qid) }).catch(() => {})
105
- // Deliveries run beside the job (a dead webhook's retries never hold up reading events).
106
- return job.then((made) => { const d = deliver(qid, made); inflight.add(d); d.finally(() => inflight.delete(d)); return made })
107
- }
108
- const inflight = new Set()
109
- /** Waits for every webhook POST in flight (tests, and a clean shutdown). */
110
- const eventsSettled = async () => { while (inflight.size) await Promise.all([...inflight]) }
111
-
112
- /** The event as a member sees it (12's shape; recipients are not shown). */
113
- const view = (e) => {
114
- const { recipients, quilt_id: q, ...rest } = e
115
- void recipients
116
- return { ...rest, quilt: q, ...(e.square ? { square_short: squareShort(e.square) } : {}) }
117
- }
118
-
119
- /** POSTs new events to every recipient that subscribed (one chain per member, in order). */
120
- async function deliver (qid, events) {
121
- const waits = []
122
- for (const e of events) {
123
- for (const m of new Set(e.recipients || [])) {
124
- const sub = await store.webhook(qid, m)
125
- if (!sub || !sub.events.includes(e.kind)) continue
126
- const key = `${qid}\u0000${m}`
127
- const payload = { event: e.kind, id: `${qid}:${e.event_seq}`, to: m, ts: e.at || now(), ...view(e) }
128
- const next = (sending.get(key) || Promise.resolve()).then(() => deliverWebhook(sub, payload, { delays, log, ...(fetch ? { fetch } : {}) })).catch(() => {})
129
- sending.set(key, next)
130
- waits.push(next)
131
- }
132
- }
133
- await Promise.all(waits)
134
- }
135
-
136
- /** GET /events: the caller's events after `after` (the job runs first, so none is missing). */
137
- async function events (me, ref, { after = 0, kinds, limit = 100 } = {}) {
138
- const q = await quiltFor(me, ref)
139
- await pump(q.id)
140
- const list = await store.eventsFor(q.id, me.member_id, { after: Number(after) || 0, kinds: kinds && kinds.length ? kinds : undefined, limit: Math.min(Number(limit) || 100, 1000) })
141
- return { events: list.map(view), after: list.length ? list.at(-1).event_seq : Number(after) || 0 }
142
- }
143
-
144
- /** POST /webhook: { url, secret?, events?, bearer? } subscribes the caller; { url: null } removes it. */
145
- async function subscribe (me, ref, body = {}) {
146
- const q = await quiltFor(me, ref)
147
- if (body.url === null || body.off) { await store.setWebhook({ quilt_id: q.id, member_id: me.member_id, sub: null }); return { ok: true, removed: true } }
148
- const kinds = body.events == null ? QUILT_EVENT_KINDS.slice() : body.events
149
- if (!Array.isArray(kinds) || !kinds.length || kinds.some((k) => !QUILT_EVENT_KINDS.includes(k))) throw new QuiltError(400, 'bad_request', `events: give some of ${QUILT_EVENT_KINDS.join(', ')}`)
150
- let sub
151
- try { sub = makeSubscription({ url: body.url, secret: body.secret, bearer: body.bearer }, { allowLocal: allowLocalWebhooks }) } catch (err) { throw new QuiltError(400, 'bad_request', err.message) }
152
- const row = { url: sub.url, secret: sub.secret, events: [...new Set(kinds)], since: sub.since, ...(sub.bearer ? { bearer: sub.bearer } : {}) }
153
- // Events already in the table are not news to a new subscription: the job runs first.
154
- await pump(q.id)
155
- await store.setWebhook({ quilt_id: q.id, member_id: me.member_id, sub: row })
156
- return { ok: true, url: row.url, events: row.events, ...(sub.made ? { secret: row.secret } : {}) }
157
- }
158
-
159
- return { events, subscribe, pumpEvents: pump, eventsSettled }
160
- }
@@ -1,130 +0,0 @@
1
- // The local driver of the one-way git export (plans/quilt-source-control/14, Export): reads a
2
- // line's patches from the server (GET /quilts/:q/export/git) and writes each as one commit on a
3
- // branch of a plain git repository with `git fast-import`, so CI and deploys that expect git
4
- // keep working. The commit fields come from the server whole, so the same patch always gives
5
- // the same commit id, on any machine, in any number of runs. A run picks up after the last
6
- // patch it exported (state in the repository's `quilt-export.json`), and refuses a branch that
7
- // moved under it: the export is one way, never a merge.
8
- import { spawn, execFileSync } from 'node:child_process'
9
- import fs from 'node:fs'
10
- import path from 'node:path'
11
-
12
- const MODES = { file: '100644', exec: '100755', symlink: '120000', gitlink: '160000' }
13
- const GIT_ENV = { ...process.env, GIT_NO_REPLACE_OBJECTS: '1', GIT_CONFIG_NOSYSTEM: '1' }
14
-
15
- const git = (dir, args) => execFileSync('git', ['-C', dir, ...args], { env: GIT_ENV, maxBuffer: 256 * 1024 * 1024 }).toString().trim()
16
- const tryGit = (dir, args) => { try { return git(dir, args) } catch { return null } }
17
-
18
- /** A path as fast-import reads it: C-quoted when it would otherwise be misread. */
19
- const fiPath = (p) => (/^"|[\n"\\]/.test(p) ? '"' + p.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"' : p)
20
- const ident = (who) => `${who.name} <${who.email}> ${who.at} +0000`
21
-
22
- /** The repository at `dir`, made bare when it does not exist yet: its git dir. */
23
- function openRepo (dir) {
24
- if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); git(dir, ['init', '-q', '--bare']) }
25
- const gd = tryGit(dir, ['rev-parse', '--absolute-git-dir'])
26
- if (!gd) throw new Error(`${dir} is not a git repository`)
27
- return gd
28
- }
29
-
30
- /** fast-import as a writable stream with backpressure; done() → its exit. */
31
- function fastImport (dir, marks) {
32
- const p = spawn('git', ['-C', dir, 'fast-import', '--quiet', '--date-format=raw', `--export-marks=${marks}`], { env: GIT_ENV, stdio: ['pipe', 'ignore', 'pipe'] })
33
- let err = ''
34
- p.stderr.on('data', (d) => { err += d })
35
- const exit = new Promise((resolve, reject) => p.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`git fast-import failed: ${err.trim()}`)))))
36
- exit.catch(() => {})
37
- return {
38
- write (chunk) { return p.stdin.write(chunk) ? null : new Promise((resolve) => p.stdin.once('drain', resolve)) },
39
- async done () { p.stdin.end(); await exit }
40
- }
41
- }
42
-
43
- /** Every blob in `ids`, as Buffers, in as many requests as the server's size cap needs. */
44
- async function fetchBlobs (client, q, ids) {
45
- const out = new Map()
46
- let todo = [...new Set(ids)]
47
- while (todo.length) {
48
- const r = await client.blobs(q, todo.slice(0, 2000))
49
- const got = Object.entries(r.blobs || {})
50
- if (!got.length) throw new Error(`the server returned none of ${todo.length} blobs`)
51
- for (const [id, b64] of got) out.set(id, Buffer.from(b64, 'base64'))
52
- todo = todo.filter((id) => !out.has(id))
53
- }
54
- return out
55
- }
56
-
57
- /**
58
- * Exports `line` of quilt `q` to `branch` of the repository at `dir` (created bare when
59
- * missing). Returns { exported, patch_no, sha, branch }.
60
- */
61
- export async function exportGit (client, { q, dir, line = 'main', branch, page = 200, log = () => {} }) {
62
- branch = branch || line
63
- const gitDir = openRepo(dir)
64
- const stateFile = path.join(gitDir, 'quilt-export.json')
65
- const shaFile = path.join(gitDir, 'quilt-export-shas')
66
- const all = fs.existsSync(stateFile) ? JSON.parse(fs.readFileSync(stateFile, 'utf8')) : {}
67
- const key = `${line}→${branch}`
68
- const state = all[key] || null
69
- const tip = tryGit(dir, ['rev-parse', '--verify', '-q', `refs/heads/${branch}`])
70
- if (state && state.quilt !== undefined && state.quilt !== null) {
71
- const info = await client.quilt(q)
72
- if (info.id !== state.quilt) throw new Error(`${dir} holds the export of another quilt (${state.quilt})`)
73
- }
74
- if (state ? tip !== state.sha : tip) throw new Error(`branch ${branch} in ${dir} moved since the last export (${tip || 'gone'}, expected ${state ? state.sha : 'no branch'}): the export never merges or forces; point it at a new branch or repository`)
75
-
76
- let after = state ? state.patch_no : -1
77
- let sha = state ? state.sha : null
78
- let exported = 0
79
- let version = state ? state.version : null
80
- let quiltId = state ? state.quilt : null
81
- for (;;) {
82
- const r = await client.exportPatches(q, { line, after, limit: page })
83
- if (version !== null && version !== r.version) throw new Error(`export format ${r.version} differs from the ${version} this branch was exported with: export to a new branch`)
84
- version = r.version; quiltId = r.quilt
85
- if (!r.patches.length) break
86
- const blobs = await fetchBlobs(client, q, r.patches.flatMap((p) => p.changes.filter((c) => c.op === 'put').map((c) => c.blob)))
87
- const marksFile = path.join(gitDir, 'quilt-export.marks')
88
- const fi = fastImport(dir, marksFile)
89
- const send = async (s) => { const w = fi.write(s); if (w) await w }
90
- let mark = 0
91
- const blobMark = new Map()
92
- for (const [id, bytes] of blobs) {
93
- // A gitlink's content is the submodule commit it pins; git writes no blob for it.
94
- if (r.patches.some((p) => p.changes.some((c) => c.blob === id && c.mode === 'gitlink'))) blobMark.set(id, bytes.toString('utf8').trim())
95
- if (blobMark.has(id)) continue
96
- blobMark.set(id, ++mark)
97
- await send(`blob\nmark :${mark}\ndata ${bytes.length}\n`); await send(bytes); await send('\n')
98
- }
99
- const commitMark = new Map()
100
- let first = true
101
- for (const p of r.patches) {
102
- commitMark.set(p.patch_no, ++mark)
103
- const msg = Buffer.from(p.message, 'utf8')
104
- let s = `commit refs/heads/${branch}\nmark :${mark}\nauthor ${ident(p.author)}\ncommitter ${ident(p.committer)}\ndata ${msg.length}\n`
105
- await send(s); await send(msg); s = '\n'
106
- if (first && sha) s += `from ${sha}\n`
107
- first = false
108
- for (const c of p.changes) {
109
- if (c.op === 'delete') s += `D ${fiPath(c.path)}\n`
110
- else if (c.mode === 'gitlink') s += `M 160000 ${blobMark.get(c.blob)} ${fiPath(c.path)}\n`
111
- else s += `M ${MODES[c.mode || 'file'] || MODES.file} :${blobMark.get(c.blob)} ${fiPath(c.path)}\n`
112
- }
113
- await send(s + '\n')
114
- }
115
- await fi.done()
116
- const marks = new Map(fs.readFileSync(marksFile, 'utf8').split('\n').filter(Boolean).map((l) => { const [m, h] = l.split(' '); return [Number(m.slice(1)), h] }))
117
- fs.rmSync(marksFile, { force: true })
118
- const lines = r.patches.map((p) => `${line} ${p.patch_no} ${marks.get(commitMark.get(p.patch_no))}`)
119
- fs.appendFileSync(shaFile, lines.join('\n') + '\n')
120
- const last = r.patches[r.patches.length - 1]
121
- after = last.patch_no
122
- sha = marks.get(commitMark.get(last.patch_no))
123
- exported += r.patches.length
124
- all[key] = { quilt: quiltId, line, branch, patch_no: after, sha, version }
125
- fs.writeFileSync(stateFile, JSON.stringify(all, null, 2) + '\n')
126
- log(` ${line}/${after} → ${sha.slice(0, 12)} (${exported} exported)`)
127
- }
128
- if (sha && !tryGit(dir, ['symbolic-ref', '-q', 'HEAD'])?.endsWith(`/${branch}`) && !tryGit(dir, ['rev-parse', '--verify', '-q', 'HEAD'])) git(dir, ['symbolic-ref', 'HEAD', `refs/heads/${branch}`])
129
- return { exported, patch_no: after, sha, branch }
130
- }
@@ -1,81 +0,0 @@
1
- // The server half of the one-way git export (plans/quilt-source-control/14, Export): the
2
- // patches of a line in order, each with everything the exported git commit is made from, so a
3
- // driver (export-git.js) can write the same git objects from them on any machine. The git
4
- // fields are worked out here, from the commit record and the merge row only, so the same patch
5
- // always gives the same commit id: nothing from the clock, the driver or a member's current
6
- // profile gets in (14, Determinism).
7
- import { QuiltError } from './service.js'
8
-
9
- const EXPORT_VERSION = 1
10
- const COMMITTER = { name: 'Quilt', email: 'export@heyquilt.com' }
11
-
12
- /** A member slug safe for an email local part. */
13
- const slugOf = (id) => String(id || 'unknown').replace(/^[a-z]+:/, '').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'unknown'
14
-
15
- /**
16
- * The git identity of a commit's author, frozen in the commit record. A stub member from a git
17
- * import keeps the email git had (it was public there); everyone else gets the noreply address
18
- * of 14 (`<slug>@people|agents.noreply.heyquilt.com`), since no member has opted in to a
19
- * public email yet.
20
- */
21
- export function exportAuthor (author = {}) {
22
- const id = String(author.member_id || '')
23
- const kind = author.kind === 'agent' ? 'agent' : 'person'
24
- const stub = id.startsWith('stub:') ? id.slice(5) : ''
25
- const email = stub.includes('@') ? stub : `${slugOf(id)}@${kind === 'agent' ? 'agents' : 'people'}.noreply.heyquilt.com`
26
- const name = String(author.name || stub || slugOf(id)).replace(/[<>\n]/g, '').trim() || slugOf(id)
27
- return { name, email, slug: slugOf(id), kind, ...(author.tool ? { tool: author.tool } : {}), ...(author.model ? { model: author.model } : {}) }
28
- }
29
-
30
- /** The commit message with 14's trailers, in their fixed order. */
31
- export function exportMessage ({ commit, line, patch_no: patchNo, git_sha: gitSha }) {
32
- const a = exportAuthor(commit.author)
33
- const trailers = [
34
- `Quilt-Commit: ${commit.id}`,
35
- `Quilt-Square: ${commit.square_id}`,
36
- `Quilt-Patch: ${line}/${patchNo}`,
37
- `Quilt-Author: ${a.slug} (${[a.kind, a.tool, a.model].filter(Boolean).join(', ')})`,
38
- ...(commit.on_behalf_of ? [`Quilt-On-Behalf-Of: ${slugOf(commit.on_behalf_of)}`] : []),
39
- ...(commit.task_id ? [`Quilt-Task: ${commit.task_id}`] : []),
40
- ...Object.keys(commit.stack || {}).sort().map((k) => `Quilt-Stack-${k}: ${commit.stack[k]}`),
41
- ...(gitSha ? [`Quilt-Git-Sha: ${gitSha}`] : [])
42
- ]
43
- const body = String(commit.message || '').replace(/\s+$/, '')
44
- return `${body || '(no message)'}\n\n${trailers.join('\n')}\n`
45
- }
46
-
47
- export function exportOps (svc) {
48
- const { quiltFor, getCommit, store } = svc.internals
49
-
50
- /**
51
- * GET /quilts/:q/export/git: the patches on `line` after `after`, oldest first, at most
52
- * `limit`: { version, line, patch_count, patches: [{ patch_no, commit, parent, author,
53
- * committer, message, changes }] }. `changes` are the patch commit's changes against its
54
- * first parent (the previous patch), which is what the exported commit changes too.
55
- */
56
- async function exportPatches (me, ref, { line = 'main', after = -1, limit = 200 } = {}) {
57
- const q = await quiltFor(me, ref)
58
- const ln = await store.getLine(q.id, line)
59
- if (!ln) throw new QuiltError(404, 'not_found', `no line ${line}`)
60
- const max = Math.max(1, Math.min(Number(limit) || 200, 1000))
61
- const rows = await store.patches(q.id, line, { after: Number(after), limit: max, desc: false })
62
- const patches = []
63
- for (const row of rows) {
64
- const c = await getCommit(q.id, row.commit)
65
- const gitSha = store.gitShaOf ? await store.gitShaOf(q.id, c.id) : null
66
- const a = exportAuthor(c.author)
67
- patches.push({
68
- patch_no: row.patch_no,
69
- commit: c.id,
70
- parent: c.parents[0] || null,
71
- author: { name: a.name, email: a.email, at: Math.floor((c.authored_at || 0) / 1000) },
72
- committer: { ...COMMITTER, at: Math.floor((row.at || 0) / 1000) },
73
- message: exportMessage({ commit: c, line, patch_no: row.patch_no, git_sha: gitSha }),
74
- changes: c.changes.map((ch) => (ch.op === 'delete' ? { path: ch.path, op: 'delete' } : { path: ch.path, op: 'put', blob: ch.blob, mode: ch.mode }))
75
- })
76
- }
77
- return { version: EXPORT_VERSION, quilt: q.id, line, patch_count: ln.patch_count, patches }
78
- }
79
-
80
- return { exportPatches }
81
- }
@@ -1,41 +0,0 @@
1
- // A fast front for merge-smart.js's mergeSmart (whose node-diff3 LCS is quadratic: seconds on a
2
- // 7,000-line file). A clean line merge comes from the Myers hunks in textdiff.js (`nway`, the
3
- // same N-way merge the conflict format uses); only a clash goes to mergeSmart's ladder, and then
4
- // with the lines all three versions share at the start and end cut off, so it reads only the
5
- // region that differs. Same answer, same `how`, as mergeSmart for a clean merge; a clash is
6
- // mergeSmart's own answer on the middle, put back between the shared lines.
7
- import { mergeSmart } from './merge-smart.js'
8
- import { nway } from './conflict-format.js'
9
-
10
- export function fastMerge (base, ours, theirs) {
11
- const [o, a, b] = [base.split('\n'), ours.split('\n'), theirs.split('\n')]
12
- const regions = nway(o, [a, b])
13
- if (regions.every((r) => r.ok)) return { text: regions.flatMap((r) => r.ok).join('\n'), conflicts: [], how: 'lines' }
14
- // Every clash is two inserts at one place the base had nothing (07's `additive` step): ours
15
- // then theirs, shared leading lines once. Shared trailing lines are kept on both sides, not
16
- // once as 07 says: two added functions share their closing `}`, and taking it once dropped
17
- // one function's closing brace (found by real agents adding functions side by side).
18
- if (regions.every((r) => r.ok || (!r.base.length && r.sides.length === 2))) {
19
- return { text: regions.flatMap((r) => r.ok || additive(r.sides[0], r.sides[1])).join('\n'), conflicts: [], how: 'additive' }
20
- }
21
- // Shared lines at both ends, leaving at least one line in each middle.
22
- const min = Math.min(o.length, a.length, b.length) - 1
23
- let p = 0
24
- while (p < min && o[p] === a[p] && o[p] === b[p]) p++
25
- let s = 0
26
- while (s < min - p && o[o.length - 1 - s] === a[a.length - 1 - s] && o[o.length - 1 - s] === b[b.length - 1 - s]) s++
27
- if (!p && !s) return mergeSmart(base, ours, theirs)
28
- const mid = (x) => x.slice(p, x.length - s).join('\n')
29
- const m = mergeSmart(mid(o), mid(a), mid(b))
30
- if (m.text === '') return mergeSmart(base, ours, theirs) // zero lines or one empty line: ambiguous
31
- return { ...m, text: [...o.slice(0, p), m.text, ...o.slice(o.length - s)].join('\n') }
32
- }
33
-
34
- /** Two inserts at one place: ours, then what theirs adds after the leading lines both share. */
35
- function additive (a, b) {
36
- let pre = 0
37
- while (pre < a.length && pre < b.length && a[pre] === b[pre]) pre++
38
- if (pre === b.length) return a
39
- if (pre === a.length) return b
40
- return [...a, ...b.slice(pre)]
41
- }
@@ -1,44 +0,0 @@
1
- // Ids for Quilt source control (plans/quilt-source-control/03): BLAKE3-256 with a
2
- // domain-separation context per kind of object, so bytes of one kind can never be presented
3
- // as another. Ids travel as 64 hex characters.
4
- import { blake3 } from '@noble/hashes/blake3.js'
5
-
6
- const enc = new TextEncoder()
7
- const contexts = new Map()
8
-
9
- /** The context bytes for a kind and format version, e.g. 'commit' → "quilt commit v1". */
10
- function context (kind) {
11
- let c = contexts.get(kind)
12
- if (!c) { c = enc.encode(`quilt ${kind} v1`); contexts.set(kind, c) }
13
- return c
14
- }
15
-
16
- export const KINDS = ['commit', 'blob', 'tree', 'chunk', 'pack', 'term', 'manifest']
17
-
18
- /** The id of `bytes` as an object of `kind`: BLAKE3 derive-key mode, 32 bytes, as hex. */
19
- // Not object kinds: keys made with their own contexts so they can never equal an object id.
20
- const KEY_KINDS = ['idempotency', 'resolution']
21
-
22
- /** A key (not an object id) in its own context, e.g. 'idempotency' → "quilt idempotency v1". */
23
- export function keyOf (kind, bytes) {
24
- if (!KEY_KINDS.includes(kind)) throw new Error(`unknown key kind ${kind}`)
25
- return Buffer.from(blake3(bytes, { context: context(kind) })).toString('hex')
26
- }
27
-
28
- export function idOf (kind, bytes) {
29
- if (!KINDS.includes(kind)) throw new Error(`unknown object kind ${kind}`)
30
- return Buffer.from(blake3(bytes, { context: context(kind) })).toString('hex')
31
- }
32
-
33
- export const ID = /^[0-9a-f]{64}$/
34
-
35
- /** Whether `s` looks like an id (64 lowercase hex). */
36
- export const isId = (s) => typeof s === 'string' && ID.test(s)
37
-
38
- /** An id as its 32 bytes. */
39
- export const idBytes = (hex) => Buffer.from(hex, 'hex')
40
-
41
- /** A quilt's shard (03): blake3 of its id's 16 bytes (plain hash mode), first byte mod 64. */
42
- export function shardOf (quiltId) {
43
- return blake3(Buffer.from(String(quiltId).replace(/-/g, ''), 'hex'))[0] % 64
44
- }