@dev.fast/whiteboard 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +146 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/account-alias-DOHN1RSH.js +973 -0
- package/dist/agent-cli-ChOq0ZuO.js +117 -0
- package/dist/agent-cli-DzWfZr6G.js +2 -0
- package/dist/agent-client-CPQo7iTI.js +305 -0
- package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
- package/dist/build-info.json +1 -0
- package/dist/cli-hVgHcdsv.js +47 -0
- package/dist/cli-runner-D8y2_luU.js +5095 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/client-DFoh50W3.js +950 -0
- package/dist/desktop-discovery-DBO-PM2V.js +164 -0
- package/dist/error-message-OtiDonty.js +6 -0
- package/dist/fs-utils-BMPLt0cr.js +22 -0
- package/dist/fuzzy-match-BoAmcyak.js +30 -0
- package/dist/headless-host-BJxf4dUU.js +94 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/local-data-CxLk75rx.js +4491 -0
- package/dist/mcp-QeF8vkdf.js +140 -0
- package/dist/package-paths-B6-zxvIO.js +47 -0
- package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
- package/dist/profile-4ry2f3AM.js +76 -0
- package/dist/profile-Bh5FIrmf.js +2 -0
- package/dist/request-origin-D_4QIdoN.js +811 -0
- package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
- package/dist/review-home-paths-6zZH-1c9.js +2560 -0
- package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +2 -0
- package/dist/s3-SzLpeCG6.js +345 -0
- package/dist/s3-config-BgxkSoOy.js +2 -0
- package/dist/s3-config-CAxhO9u_.js +676 -0
- package/dist/server/desktop-host.d.ts +4 -0
- package/dist/server/desktop-host.js +1702 -0
- package/dist/server-discovery-Dbzn3w6Z.js +66 -0
- package/dist/sharing/index.d.ts +1977 -0
- package/dist/sharing/index.js +2 -0
- package/dist/src-CGP5ytbV.js +283 -0
- package/dist/src-CmdiBL20.js +3676 -0
- package/dist/src-DnwdaQ2r.js +865 -0
- package/dist/src-X9phtB2j.js +68 -0
- package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
- package/dist/tool-failure-C8zB73HV.js +98 -0
- package/dist/tutorial-trace-DrBvkaeL.js +43 -0
- package/instructions/authoring.md +36 -0
- package/instructions/file-lenses.md +13 -0
- package/instructions/scratchpad.md +23 -0
- package/instructions/trace-archaeology.md +113 -0
- package/onboarding.md +8 -0
- package/package.json +106 -3
- package/src/agent-selection.ts +100 -0
- package/src/agent-session-ref.ts +80 -0
- package/src/ask/agents.ts +351 -0
- package/src/ask/checkout-files.ts +47 -0
- package/src/ask/file-refs.ts +111 -0
- package/src/ask/pi-mcp.ts +154 -0
- package/src/ask/protocol.ts +244 -0
- package/src/ask/thread-state.ts +350 -0
- package/src/ask/thread.ts +1400 -0
- package/src/ask/threads.ts +219 -0
- package/src/ask/watch.ts +79 -0
- package/src/cli-install.ts +1041 -0
- package/src/cli-runner.ts +1461 -0
- package/src/cli-runtime-info.ts +35 -0
- package/src/cli.ts +222 -0
- package/src/connect-prompts.ts +248 -0
- package/src/cursor-deeplink.ts +11 -0
- package/src/desktop-discovery.ts +355 -0
- package/src/diff-selection-migration.ts +63 -0
- package/src/embedded-posthog-key.ts +6 -0
- package/src/error-telemetry.ts +247 -0
- package/src/evidence.ts +14 -0
- package/src/exception-telemetry.ts +126 -0
- package/src/fixtures/blocks/call_stack_diff.json +27 -0
- package/src/fixtures/blocks/callout.json +11 -0
- package/src/fixtures/blocks/code.json +9 -0
- package/src/fixtures/blocks/code_peek.json +8 -0
- package/src/fixtures/blocks/database_lens.json +51 -0
- package/src/fixtures/blocks/divider.json +1 -0
- package/src/fixtures/blocks/fixtures.ts +33 -0
- package/src/fixtures/blocks/flow_diagram.json +36 -0
- package/src/fixtures/blocks/ids.ts +8 -0
- package/src/fixtures/blocks/image.json +9 -0
- package/src/fixtures/blocks/markdown.json +7 -0
- package/src/fixtures/blocks/section.json +11 -0
- package/src/fixtures/blocks/sequence.json +31 -0
- package/src/fixtures/blocks/software_map.json +7 -0
- package/src/fixtures/blocks/trace_quote.json +9 -0
- package/src/fixtures/blocks/tutorial.json +51 -0
- package/src/fs-utils.ts +32 -0
- package/src/fuzzy-match.ts +94 -0
- package/src/install.ts +29 -0
- package/src/legacy-skills.ts +133 -0
- package/src/lens-selection.ts +230 -0
- package/src/markdown-latex-math.ts +230 -0
- package/src/markdown.ts +81 -0
- package/src/package-paths.ts +53 -0
- package/src/posthog-capture-client.ts +610 -0
- package/src/review-api/README.md +295 -0
- package/src/review-api/activity.ts +340 -0
- package/src/review-api/agent-cli.ts +219 -0
- package/src/review-api/agent-client.ts +188 -0
- package/src/review-api/anchor-quotes.ts +88 -0
- package/src/review-api/ask-history.ts +242 -0
- package/src/review-api/authoring-tools.ts +211 -0
- package/src/review-api/blocks/call_stack_diff.ts +75 -0
- package/src/review-api/blocks/callout.ts +24 -0
- package/src/review-api/blocks/code.ts +9 -0
- package/src/review-api/blocks/code_peek.ts +13 -0
- package/src/review-api/blocks/database_lens.ts +150 -0
- package/src/review-api/blocks/definition.ts +40 -0
- package/src/review-api/blocks/divider.ts +6 -0
- package/src/review-api/blocks/flow_diagram.ts +111 -0
- package/src/review-api/blocks/image.ts +10 -0
- package/src/review-api/blocks/index.ts +90 -0
- package/src/review-api/blocks/markdown.ts +17 -0
- package/src/review-api/blocks/section.ts +24 -0
- package/src/review-api/blocks/sequence.ts +56 -0
- package/src/review-api/blocks/software_map.ts +9 -0
- package/src/review-api/blocks/trace_quote.ts +10 -0
- package/src/review-api/blocks/tutorial.ts +39 -0
- package/src/review-api/checkout-fs.ts +14 -0
- package/src/review-api/client.ts +1 -0
- package/src/review-api/comparison-coverage.ts +304 -0
- package/src/review-api/component-reference.ts +30 -0
- package/src/review-api/diff-lenses.ts +175 -0
- package/src/review-api/document-headings.ts +51 -0
- package/src/review-api/document-text.ts +200 -0
- package/src/review-api/document.ts +906 -0
- package/src/review-api/file-lenses.ts +100 -0
- package/src/review-api/http.ts +1877 -0
- package/src/review-api/image-decode.ts +29 -0
- package/src/review-api/input-error.ts +9 -0
- package/src/review-api/instructions.ts +89 -0
- package/src/review-api/lens-alignment.ts +52 -0
- package/src/review-api/local-data.ts +1837 -0
- package/src/review-api/map-input.ts +154 -0
- package/src/review-api/mcp-client-agent.ts +32 -0
- package/src/review-api/mcp.ts +240 -0
- package/src/review-api/origin.ts +43 -0
- package/src/review-api/profile.ts +146 -0
- package/src/review-api/public-tools.ts +102 -0
- package/src/review-api/pull-request.ts +389 -0
- package/src/review-api/read-schemas.ts +85 -0
- package/src/review-api/recovery.ts +2 -0
- package/src/review-api/request-origin.ts +33 -0
- package/src/review-api/review-progress.ts +382 -0
- package/src/review-api/status-tool.ts +10 -0
- package/src/review-api/store-schema.ts +22 -0
- package/src/review-api/store.ts +1647 -0
- package/src/review-api/tool-failure.ts +64 -0
- package/src/review-api/trace-schema.ts +13 -0
- package/src/review-api/traces.ts +119 -0
- package/src/review-api/unsupported-files.integration.ts +91 -0
- package/src/review-api/workspaces.ts +692 -0
- package/src/review-api/worktree-source.ts +170 -0
- package/src/review-api/worktree-structural.integration.ts +320 -0
- package/src/review-app-launcher.ts +432 -0
- package/src/review-app-picker.ts +168 -0
- package/src/review-app.ts +134 -0
- package/src/review-bundled-tools.ts +249 -0
- package/src/review-checkout-paths.ts +37 -0
- package/src/review-diff-files.ts +89 -0
- package/src/review-head-checkout.ts +300 -0
- package/src/review-home-paths.ts +73 -0
- package/src/review-info.ts +59 -0
- package/src/review-instances.ts +113 -0
- package/src/review-logger.ts +183 -0
- package/src/review-preferences.ts +88 -0
- package/src/review-prepare.ts +291 -0
- package/src/review-stack.ts +112 -0
- package/src/review-telemetry.ts +1066 -0
- package/src/runtime.ts +74 -0
- package/src/server/account-alias.ts +30 -0
- package/src/server/bounded-stream.ts +34 -0
- package/src/server/bug-report.ts +301 -0
- package/src/server/client-error-budget.ts +45 -0
- package/src/server/crash-report.ts +273 -0
- package/src/server/desktop-host-shutdown.ts +64 -0
- package/src/server/desktop-host.ts +190 -0
- package/src/server/desktop-server.ts +703 -0
- package/src/server/diffr-config.ts +509 -0
- package/src/server/diffr-languages.ts +97 -0
- package/src/server/global-verb-relay.ts +190 -0
- package/src/server/headless-host.ts +149 -0
- package/src/server/hono-http.ts +162 -0
- package/src/server/http-json.ts +24 -0
- package/src/server/json-review-reporting.ts +174 -0
- package/src/server/process-error-telemetry.ts +174 -0
- package/src/server/review-api-parsers.ts +90 -0
- package/src/server/review-lifecycle-telemetry.ts +73 -0
- package/src/server/review-open-watchdog.ts +46 -0
- package/src/server/review-server-core.ts +290 -0
- package/src/server/structural-comparisons.ts +153 -0
- package/src/server/structural-diff.ts +186 -0
- package/src/server/tutorial-service.ts +241 -0
- package/src/server/ui-telemetry.ts +198 -0
- package/src/server-discovery.ts +95 -0
- package/src/session-markers.ts +132 -0
- package/src/sharing/auth.ts +41 -0
- package/src/sharing/cli.ts +83 -0
- package/src/sharing/client.ts +334 -0
- package/src/sharing/export.ts +195 -0
- package/src/sharing/host.ts +316 -0
- package/src/sharing/import.ts +787 -0
- package/src/sharing/index.ts +16 -0
- package/src/sharing/repository.ts +145 -0
- package/src/sharing/routes.ts +34 -0
- package/src/slug.ts +23 -0
- package/src/software-map-diff-counts.ts +517 -0
- package/src/software-map-model.ts +1147 -0
- package/src/software-map-topology-diff.ts +260 -0
- package/src/source.ts +92 -0
- package/src/startup-trace.ts +232 -0
- package/src/stored-document-migration.ts +151 -0
- package/src/telemetry-clean-text.ts +257 -0
- package/src/telemetry-config.ts +298 -0
- package/src/telemetry-debug-sink.ts +38 -0
- package/src/telemetry.ts +13 -0
- package/src/trace-cli.ts +156 -0
- package/src/trace-storage-cli.ts +509 -0
- package/src/tutorial-conversation.ts +18 -0
- package/src/ui-telemetry-events.ts +765 -0
- package/src/unified-diff.ts +71 -0
- package/src/viewed-coverage.ts +259 -0
- package/src/windows-cli.ts +125 -0
- package/tutorial/document.json +275 -0
- package/tutorial/runtime-manifest.json +12 -0
- package/tutorial/sample-service/package.json +9 -0
- package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
- package/tutorial/sample-service/src/app.ts +26 -0
- package/tutorial/sample-service/src/database/schema.ts +10 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +34 -0
- package/tutorial/sample-service/src/orders/order.ts +21 -0
- package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
- package/tutorial/sample-service/tsconfig.json +12 -0
- package/tutorial/software-map.json +139 -0
- package/tutorial/trace.json +20 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type JsonValue,
|
|
6
|
+
REVIEW_DESKTOP_DISCOVERY_VERSION,
|
|
7
|
+
type ReviewDesktopDiscovery,
|
|
8
|
+
isJsonObject,
|
|
9
|
+
jsonNumber,
|
|
10
|
+
jsonProperty,
|
|
11
|
+
parseJsonText,
|
|
12
|
+
parseReviewDesktopDiscovery,
|
|
13
|
+
} from "@dev.fast/review-protocol";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
reviewDefaultInstancePath,
|
|
17
|
+
reviewDevInstanceKey,
|
|
18
|
+
reviewInstancesDir,
|
|
19
|
+
reviewLegacyDiscoveryPath,
|
|
20
|
+
} from "./review-home-paths";
|
|
21
|
+
|
|
22
|
+
export class ReviewDesktopProtocolMismatchError extends Error {
|
|
23
|
+
readonly name = "ReviewDesktopProtocolMismatchError";
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
readonly actualVersion: number,
|
|
27
|
+
readonly expectedVersion = REVIEW_DESKTOP_DISCOVERY_VERSION,
|
|
28
|
+
) {
|
|
29
|
+
super(
|
|
30
|
+
`Whiteboard Desktop uses protocol ${actualVersion}, but this Whiteboard CLI needs protocol ${expectedVersion}. Update the Whiteboard CLI and Whiteboard Desktop to compatible versions, then try again.`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class ReviewDesktopDiscoveryUnreadableError extends Error {
|
|
36
|
+
readonly name = "ReviewDesktopDiscoveryUnreadableError";
|
|
37
|
+
|
|
38
|
+
constructor(filePath: string, detail?: string) {
|
|
39
|
+
super(
|
|
40
|
+
`Whiteboard Desktop discovery is unreadable at ${filePath}. Restart Whiteboard Desktop and try again.${detail ? ` ${detail}` : ""}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One record, or null when the file does not exist. */
|
|
46
|
+
export async function readReviewDesktopDiscoveryFile(
|
|
47
|
+
filePath: string,
|
|
48
|
+
): Promise<ReviewDesktopDiscovery | null> {
|
|
49
|
+
let source: string;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
source = await readFile(filePath, "utf8");
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw new ReviewDesktopDiscoveryUnreadableError(filePath, String(error));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let value: JsonValue;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
value = parseJsonText(source);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new ReviewDesktopDiscoveryUnreadableError(filePath, String(error));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return parseReviewDesktopDiscovery(value);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const version = isJsonObject(value)
|
|
73
|
+
? jsonNumber(jsonProperty(value, "version"))
|
|
74
|
+
: undefined;
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
version !== undefined &&
|
|
78
|
+
Number.isInteger(version) &&
|
|
79
|
+
version !== REVIEW_DESKTOP_DISCOVERY_VERSION
|
|
80
|
+
) {
|
|
81
|
+
throw new ReviewDesktopProtocolMismatchError(version);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new ReviewDesktopDiscoveryUnreadableError(filePath, String(error));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function isHealthyReviewDesktop(
|
|
89
|
+
discovery: ReviewDesktopDiscovery,
|
|
90
|
+
fetch = globalThis.fetch,
|
|
91
|
+
): Promise<boolean> {
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(`${discovery.url}/health`, {
|
|
94
|
+
signal: AbortSignal.timeout(1_500),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!response.ok) return false;
|
|
98
|
+
const health = await response.json();
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
isJsonObject(health) &&
|
|
102
|
+
health.ok === true &&
|
|
103
|
+
health.instanceId === discovery.instanceId &&
|
|
104
|
+
health.desktopAttached === true
|
|
105
|
+
);
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The selected Desktop's record while it is running, else null. */
|
|
112
|
+
export function healthyReviewInstance(
|
|
113
|
+
selection: ReviewInstanceSelection,
|
|
114
|
+
): ReviewDesktopDiscovery | null {
|
|
115
|
+
return selection.instance?.healthy ? selection.instance.discovery : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function requireHealthyReviewDesktop(
|
|
119
|
+
fetch = globalThis.fetch,
|
|
120
|
+
): Promise<ReviewDesktopDiscovery> {
|
|
121
|
+
const selection = await selectReviewInstance({ fetch });
|
|
122
|
+
const discovery = healthyReviewInstance(selection);
|
|
123
|
+
|
|
124
|
+
if (!discovery) throw reviewInstanceUnavailable(selection);
|
|
125
|
+
|
|
126
|
+
return discovery;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Shell-session override; agents inherit it through the bare shim. */
|
|
130
|
+
export const REVIEW_INSTANCE_ENV = "DEV_REVIEW_INSTANCE";
|
|
131
|
+
|
|
132
|
+
// Keys name files, so nothing else may select one.
|
|
133
|
+
function checkedInstanceKey(key: string, origin = "") {
|
|
134
|
+
if (/^(?:stable|preview|dev-[A-Za-z0-9_.-]+)$/.test(key)) return key;
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Unknown Whiteboard instance ${JSON.stringify(key)}${origin}. Use stable, preview, or a dev-… key from \`whiteboard instances\`.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type ReviewInstanceIdentity = Required<
|
|
141
|
+
Pick<ReviewDesktopDiscovery, "key" | "channel">
|
|
142
|
+
> &
|
|
143
|
+
Pick<ReviewDesktopDiscovery, "checkout" | "appPath" | "appVersion">;
|
|
144
|
+
|
|
145
|
+
/** Identity of the Desktop hosting this process, from the env electron-main passes. */
|
|
146
|
+
export function reviewInstanceIdentity(
|
|
147
|
+
env: NodeJS.ProcessEnv,
|
|
148
|
+
): ReviewInstanceIdentity {
|
|
149
|
+
const checkout = env.DEV_FAST_REVIEW_CHECKOUT?.trim();
|
|
150
|
+
|
|
151
|
+
const channel = checkout
|
|
152
|
+
? "dev"
|
|
153
|
+
: env.DEV_FAST_REVIEW_RELEASE_CHANNEL === "preview"
|
|
154
|
+
? "preview"
|
|
155
|
+
: "stable";
|
|
156
|
+
|
|
157
|
+
const identity: ReviewInstanceIdentity = {
|
|
158
|
+
key: checkout ? reviewDevInstanceKey(checkout) : channel,
|
|
159
|
+
channel,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (checkout) identity.checkout = path.resolve(checkout);
|
|
163
|
+
const appPath = env.DEV_FAST_REVIEW_APP_PATH?.trim();
|
|
164
|
+
|
|
165
|
+
if (appPath) identity.appPath = appPath;
|
|
166
|
+
const appVersion = env.DEV_FAST_REVIEW_APP_VERSION?.trim();
|
|
167
|
+
|
|
168
|
+
if (appVersion) identity.appVersion = appVersion;
|
|
169
|
+
|
|
170
|
+
return identity;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface ReviewInstance {
|
|
174
|
+
key: string;
|
|
175
|
+
filePath: string;
|
|
176
|
+
discovery: ReviewDesktopDiscovery;
|
|
177
|
+
healthy: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface ReviewInstanceDependencies {
|
|
181
|
+
env?: NodeJS.ProcessEnv;
|
|
182
|
+
fetch?: typeof globalThis.fetch;
|
|
183
|
+
warn?: (message: string) => void;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Records by key, plus the files that could not be read, by key. */
|
|
187
|
+
export async function readReviewInstances(
|
|
188
|
+
dependencies: ReviewInstanceDependencies,
|
|
189
|
+
) {
|
|
190
|
+
const env = dependencies.env ?? process.env;
|
|
191
|
+
const directory = reviewInstancesDir(env);
|
|
192
|
+
const names = await readdir(directory).catch((): string[] => []);
|
|
193
|
+
|
|
194
|
+
const files = names
|
|
195
|
+
.filter((name) => name.endsWith(".json"))
|
|
196
|
+
.map((name): [string, string] => [
|
|
197
|
+
path.basename(name, ".json"),
|
|
198
|
+
path.join(directory, name),
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
// A stable Desktop that predates instances wrote only the legacy file; it
|
|
202
|
+
// stands in for stable only while no stable record exists at all.
|
|
203
|
+
if (!names.includes("stable.json"))
|
|
204
|
+
files.push(["stable", reviewLegacyDiscoveryPath(env)]);
|
|
205
|
+
|
|
206
|
+
const records: Omit<ReviewInstance, "healthy">[] = [];
|
|
207
|
+
const broken = new Map<string, Error>();
|
|
208
|
+
|
|
209
|
+
for (const [key, filePath] of files) {
|
|
210
|
+
try {
|
|
211
|
+
const discovery = await readReviewDesktopDiscoveryFile(filePath);
|
|
212
|
+
|
|
213
|
+
if (discovery) records.push({ key, filePath, discovery });
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const problem = error instanceof Error ? error : new Error(String(error));
|
|
216
|
+
broken.set(key, problem);
|
|
217
|
+
dependencies.warn?.(`Skipping ${filePath}: ${problem.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const instances = await Promise.all(
|
|
222
|
+
records.map(async (record) => ({
|
|
223
|
+
...record,
|
|
224
|
+
healthy: await isHealthyReviewDesktop(
|
|
225
|
+
record.discovery,
|
|
226
|
+
dependencies.fetch,
|
|
227
|
+
),
|
|
228
|
+
})),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
return { instances, broken };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function readDefaultReviewInstance(
|
|
235
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
236
|
+
): Promise<string | undefined> {
|
|
237
|
+
const value = await readFile(reviewDefaultInstancePath(env), "utf8")
|
|
238
|
+
.then((text) => text.trim())
|
|
239
|
+
.catch(() => "");
|
|
240
|
+
|
|
241
|
+
return value || undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function writeDefaultReviewInstance(
|
|
245
|
+
key: string,
|
|
246
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
const filePath = reviewDefaultInstancePath(env);
|
|
249
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
250
|
+
await writeFile(filePath, `${checkedInstanceKey(key)}\n`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function clearDefaultReviewInstance(
|
|
254
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
255
|
+
): Promise<void> {
|
|
256
|
+
await rm(reviewDefaultInstancePath(env), { force: true });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface ReviewInstanceSelection {
|
|
260
|
+
key: string;
|
|
261
|
+
source: "env" | "default" | "only-running" | "fallback";
|
|
262
|
+
/** The selected key's record, when one exists. */
|
|
263
|
+
instance?: ReviewInstance;
|
|
264
|
+
instances: ReviewInstance[];
|
|
265
|
+
/** Why the selected key's record could not be read, when it has a broken one. */
|
|
266
|
+
problem?: Error;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** DEV_REVIEW_INSTANCE, then the machine default, then the only running Desktop, then stable. */
|
|
270
|
+
export async function selectReviewInstance(
|
|
271
|
+
dependencies: ReviewInstanceDependencies = {},
|
|
272
|
+
): Promise<ReviewInstanceSelection> {
|
|
273
|
+
const env = dependencies.env ?? process.env;
|
|
274
|
+
const { instances, broken } = await readReviewInstances(dependencies);
|
|
275
|
+
const fromEnv = env[REVIEW_INSTANCE_ENV]?.trim();
|
|
276
|
+
|
|
277
|
+
const fromDefault = fromEnv
|
|
278
|
+
? undefined
|
|
279
|
+
: await readDefaultReviewInstance(env);
|
|
280
|
+
|
|
281
|
+
const running = instances.filter((instance) => instance.healthy);
|
|
282
|
+
|
|
283
|
+
const [key, source]: [string, ReviewInstanceSelection["source"]] = fromEnv
|
|
284
|
+
? [checkedInstanceKey(fromEnv, ` from ${REVIEW_INSTANCE_ENV}`), "env"]
|
|
285
|
+
: fromDefault
|
|
286
|
+
? [
|
|
287
|
+
checkedInstanceKey(
|
|
288
|
+
fromDefault,
|
|
289
|
+
" as the machine default (`whiteboard instances clear` removes it)",
|
|
290
|
+
),
|
|
291
|
+
"default",
|
|
292
|
+
]
|
|
293
|
+
: running.length === 1
|
|
294
|
+
? [running[0]!.key, "only-running"]
|
|
295
|
+
: ["stable", "fallback"];
|
|
296
|
+
|
|
297
|
+
const selection: ReviewInstanceSelection = { key, source, instances };
|
|
298
|
+
const instance = instances.find((instance) => instance.key === key);
|
|
299
|
+
|
|
300
|
+
// A broken record is a diagnosis, not "not running": `app pick` must not
|
|
301
|
+
// launch a second Desktop over it. With nothing selected explicitly, any
|
|
302
|
+
// broken record may be the one Desktop that is running.
|
|
303
|
+
const problem = instance
|
|
304
|
+
? undefined
|
|
305
|
+
: fromEnv || fromDefault
|
|
306
|
+
? broken.get(key)
|
|
307
|
+
: broken.values().next().value;
|
|
308
|
+
|
|
309
|
+
if (instance) selection.instance = instance;
|
|
310
|
+
|
|
311
|
+
if (problem) selection.problem = problem;
|
|
312
|
+
|
|
313
|
+
return selection;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Never a redirect: names what is running and how to start or pick one. */
|
|
317
|
+
export function reviewInstanceUnavailable(
|
|
318
|
+
selection: ReviewInstanceSelection,
|
|
319
|
+
): Error {
|
|
320
|
+
if (selection.problem) return selection.problem;
|
|
321
|
+
|
|
322
|
+
const running = selection.instances
|
|
323
|
+
.filter((instance) => instance.healthy)
|
|
324
|
+
.map((instance) => instance.key);
|
|
325
|
+
|
|
326
|
+
const others = running.length
|
|
327
|
+
? ` Running: ${running.join(", ")}.`
|
|
328
|
+
: " No Whiteboard is running.";
|
|
329
|
+
|
|
330
|
+
if (selection.source === "fallback" && running.length > 1)
|
|
331
|
+
return new ReviewInstanceUnavailableError(
|
|
332
|
+
`Several Whiteboard instances are running and none is selected.${others} Choose one with \`whiteboard instances use <key>\`, or \`export ${REVIEW_INSTANCE_ENV}=<key>\` for this shell.`,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
return new ReviewInstanceUnavailableError(
|
|
336
|
+
`Whiteboard \`${selection.key}\` is not running. ${reviewInstanceStartHint(selection)}, or pick another instance with \`whiteboard instances\`.${others}`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** A command could not reach the selected Desktop instance. */
|
|
341
|
+
export class ReviewInstanceUnavailableError extends Error {
|
|
342
|
+
readonly name = "ReviewInstanceUnavailableError";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function reviewInstanceStartHint(
|
|
346
|
+
selection: Pick<ReviewInstanceSelection, "key" | "instance">,
|
|
347
|
+
): string {
|
|
348
|
+
if (!selection.key.startsWith("dev-"))
|
|
349
|
+
return "Start it with `whiteboard app launch`";
|
|
350
|
+
const checkout = selection.instance?.discovery.checkout;
|
|
351
|
+
|
|
352
|
+
return checkout
|
|
353
|
+
? `Start it with \`pnpm dev\` in ${checkout}`
|
|
354
|
+
: "Start it with `pnpm dev` in its checkout";
|
|
355
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type JsonValue,
|
|
3
|
+
isJsonObject,
|
|
4
|
+
isNumberValue,
|
|
5
|
+
isStringValue,
|
|
6
|
+
jsonValueSchema,
|
|
7
|
+
} from "@dev.fast/review-protocol";
|
|
8
|
+
|
|
9
|
+
/** Upgrade retained document attachments at the read/import boundary. New edits
|
|
10
|
+
* use the strict DiffSelection schema and never accept these retired forms. */
|
|
11
|
+
// This is the decoder boundary for stored documents in retired wire formats.
|
|
12
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters
|
|
13
|
+
export function migrateDiffSelections(input: unknown): JsonValue {
|
|
14
|
+
return migrateNode(jsonValueSchema.parse(input));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function migrateNode(value: JsonValue, attachment = false): JsonValue {
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return value.map((child) => migrateNode(child, attachment));
|
|
20
|
+
|
|
21
|
+
if (!isJsonObject(value)) return value;
|
|
22
|
+
|
|
23
|
+
if (attachment && isStringValue(value.file)) {
|
|
24
|
+
if (isNumberValue(value.fromLine) && isNumberValue(value.toLine)) {
|
|
25
|
+
const side = value.side ?? value.graph ?? "head";
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
file: value.file,
|
|
29
|
+
start: { side, line: value.fromLine },
|
|
30
|
+
end: { side, line: value.toLine },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
isJsonObject(value.start) &&
|
|
36
|
+
isJsonObject(value.end) &&
|
|
37
|
+
"baseLine" in value.start
|
|
38
|
+
) {
|
|
39
|
+
const endpoint = (row: typeof value.start) =>
|
|
40
|
+
isNumberValue(row.headLine)
|
|
41
|
+
? { side: "head", line: row.headLine }
|
|
42
|
+
: { side: "base", line: row.baseLine };
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
file: value.file,
|
|
46
|
+
start: endpoint(value.start),
|
|
47
|
+
end: endpoint(value.end),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return Object.fromEntries(
|
|
53
|
+
Object.entries(value).map(([key, child]) => [
|
|
54
|
+
key,
|
|
55
|
+
migrateNode(
|
|
56
|
+
child,
|
|
57
|
+
["source", "peek", "sources", "contextSources", "callSite"].includes(
|
|
58
|
+
key,
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Placeholder replaced by release builds (scripts/embed-posthog-key.mjs).
|
|
2
|
+
// Source checkouts embed no key: telemetry capture stays disabled unless
|
|
3
|
+
// PROGRESSIVE_REVIEW_POSTHOG_KEY (or DEV_FAST_POSTHOG_KEY / POSTHOG_KEY) is
|
|
4
|
+
// set in the environment.
|
|
5
|
+
export const EMBEDDED_PROGRESSIVE_REVIEW_POSTHOG_KEY: string | undefined =
|
|
6
|
+
undefined;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Turns a raw JavaScript error into reportable telemetry properties.
|
|
2
|
+
//
|
|
3
|
+
// This module is the only place that sees raw error text on the way to PostHog.
|
|
4
|
+
// Callers hand it the `error` envelope of a loopback telemetry request, and it
|
|
5
|
+
// returns four things: the class name, the message with paths, addresses and
|
|
6
|
+
// secrets replaced by markers, a digest of the original message, and stack
|
|
7
|
+
// frames rewritten to start inside the shipped Review bundle.
|
|
8
|
+
//
|
|
9
|
+
// The message is cleaned with a port of VS Code's cleaner (telemetry-clean-text
|
|
10
|
+
// .ts) rather than a rule of our own, because that one is proven at scale in the
|
|
11
|
+
// product this is a fork of.
|
|
12
|
+
//
|
|
13
|
+
// ui-telemetry-events.ts re-checks the result independently — the frames
|
|
14
|
+
// against a bundle-path pattern, the message against the same secret shapes plus
|
|
15
|
+
// a refusal of any surviving path separator — so a bug here cannot by itself
|
|
16
|
+
// leak a path.
|
|
17
|
+
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
type JsonObject,
|
|
22
|
+
type JsonValue,
|
|
23
|
+
isJsonObject,
|
|
24
|
+
jsonString,
|
|
25
|
+
} from "@dev.fast/review-protocol";
|
|
26
|
+
|
|
27
|
+
import { cleanTelemetryText } from "./telemetry-clean-text";
|
|
28
|
+
import {
|
|
29
|
+
BUNDLE_FRAME_PATTERN,
|
|
30
|
+
BUNDLE_FRAME_SEPARATOR,
|
|
31
|
+
isReportableCleanedMessage,
|
|
32
|
+
} from "./ui-telemetry-events";
|
|
33
|
+
|
|
34
|
+
export interface DerivedErrorTelemetryProperties {
|
|
35
|
+
error_name?: string;
|
|
36
|
+
message?: string;
|
|
37
|
+
message_hash?: string;
|
|
38
|
+
frames?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Errors whose message quotes the document being checked. A review tool runs
|
|
43
|
+
* authored prose through schemas, so these carry review content by design —
|
|
44
|
+
* unlike an editor, which mostly reports on files. Keep the class, the digest
|
|
45
|
+
* and the frames; drop the message. VS Code drops file-operation errors and
|
|
46
|
+
* errors carrying a system code for the same reason.
|
|
47
|
+
*/
|
|
48
|
+
const SCHEMA_ERROR_NAMES = new Set(["ZodError", "ValidationError"]);
|
|
49
|
+
|
|
50
|
+
const MESSAGE_HASH_LENGTH = 16;
|
|
51
|
+
|
|
52
|
+
const MAX_FRAMES = 10;
|
|
53
|
+
|
|
54
|
+
const MAX_STACK_LENGTH = 16_384;
|
|
55
|
+
|
|
56
|
+
const MAX_ERROR_NAME_LENGTH = 40;
|
|
57
|
+
|
|
58
|
+
const ERROR_NAME_PATTERN = /^[A-Za-z0-9_$-]+$/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Path segments that exist only inside the shipped bundle. A frame keeps the
|
|
62
|
+
* text that follows the LAST occurrence of one of these and discards everything
|
|
63
|
+
* before it, which is what removes the absolute prefix. A frame with no such
|
|
64
|
+
* segment is not ours, so it is dropped.
|
|
65
|
+
*/
|
|
66
|
+
const BUNDLE_ANCHORS = ["/out/", "/assets/", "/review-runtime/"];
|
|
67
|
+
|
|
68
|
+
/** `at fn (url:line:col)`, `at url:line:col`, and the bare `url:line:col`. */
|
|
69
|
+
const STACK_FRAME_PATTERN = /(?:\(|\bat\s+|^\s*)([^()\s]+?):(\d+):(\d+)\)?\s*$/;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `cause` is the raw error envelope a client attached beside its event
|
|
73
|
+
* properties: `{ name, message, stack }` as JSON, or anything else a hostile
|
|
74
|
+
* client sent, which derives nothing.
|
|
75
|
+
*/
|
|
76
|
+
export function deriveErrorTelemetryProperties(
|
|
77
|
+
cause: unknown,
|
|
78
|
+
): DerivedErrorTelemetryProperties {
|
|
79
|
+
const derived: DerivedErrorTelemetryProperties = {};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
if (!isJsonObject(cause)) return derived;
|
|
83
|
+
|
|
84
|
+
const name = jsonString(cause.name);
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
name !== undefined &&
|
|
88
|
+
name.length > 0 &&
|
|
89
|
+
name.length <= MAX_ERROR_NAME_LENGTH &&
|
|
90
|
+
ERROR_NAME_PATTERN.test(name)
|
|
91
|
+
) {
|
|
92
|
+
derived.error_name = name;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const message = jsonString(cause.message);
|
|
96
|
+
|
|
97
|
+
if (message !== undefined && message.length > 0) {
|
|
98
|
+
// The digest goes on every report, cleaned message or not. It is what
|
|
99
|
+
// groups the reports whose message does not survive the checks below.
|
|
100
|
+
derived.message_hash = hashErrorMessage(message);
|
|
101
|
+
|
|
102
|
+
if (!SCHEMA_ERROR_NAMES.has(derived.error_name ?? "")) {
|
|
103
|
+
const cleaned = cleanTelemetryText(message, NO_DELETED_DIRECTORIES);
|
|
104
|
+
|
|
105
|
+
// Ask the allowlist's own check before sending. Failing here rather
|
|
106
|
+
// than there keeps a message that the cleaner could not finish out of
|
|
107
|
+
// the payload entirely, instead of relying on the later gate.
|
|
108
|
+
if (isReportableCleanedMessage(cleaned)) derived.message = cleaned;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const frames = packBundleFrames(cause.stack);
|
|
113
|
+
|
|
114
|
+
if (frames) derived.frames = frames;
|
|
115
|
+
} catch {
|
|
116
|
+
// Telemetry must never break the request that carried it.
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return derived;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Properties only this module may produce, because each is a claim about work
|
|
124
|
+
* the server did that the allowlist cannot check for itself: whether a message
|
|
125
|
+
* was cleaned, and whether a digest or a frame list really came from this
|
|
126
|
+
* error. A client that sent one directly would reach the allowlist with no
|
|
127
|
+
* cleaning behind it.
|
|
128
|
+
*
|
|
129
|
+
* `error_name` is deliberately absent. The allowlist caps it at 40 identifier
|
|
130
|
+
* characters, which makes it safe whoever sends it, and `bug_report_send_failed`
|
|
131
|
+
* sends it with no error envelope.
|
|
132
|
+
*/
|
|
133
|
+
const SERVER_DERIVED_PROPERTIES = [
|
|
134
|
+
"message",
|
|
135
|
+
"message_hash",
|
|
136
|
+
"frames",
|
|
137
|
+
] as const;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Merge a client's event properties with the ones derived from its raw error.
|
|
141
|
+
* This is the only supported way to build an error-bearing telemetry payload:
|
|
142
|
+
* it removes any server-derived property the client tried to assert before
|
|
143
|
+
* adding the real ones.
|
|
144
|
+
*/
|
|
145
|
+
export function mergeErrorTelemetryProperties(
|
|
146
|
+
clientProperties: JsonObject,
|
|
147
|
+
cause: unknown,
|
|
148
|
+
): JsonObject {
|
|
149
|
+
const merged = { ...clientProperties };
|
|
150
|
+
|
|
151
|
+
for (const key of SERVER_DERIVED_PROPERTIES) delete merged[key];
|
|
152
|
+
|
|
153
|
+
if (cause) Object.assign(merged, deriveErrorTelemetryProperties(cause));
|
|
154
|
+
|
|
155
|
+
return merged;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Review deliberately gives the cleaner NO directories to delete, which is the
|
|
160
|
+
* one place its configuration differs from VS Code's.
|
|
161
|
+
*
|
|
162
|
+
* VS Code passes five — its app root, extensions path, user data path, home
|
|
163
|
+
* directory and temporary directory — and deletes each prefix outright so the
|
|
164
|
+
* remainder reads as a relative path. That is right for a stack trace, where
|
|
165
|
+
* the remainder is VS Code's own file. It is wrong here: a path under the home
|
|
166
|
+
* directory then keeps everything after it, so
|
|
167
|
+
* `/Users/you/work/acme-repo/plan.md` would be sent as
|
|
168
|
+
* `/work/acme-repo/plan.md` — the repository name intact. A review tool cannot
|
|
169
|
+
* disclose that.
|
|
170
|
+
*
|
|
171
|
+
* With no directories to delete, the cleaner's overlap check never fires and
|
|
172
|
+
* the whole path is replaced by a marker instead, which is what we want. Frames
|
|
173
|
+
* are unaffected: they are anchored on the bundle directory separately.
|
|
174
|
+
*/
|
|
175
|
+
const NO_DELETED_DIRECTORIES: RegExp[] = [];
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A stable fingerprint of the message. Two reports with the same digest had the
|
|
179
|
+
* same cause; the digest itself reveals nothing, because it is one-way and the
|
|
180
|
+
* message is never sent alongside it.
|
|
181
|
+
*/
|
|
182
|
+
export function hashErrorMessage(message: string): string {
|
|
183
|
+
return createHash("sha256")
|
|
184
|
+
.update(message, "utf8")
|
|
185
|
+
.digest("hex")
|
|
186
|
+
.slice(0, MESSAGE_HASH_LENGTH);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Rewrite a stack into bundle-relative `file:line:col` frames joined by "|".
|
|
191
|
+
* Returns undefined when no frame resolves inside the bundle.
|
|
192
|
+
*/
|
|
193
|
+
export function packBundleFrames(
|
|
194
|
+
stack: JsonValue | undefined,
|
|
195
|
+
): string | undefined {
|
|
196
|
+
const text = Array.isArray(stack) ? stack.join("\n") : jsonString(stack);
|
|
197
|
+
|
|
198
|
+
if (!text) return undefined;
|
|
199
|
+
|
|
200
|
+
const frames: string[] = [];
|
|
201
|
+
|
|
202
|
+
for (const line of text.slice(0, MAX_STACK_LENGTH).split("\n")) {
|
|
203
|
+
const match = STACK_FRAME_PATTERN.exec(line);
|
|
204
|
+
|
|
205
|
+
if (!match) continue;
|
|
206
|
+
const file = bundleRelativePath(match[1]);
|
|
207
|
+
|
|
208
|
+
if (!file) continue;
|
|
209
|
+
const frame = `${file}:${match[2]}:${match[3]}`;
|
|
210
|
+
|
|
211
|
+
// A user directory can be called "out" too, so anchoring alone is not
|
|
212
|
+
// enough: the result must also start inside a known bundle directory.
|
|
213
|
+
if (!BUNDLE_FRAME_PATTERN.test(frame)) continue;
|
|
214
|
+
frames.push(frame);
|
|
215
|
+
|
|
216
|
+
if (frames.length >= MAX_FRAMES) break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return frames.length > 0 ? frames.join(BUNDLE_FRAME_SEPARATOR) : undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function bundleRelativePath(location: string): string | undefined {
|
|
223
|
+
// Drop a query or fragment first: either can carry arbitrary text, and a
|
|
224
|
+
// cache-busting query is common on the canvas bundle.
|
|
225
|
+
const clean = location.split(/[?#]/, 1)[0];
|
|
226
|
+
let cut = -1;
|
|
227
|
+
let anchorLength = 0;
|
|
228
|
+
|
|
229
|
+
for (const anchor of BUNDLE_ANCHORS) {
|
|
230
|
+
const index = clean.lastIndexOf(anchor);
|
|
231
|
+
|
|
232
|
+
if (index > cut) {
|
|
233
|
+
cut = index;
|
|
234
|
+
anchorLength = anchor.length;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (cut < 0) return undefined;
|
|
239
|
+
const relative = clean.slice(cut + anchorLength);
|
|
240
|
+
// "/review-runtime/" and "/assets/" name the directory the frame lives in, so
|
|
241
|
+
// put it back; "/out/" is a build directory and is not part of the path we
|
|
242
|
+
// report.
|
|
243
|
+
const prefix = clean.slice(cut + 1, cut + anchorLength);
|
|
244
|
+
const path = prefix === "out/" ? relative : `${prefix}${relative}`;
|
|
245
|
+
|
|
246
|
+
return path.length > 0 ? path : undefined;
|
|
247
|
+
}
|
package/src/evidence.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Quote matching shared by legacy publish and JSON accept: authors paste
|
|
2
|
+
* transcript text with arbitrary line wrapping, so both sides compare on
|
|
3
|
+
* whitespace-normalized text. */
|
|
4
|
+
export function normalizeQuoteText(text: string): string {
|
|
5
|
+
return text.trim().replace(/\s+/g, " ");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function textIncludesQuote(text: string, quote: string): boolean {
|
|
9
|
+
const normalizedQuote = normalizeQuoteText(quote);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
normalizedQuote !== "" && normalizeQuoteText(text).includes(normalizedQuote)
|
|
13
|
+
);
|
|
14
|
+
}
|