pi-jscpd 0.1.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/CHANGELOG.md +99 -0
- package/CONTRIBUTING.md +144 -0
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/SECURITY.md +93 -0
- package/docs/automatic-checkpoint.md +235 -0
- package/docs/compatibility.md +119 -0
- package/docs/effect-architecture.md +128 -0
- package/docs/fallow-coexistence.md +120 -0
- package/docs/overlay-interaction.md +347 -0
- package/docs/release.md +115 -0
- package/package.json +86 -0
- package/scripts/check-compatibility.mjs +103 -0
- package/skills/jscpd/SKILL.md +90 -0
- package/src/acknowledgements.ts +268 -0
- package/src/automatic.ts +396 -0
- package/src/baseline.ts +400 -0
- package/src/capability.ts +569 -0
- package/src/changed-files.ts +372 -0
- package/src/changed.ts +548 -0
- package/src/clone-identity.ts +373 -0
- package/src/config.ts +414 -0
- package/src/contract.ts +39 -0
- package/src/dispatch.ts +90 -0
- package/src/effect/clock.ts +10 -0
- package/src/effect/errors.ts +311 -0
- package/src/effect/filesystem.ts +240 -0
- package/src/effect/runtime-boundary.ts +25 -0
- package/src/effect/runtime-contract.ts +18 -0
- package/src/effect/services.ts +131 -0
- package/src/extension.ts +708 -0
- package/src/fallow.ts +479 -0
- package/src/finding-presentation.ts +73 -0
- package/src/index.ts +8 -0
- package/src/jscpd-report.ts +819 -0
- package/src/jscpd.ts +748 -0
- package/src/overlay.ts +1166 -0
- package/src/parser.ts +189 -0
- package/src/path-utils.ts +44 -0
- package/src/presentation.ts +232 -0
- package/src/process.ts +425 -0
- package/src/registry.ts +102 -0
- package/src/scan.ts +441 -0
- package/src/scheduler.ts +434 -0
- package/src/session-state.ts +229 -0
- package/src/status.ts +534 -0
- package/src/types.ts +334 -0
- package/src/value-utils.ts +14 -0
- package/src/verification.ts +220 -0
package/src/baseline.ts
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { Cause, Context, Deferred, Effect, FiberId, Layer, MutableRef } from "effect";
|
|
2
|
+
import {
|
|
3
|
+
createJscpdExecutionPath,
|
|
4
|
+
type JscpdCapabilityResult,
|
|
5
|
+
type JscpdCapabilityService,
|
|
6
|
+
} from "./capability.js";
|
|
7
|
+
import { indexJscpdCloneReportEffect, type JscpdCloneSnapshot } from "./clone-identity.js";
|
|
8
|
+
import type { JscpdFileSystem, JscpdProcess } from "./effect/services.js";
|
|
9
|
+
import type { JscpdRunFailureReason, JscpdRunResult, JscpdService } from "./jscpd.js";
|
|
10
|
+
import { consumeJscpdV5JsonReportEffect } from "./jscpd-report.js";
|
|
11
|
+
import { optionalCanonicalDirectoryEffect } from "./path-utils.js";
|
|
12
|
+
import {
|
|
13
|
+
adapterRunEffect,
|
|
14
|
+
capabilityProbeEffect,
|
|
15
|
+
createJscpdScanArguments,
|
|
16
|
+
JSCPD_CLONE_POSITIVE_EXIT_CODES,
|
|
17
|
+
} from "./scan.js";
|
|
18
|
+
import type { JscpdReportErrorCode, JscpdScanReport } from "./types.js";
|
|
19
|
+
|
|
20
|
+
export interface JscpdBaselineStartContext {
|
|
21
|
+
readonly cwd: string;
|
|
22
|
+
readonly enabled: boolean;
|
|
23
|
+
readonly timeoutMs: number;
|
|
24
|
+
readonly hasPriorChanges: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type JscpdBaselineState =
|
|
28
|
+
| { readonly status: "unstarted" }
|
|
29
|
+
| { readonly status: "pending" }
|
|
30
|
+
| {
|
|
31
|
+
readonly status: "accepted";
|
|
32
|
+
readonly outcome: "clean" | "findings";
|
|
33
|
+
/** Strict normalized report retained in memory only. */
|
|
34
|
+
readonly report: JscpdScanReport;
|
|
35
|
+
/** Content-aware identities captured before source mutation. */
|
|
36
|
+
readonly snapshot: JscpdCloneSnapshot;
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
readonly status: "unavailable";
|
|
40
|
+
readonly reason: "disabled" | "missing-binary" | "incompatible-version";
|
|
41
|
+
}
|
|
42
|
+
| { readonly status: "partial"; readonly reason: "restored-after-changes" }
|
|
43
|
+
| { readonly status: "cancelled"; readonly stage: "probe" | "scan" | "lifecycle" }
|
|
44
|
+
| {
|
|
45
|
+
readonly status: "timed-out";
|
|
46
|
+
readonly stage: "probe" | "scan";
|
|
47
|
+
readonly timeoutMs: number;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
readonly status: "failed";
|
|
51
|
+
readonly stage: "project" | "probe" | "scan";
|
|
52
|
+
readonly reason: string;
|
|
53
|
+
readonly reportError?: JscpdReportErrorCode;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export interface JscpdBaselineService {
|
|
57
|
+
/** Replace current lifecycle state and schedule at most one capture for this generation. */
|
|
58
|
+
startEffect: (
|
|
59
|
+
context: JscpdBaselineStartContext,
|
|
60
|
+
) => Effect.Effect<JscpdBaselineState, never, JscpdFileSystem | JscpdProcess>;
|
|
61
|
+
/** Await the current capture, if any. */
|
|
62
|
+
readonly waitEffect: Effect.Effect<JscpdBaselineState>;
|
|
63
|
+
/** Cancel current work and mark automatic capture disabled. */
|
|
64
|
+
disable(): void;
|
|
65
|
+
/** Cancel current work and clear ephemeral report state. */
|
|
66
|
+
invalidate(): void;
|
|
67
|
+
current(): JscpdBaselineState;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface JscpdBaselineOptions {
|
|
71
|
+
/** Stable PATH override used by deterministic tests. */
|
|
72
|
+
readonly path?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ActiveCapture {
|
|
76
|
+
readonly generation: number;
|
|
77
|
+
readonly controller: AbortController;
|
|
78
|
+
readonly completion: Deferred.Deferred<JscpdBaselineState>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface BaselineOwnerState {
|
|
82
|
+
readonly generation: number;
|
|
83
|
+
readonly baseline: JscpdBaselineState;
|
|
84
|
+
readonly active?: ActiveCapture;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface JscpdBaselineEffectService {
|
|
88
|
+
readonly start: (
|
|
89
|
+
context: JscpdBaselineStartContext,
|
|
90
|
+
) => Effect.Effect<JscpdBaselineState, never, JscpdFileSystem | JscpdProcess>;
|
|
91
|
+
readonly wait: Effect.Effect<JscpdBaselineState>;
|
|
92
|
+
readonly disable: Effect.Effect<void>;
|
|
93
|
+
readonly invalidate: Effect.Effect<void>;
|
|
94
|
+
readonly current: Effect.Effect<JscpdBaselineState>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const JscpdBaseline = Context.GenericTag<JscpdBaselineEffectService>(
|
|
98
|
+
"pi-jscpd/effect/Baseline",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const UNSTARTED: JscpdBaselineState = Object.freeze({ status: "unstarted" });
|
|
102
|
+
const PENDING: JscpdBaselineState = Object.freeze({ status: "pending" });
|
|
103
|
+
|
|
104
|
+
/** Capture one full-project normalized report without exposing it to model context or disk. */
|
|
105
|
+
export function createJscpdBaselineService(
|
|
106
|
+
capabilityService: JscpdCapabilityService,
|
|
107
|
+
adapterService: JscpdService,
|
|
108
|
+
options: JscpdBaselineOptions = {},
|
|
109
|
+
): JscpdBaselineService {
|
|
110
|
+
return baselineServiceFor(new BaselineOwner(capabilityService, adapterService, options));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createJscpdBaselineLayer(
|
|
114
|
+
capabilityService: JscpdCapabilityService,
|
|
115
|
+
adapterService: JscpdService,
|
|
116
|
+
options: JscpdBaselineOptions = {},
|
|
117
|
+
) {
|
|
118
|
+
return Layer.scoped(
|
|
119
|
+
JscpdBaseline,
|
|
120
|
+
Effect.acquireRelease(
|
|
121
|
+
Effect.sync(() => new BaselineOwner(capabilityService, adapterService, options)),
|
|
122
|
+
(owner) => Effect.sync(() => owner.invalidate()),
|
|
123
|
+
).pipe(Effect.map(baselineEffectServiceFor)),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class BaselineOwner {
|
|
128
|
+
readonly #capability: JscpdCapabilityService;
|
|
129
|
+
readonly #adapter: JscpdService;
|
|
130
|
+
readonly #path: string | undefined;
|
|
131
|
+
readonly #state = MutableRef.make<BaselineOwnerState>({
|
|
132
|
+
generation: 0,
|
|
133
|
+
baseline: UNSTARTED,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
constructor(
|
|
137
|
+
capability: JscpdCapabilityService,
|
|
138
|
+
adapter: JscpdService,
|
|
139
|
+
options: JscpdBaselineOptions,
|
|
140
|
+
) {
|
|
141
|
+
this.#capability = capability;
|
|
142
|
+
this.#adapter = adapter;
|
|
143
|
+
this.#path = options.path;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
startEffect(
|
|
147
|
+
context: JscpdBaselineStartContext,
|
|
148
|
+
): Effect.Effect<JscpdBaselineState, never, JscpdFileSystem | JscpdProcess> {
|
|
149
|
+
return Effect.suspend(() => this.startPreparedEffect(context));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
startPreparedEffect(
|
|
153
|
+
context: JscpdBaselineStartContext,
|
|
154
|
+
): Effect.Effect<JscpdBaselineState, never, JscpdFileSystem | JscpdProcess> {
|
|
155
|
+
const completion = Deferred.unsafeMake<JscpdBaselineState>(FiberId.none);
|
|
156
|
+
const started = this.#beginCapture(context, completion);
|
|
157
|
+
if (!started.active) return Effect.succeed(started.baseline);
|
|
158
|
+
const active = started.active;
|
|
159
|
+
const capture = captureBaselineEffect(
|
|
160
|
+
this.#capability,
|
|
161
|
+
this.#adapter,
|
|
162
|
+
context,
|
|
163
|
+
active.controller,
|
|
164
|
+
this.#path,
|
|
165
|
+
).pipe(
|
|
166
|
+
Effect.catchAllCause((cause) =>
|
|
167
|
+
Cause.isInterruptedOnly(cause)
|
|
168
|
+
? Effect.failCause(cause)
|
|
169
|
+
: Effect.succeed(failed("scan", "internal-error")),
|
|
170
|
+
),
|
|
171
|
+
Effect.map((result) => this.#settleCapture(result, active)),
|
|
172
|
+
Effect.tap((result) => Deferred.succeed(completion, result)),
|
|
173
|
+
Effect.onInterrupt(() => Effect.sync(() => this.#interruptCapture(active))),
|
|
174
|
+
);
|
|
175
|
+
return Effect.raceFirst(capture, Deferred.await(completion));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
waitEffect(): Effect.Effect<JscpdBaselineState> {
|
|
179
|
+
const state = MutableRef.get(this.#state);
|
|
180
|
+
return state.active ? Deferred.await(state.active.completion) : Effect.succeed(state.baseline);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
disable(): void {
|
|
184
|
+
this.#replaceState(Object.freeze({ status: "unavailable", reason: "disabled" }));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
invalidate(): void {
|
|
188
|
+
this.#replaceState(UNSTARTED);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
current(): JscpdBaselineState {
|
|
192
|
+
return MutableRef.get(this.#state).baseline;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#beginCapture(
|
|
196
|
+
context: JscpdBaselineStartContext,
|
|
197
|
+
completion: Deferred.Deferred<JscpdBaselineState>,
|
|
198
|
+
): BaselineOwnerState {
|
|
199
|
+
const baseline = initialState(context);
|
|
200
|
+
const generation = this.#replaceState(baseline);
|
|
201
|
+
if (baseline.status !== "unstarted") return MutableRef.get(this.#state);
|
|
202
|
+
const active = { generation, controller: new AbortController(), completion };
|
|
203
|
+
const started = { generation, baseline: PENDING, active };
|
|
204
|
+
MutableRef.set(this.#state, started);
|
|
205
|
+
return started;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#replaceState(baseline: JscpdBaselineState): number {
|
|
209
|
+
const current = MutableRef.get(this.#state);
|
|
210
|
+
cancelActiveCapture(current.active);
|
|
211
|
+
const generation = current.generation + 1;
|
|
212
|
+
MutableRef.set(this.#state, { generation, baseline });
|
|
213
|
+
return generation;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#interruptCapture(capture: ActiveCapture): void {
|
|
217
|
+
capture.controller.abort();
|
|
218
|
+
const result = this.#settleCapture(lifecycleCancelled(), capture);
|
|
219
|
+
Deferred.unsafeDone(capture.completion, Effect.succeed(result));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#settleCapture(result: JscpdBaselineState, capture: ActiveCapture): JscpdBaselineState {
|
|
223
|
+
const current = MutableRef.get(this.#state);
|
|
224
|
+
if (current.generation !== capture.generation || current.active !== capture) {
|
|
225
|
+
return lifecycleCancelled();
|
|
226
|
+
}
|
|
227
|
+
MutableRef.set(this.#state, { generation: current.generation, baseline: result });
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function cancelActiveCapture(active: ActiveCapture | undefined): void {
|
|
233
|
+
if (!active) return;
|
|
234
|
+
active.controller.abort();
|
|
235
|
+
Deferred.unsafeDone(active.completion, Effect.succeed(lifecycleCancelled()));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function lifecycleCancelled(): JscpdBaselineState {
|
|
239
|
+
return Object.freeze({ status: "cancelled", stage: "lifecycle" });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function baselineServiceFor(owner: BaselineOwner): JscpdBaselineService {
|
|
243
|
+
return {
|
|
244
|
+
startEffect: (context) => owner.startEffect(context),
|
|
245
|
+
waitEffect: Effect.suspend(() => owner.waitEffect()),
|
|
246
|
+
disable: () => owner.disable(),
|
|
247
|
+
invalidate: () => owner.invalidate(),
|
|
248
|
+
current: () => owner.current(),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function baselineEffectServiceFor(owner: BaselineOwner): JscpdBaselineEffectService {
|
|
253
|
+
return {
|
|
254
|
+
start: (context) => owner.startEffect(context),
|
|
255
|
+
wait: Effect.suspend(() => owner.waitEffect()),
|
|
256
|
+
disable: Effect.sync(() => owner.disable()),
|
|
257
|
+
invalidate: Effect.sync(() => owner.invalidate()),
|
|
258
|
+
current: Effect.sync(() => owner.current()),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function initialState(context: JscpdBaselineStartContext): JscpdBaselineState {
|
|
263
|
+
if (context.hasPriorChanges) {
|
|
264
|
+
return Object.freeze({ status: "partial", reason: "restored-after-changes" });
|
|
265
|
+
}
|
|
266
|
+
if (!context.enabled) {
|
|
267
|
+
return Object.freeze({ status: "unavailable", reason: "disabled" });
|
|
268
|
+
}
|
|
269
|
+
return UNSTARTED;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function captureBaselineEffect(
|
|
273
|
+
capabilityService: JscpdCapabilityService,
|
|
274
|
+
adapterService: JscpdService,
|
|
275
|
+
context: JscpdBaselineStartContext,
|
|
276
|
+
controller: AbortController,
|
|
277
|
+
path: string | undefined,
|
|
278
|
+
): Effect.Effect<JscpdBaselineState, never, JscpdFileSystem | JscpdProcess> {
|
|
279
|
+
return Effect.gen(function* () {
|
|
280
|
+
const cwd = yield* optionalCanonicalDirectoryEffect(context.cwd);
|
|
281
|
+
if (!cwd) return failed("project", "invalid-project");
|
|
282
|
+
|
|
283
|
+
const capability = yield* safeProbeEffect(capabilityService, { cwd, path }, controller);
|
|
284
|
+
if (capability.status !== "available") return stateFromCapability(capability);
|
|
285
|
+
|
|
286
|
+
const result = yield* runBaselineAdapterEffect(
|
|
287
|
+
adapterService,
|
|
288
|
+
capability,
|
|
289
|
+
context,
|
|
290
|
+
cwd,
|
|
291
|
+
path,
|
|
292
|
+
controller,
|
|
293
|
+
);
|
|
294
|
+
return yield* stateFromRunResultEffect(result, cwd);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function safeProbeEffect(
|
|
299
|
+
capabilityService: JscpdCapabilityService,
|
|
300
|
+
request: Omit<Parameters<JscpdCapabilityService["probeEffect"]>[0], "signal">,
|
|
301
|
+
controller: AbortController,
|
|
302
|
+
): Effect.Effect<JscpdCapabilityResult, never, JscpdProcess> {
|
|
303
|
+
return capabilityProbeEffect(capabilityService, {
|
|
304
|
+
...request,
|
|
305
|
+
signal: controller.signal,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function runBaselineAdapterEffect(
|
|
310
|
+
adapterService: JscpdService,
|
|
311
|
+
capability: Extract<JscpdCapabilityResult, { status: "available" }>,
|
|
312
|
+
context: JscpdBaselineStartContext,
|
|
313
|
+
cwd: string,
|
|
314
|
+
path: string | undefined,
|
|
315
|
+
controller: AbortController,
|
|
316
|
+
): Effect.Effect<JscpdRunResult<JscpdScanReport>, never, JscpdFileSystem | JscpdProcess> {
|
|
317
|
+
return adapterRunEffect(adapterService, {
|
|
318
|
+
executable: capability.executable,
|
|
319
|
+
cwd,
|
|
320
|
+
path: createJscpdExecutionPath(cwd, path, capability.source),
|
|
321
|
+
signal: controller.signal,
|
|
322
|
+
timeoutMs: context.timeoutMs,
|
|
323
|
+
reportExitCodes: JSCPD_CLONE_POSITIVE_EXIT_CODES,
|
|
324
|
+
createArguments: ({ directory }) => createJscpdScanArguments(directory, ["."]),
|
|
325
|
+
consumeReportEffect: (bytes) => consumeJscpdV5JsonReportEffect(bytes, cwd),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function stateFromCapability(
|
|
330
|
+
capability: Exclude<JscpdCapabilityResult, { status: "available" }>,
|
|
331
|
+
): JscpdBaselineState {
|
|
332
|
+
switch (capability.status) {
|
|
333
|
+
case "missing":
|
|
334
|
+
return Object.freeze({ status: "unavailable", reason: "missing-binary" });
|
|
335
|
+
case "incompatible":
|
|
336
|
+
return Object.freeze({ status: "unavailable", reason: "incompatible-version" });
|
|
337
|
+
case "cancelled":
|
|
338
|
+
return Object.freeze({ status: "cancelled", stage: "probe" });
|
|
339
|
+
case "timed-out":
|
|
340
|
+
return Object.freeze({
|
|
341
|
+
status: "timed-out",
|
|
342
|
+
stage: "probe",
|
|
343
|
+
timeoutMs: capability.timeoutMs,
|
|
344
|
+
});
|
|
345
|
+
case "failed":
|
|
346
|
+
return failed("probe", capability.reason);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function stateFromRunResultEffect(
|
|
351
|
+
result: JscpdRunResult<JscpdScanReport>,
|
|
352
|
+
cwd: string,
|
|
353
|
+
): Effect.Effect<JscpdBaselineState, never, JscpdFileSystem> {
|
|
354
|
+
switch (result.status) {
|
|
355
|
+
case "report":
|
|
356
|
+
return acceptedEffect("findings", result.value, cwd);
|
|
357
|
+
case "no-findings":
|
|
358
|
+
return result.value
|
|
359
|
+
? acceptedEffect("clean", result.value, cwd)
|
|
360
|
+
: Effect.succeed(failed("scan", "invalid-report"));
|
|
361
|
+
case "no-report":
|
|
362
|
+
return Effect.succeed(failed("scan", "missing-report"));
|
|
363
|
+
case "cancelled":
|
|
364
|
+
return Effect.succeed(Object.freeze({ status: "cancelled", stage: "scan" }));
|
|
365
|
+
case "invalidated":
|
|
366
|
+
return Effect.succeed(lifecycleCancelled());
|
|
367
|
+
case "timed-out":
|
|
368
|
+
return Effect.succeed(
|
|
369
|
+
Object.freeze({ status: "timed-out", stage: "scan", timeoutMs: result.timeoutMs }),
|
|
370
|
+
);
|
|
371
|
+
case "failed":
|
|
372
|
+
return Effect.succeed(failedFromRun(result.reason, result.reportError));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function acceptedEffect(
|
|
377
|
+
outcome: "clean" | "findings",
|
|
378
|
+
report: JscpdScanReport,
|
|
379
|
+
cwd: string,
|
|
380
|
+
): Effect.Effect<JscpdBaselineState, never, JscpdFileSystem> {
|
|
381
|
+
return indexJscpdCloneReportEffect(report, cwd).pipe(
|
|
382
|
+
Effect.map((snapshot) => Object.freeze({ status: "accepted", outcome, report, snapshot })),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function failedFromRun(
|
|
387
|
+
reason: JscpdRunFailureReason,
|
|
388
|
+
reportError: JscpdReportErrorCode | undefined,
|
|
389
|
+
): JscpdBaselineState {
|
|
390
|
+
return reportError
|
|
391
|
+
? Object.freeze({ status: "failed", stage: "scan", reason, reportError })
|
|
392
|
+
: failed("scan", reason);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function failed(
|
|
396
|
+
stage: Extract<JscpdBaselineState, { status: "failed" }>["stage"],
|
|
397
|
+
reason: string,
|
|
398
|
+
): JscpdBaselineState {
|
|
399
|
+
return Object.freeze({ status: "failed", stage, reason });
|
|
400
|
+
}
|