@popcorn245/cornfield 0.7.0 → 0.9.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/README.md +1 -0
- package/core/agent-contract.js +5 -2
- package/core/agent-contract.schema.json +9 -9
- package/core/config.js +94 -11
- package/core/mentions.js +239 -0
- package/explorer/explorer.css +12 -1
- package/explorer/forge-panel-model.js +88 -21
- package/explorer/forge-panel.js +512 -19
- package/explorer/index.html +46 -0
- package/explorer/panels.js +24 -0
- package/explorer/review-tokens-model.js +5 -1
- package/explorer/review-tokens.js +11 -7
- package/explorer/runs-model.js +107 -0
- package/explorer/state.js +7 -0
- package/package.json +1 -1
- package/server/agent-api.js +227 -0
- package/server/agent-runs.js +769 -0
- package/server/db.js +68 -0
- package/server/forge-actions.js +106 -5
- package/server/forge-forgejo.js +33 -5
- package/server/forge-github.js +12 -4
- package/server/forge-routes.js +33 -1
- package/server/forge.js +24 -7
- package/server/hooks.js +100 -1
- package/server/proposal-pulls.js +3 -3
- package/server/review-api.js +26 -12
- package/server/review-tokens.js +42 -25
- package/server/server.js +32 -8
- package/server/spec-poll.js +71 -14
- package/server/threads.js +275 -0
|
@@ -24,17 +24,21 @@
|
|
|
24
24
|
* where a step's plan is read, what the confirm step shows of it, what is sent to confirm it, and what is said of one that is made already
|
|
25
25
|
* landingUrl, cancelLandingUrl, landingWords
|
|
26
26
|
* a spec pull request waiting to land on its approval (`proposals.approveLands`, server/lander.js): where it is read, where it is cancelled, and the words
|
|
27
|
+
* threadsUrl, threadUrl, IDEAS_URL, threadKindWords, threadMeta, litForThread, mentionsIndex, mentionsOf, messageNodes, ideaRequest, ideaWords
|
|
28
|
+
* the Threads tab (docs/threads.md): what to ask, a thread's words, which nodes its text lights and which threads mention a node
|
|
27
29
|
* notesUrl, sinceChoices, notesSummary, validTag, confirmWords, postedWords, draftRequest
|
|
28
30
|
* the Release notes view: what to ask, the tags to start from, and the words around a draft
|
|
29
31
|
*/
|
|
30
32
|
|
|
31
33
|
import { relativeTime } from './review-model.js';
|
|
34
|
+
import { nodesForFile, linksOf, parseMentions, resolveMentions } from '../core/mentions.js';
|
|
32
35
|
|
|
33
|
-
export const KINDS = Object.freeze(['pulls', 'issues', 'deploys', 'notes']);
|
|
36
|
+
export const KINDS = Object.freeze(['pulls', 'issues', 'threads', 'deploys', 'notes']);
|
|
34
37
|
/** The states a list offers: [value, label]; a pull request that landed is "merged", one closed without landing "closed" */
|
|
35
38
|
export const STATES = Object.freeze({
|
|
36
39
|
pulls: [['open', 'Open'], ['merged', 'Merged'], ['closed', 'Closed'], ['all', 'All']],
|
|
37
40
|
issues: [['open', 'Open'], ['closed', 'Closed'], ['all', 'All']],
|
|
41
|
+
threads: [['open', 'Open'], ['closed', 'Closed'], ['all', 'All']],
|
|
38
42
|
deploys: [['all', 'All'], ['success', 'Succeeded'], ['failure', 'Failed'], ['running', 'Running or waiting'], ['other', 'Cancelled or skipped']],
|
|
39
43
|
});
|
|
40
44
|
/** How many a list asks for (the server's own default; its most is 300) */
|
|
@@ -50,9 +54,13 @@ export const defaultFilters = (kind = 'pulls') => ({ state: kind === 'deploys' ?
|
|
|
50
54
|
export const serverState = (state) => (state === 'merged' ? 'closed' : state);
|
|
51
55
|
|
|
52
56
|
/** The address of a list */
|
|
53
|
-
export const listUrl = (kind, state) => (kind === 'deploys' ? `/api/forge/deploys?limit=${LIST_LIMIT}` : `/api/forge/${kind}?state=${serverState(state)}&limit=${LIST_LIMIT}`);
|
|
57
|
+
export const listUrl = (kind, state) => (kind === 'deploys' ? `/api/forge/deploys?limit=${LIST_LIMIT}` : kind === 'threads' ? threadsUrl(state) : `/api/forge/${kind}?state=${serverState(state)}&limit=${LIST_LIMIT}`);
|
|
54
58
|
/** What a list is kept under: a deploys list is filtered by state here, so every state is one list */
|
|
55
59
|
export const listKeyOf = (kind, state) => (kind === 'deploys' ? 'deploys' : `${kind}|${serverState(state)}`);
|
|
60
|
+
/** The address of the threads list for a state, of one thread, and where a new idea is posted */
|
|
61
|
+
export const threadsUrl = (state) => `/api/forge/threads?state=${state}&limit=${LIST_LIMIT}`;
|
|
62
|
+
export const threadUrl = (n) => `/api/forge/threads/${n}`;
|
|
63
|
+
export const IDEAS_URL = '/api/forge/threads';
|
|
56
64
|
/** The address of one pull request with its files and checks */
|
|
57
65
|
export const pullUrl = (n) => `/api/forge/pulls/${n}`;
|
|
58
66
|
|
|
@@ -152,25 +160,8 @@ export function checksWords(checks) {
|
|
|
152
160
|
|
|
153
161
|
const clean = (p) => String(p).replace(/^\.\//, '').replace(/\/+$/, '');
|
|
154
162
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
* (longest) `file` wins. A file that holds several nodes (a page and what renders it) lights all of them.
|
|
158
|
-
* @param {string} path a repo-relative path
|
|
159
|
-
* @param {{ id: string, file?: string }[]} nodes
|
|
160
|
-
* @returns {object[]} the nodes, [] when none holds it
|
|
161
|
-
*/
|
|
162
|
-
export function nodesForFile(path, nodes) {
|
|
163
|
-
const p = clean(path);
|
|
164
|
-
let best = -1;
|
|
165
|
-
let out = [];
|
|
166
|
-
for (const node of nodes) {
|
|
167
|
-
if (typeof node.file !== 'string' || node.file === '') continue;
|
|
168
|
-
const file = clean(node.file);
|
|
169
|
-
if (p !== file && !p.startsWith(`${file}/`)) continue;
|
|
170
|
-
if (file.length > best) { best = file.length; out = [node]; } else if (file.length === best) out.push(node);
|
|
171
|
-
}
|
|
172
|
-
return out;
|
|
173
|
-
}
|
|
163
|
+
// The map's rule for a file's node(s) is core/mentions.js's too (a thread's [[server/auth.js]] resolves the same way)
|
|
164
|
+
export { nodesForFile };
|
|
174
165
|
|
|
175
166
|
const TEST_PATH = /(?:^|\/)(?:tests?|__tests__|spec|specs)\/|\.(?:test|spec)\.[a-z]+$|_test\.[a-z]+$|(?:^|\/)test_[^/]+\.py$/i;
|
|
176
167
|
/** Whether a path looks like a test file */
|
|
@@ -279,6 +270,82 @@ export function litForDeploy(deploy, nodes) {
|
|
|
279
270
|
: { nodes: hit, missing, changed: ids, failing: new Set() };
|
|
280
271
|
}
|
|
281
272
|
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Threads: a conversation on the forge that points at the map (core/mentions.js)
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
/** "Idea", or "Proposal 0040-meals" for a spec PR's conversation */
|
|
278
|
+
export const threadKindWords = (t) => (t.kind === 'proposal' ? `Proposal${t.slugs && t.slugs.length ? ` ${t.slugs.join(', ')}` : ''}` : 'Idea');
|
|
279
|
+
|
|
280
|
+
/** "#41 · maintainer-one · 3 comments · 2 days ago" (the time of the last message) */
|
|
281
|
+
export function threadMeta(t, nowMs) {
|
|
282
|
+
const count = t.kind === 'proposal' && !t.comments ? 'no comments' : `${t.comments} ${t.comments === 1 ? 'comment' : 'comments'}`;
|
|
283
|
+
return [`#${t.number}`, t.author ? t.author.login : 'someone', count, relativeTime(t.updatedAt, nowMs)].filter(Boolean).join(' · ');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The nodes the references in some texts stand for, against the map now on screen. `refs` are as the server gives them (or
|
|
288
|
+
* as `linksOf` finds them); what is on no node stays text.
|
|
289
|
+
* @param {string[]} refs
|
|
290
|
+
* @param {{ id: string, file?: string }[]} nodes
|
|
291
|
+
* @returns {{ ids: string[], nodes: object[], unresolved: string[], resolved: { ref: string, ids: string[] }[] }}
|
|
292
|
+
*/
|
|
293
|
+
export function mentionsOf(refs, nodes) {
|
|
294
|
+
const got = resolveMentions(refs, nodes);
|
|
295
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
296
|
+
return { ...got, nodes: got.ids.map((id) => byId.get(id)).filter(Boolean) };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* What an open thread lights: the nodes its messages mention, as a pull request's files light theirs (amber, the rest
|
|
301
|
+
* faded). `messages` carry `body`; each message also says which nodes are its own (for the chips under it).
|
|
302
|
+
* @param {{ messages: { id: string, body: string }[] }} thread
|
|
303
|
+
* @param {{ id: string, file?: string }[]} nodes
|
|
304
|
+
* @returns {{ nodes: object[], unresolved: string[], changed: Set<string>, failing: Set<string>, perMessage: Map<string, { nodes: object[], unresolved: string[] }> }}
|
|
305
|
+
*/
|
|
306
|
+
export function litForThread(thread, nodes) {
|
|
307
|
+
const all = linksOf(thread.messages.map((m) => m.body));
|
|
308
|
+
const got = mentionsOf(all.refs, nodes);
|
|
309
|
+
const perMessage = new Map();
|
|
310
|
+
for (const m of thread.messages) perMessage.set(m.id, messageNodes(m.body, nodes));
|
|
311
|
+
return { nodes: got.nodes, unresolved: got.unresolved, changed: new Set(got.ids), failing: new Set(), perMessage };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The nodes one message mentions (its `[[…]]` and its `cornfield-thread` block), and the references that are on none */
|
|
315
|
+
export function messageNodes(body, nodes) {
|
|
316
|
+
const got = parseMentions(body);
|
|
317
|
+
const found = mentionsOf([...got.refs, ...(got.block ? got.block.nodes : [])], nodes);
|
|
318
|
+
return { nodes: found.nodes, unresolved: found.unresolved };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Which threads mention which nodes, from the list the server gave (each thread's references as written), against the map
|
|
323
|
+
* on screen.
|
|
324
|
+
* @param {{ number: number, refs: string[] }[]} items
|
|
325
|
+
* @param {{ id: string, file?: string }[]} nodes
|
|
326
|
+
* @returns {Map<string, object[]>} node id -> the threads, in the order of `items`
|
|
327
|
+
*/
|
|
328
|
+
export function mentionsIndex(items, nodes) {
|
|
329
|
+
const index = new Map();
|
|
330
|
+
for (const t of items) {
|
|
331
|
+
for (const id of resolveMentions(t.refs || [], nodes).ids) index.set(id, [...(index.get(id) || []), t]);
|
|
332
|
+
}
|
|
333
|
+
return index;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** A new idea's request body: the title and the text, trimmed, or null when there is no title */
|
|
337
|
+
export function ideaRequest(title, body) {
|
|
338
|
+
const t = String(title ?? '').trim();
|
|
339
|
+
return t ? { title: t, body: String(body ?? '').trim() } : null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** What a new idea came to, in words */
|
|
343
|
+
export function ideaWords(forgeName, res) {
|
|
344
|
+
if (res.ok) return `Opened idea #${res.body.number} on ${forgeName}. It shows in the list within a minute.`;
|
|
345
|
+
if (res.status === 401) return 'Your sign-in has ended. Sign in again to open an idea.';
|
|
346
|
+
return errorWords(res.status, res.body);
|
|
347
|
+
}
|
|
348
|
+
|
|
282
349
|
// ---------------------------------------------------------------------------
|
|
283
350
|
// The picture
|
|
284
351
|
// ---------------------------------------------------------------------------
|