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/automatic.ts
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { Cause, Context, Effect, Layer, MutableRef } from "effect";
|
|
2
|
+
import type { JscpdAcknowledgedFinding, JscpdAcknowledgementTracker } from "./acknowledgements.js";
|
|
3
|
+
import type { JscpdWorkflowRequirements as JscpdRuntimeRequirements } from "./effect/services.js";
|
|
4
|
+
import { JscpdPiPort } from "./effect/services.js";
|
|
5
|
+
import type { JscpdAutomaticScanDisposition } from "./scheduler.js";
|
|
6
|
+
import type { JscpdCommandExecutor, JscpdExecutionResult } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export const JSCPD_AUTOMATIC_MESSAGE_TYPE = "pi-jscpd/automatic-findings";
|
|
9
|
+
export const JSCPD_AUTOMATIC_STATUS_KEY = "pi-jscpd";
|
|
10
|
+
const MAX_AUTOMATIC_FINDINGS = 5;
|
|
11
|
+
|
|
12
|
+
export function boundedJscpdAutomaticFindingLimit(configuredLimit: number): number {
|
|
13
|
+
if (!Number.isSafeInteger(configuredLimit) || configuredLimit < 1) return MAX_AUTOMATIC_FINDINGS;
|
|
14
|
+
return Math.min(configuredLimit, MAX_AUTOMATIC_FINDINGS);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface JscpdAutomaticCheck {
|
|
18
|
+
readonly runEffect: (
|
|
19
|
+
context: JscpdAutomaticCheckEffectContext<JscpdRuntimeRequirements, unknown>,
|
|
20
|
+
) => Effect.Effect<JscpdAutomaticScanDisposition, never, JscpdRuntimeRequirements>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface JscpdAutomaticCheckOptions {
|
|
24
|
+
/** Internal result sink used when a run does not supply its lifecycle-bound sink. */
|
|
25
|
+
readonly onResult?: JscpdAutomaticResultEffectHandler<never, unknown>;
|
|
26
|
+
readonly beforeRun?: Effect.Effect<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface JscpdAutomaticCheckEffectScope {
|
|
30
|
+
readonly cwd: string;
|
|
31
|
+
readonly signal: AbortSignal;
|
|
32
|
+
readonly isCurrent?: () => boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface JscpdAutomaticCheckEffectContext<R = never, E = never>
|
|
36
|
+
extends JscpdAutomaticCheckEffectScope {
|
|
37
|
+
readonly onResult?: JscpdAutomaticResultEffectHandler<R, E>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type JscpdAutomaticResultEffectHandler<R = never, E = never> = (
|
|
41
|
+
result: JscpdExecutionResult,
|
|
42
|
+
context: JscpdAutomaticCheckEffectScope,
|
|
43
|
+
) => Effect.Effect<JscpdAutomaticScanDisposition | undefined, E, R>;
|
|
44
|
+
|
|
45
|
+
interface JscpdAutomaticCheckEffectService {
|
|
46
|
+
readonly run: <R, E>(
|
|
47
|
+
context: JscpdAutomaticCheckEffectContext<R, E>,
|
|
48
|
+
) => Effect.Effect<JscpdAutomaticScanDisposition, never, R | JscpdRuntimeRequirements>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const JscpdAutomaticChecking = Context.GenericTag<JscpdAutomaticCheckEffectService>(
|
|
52
|
+
"pi-jscpd/effect/AutomaticChecking",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export interface JscpdAutomaticFindingDetails {
|
|
56
|
+
readonly source: "automatic";
|
|
57
|
+
readonly findings: number;
|
|
58
|
+
readonly omittedFindings: number;
|
|
59
|
+
readonly ambiguousFindings: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface JscpdAutomaticResultActions {
|
|
63
|
+
readonly isCurrent: () => boolean;
|
|
64
|
+
readonly isIdle: () => boolean;
|
|
65
|
+
readonly hasPendingMessages: () => boolean;
|
|
66
|
+
readonly acknowledgements: JscpdAutomaticAcknowledgementTransaction;
|
|
67
|
+
readonly sendFinding: (content: string, details: JscpdAutomaticFindingDetails) => void;
|
|
68
|
+
readonly record: (result: JscpdExecutionResult) => void;
|
|
69
|
+
readonly persist: () => void;
|
|
70
|
+
readonly setStatus?: (text: string) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface JscpdAutomaticResultEffectActions<R = never> {
|
|
74
|
+
readonly isCurrent: Effect.Effect<boolean, never, R>;
|
|
75
|
+
readonly isIdle: Effect.Effect<boolean, never, R>;
|
|
76
|
+
readonly hasPendingMessages: Effect.Effect<boolean, never, R>;
|
|
77
|
+
readonly acknowledgements: JscpdAutomaticAcknowledgementEffectTransaction;
|
|
78
|
+
readonly sendFinding: (
|
|
79
|
+
content: string,
|
|
80
|
+
details: JscpdAutomaticFindingDetails,
|
|
81
|
+
) => Effect.Effect<void, unknown, R>;
|
|
82
|
+
readonly record: (result: JscpdExecutionResult) => Effect.Effect<void, unknown, R>;
|
|
83
|
+
readonly persist: Effect.Effect<void, unknown, R>;
|
|
84
|
+
readonly setStatus?: (text: string) => Effect.Effect<void, unknown, R>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface JscpdAutomaticPiResultActions<R = never>
|
|
88
|
+
extends Omit<JscpdAutomaticResultEffectActions<R>, "sendFinding" | "setStatus"> {
|
|
89
|
+
readonly hasUI: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface JscpdAutomaticAcknowledgementTransaction {
|
|
93
|
+
readonly tracker: JscpdAcknowledgementTracker;
|
|
94
|
+
readonly effects: JscpdAutomaticAcknowledgementEffectTransaction;
|
|
95
|
+
discard(): void;
|
|
96
|
+
ready(): boolean;
|
|
97
|
+
commit(): boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface JscpdAutomaticAcknowledgementEffectTransaction {
|
|
101
|
+
readonly discard: Effect.Effect<void>;
|
|
102
|
+
readonly ready: Effect.Effect<boolean>;
|
|
103
|
+
readonly commit: Effect.Effect<boolean>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface StagedAcknowledgements {
|
|
107
|
+
readonly expectedRevision: number;
|
|
108
|
+
readonly active: readonly JscpdAcknowledgedFinding[];
|
|
109
|
+
readonly surfaced: readonly JscpdAcknowledgedFinding[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Stage automatic acknowledgement changes until finding delivery succeeds. */
|
|
113
|
+
export function createJscpdAutomaticAcknowledgementTransaction(
|
|
114
|
+
source: JscpdAcknowledgementTracker,
|
|
115
|
+
): JscpdAutomaticAcknowledgementTransaction {
|
|
116
|
+
const staged = MutableRef.make<StagedAcknowledgements | undefined>(undefined);
|
|
117
|
+
const tracker: JscpdAcknowledgementTracker = {
|
|
118
|
+
restore: () => undefined,
|
|
119
|
+
reset: () => undefined,
|
|
120
|
+
scope: () => source.scope(),
|
|
121
|
+
revision: () => source.revision(),
|
|
122
|
+
findings: () => source.findings(),
|
|
123
|
+
has: (fingerprint) => source.has(fingerprint),
|
|
124
|
+
invalidatePaths: () => false,
|
|
125
|
+
reconcile(expectedRevision, active, surfaced) {
|
|
126
|
+
MutableRef.set(
|
|
127
|
+
staged,
|
|
128
|
+
Object.freeze({
|
|
129
|
+
expectedRevision,
|
|
130
|
+
active: Object.freeze([...active]),
|
|
131
|
+
surfaced: Object.freeze([...surfaced]),
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
return false;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
const discard = () => MutableRef.set(staged, undefined);
|
|
138
|
+
const ready = () => {
|
|
139
|
+
const pending = MutableRef.get(staged);
|
|
140
|
+
return !!pending && source.revision() === pending.expectedRevision;
|
|
141
|
+
};
|
|
142
|
+
const commit = () => {
|
|
143
|
+
const pending = MutableRef.getAndSet(staged, undefined);
|
|
144
|
+
return pending
|
|
145
|
+
? source.reconcile(pending.expectedRevision, pending.active, pending.surfaced)
|
|
146
|
+
: false;
|
|
147
|
+
};
|
|
148
|
+
const effects = Object.freeze({
|
|
149
|
+
discard: Effect.sync(discard),
|
|
150
|
+
ready: Effect.sync(ready),
|
|
151
|
+
commit: Effect.sync(commit),
|
|
152
|
+
});
|
|
153
|
+
return Object.freeze({ tracker: Object.freeze(tracker), effects, discard, ready, commit });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Deliver only current actionable findings; clean and failed checks remain outside model context. */
|
|
157
|
+
export function handleJscpdAutomaticResultEffect<R>(
|
|
158
|
+
result: JscpdExecutionResult,
|
|
159
|
+
actions: JscpdAutomaticResultEffectActions<R>,
|
|
160
|
+
): Effect.Effect<JscpdAutomaticScanDisposition, never, R> {
|
|
161
|
+
return Effect.gen(function* () {
|
|
162
|
+
if (!(yield* automaticDeliveryEligibleEffect(result, actions))) return "deferred";
|
|
163
|
+
|
|
164
|
+
if (isActionableChangedResult(result)) {
|
|
165
|
+
const delivered = yield* completeAdvisoryEffect(
|
|
166
|
+
actions.sendFinding(result.message, automaticFindingDetails(result)),
|
|
167
|
+
);
|
|
168
|
+
if (!delivered) return "deferred";
|
|
169
|
+
yield* actions.acknowledgements.commit;
|
|
170
|
+
} else if (isScannedChangedResult(result)) {
|
|
171
|
+
yield* actions.acknowledgements.commit;
|
|
172
|
+
} else {
|
|
173
|
+
yield* actions.acknowledgements.discard;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
yield* ignoreAdvisoryFailure(actions.record(result).pipe(Effect.zipRight(actions.persist)));
|
|
177
|
+
const status = compactJscpdAutomaticStatus(result);
|
|
178
|
+
if (status && actions.setStatus) {
|
|
179
|
+
yield* ignoreAdvisoryFailure(actions.setStatus(status));
|
|
180
|
+
}
|
|
181
|
+
return "attempted";
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function automaticDeliveryEligibleEffect<R>(
|
|
186
|
+
result: JscpdExecutionResult,
|
|
187
|
+
actions: JscpdAutomaticResultEffectActions<R>,
|
|
188
|
+
): Effect.Effect<boolean, never, R> {
|
|
189
|
+
return Effect.gen(function* () {
|
|
190
|
+
if (!(yield* actions.isCurrent)) return false;
|
|
191
|
+
if (!(yield* actions.isIdle)) return false;
|
|
192
|
+
if (yield* actions.hasPendingMessages) return false;
|
|
193
|
+
return !isScannedChangedResult(result) || (yield* actions.acknowledgements.ready);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Effect-native Pi delivery keeps the durable message quiet and acknowledges only after success. */
|
|
198
|
+
export function handleJscpdAutomaticPiResultEffect<R>(
|
|
199
|
+
result: JscpdExecutionResult,
|
|
200
|
+
actions: JscpdAutomaticPiResultActions<R>,
|
|
201
|
+
): Effect.Effect<JscpdAutomaticScanDisposition, never, R | JscpdPiPort> {
|
|
202
|
+
return Effect.flatMap(JscpdPiPort, (pi) =>
|
|
203
|
+
handleJscpdAutomaticResultEffect(result, {
|
|
204
|
+
...actions,
|
|
205
|
+
sendFinding: (content, details) =>
|
|
206
|
+
pi.sendMessage(
|
|
207
|
+
{
|
|
208
|
+
customType: JSCPD_AUTOMATIC_MESSAGE_TYPE,
|
|
209
|
+
content,
|
|
210
|
+
display: actions.hasUI,
|
|
211
|
+
details,
|
|
212
|
+
},
|
|
213
|
+
false,
|
|
214
|
+
),
|
|
215
|
+
setStatus: actions.hasUI
|
|
216
|
+
? (text) => pi.setStatus(JSCPD_AUTOMATIC_STATUS_KEY, text)
|
|
217
|
+
: undefined,
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function compactJscpdAutomaticStatus(result: JscpdExecutionResult): string | undefined {
|
|
223
|
+
if (result.status === "changed") {
|
|
224
|
+
if (!result.scanPerformed) return undefined;
|
|
225
|
+
const count = result.findings.length + result.omittedFindings;
|
|
226
|
+
return result.outcome === "clean"
|
|
227
|
+
? "jscpd: clean"
|
|
228
|
+
: `jscpd: ${count} new duplicate block${count === 1 ? "" : "s"}`;
|
|
229
|
+
}
|
|
230
|
+
if (result.status === "changed-unavailable") return "jscpd: check unavailable";
|
|
231
|
+
if (result.status === "unavailable") {
|
|
232
|
+
return result.reason === "disabled" ? "jscpd: disabled" : "jscpd: check unavailable";
|
|
233
|
+
}
|
|
234
|
+
if (result.status === "failed") {
|
|
235
|
+
return result.reason === "scan-timed-out" ? "jscpd: check timed out" : "jscpd: check failed";
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Execute one fail-open changed check without directly injecting anything into Pi context. */
|
|
241
|
+
export function createJscpdAutomaticCheck(
|
|
242
|
+
executor: JscpdCommandExecutor,
|
|
243
|
+
options: JscpdAutomaticCheckOptions = {},
|
|
244
|
+
): JscpdAutomaticCheck {
|
|
245
|
+
const service = createAutomaticCheckEffectService(executor, options);
|
|
246
|
+
return {
|
|
247
|
+
runEffect: (context) => service.run(context),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function createJscpdAutomaticCheckLayer(
|
|
252
|
+
executor: JscpdCommandExecutor,
|
|
253
|
+
options: JscpdAutomaticCheckOptions = {},
|
|
254
|
+
) {
|
|
255
|
+
return Layer.succeed(
|
|
256
|
+
JscpdAutomaticChecking,
|
|
257
|
+
createAutomaticCheckEffectService(executor, options),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function createAutomaticCheckEffectService(
|
|
262
|
+
executor: JscpdCommandExecutor,
|
|
263
|
+
options: JscpdAutomaticCheckOptions,
|
|
264
|
+
): JscpdAutomaticCheckEffectService {
|
|
265
|
+
return {
|
|
266
|
+
run: (context) =>
|
|
267
|
+
Effect.gen(function* () {
|
|
268
|
+
if (options.beforeRun) yield* options.beforeRun;
|
|
269
|
+
const result = yield* executeAutomaticChangedEffect(executor, context);
|
|
270
|
+
const disposition = automaticDisposition(result, context);
|
|
271
|
+
if (disposition === "deferred") return disposition;
|
|
272
|
+
const handler = context.onResult ?? options.onResult;
|
|
273
|
+
if (handler) {
|
|
274
|
+
const handled = yield* completeAutomaticResultHandler(
|
|
275
|
+
Effect.suspend(() => handler(result, context)),
|
|
276
|
+
);
|
|
277
|
+
if (handled === "deferred") return "deferred";
|
|
278
|
+
}
|
|
279
|
+
return isCurrent(context) ? "attempted" : "deferred";
|
|
280
|
+
}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function executeAutomaticChangedEffect<R, E>(
|
|
285
|
+
executor: JscpdCommandExecutor,
|
|
286
|
+
context: JscpdAutomaticCheckEffectContext<R, E>,
|
|
287
|
+
): Effect.Effect<JscpdExecutionResult, never, JscpdRuntimeRequirements> {
|
|
288
|
+
return Effect.suspend(() =>
|
|
289
|
+
executor.executeEffect(
|
|
290
|
+
{ command: "changed", args: [] },
|
|
291
|
+
{ cwd: context.cwd, signal: context.signal },
|
|
292
|
+
),
|
|
293
|
+
).pipe(
|
|
294
|
+
Effect.catchAllCause((cause) =>
|
|
295
|
+
Cause.isInterruptedOnly(cause) ? Effect.failCause(cause) : Effect.succeed(automaticFailure()),
|
|
296
|
+
),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function completeAutomaticResultHandler<R, E>(
|
|
301
|
+
effect: Effect.Effect<JscpdAutomaticScanDisposition | undefined, E, R>,
|
|
302
|
+
): Effect.Effect<JscpdAutomaticScanDisposition | undefined, never, R> {
|
|
303
|
+
return effect.pipe(
|
|
304
|
+
Effect.catchAllCause((cause) =>
|
|
305
|
+
Cause.isInterruptedOnly(cause) ? Effect.interrupt : Effect.succeed("deferred" as const),
|
|
306
|
+
),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function createJscpdAutomaticResultEffectActions(
|
|
311
|
+
actions: JscpdAutomaticResultActions,
|
|
312
|
+
): JscpdAutomaticResultEffectActions {
|
|
313
|
+
return {
|
|
314
|
+
isCurrent: Effect.sync(actions.isCurrent),
|
|
315
|
+
isIdle: Effect.sync(actions.isIdle),
|
|
316
|
+
hasPendingMessages: Effect.sync(actions.hasPendingMessages),
|
|
317
|
+
acknowledgements: actions.acknowledgements.effects,
|
|
318
|
+
sendFinding: (content, details) => Effect.sync(() => actions.sendFinding(content, details)),
|
|
319
|
+
record: (result) => Effect.sync(() => actions.record(result)),
|
|
320
|
+
persist: Effect.sync(actions.persist),
|
|
321
|
+
setStatus: actions.setStatus
|
|
322
|
+
? (text) => Effect.sync(() => actions.setStatus?.(text))
|
|
323
|
+
: undefined,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function automaticFindingDetails(
|
|
328
|
+
result: Extract<JscpdExecutionResult, { status: "changed" }>,
|
|
329
|
+
): JscpdAutomaticFindingDetails {
|
|
330
|
+
return Object.freeze({
|
|
331
|
+
source: "automatic",
|
|
332
|
+
findings: result.findings.length,
|
|
333
|
+
omittedFindings: result.omittedFindings,
|
|
334
|
+
ambiguousFindings: result.ambiguousFindings,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function completeAdvisoryEffect<R>(
|
|
339
|
+
effect: Effect.Effect<void, unknown, R>,
|
|
340
|
+
): Effect.Effect<boolean, never, R> {
|
|
341
|
+
return effect.pipe(
|
|
342
|
+
Effect.as(true),
|
|
343
|
+
Effect.catchAllCause((cause) =>
|
|
344
|
+
Cause.isInterruptedOnly(cause) ? Effect.interrupt : Effect.succeed(false),
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function ignoreAdvisoryFailure<R>(
|
|
350
|
+
effect: Effect.Effect<void, unknown, R>,
|
|
351
|
+
): Effect.Effect<void, never, R> {
|
|
352
|
+
return effect.pipe(
|
|
353
|
+
Effect.catchAllCause((cause) =>
|
|
354
|
+
Cause.isInterruptedOnly(cause) ? Effect.interrupt : Effect.void,
|
|
355
|
+
),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isScannedChangedResult(
|
|
360
|
+
result: JscpdExecutionResult,
|
|
361
|
+
): result is Extract<JscpdExecutionResult, { status: "changed" }> {
|
|
362
|
+
return result.status === "changed" && result.scanPerformed;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function isActionableChangedResult(
|
|
366
|
+
result: JscpdExecutionResult,
|
|
367
|
+
): result is Extract<JscpdExecutionResult, { status: "changed" }> {
|
|
368
|
+
return result.status === "changed" && result.outcome === "findings" && result.findings.length > 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function automaticDisposition(
|
|
372
|
+
result: JscpdExecutionResult,
|
|
373
|
+
context: Pick<JscpdAutomaticCheckEffectContext<unknown, unknown>, "signal" | "isCurrent">,
|
|
374
|
+
): JscpdAutomaticScanDisposition {
|
|
375
|
+
if (!isCurrent(context)) return "deferred";
|
|
376
|
+
if (result.status === "changed-unavailable" && result.reason === "baseline-pending") {
|
|
377
|
+
return "deferred";
|
|
378
|
+
}
|
|
379
|
+
if (result.status === "failed" && result.reason === "scan-cancelled") return "deferred";
|
|
380
|
+
if (result.status === "unavailable" && result.reason === "probe-cancelled") return "deferred";
|
|
381
|
+
return "attempted";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function isCurrent(
|
|
385
|
+
context: Pick<JscpdAutomaticCheckEffectContext<unknown, unknown>, "signal" | "isCurrent">,
|
|
386
|
+
): boolean {
|
|
387
|
+
return !context.signal.aborted && (context.isCurrent?.() ?? true);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function automaticFailure(): JscpdExecutionResult {
|
|
391
|
+
return Object.freeze({
|
|
392
|
+
status: "failed",
|
|
393
|
+
reason: "process-failed",
|
|
394
|
+
message: "The automatic jscpd check failed safely; no result was used.",
|
|
395
|
+
});
|
|
396
|
+
}
|