@dev.fast/whiteboard 0.3.4-preview.20261019.127 → 0.3.4

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 (115) hide show
  1. package/dist/{account-alias-B5-HYi1D.js → account-alias-CprZojXd.js} +8 -13
  2. package/dist/agent-cli-C6SPSII0.js +2 -0
  3. package/dist/{agent-cli-CohHf6Ua.js → agent-cli-CThN7Q59.js} +10 -15
  4. package/dist/{agent-client-Dtc-Kn_w.js → agent-client-BOOi7EUR.js} +3 -13
  5. package/dist/{authoring-tools-B7KU04f7.js → authoring-tools-BNwuMnz6.js} +27 -34
  6. package/dist/{background-server-C03RKxKS.js → background-server-CkvxA-6O.js} +5 -5
  7. package/dist/{background-server-CB9C4uoc.js → background-server-DLSDogkI.js} +1 -1
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-CJaEb7zC.js → cli-R4yfa5DT.js} +1 -1
  10. package/dist/{cli-runner-DGDIqh3V.js → cli-runner-Cuc9FSzu.js} +26 -26
  11. package/dist/cli.js +3 -4
  12. package/dist/{client-lKVF_Elo.js → client-BN_FDxFr.js} +47 -253
  13. package/dist/{fuzzy-match-BmVJJm9I.js → fuzzy-match-BO9bK37N.js} +4 -6
  14. package/dist/{headless-host-DRtjf9WA.js → headless-host-CVBOQyRS.js} +1 -1
  15. package/dist/{headless-host-XhL1X4vX.js → headless-host-Cog6SpS9.js} +140 -513
  16. package/dist/{local-data-Cpl4ENbJ.js → local-data-D_9tvdsp.js} +323 -2419
  17. package/dist/{mcp-ovmiUE_x.js → mcp-D1hziANa.js} +9 -5
  18. package/dist/{package-paths-BWxO-UEL.js → package-paths-C--__jEv.js} +1 -1
  19. package/dist/profile-CYPmvBoA.js +2 -0
  20. package/dist/{profile-BadSkCkI.js → profile-DokFw782.js} +8 -20
  21. package/dist/{remote-attach-Dd-YobM6.js → remote-attach-CEJ-kvDY.js} +4 -4
  22. package/dist/{remote-extensions-CNXwqK0D.js → remote-extensions-Blcjr9Ze.js} +3 -3
  23. package/dist/{remote-extensions-VP-kfq0C.js → remote-extensions-GN8Gf_qG.js} +1 -1
  24. package/dist/{remote-language-server-BXk87KCr.js → remote-language-server-BTKBQ7x4.js} +5 -5
  25. package/dist/{remote-uninstall-DuOzsvaD.js → remote-uninstall-DW7Gdryh.js} +6 -6
  26. package/dist/{request-origin-B9qCPiVB.js → request-origin-Zcn_CjX4.js} +12 -45
  27. package/dist/{review-app-launcher-C7el2Uoy.js → review-app-launcher-CkwLclzR.js} +2 -2
  28. package/dist/{review-home-paths-D8AoCdd2.js → review-home-paths-j2BRAaNv.js} +17 -19
  29. package/dist/{review-telemetry-BsoRMC2w.js → review-telemetry-BLNwYnLV.js} +5 -5
  30. package/dist/runtime-CKiWhGrf.js +52 -0
  31. package/dist/runtime.d.ts +1 -1
  32. package/dist/runtime.js +1 -1
  33. package/dist/{s3-CsG2EWED.js → s3-DtJFWC0G.js} +1 -1
  34. package/dist/{s3-config-CjcLyFV5.js → s3-config-BTO12tCC.js} +2 -4
  35. package/dist/{s3-config-zz4GWh2T.js → s3-config-CSy3fx8H.js} +1 -1
  36. package/dist/server/desktop-host.js +76 -29
  37. package/dist/{server-discovery-CKjw3-6h.js → server-discovery-Bey8IqXH.js} +3 -3
  38. package/dist/sharing/index.d.ts +70 -484
  39. package/dist/sharing/index.js +1 -1
  40. package/dist/{src-WPaQn75m.js → src-BMPuxWv4.js} +73 -208
  41. package/dist/{src-BiWNbcF_.js → src-DsM3jO8G.js} +12 -89
  42. package/dist/{stored-document-migration-dFbewRTG.js → stored-document-migration-CChxtFAy.js} +269 -270
  43. package/dist/{thread-state-Do5LX10I.js → thread-state-5pYr6_zM.js} +2 -7
  44. package/dist/{tool-failure-DvI-gys2.js → tool-failure-C9MpbXJJ.js} +3 -10
  45. package/instructions/authoring.md +2 -3
  46. package/instructions/scratchpad.md +23 -0
  47. package/package.json +4 -4
  48. package/src/ask/agents.ts +1 -13
  49. package/src/ask/checkout-files.ts +5 -2
  50. package/src/ask/thread-state.ts +0 -4
  51. package/src/ask/thread.ts +0 -16
  52. package/src/ask/threads.ts +3 -24
  53. package/src/cli-install.ts +2 -13
  54. package/src/cli-runner.ts +1 -4
  55. package/src/cli.ts +1 -4
  56. package/src/diff-selection-migration.ts +34 -48
  57. package/src/lens-selection.ts +2 -38
  58. package/src/review-api/README.md +13 -52
  59. package/src/review-api/activity.ts +4 -1
  60. package/src/review-api/agent-cli.ts +1 -9
  61. package/src/review-api/anchor-quotes.ts +7 -14
  62. package/src/review-api/ask-history.ts +51 -137
  63. package/src/review-api/authoring-tools.ts +30 -45
  64. package/src/review-api/blocks/definition.ts +1 -1
  65. package/src/review-api/blocks/markdown.ts +2 -2
  66. package/src/review-api/document-headings.ts +27 -58
  67. package/src/review-api/document-text.ts +2 -31
  68. package/src/review-api/document.ts +34 -281
  69. package/src/review-api/github-provider.ts +13 -51
  70. package/src/review-api/http.ts +123 -702
  71. package/src/review-api/instructions.ts +19 -0
  72. package/src/review-api/local-data.ts +46 -310
  73. package/src/review-api/mcp.ts +11 -3
  74. package/src/review-api/onboarding.ts +1 -1
  75. package/src/review-api/profile.ts +6 -37
  76. package/src/review-api/public-tools.ts +2 -24
  77. package/src/review-api/pull-request.ts +49 -82
  78. package/src/review-api/request-origin.ts +0 -10
  79. package/src/review-api/review-progress.ts +27 -61
  80. package/src/review-api/store-schema.ts +29 -38
  81. package/src/review-api/store.ts +478 -2142
  82. package/src/review-api/worktree-events.ts +5 -3
  83. package/src/review-api/worktree-source.ts +7 -3
  84. package/src/review-api/worktree-structural.integration.ts +0 -72
  85. package/src/review-preferences.ts +88 -0
  86. package/src/review-stack.ts +23 -15
  87. package/src/runtime.ts +30 -4
  88. package/src/server/desktop-host.ts +1 -6
  89. package/src/server/desktop-server.ts +31 -1
  90. package/src/server/headless-host.ts +2 -0
  91. package/src/server/hono-http.ts +1 -2
  92. package/src/server/legacy-tutorial-cleanup.ts +3 -2
  93. package/src/server/review-gateway-list.ts +5 -3
  94. package/src/server/review-gateway-memory.ts +1 -1
  95. package/src/server/review-lifecycle-telemetry.ts +2 -2
  96. package/src/server/review-server-core.ts +5 -1
  97. package/src/server/structural-comparisons.ts +0 -47
  98. package/src/sharing/export.ts +7 -36
  99. package/src/sharing/host.ts +14 -34
  100. package/src/sharing/import.ts +24 -156
  101. package/src/sharing/repository.ts +20 -128
  102. package/src/startup-trace.ts +3 -3
  103. package/src/stored-document-migration.ts +19 -110
  104. package/src/ui-telemetry-events.ts +11 -33
  105. package/dist/agent-cli-_YyMIMRY.js +0 -2
  106. package/dist/lens-selection-CyJapcsl.js +0 -214
  107. package/dist/profile-DJvkBJNQ.js +0 -2
  108. package/dist/runtime-Bj2tD33R.js +0 -27
  109. package/src/heading-anchors.ts +0 -42
  110. package/src/review-api/board-order.ts +0 -144
  111. package/src/review-api/checkout-groups.ts +0 -560
  112. package/src/review-api/foreign-references.ts +0 -328
  113. package/src/review-api/link-preview.ts +0 -113
  114. package/src/review-api/scratchpad-plan-upgrade.ts +0 -250
  115. package/src/review-api/store-migrations.ts +0 -357
@@ -1,214 +0,0 @@
1
- import "./src-BiWNbcF_.js";
2
- import { z } from "zod";
3
- //#region src/viewed-coverage.ts
4
- const coverageSchema = z.object({
5
- base: z.array(z.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()])),
6
- head: z.array(z.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()]))
7
- });
8
- const emptyCoverage = () => ({
9
- base: [],
10
- head: []
11
- });
12
- function unionIntervals(ranges) {
13
- const result = [];
14
- for (const [start, end] of [...ranges].sort((a, b) => a[0] - b[0])) {
15
- if (end <= start) continue;
16
- const last = result.at(-1);
17
- if (last && start <= last[1]) last[1] = Math.max(last[1], end);
18
- else result.push([start, end]);
19
- }
20
- return result;
21
- }
22
- function intersectIntervals(left, right) {
23
- const result = [];
24
- const a = unionIntervals(left), b = unionIntervals(right);
25
- let i = 0, j = 0;
26
- while (i < a.length && j < b.length) {
27
- const start = Math.max(a[i][0], b[j][0]), end = Math.min(a[i][1], b[j][1]);
28
- if (start < end) result.push([start, end]);
29
- if (a[i][1] < b[j][1]) i++;
30
- else j++;
31
- }
32
- return result;
33
- }
34
- function subtractIntervals(left, right) {
35
- const result = [];
36
- const removed = unionIntervals(right);
37
- for (const [start, end] of unionIntervals(left)) {
38
- let cursor = start;
39
- for (const [from, to] of removed) {
40
- if (to <= cursor) continue;
41
- if (from >= end) break;
42
- if (from > cursor) result.push([cursor, from]);
43
- cursor = Math.min(end, Math.max(cursor, to));
44
- }
45
- if (cursor < end) result.push([cursor, end]);
46
- }
47
- return result;
48
- }
49
- function updateCoverage(current, scope, viewed) {
50
- const apply = (side) => viewed ? unionIntervals([...current[side], ...scope[side]]) : subtractIntervals(current[side], scope[side]);
51
- return {
52
- base: apply("base"),
53
- head: apply("head")
54
- };
55
- }
56
- function scopedCoverage(file, sources) {
57
- if (!sources) return file.changed;
58
- const side = (side) => intersectIntervals(file.changed[side], sources.flatMap((source) => source.side === side && source.file === (side === "base" ? file.previousPath ?? file.path : file.path) ? [[source.fromLine - 1, source.toLine]] : []));
59
- return {
60
- base: side("base"),
61
- head: side("head")
62
- };
63
- }
64
- function coverageSources(file, coverage = file.viewed) {
65
- return ["base", "head"].flatMap((side) => coverage[side].map(([start, end]) => ({
66
- side,
67
- file: side === "base" ? file.previousPath ?? file.path : file.path,
68
- fromLine: start + 1,
69
- toLine: end
70
- })));
71
- }
72
- //#endregion
73
- //#region src/lens-selection.ts
74
- const ANCHOR_FORMS = "\"head/path#L10-L24\" or \"base/path#L7\" (one side, lines 10–24 or line 7); across sides, GitHub diff style \"diff/path#L84-R90\" (L = base line, R = head line)";
75
- const sameSideAnchor = /^(head|base)\/(.+)#L(\d+)(?:-L(\d+))?$/;
76
- const crossSideAnchor = /^diff\/(.+)#([LR])(\d+)-([LR])(\d+)$/;
77
- const commitLinkAnchor = /^([0-9a-f]{7,40})\/(.+#L\d+(?:-L\d+)?)$/i;
78
- /** A source link's commit form, "<sha>/path#L10-L24" (without its
79
- * `whiteboard:source/` prefix): lines of that commit of the board's own
80
- * repository, read as their head side. Undefined for any other form. */
81
- function commitLinkTarget(target) {
82
- const match = commitLinkAnchor.exec(target);
83
- return match ? {
84
- commit: match[1].toLowerCase(),
85
- anchor: `head/${match[2]}`
86
- } : void 0;
87
- }
88
- /** An anchor's selection, as markdown source links write it too (without
89
- * their `whiteboard:source/` prefix). Undefined when it isn't one. */
90
- function parseAnchor(text) {
91
- const same = sameSideAnchor.exec(text);
92
- if (same) {
93
- const side = same[1] === "base" ? "base" : "head";
94
- return {
95
- file: same[2],
96
- start: {
97
- side,
98
- line: Number(same[3])
99
- },
100
- end: {
101
- side,
102
- line: Number(same[4] ?? same[3])
103
- }
104
- };
105
- }
106
- const cross = crossSideAnchor.exec(text);
107
- if (!cross || cross[2] === cross[4]) return void 0;
108
- const side = (letter) => letter === "L" ? "base" : "head";
109
- return {
110
- file: cross[1],
111
- start: {
112
- side: side(cross[2]),
113
- line: Number(cross[3])
114
- },
115
- end: {
116
- side: side(cross[4]),
117
- line: Number(cross[5])
118
- }
119
- };
120
- }
121
- /** Why a parsed selection can't be quoted, or undefined when it can. */
122
- function selectionProblem(selection) {
123
- if (selection.start.line < 1 || selection.end.line < 1) return "Source lines count from 1.";
124
- if (selection.start.side === selection.end.side && selection.start.line > selection.end.line) return "Source range ends before it starts.";
125
- if (selection.pins && selection.pins.base === void 0 && (selection.start.side === "base" || selection.end.side === "base")) return "A base-side anchor needs base pins.";
126
- }
127
- const anchorSchema = z.string({ error: `Expected a source anchor: ${ANCHOR_FORMS}.` }).superRefine((text, context) => {
128
- const selection = parseAnchor(text);
129
- const problem = selection ? selectionProblem(selection) : `Use ${ANCHOR_FORMS} for a source anchor.`;
130
- if (problem) context.addIssue({
131
- code: "custom",
132
- message: problem
133
- });
134
- }).describe("head/path#L10-L24, base/path#L7, or diff/path#L84-R90");
135
- /** A stored anchor's selection, read at the pins of whatever holds it. */
136
- function anchorSelection(anchor, pins) {
137
- const selection = parseAnchor(anchor);
138
- if (!selection) throw new Error(`Not a source anchor: ${anchor}`);
139
- return pins ? {
140
- ...selection,
141
- pins
142
- } : selection;
143
- }
144
- /** The anchor that writes a selection; its pins belong to its holder. */
145
- function formatAnchor(selection) {
146
- const { file, start, end } = selection;
147
- if (start.side === end.side) return start.line === end.line ? `${start.side}/${file}#L${start.line}` : `${start.side}/${file}#L${start.line}-L${end.line}`;
148
- const letter = (side) => side === "base" ? "L" : "R";
149
- return `diff/${file}#${letter(start.side)}${start.line}-${letter(end.side)}${end.line}`;
150
- }
151
- /** Pins-less selections keep the key they always had. */
152
- function selectionKey(source) {
153
- return JSON.stringify([
154
- source.file,
155
- source.start.side,
156
- source.start.line,
157
- source.end.side,
158
- source.end.line,
159
- ...source.pins ? [sourcePinsKey(source.pins)] : []
160
- ]);
161
- }
162
- function sourcePinsKey(pins) {
163
- return `${pins.repositoryId}:${pins.base ?? ""}:${pins.head}`;
164
- }
165
- /** The comparison a source's pins name: head alone means head against
166
- * itself. Reference coverage is grouped under this key on both sides. */
167
- function comparisonKey(pins) {
168
- return `${pins.repositoryId}:${pins.base ?? pins.head}:${pins.head}`;
169
- }
170
- /** Navigation/quote anchors only; never use these as lens coverage. */
171
- function sourceAnchors(source) {
172
- return ["head", "base"].flatMap((side) => {
173
- const lines = [source.start, source.end].flatMap((endpoint) => endpoint.side === side ? [endpoint.line] : []);
174
- if (!lines.length) return [];
175
- const anchor = {
176
- file: source.file,
177
- side,
178
- fromLine: Math.min(...lines),
179
- toLine: Math.max(...lines)
180
- };
181
- if (source.pins) anchor.pins = source.pins;
182
- return [anchor];
183
- });
184
- }
185
- /** Why a selection cannot be resolved at its pins, as telemetry reports it. */
186
- const SELECTION_UNAVAILABLE_REASONS = [
187
- "missing_file",
188
- "no_alignment",
189
- "outside_lines",
190
- "other"
191
- ];
192
- var SelectionUnavailableError = class extends Error {
193
- reason;
194
- constructor(reason, message) {
195
- super(message);
196
- this.reason = reason;
197
- }
198
- };
199
- /** Resolve once against the renderer's rows. No matching or context expansion. */
200
- function resolveDiffSelection(source, rows, file) {
201
- const locate = (endpoint) => rows.findIndex((row) => row[endpoint.side === "base" ? 0 : 1] === endpoint.line - 1);
202
- const start = locate(source.start);
203
- const end = locate(source.end);
204
- if (start < 0 || end < start) throw new SelectionUnavailableError("outside_lines", "Lens endpoints do not identify an ordered interval in this diff.");
205
- const selected = rows.slice(start, end + 1);
206
- return ["base", "head"].flatMap((side, index) => unionIntervals(selected.flatMap((row) => row[index] === null ? [] : [[row[index], row[index] + 1]])).map(([from, to]) => ({
207
- file: side === "base" ? file.previousPath ?? file.path : file.path,
208
- side,
209
- fromLine: from + 1,
210
- toLine: to
211
- })));
212
- }
213
- //#endregion
214
- export { scopedCoverage as _, commitLinkTarget as a, updateCoverage as b, parseAnchor as c, selectionProblem as d, sourceAnchors as f, intersectIntervals as g, emptyCoverage as h, anchorSelection as i, resolveDiffSelection as l, coverageSources as m, SelectionUnavailableError as n, comparisonKey as o, coverageSchema as p, anchorSchema as r, formatAnchor as s, SELECTION_UNAVAILABLE_REASONS as t, selectionKey as u, subtractIntervals as v, unionIntervals as y };
@@ -1,2 +0,0 @@
1
- import { t as openReviewProfile } from "./profile-BadSkCkI.js";
2
- export { openReviewProfile };
@@ -1,27 +0,0 @@
1
- import { E as runVcs } from "./src-WPaQn75m.js";
2
- import path from "node:path";
3
- //#region src/runtime.ts
4
- async function resolveReviewRoot(cwd, execFile = execFilePromise) {
5
- const initialCwd = path.resolve(cwd);
6
- try {
7
- const { stdout } = await execFile("jj", ["root"], { cwd: initialCwd });
8
- const root = stdout.trim();
9
- if (root) return path.resolve(root);
10
- } catch {}
11
- try {
12
- const { stdout } = await execFile("git", [
13
- "-C",
14
- initialCwd,
15
- "rev-parse",
16
- "--show-toplevel"
17
- ]);
18
- const root = stdout.trim();
19
- if (root) return path.resolve(root);
20
- } catch {}
21
- return initialCwd;
22
- }
23
- function execFilePromise(file, args, options = {}) {
24
- return runVcs(file, args, options);
25
- }
26
- //#endregion
27
- export { resolveReviewRoot as t };
@@ -1,42 +0,0 @@
1
- import { slugify, uniqueId } from "./slug.js";
2
-
3
- // A board's headings, as the server publishes them and the editor shows
4
- // them: one rule for both, so a heading's anchor is the same in each.
5
-
6
- /** Whether a Markdown heading with this many `#`s is one of the board's
7
- * headings, with an anchor and a Contents row: `##` and `###`. */
8
- export function isBoardHeading(hashes: number | undefined): boolean {
9
- return hashes === 2 || hashes === 3;
10
- }
11
-
12
- /** Hands out heading anchors in document order: `slugify(text)`, made
13
- * unique, so imported `#fragment` links still land. */
14
- export function headingAnchors(): (text: string) => string {
15
- const used = new Set<string>();
16
-
17
- return (text) => {
18
- const id = uniqueId(slugify(text) || "section", used);
19
-
20
- used.add(id);
21
-
22
- return id;
23
- };
24
- }
25
-
26
- /**
27
- * A Markdown heading's depth in Contents: one step under its section,
28
- * whose blocks sit at `base`, and under the block's bigger headings before
29
- * it. `open` holds those headings by `#` count, and takes this one.
30
- */
31
- export function markdownHeadingDepth(
32
- open: number[],
33
- hashes: number,
34
- base: number,
35
- ): number {
36
- while (open.length && open.at(-1)! >= hashes) open.pop();
37
- const depth = base + open.length;
38
-
39
- open.push(hashes);
40
-
41
- return depth;
42
- }
@@ -1,144 +0,0 @@
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
- }