@dev.fast/whiteboard 0.3.3 → 0.3.4-preview.20261019.125
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/dist/{account-alias-xOdmMnDH.js → account-alias-ClRtWF_-.js} +8 -6
- package/dist/{agent-cli-DT7MdaqL.js → agent-cli-C6AM9dml.js} +15 -10
- package/dist/agent-cli-DKpWo-4F.js +2 -0
- package/dist/{agent-client-BRBzv3AV.js → agent-client-G4KxB8zB.js} +13 -3
- package/dist/{authoring-tools-DGSEag8y.js → authoring-tools-Dfo0uOSj.js} +34 -27
- package/dist/{background-server-WKzI_a98.js → background-server-BF_lLFxY.js} +5 -5
- package/dist/{background-server-C_REWFkG.js → background-server-DJfYWRoN.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-BBSDi_dX.js → cli-CqOiiTW_.js} +1 -1
- package/dist/{cli-runner-DshTBske.js → cli-runner-D8CsXff4.js} +26 -26
- package/dist/cli.js +2 -2
- package/dist/{client-BN_FDxFr.js → client-lKVF_Elo.js} +253 -47
- package/dist/{fuzzy-match-bFX6wm8j.js → fuzzy-match-DH9lZivo.js} +42 -14
- package/dist/{headless-host-CS4Gv0c5.js → headless-host-C4bhVPZx.js} +513 -140
- package/dist/{headless-host-CzvYpKOX.js → headless-host-CeNnXxiL.js} +1 -1
- package/dist/lens-selection-CyJapcsl.js +214 -0
- package/dist/{local-data-DtQTIzHX.js → local-data-BdqqCYUD.js} +2434 -328
- package/dist/{mcp-DpKnnmHH.js → mcp-B2mvDEhC.js} +5 -9
- package/dist/{package-paths-jMg6JxDP.js → package-paths-Cm1tR8c4.js} +1 -1
- package/dist/{profile-DaqmODhc.js → profile-DcigfSms.js} +20 -8
- package/dist/profile-DthRnZ7x.js +2 -0
- package/dist/{remote-attach-DCngg8cl.js → remote-attach-DGwVRmY5.js} +4 -4
- package/dist/{remote-extensions-BCAbQZTv.js → remote-extensions-CferWwM9.js} +3 -3
- package/dist/{remote-extensions-Cf3L-ZGG.js → remote-extensions-D8W2IgKH.js} +1 -1
- package/dist/{remote-language-server-UWQgpoh-.js → remote-language-server-By-rEosC.js} +5 -5
- package/dist/{remote-uninstall-CcDOcGyi.js → remote-uninstall-DgHnDAFH.js} +6 -6
- package/dist/{request-origin-Zcn_CjX4.js → request-origin-DDmfnytX.js} +38 -10
- package/dist/{review-app-launcher-tqSQPlbu.js → review-app-launcher-CG3TFZEx.js} +2 -2
- package/dist/{review-home-paths-Lc02WFuX.js → review-home-paths-D1JVitqN.js} +19 -17
- package/dist/{review-telemetry-BHWJsnoD.js → review-telemetry-Myzm7o6-.js} +5 -5
- package/dist/runtime-Bj2tD33R.js +27 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-77-qkEhs.js → s3-DtJFWC0G.js} +1 -1
- package/dist/{s3-config-DDWWMh5x.js → s3-config-BTO12tCC.js} +1 -1
- package/dist/{s3-config-0xuCh-F_.js → s3-config-CSy3fx8H.js} +1 -1
- package/dist/server/desktop-host.js +29 -76
- package/dist/{server-discovery-BVqrE1_-.js → server-discovery-Bwa_6Tsj.js} +3 -3
- package/dist/sharing/index.d.ts +486 -70
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DsM3jO8G.js → src-BiWNbcF_.js} +89 -12
- package/dist/{src-BMPuxWv4.js → src-WPaQn75m.js} +208 -73
- package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-dFbewRTG.js} +270 -269
- package/dist/{thread-state-5pYr6_zM.js → thread-state-Do5LX10I.js} +7 -2
- package/dist/{tool-failure-BFt2xj9i.js → tool-failure-C9J3r7KD.js} +10 -3
- package/instructions/authoring.md +3 -2
- package/package.json +4 -4
- package/src/agent-connect.ts +8 -2
- package/src/ask/agents.ts +51 -9
- package/src/ask/checkout-files.ts +2 -5
- package/src/ask/thread-state.ts +4 -0
- package/src/ask/thread.ts +16 -0
- package/src/ask/threads.ts +24 -3
- package/src/cli-runner.ts +4 -1
- package/src/diff-selection-migration.ts +48 -34
- package/src/heading-anchors.ts +42 -0
- package/src/lens-selection.ts +38 -2
- package/src/review-api/README.md +52 -13
- package/src/review-api/activity.ts +1 -4
- package/src/review-api/agent-cli.ts +9 -1
- package/src/review-api/anchor-quotes.ts +14 -7
- package/src/review-api/ask-history.ts +137 -51
- package/src/review-api/authoring-tools.ts +45 -30
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/board-order.ts +144 -0
- package/src/review-api/checkout-groups.ts +560 -0
- package/src/review-api/document-headings.ts +58 -27
- package/src/review-api/document-text.ts +31 -2
- package/src/review-api/document.ts +281 -34
- package/src/review-api/foreign-references.ts +328 -0
- package/src/review-api/github-provider.ts +51 -13
- package/src/review-api/http.ts +702 -123
- package/src/review-api/instructions.ts +0 -19
- package/src/review-api/link-preview.ts +113 -0
- package/src/review-api/local-data.ts +310 -46
- package/src/review-api/mcp.ts +3 -11
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +37 -6
- package/src/review-api/public-tools.ts +24 -2
- package/src/review-api/pull-request.ts +82 -49
- package/src/review-api/request-origin.ts +10 -0
- package/src/review-api/review-progress.ts +61 -27
- package/src/review-api/scratchpad-plan-upgrade.ts +250 -0
- package/src/review-api/store-migrations.ts +357 -0
- package/src/review-api/store-schema.ts +38 -29
- package/src/review-api/store.ts +2144 -480
- package/src/review-api/workspaces.ts +52 -13
- package/src/review-api/worktree-events.ts +3 -5
- package/src/review-api/worktree-source.ts +3 -7
- package/src/review-api/worktree-structural.integration.ts +72 -0
- package/src/review-stack.ts +15 -23
- package/src/runtime.ts +4 -30
- package/src/server/desktop-host.ts +6 -1
- package/src/server/desktop-server.ts +1 -31
- package/src/server/headless-host.ts +0 -2
- package/src/server/hono-http.ts +2 -1
- package/src/server/legacy-tutorial-cleanup.ts +2 -3
- package/src/server/review-gateway-list.ts +3 -5
- package/src/server/review-gateway-memory.ts +1 -1
- package/src/server/review-lifecycle-telemetry.ts +2 -2
- package/src/server/review-server-core.ts +1 -5
- package/src/server/structural-comparisons.ts +47 -0
- package/src/sharing/export.ts +36 -7
- package/src/sharing/host.ts +34 -14
- package/src/sharing/import.ts +156 -24
- package/src/sharing/repository.ts +128 -20
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +110 -19
- package/src/ui-telemetry-events.ts +25 -9
- package/dist/agent-cli-DeCrUC2s.js +0 -2
- package/dist/profile-Cz7L781D.js +0 -2
- package/dist/runtime-CKiWhGrf.js +0 -52
- package/instructions/scratchpad.md +0 -23
- package/src/review-preferences.ts +0 -88
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type JsonValue, isJsonObject } from "@dev.fast/json";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
import {
|
|
@@ -15,10 +16,7 @@ import { commandSchema } from "./store.js";
|
|
|
15
16
|
/** The host publishes its input schemas, except where agents write less than
|
|
16
17
|
* the host accepts (publishedEditSchema, publishedLensEditSchema) or name a
|
|
17
18
|
* checkout by path (pathTargetSchema); adapters validate nothing. */
|
|
18
|
-
export function authoringTools(
|
|
19
|
-
scratchpadAvailable = false,
|
|
20
|
-
traceEnabled = false,
|
|
21
|
-
) {
|
|
19
|
+
export function authoringTools(traceEnabled = false) {
|
|
22
20
|
const id = z.string().min(1);
|
|
23
21
|
const review = { reviewId: id };
|
|
24
22
|
|
|
@@ -31,19 +29,21 @@ export function authoringTools(
|
|
|
31
29
|
...trace.shape,
|
|
32
30
|
kind: z.enum(["image", "trace"]),
|
|
33
31
|
})
|
|
32
|
+
.omit({ repositoryId: true })
|
|
33
|
+
.extend({ repositoryPath: id })
|
|
34
34
|
.partial()
|
|
35
|
-
.required({ id: true,
|
|
35
|
+
.required({ id: true, repositoryPath: true, kind: true });
|
|
36
36
|
|
|
37
37
|
const descriptions = {
|
|
38
38
|
create:
|
|
39
|
-
'Create a
|
|
39
|
+
'Create a whiteboard. target names its repository by the checkout\'s path, and what it shows: {kind:"project"} for a plan in that repository\'s project with no change; {kind:"worktree", base?} for the saved files in the checkout, uncommitted ones included and untracked ones left out (git add -N a new file to include it), against base, by default the repository\'s default branch, from the merge base of base and HEAD, which follows rebases; {kind:"commits", head, base?} for immutable commits (omitted base means source at head with no diff; supply the parent to review introduced changes); {kind:"pullRequest", pullRequest} for a GitHub PR by URL or number, fetched into that checkout and pinned at the PR head and GitHub diff base. For a branch stacked on another, pass the branch below it as base; Desktop links whiteboards into a stack through their bases. Revisions are resolved on acceptance. With a PR and no title the PR\'s title is used. When the existing review for that PR is in Trash, creation reports its ID; restore it with review_lifecycle state:"active" before continuing. When an active review for that PR exists, it is returned instead, reporting whether its head moved and whether an agent is working on it; update it in place, move its target with review_set_target, or create a separate review with reuseExisting:false. The result carries review, as review_list shows it, with its target and repositoryPath, so no follow-up read is needed before diffing. When Desktop is available the review opens there and the result reports opened, softwareMapEnabled and environmentIssues, as review_open does; set open:false to author in the background without taking over Desktop. parentId and dependsOn set its place among the project\'s reviews, as review_set_parent and review_set_dependencies do.',
|
|
40
40
|
set_target:
|
|
41
|
-
"Move the review to a new target, such as the commits after a rebase or amend,
|
|
41
|
+
"Move the review to a new target (the same targets review_create takes), such as the commits after a rebase or amend, or commits or a PR for a board that had none. The document and component IDs are kept. Revisions are resolved on acceptance. Returns warnings for retained source ranges in files the new commits changed, to verify, and resources that no longer match; fix them with review_edit. Earlier versions keep their source. A PR target names the PR; other targets keep it within the same repository and clear it across repositories.",
|
|
42
42
|
edit: [
|
|
43
|
-
"Edit one document component. content is one component, named by its type; each type's fields are in the Components section of session_get_instructions
|
|
44
|
-
"Anchors are head/path#L10-L20 or base/path#L7, or diff/path#L84-R90 for a range across sides, with repository-relative paths and 1-based inclusive lines. pins {
|
|
45
|
-
"insert adds content: parentId nests it, afterId places it, and omitted placement appends
|
|
46
|
-
"The host assigns short durable IDs. The result names the edited component and, for an insert or replace, its first-level children, and quotes the first and last line of each anchor the edit added or changed (the first 20; unquotedAnchors counts the rest): check they are the lines you meant. Accepted edits save at once.",
|
|
43
|
+
"Edit one document component. content is one component, named by its type; each type's fields are in the Components section of session_get_instructions.",
|
|
44
|
+
"Anchors are head/path#L10-L20 or base/path#L7, or diff/path#L84-R90 for a range across sides, with repository-relative paths and 1-based inclusive lines. pins {head, base?} say which commits of the whiteboard's repository anchors quote: a step, frame, attachment or operation without pins uses its block's, and a block without them the review's.",
|
|
45
|
+
"insert adds content: parentId nests it, afterId places it, and omitted placement appends. update patches a component's own fields; null removes an optional one. A section's children and a diagram's units change through their own IDs; a call stack's frames change by replacing the call stack. move and remove take a targetId; removing a flow_node removes its edges.",
|
|
46
|
+
"The host assigns short durable IDs. The result names the edited component and, for an insert or replace, its first-level children, and quotes the first and last line of each anchor the edit added or changed (the first 20; unquotedAnchors counts the rest): check they are the lines you meant. Accepted edits save at once. An edit to a block the user is editing is refused with an error: edit another block, or retry later.",
|
|
47
47
|
"While a reader may be watching, write small and often: one paragraph per edit. Insert a new diagram whole. Change one already drawn a unit at a time (a flow_node, flow_edge or step, with parentId naming the diagram), and link each added flow_node to a node already drawn.",
|
|
48
48
|
].join("\n"),
|
|
49
49
|
lens_edit:
|
|
@@ -56,7 +56,11 @@ export function authoringTools(
|
|
|
56
56
|
lifecycle:
|
|
57
57
|
"Move a review between active and trash. Trash retains the review for 30 days before automatic deletion. Moving it back to active restores it; moving it to trash again does not restart that period.",
|
|
58
58
|
delete:
|
|
59
|
-
"Permanently delete a review and its history. The review must be in Trash. This cannot be undone.",
|
|
59
|
+
"Permanently delete a review and its history. The review must be in Trash. This cannot be undone. Its children move up to top level, and dependencies on it are dropped.",
|
|
60
|
+
set_parent:
|
|
61
|
+
"Nest the review under another review of the same project, or make it top-level with parentId:null. Nesting is one level deep: the parent must be top-level, and a review with children can't become a child. The sidebar shows children under their parent. Saves no review version. Returns the review's relations: parentId, children in sibling order, and dependsOn.",
|
|
62
|
+
set_dependencies:
|
|
63
|
+
"Replace the list of reviews this one builds on: any reviews of the same project. Siblings list after what they depend on, directly or through other reviews; a PR stack is a chain. A list that would close a cycle is refused, naming the cycle. Saves no review version. Returns the review's relations.",
|
|
60
64
|
};
|
|
61
65
|
|
|
62
66
|
const tool = (
|
|
@@ -70,7 +74,7 @@ export function authoringTools(
|
|
|
70
74
|
name: `review_${name === "restore" ? "restore_version" : name}`,
|
|
71
75
|
description,
|
|
72
76
|
inputSchema: {
|
|
73
|
-
...z.toJSONSchema(schema, { io: "input" }),
|
|
77
|
+
...withoutRepositoryIds(z.toJSONSchema(schema, { io: "input" })),
|
|
74
78
|
type: "object" as const,
|
|
75
79
|
},
|
|
76
80
|
method,
|
|
@@ -92,9 +96,6 @@ export function authoringTools(
|
|
|
92
96
|
'Read Whiteboard\'s guidance before creating or editing a review. The default topic gives the authoring workflow; "file-lenses" covers Diff-view file lenses.' +
|
|
93
97
|
(traceEnabled
|
|
94
98
|
? ' Call review_get_instructions({topic:"trace-archaeology"}) for why code exists, what an agent was thinking, or whether an agent solved this before.'
|
|
95
|
-
: "") +
|
|
96
|
-
(scratchpadAvailable
|
|
97
|
-
? ' When the user asks in conversation to be shown how code works or wants a diagram, without asking for a review, draw it on the scratchpad rather than answering only in chat: call review_get_instructions({topic:"scratchpad"}) first. A request for a review or to use Whiteboard means authoring a review with the default topic.'
|
|
98
99
|
: ""),
|
|
99
100
|
instructionsQuerySchema.partial(),
|
|
100
101
|
"GET",
|
|
@@ -124,10 +125,12 @@ export function authoringTools(
|
|
|
124
125
|
...commandSchema.shape.operation.options.map((operation) => {
|
|
125
126
|
const type = operation.shape.type.value;
|
|
126
127
|
|
|
127
|
-
//
|
|
128
|
+
// Hidden from agents: they name checkouts by path inside the target
|
|
129
|
+
// and give PRs as a target.
|
|
128
130
|
const {
|
|
129
131
|
type: _type,
|
|
130
132
|
repositoryId: _repositoryId,
|
|
133
|
+
pullRequestUrl: _pullRequestUrl,
|
|
131
134
|
...fields
|
|
132
135
|
}: Record<string, z.ZodType> = operation.shape;
|
|
133
136
|
|
|
@@ -137,12 +140,7 @@ export function authoringTools(
|
|
|
137
140
|
z.strictObject({
|
|
138
141
|
...fields,
|
|
139
142
|
...(type === "create" && {
|
|
140
|
-
target: pathTargetSchema
|
|
141
|
-
repositoryPath: id
|
|
142
|
-
.optional()
|
|
143
|
-
.describe(
|
|
144
|
-
"Only with pullRequestUrl and no target: the local checkout to fetch the PR into. Default: the existing review's, else the first registered checkout with a remote for the PR's repository.",
|
|
145
|
-
),
|
|
143
|
+
target: pathTargetSchema,
|
|
146
144
|
open: z.boolean().optional(),
|
|
147
145
|
}),
|
|
148
146
|
...(type === "set_target" && { target: pathTargetSchema }),
|
|
@@ -196,13 +194,6 @@ export function authoringTools(
|
|
|
196
194
|
"POST",
|
|
197
195
|
"/workspace-cleanup",
|
|
198
196
|
),
|
|
199
|
-
tool(
|
|
200
|
-
"register_repository",
|
|
201
|
-
"Register a local Git or jj repository and return its id, for pins on scratchpad blocks. session_create and session_set_target take the checkout's path instead.",
|
|
202
|
-
z.strictObject({ path: id }),
|
|
203
|
-
"POST",
|
|
204
|
-
"/repositories",
|
|
205
|
-
),
|
|
206
197
|
tool(
|
|
207
198
|
"upload",
|
|
208
199
|
'Retain an image or trace for use in a review. kind:"image" takes base64; kind:"trace" takes trace. Reusing an upload ID requires identical content; rejected uploads are not saved.',
|
|
@@ -212,3 +203,27 @@ export function authoringTools(
|
|
|
212
203
|
),
|
|
213
204
|
];
|
|
214
205
|
}
|
|
206
|
+
|
|
207
|
+
/** Drops repositoryId from pins: the host fills in the board's own. */
|
|
208
|
+
function withoutRepositoryIds<T>(schema: T): T {
|
|
209
|
+
// SAFETY: a JSON round trip of a JSON Schema, dropping one property.
|
|
210
|
+
return JSON.parse(JSON.stringify(schema), (_key, value: JsonValue) => {
|
|
211
|
+
if (
|
|
212
|
+
!isJsonObject(value) ||
|
|
213
|
+
!isJsonObject(value.properties) ||
|
|
214
|
+
!("repositoryId" in value.properties) ||
|
|
215
|
+
!("head" in value.properties)
|
|
216
|
+
)
|
|
217
|
+
return value;
|
|
218
|
+
|
|
219
|
+
const { repositoryId: _repositoryId, ...properties } = value.properties;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
...value,
|
|
223
|
+
properties,
|
|
224
|
+
...(Array.isArray(value.required) && {
|
|
225
|
+
required: value.required.filter((name) => name !== "repositoryId"),
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
}) as T;
|
|
229
|
+
}
|
|
@@ -13,7 +13,7 @@ export const identity = { id: text.optional() };
|
|
|
13
13
|
* them reads its document's. */
|
|
14
14
|
export const elementPins = sourcePinsSchema
|
|
15
15
|
.optional()
|
|
16
|
-
.describe("Pins for the anchors here
|
|
16
|
+
.describe("Pins for the anchors here.");
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* One block kind: the strict schema the store parses with, and the rules a
|
|
@@ -6,12 +6,12 @@ export const markdown = {
|
|
|
6
6
|
type: "markdown",
|
|
7
7
|
schema: defineBlock("markdown", {
|
|
8
8
|
markdown: text.describe(
|
|
9
|
-
"Safe Markdown. Repository file links must use [label](
|
|
9
|
+
"Safe Markdown. Repository file links must use [label](whiteboard:source/head/path#L10-L24) or whiteboard:source/base/path#L10-L24, or whiteboard:source/<commit>/path#L10-L24 at any commit of the board's repository, with a repository-relative path and verified line numbers. Link another board as [label](whiteboard:board/<sessionId>) or one of its blocks as whiteboard:board/<sessionId>#<blockId>; both must exist. Relative paths, absolute filesystem paths, and file/editor URLs are rejected. External links use https://, http://, or mailto:; document anchors use #heading.",
|
|
10
10
|
),
|
|
11
11
|
pins: sourcePinsSchema
|
|
12
12
|
.optional()
|
|
13
13
|
.describe(
|
|
14
|
-
"Repository and commits this block's
|
|
14
|
+
"Repository and commits this block's whiteboard:source/ links resolve against, instead of the document's pins.",
|
|
15
15
|
),
|
|
16
16
|
}),
|
|
17
17
|
} as const;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/** What sibling order reads about a board. */
|
|
2
|
+
export interface SiblingFacts {
|
|
3
|
+
id: string;
|
|
4
|
+
/** When it was made: an edit never moves it. */
|
|
5
|
+
createdAt: string;
|
|
6
|
+
/** Its pins: commits of repositoryId. Plans have none. */
|
|
7
|
+
repositoryId?: string;
|
|
8
|
+
base?: string;
|
|
9
|
+
head?: string;
|
|
10
|
+
/** A PR board's head branch, and its PR's base branch, when known. */
|
|
11
|
+
headBranch?: string;
|
|
12
|
+
baseBranch?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Siblings in reading order. A comes after B when A reaches B through
|
|
17
|
+
* depends_on, directly or through boards outside the siblings. Between two
|
|
18
|
+
* siblings with no such chain either way, commits decide: A comes after B
|
|
19
|
+
* when A's base is, or descends from, B's head, or A's PR merges into B's
|
|
20
|
+
* branch. Remaining ties go newest made first, then by id, so an edit
|
|
21
|
+
* never reorders them.
|
|
22
|
+
*/
|
|
23
|
+
export function orderSiblings(
|
|
24
|
+
siblings: readonly SiblingFacts[],
|
|
25
|
+
dependsOn: (id: string) => readonly string[],
|
|
26
|
+
isAncestor: (repositoryId: string, ancestor: string, of: string) => boolean,
|
|
27
|
+
): string[] {
|
|
28
|
+
const reach = new Map(
|
|
29
|
+
siblings.map((board) => [board.id, reachable(board.id, dependsOn)]),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const after = (a: SiblingFacts, b: SiblingFacts) => {
|
|
33
|
+
if (reach.get(a.id)!.has(b.id)) return true;
|
|
34
|
+
|
|
35
|
+
if (reach.get(b.id)!.has(a.id)) return false;
|
|
36
|
+
|
|
37
|
+
return buildsOn(a, b, isAncestor) && !buildsOn(b, a, isAncestor);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const before = new Map(
|
|
41
|
+
siblings.map((a) => [
|
|
42
|
+
a.id,
|
|
43
|
+
new Set(siblings.flatMap((b) => (b !== a && after(a, b) ? [b.id] : []))),
|
|
44
|
+
]),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const newestFirst = [...siblings].sort(
|
|
48
|
+
(a, b) =>
|
|
49
|
+
Date.parse(b.createdAt) - Date.parse(a.createdAt) ||
|
|
50
|
+
a.id.localeCompare(b.id),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const placed = new Set<string>();
|
|
54
|
+
const order: string[] = [];
|
|
55
|
+
|
|
56
|
+
while (order.length < siblings.length) {
|
|
57
|
+
const remaining = newestFirst.filter((board) => !placed.has(board.id));
|
|
58
|
+
|
|
59
|
+
// Inferred order can disagree with itself; then age breaks the loop.
|
|
60
|
+
const next =
|
|
61
|
+
remaining.find((board) =>
|
|
62
|
+
[...before.get(board.id)!].every((id) => placed.has(id)),
|
|
63
|
+
) ?? remaining[0]!;
|
|
64
|
+
|
|
65
|
+
placed.add(next.id);
|
|
66
|
+
order.push(next.id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return order;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function reachable(
|
|
73
|
+
id: string,
|
|
74
|
+
dependsOn: (id: string) => readonly string[],
|
|
75
|
+
): Set<string> {
|
|
76
|
+
const seen = new Set<string>();
|
|
77
|
+
const stack = [...dependsOn(id)];
|
|
78
|
+
|
|
79
|
+
while (stack.length) {
|
|
80
|
+
const next = stack.pop()!;
|
|
81
|
+
|
|
82
|
+
if (seen.has(next)) continue;
|
|
83
|
+
seen.add(next);
|
|
84
|
+
stack.push(...dependsOn(next));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return seen;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Whether a's change sits on top of b's. */
|
|
91
|
+
function buildsOn(
|
|
92
|
+
a: SiblingFacts,
|
|
93
|
+
b: SiblingFacts,
|
|
94
|
+
isAncestor: (repositoryId: string, ancestor: string, of: string) => boolean,
|
|
95
|
+
): boolean {
|
|
96
|
+
if (a.repositoryId !== b.repositoryId) return false;
|
|
97
|
+
|
|
98
|
+
if (a.baseBranch && a.baseBranch === b.headBranch) return true;
|
|
99
|
+
|
|
100
|
+
if (!a.base || !b.head) return false;
|
|
101
|
+
|
|
102
|
+
if (a.base === b.head) return true;
|
|
103
|
+
|
|
104
|
+
// A board with no change of its own builds on nothing.
|
|
105
|
+
return a.base !== a.head && isAncestor(a.repositoryId!, b.head, a.base);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The path that adding `from → to` would close, from `from` back to itself,
|
|
110
|
+
* or undefined when it closes none.
|
|
111
|
+
*/
|
|
112
|
+
export function dependencyCycle(
|
|
113
|
+
from: string,
|
|
114
|
+
to: string,
|
|
115
|
+
dependsOn: (id: string) => readonly string[],
|
|
116
|
+
): string[] | undefined {
|
|
117
|
+
if (from === to) return [from, from];
|
|
118
|
+
|
|
119
|
+
const previous = new Map<string, string>();
|
|
120
|
+
const queue = [to];
|
|
121
|
+
const seen = new Set([to]);
|
|
122
|
+
|
|
123
|
+
while (queue.length) {
|
|
124
|
+
const current = queue.shift()!;
|
|
125
|
+
|
|
126
|
+
if (current === from) {
|
|
127
|
+
const path = [from];
|
|
128
|
+
|
|
129
|
+
for (let at = from; at !== to; at = previous.get(at)!)
|
|
130
|
+
path.unshift(previous.get(at)!);
|
|
131
|
+
|
|
132
|
+
return [from, ...path];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const next of dependsOn(current))
|
|
136
|
+
if (!seen.has(next)) {
|
|
137
|
+
seen.add(next);
|
|
138
|
+
previous.set(next, current);
|
|
139
|
+
queue.push(next);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|