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/changed.ts
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { Cause, Context, Effect, Layer } from "effect";
|
|
2
|
+
import type { JscpdAcknowledgedFinding, JscpdAcknowledgementTracker } from "./acknowledgements.js";
|
|
3
|
+
import type { JscpdBaselineService, JscpdBaselineState } from "./baseline.js";
|
|
4
|
+
import { createJscpdExecutionPath, type JscpdCapabilityService } from "./capability.js";
|
|
5
|
+
import type { JscpdChangedFileTracker } from "./changed-files.js";
|
|
6
|
+
import {
|
|
7
|
+
compareJscpdCloneSnapshots,
|
|
8
|
+
indexJscpdCloneReportEffect,
|
|
9
|
+
type JscpdIndexedCloneGroup,
|
|
10
|
+
} from "./clone-identity.js";
|
|
11
|
+
import { DEFAULT_JSCPD_CONFIG, type JscpdConfig } from "./config.js";
|
|
12
|
+
import type { JscpdFileSystem, JscpdProcess } from "./effect/services.js";
|
|
13
|
+
import type { JscpdRunResult, JscpdService } from "./jscpd.js";
|
|
14
|
+
import { consumeJscpdV5JsonReportEffect } from "./jscpd-report.js";
|
|
15
|
+
import { compareText, optionalCanonicalDirectoryEffect } from "./path-utils.js";
|
|
16
|
+
import { presentJscpdChanged } from "./presentation.js";
|
|
17
|
+
import {
|
|
18
|
+
adapterRunEffect,
|
|
19
|
+
capabilityProbeEffect,
|
|
20
|
+
capabilityUnavailableResult,
|
|
21
|
+
createJscpdScanArguments,
|
|
22
|
+
executionResult,
|
|
23
|
+
JSCPD_CLONE_POSITIVE_EXIT_CODES,
|
|
24
|
+
} from "./scan.js";
|
|
25
|
+
import type {
|
|
26
|
+
JscpdChangedUnavailableReason,
|
|
27
|
+
JscpdCommandExecutor,
|
|
28
|
+
JscpdExecutionResult,
|
|
29
|
+
JscpdScanReport,
|
|
30
|
+
} from "./types.js";
|
|
31
|
+
import type { JscpdVerificationService } from "./verification.js";
|
|
32
|
+
import {
|
|
33
|
+
compareAndRememberJscpdVerificationEffect,
|
|
34
|
+
jscpdVerificationScopeEffect,
|
|
35
|
+
withJscpdVerification,
|
|
36
|
+
} from "./verification.js";
|
|
37
|
+
|
|
38
|
+
export interface JscpdChangedExecutorOptions {
|
|
39
|
+
readonly path?: string;
|
|
40
|
+
readonly config?: () => JscpdConfig;
|
|
41
|
+
readonly stateChanged?: () => void;
|
|
42
|
+
/** Prefer the most actionable changed-file clone groups before applying the display cap. */
|
|
43
|
+
readonly prioritizeFindings?: boolean;
|
|
44
|
+
/** Ephemeral comparison state for explicit pre/post-refactor checks. */
|
|
45
|
+
readonly verification?: JscpdVerificationService;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface JscpdChangedWorkflowService {
|
|
49
|
+
readonly execute: (
|
|
50
|
+
context: Parameters<JscpdCommandExecutor["executeEffect"]>[1],
|
|
51
|
+
) => Effect.Effect<JscpdExecutionResult, never, JscpdFileSystem | JscpdProcess>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const JscpdChangedWorkflow = Context.GenericTag<JscpdChangedWorkflowService>(
|
|
55
|
+
"pi-jscpd/effect/ChangedWorkflow",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/** Run and compare one full-project report, then acknowledge only findings actually surfaced. */
|
|
59
|
+
export function createJscpdChangedExecutor(
|
|
60
|
+
capabilityService: JscpdCapabilityService,
|
|
61
|
+
service: JscpdService,
|
|
62
|
+
baselineService: JscpdBaselineService,
|
|
63
|
+
changedFiles: JscpdChangedFileTracker,
|
|
64
|
+
acknowledgements: JscpdAcknowledgementTracker,
|
|
65
|
+
options: JscpdChangedExecutorOptions = {},
|
|
66
|
+
): JscpdCommandExecutor {
|
|
67
|
+
const workflow = changedWorkflowFor(
|
|
68
|
+
capabilityService,
|
|
69
|
+
service,
|
|
70
|
+
baselineService,
|
|
71
|
+
changedFiles,
|
|
72
|
+
acknowledgements,
|
|
73
|
+
options,
|
|
74
|
+
);
|
|
75
|
+
return {
|
|
76
|
+
executeEffect: (_invocation, context) => workflow.execute(context),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createJscpdChangedWorkflowLayer(
|
|
81
|
+
capabilityService: JscpdCapabilityService,
|
|
82
|
+
service: JscpdService,
|
|
83
|
+
baselineService: JscpdBaselineService,
|
|
84
|
+
changedFiles: JscpdChangedFileTracker,
|
|
85
|
+
acknowledgements: JscpdAcknowledgementTracker,
|
|
86
|
+
options: JscpdChangedExecutorOptions = {},
|
|
87
|
+
) {
|
|
88
|
+
return Layer.succeed(
|
|
89
|
+
JscpdChangedWorkflow,
|
|
90
|
+
changedWorkflowFor(
|
|
91
|
+
capabilityService,
|
|
92
|
+
service,
|
|
93
|
+
baselineService,
|
|
94
|
+
changedFiles,
|
|
95
|
+
acknowledgements,
|
|
96
|
+
options,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function changedWorkflowFor(
|
|
102
|
+
capabilityService: JscpdCapabilityService,
|
|
103
|
+
service: JscpdService,
|
|
104
|
+
baselineService: JscpdBaselineService,
|
|
105
|
+
changedFiles: JscpdChangedFileTracker,
|
|
106
|
+
acknowledgements: JscpdAcknowledgementTracker,
|
|
107
|
+
options: JscpdChangedExecutorOptions,
|
|
108
|
+
): JscpdChangedWorkflowService {
|
|
109
|
+
const semaphore = Effect.unsafeMakeSemaphore(1);
|
|
110
|
+
return {
|
|
111
|
+
execute: (context) =>
|
|
112
|
+
Effect.gen(function* () {
|
|
113
|
+
const scope = yield* acknowledgementScopeEffect(acknowledgements);
|
|
114
|
+
const verificationScope = options.verification
|
|
115
|
+
? yield* jscpdVerificationScopeEffect(options.verification)
|
|
116
|
+
: undefined;
|
|
117
|
+
return yield* semaphore.withPermits(1)(
|
|
118
|
+
executeChangedEffect(
|
|
119
|
+
capabilityService,
|
|
120
|
+
service,
|
|
121
|
+
baselineService,
|
|
122
|
+
changedFiles,
|
|
123
|
+
acknowledgements,
|
|
124
|
+
context,
|
|
125
|
+
options,
|
|
126
|
+
scope,
|
|
127
|
+
verificationScope,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
}).pipe(
|
|
131
|
+
Effect.catchAllCause((cause) =>
|
|
132
|
+
Cause.isInterruptedOnly(cause)
|
|
133
|
+
? Effect.interrupt
|
|
134
|
+
: Effect.succeed(
|
|
135
|
+
changedUnavailable(
|
|
136
|
+
"baseline-failed",
|
|
137
|
+
"The changed-duplication check failed safely; no findings were acknowledged.",
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function executeChangedEffect(
|
|
146
|
+
capabilityService: JscpdCapabilityService,
|
|
147
|
+
service: JscpdService,
|
|
148
|
+
baselineService: JscpdBaselineService,
|
|
149
|
+
changedFiles: JscpdChangedFileTracker,
|
|
150
|
+
acknowledgements: JscpdAcknowledgementTracker,
|
|
151
|
+
context: Parameters<JscpdCommandExecutor["executeEffect"]>[1],
|
|
152
|
+
options: JscpdChangedExecutorOptions,
|
|
153
|
+
scope: number,
|
|
154
|
+
verificationScope: number | undefined,
|
|
155
|
+
): Effect.Effect<JscpdExecutionResult, never, JscpdFileSystem | JscpdProcess> {
|
|
156
|
+
return Effect.gen(function* () {
|
|
157
|
+
const prepared = yield* prepareChangedCheckEffect(
|
|
158
|
+
baselineService,
|
|
159
|
+
changedFiles,
|
|
160
|
+
acknowledgements,
|
|
161
|
+
options,
|
|
162
|
+
scope,
|
|
163
|
+
);
|
|
164
|
+
if (prepared.status === "result") return prepared.result;
|
|
165
|
+
const scanned = yield* scanCurrentChangesEffect(
|
|
166
|
+
capabilityService,
|
|
167
|
+
service,
|
|
168
|
+
acknowledgements,
|
|
169
|
+
context,
|
|
170
|
+
options,
|
|
171
|
+
prepared.config,
|
|
172
|
+
scope,
|
|
173
|
+
);
|
|
174
|
+
if (scanned.status === "result") return scanned.result;
|
|
175
|
+
return yield* compareCurrentChangesEffect(
|
|
176
|
+
scanned.report,
|
|
177
|
+
scanned.cwd,
|
|
178
|
+
prepared.baseline,
|
|
179
|
+
prepared.ownedPaths,
|
|
180
|
+
acknowledgements,
|
|
181
|
+
options,
|
|
182
|
+
prepared.config,
|
|
183
|
+
scope,
|
|
184
|
+
verificationScope,
|
|
185
|
+
context.overlayFindingLimit,
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface JscpdPreparedChangedCheck {
|
|
191
|
+
readonly status: "ready";
|
|
192
|
+
readonly config: JscpdConfig;
|
|
193
|
+
readonly ownedPaths: readonly string[];
|
|
194
|
+
readonly baseline: Extract<JscpdBaselineState, { status: "accepted" }>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface JscpdScannedChanges {
|
|
198
|
+
readonly status: "ready";
|
|
199
|
+
readonly cwd: string;
|
|
200
|
+
readonly report: JscpdScanReport;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface JscpdChangedResultStep {
|
|
204
|
+
readonly status: "result";
|
|
205
|
+
readonly result: JscpdExecutionResult;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function prepareChangedCheckEffect(
|
|
209
|
+
baselineService: JscpdBaselineService,
|
|
210
|
+
changedFiles: JscpdChangedFileTracker,
|
|
211
|
+
acknowledgements: JscpdAcknowledgementTracker,
|
|
212
|
+
options: JscpdChangedExecutorOptions,
|
|
213
|
+
scope: number,
|
|
214
|
+
): Effect.Effect<JscpdPreparedChangedCheck | JscpdChangedResultStep> {
|
|
215
|
+
return Effect.gen(function* () {
|
|
216
|
+
if ((yield* acknowledgementScopeEffect(acknowledgements)) !== scope) {
|
|
217
|
+
return resultStep(lifecycleChanged());
|
|
218
|
+
}
|
|
219
|
+
const config = options.config?.() ?? DEFAULT_JSCPD_CONFIG;
|
|
220
|
+
if (!config.enabled) return resultStep(disabledResult());
|
|
221
|
+
const ownedPaths = yield* changedFilesEffect(changedFiles);
|
|
222
|
+
if (ownedPaths.length === 0) return resultStep(noTrackedFilesResult());
|
|
223
|
+
const baseline = yield* safeBaselineEffect(baselineService);
|
|
224
|
+
if ((yield* acknowledgementScopeEffect(acknowledgements)) !== scope) {
|
|
225
|
+
return resultStep(lifecycleChanged());
|
|
226
|
+
}
|
|
227
|
+
if (baseline.status !== "accepted") return resultStep(unavailableBaseline(baseline));
|
|
228
|
+
if (baseline.snapshot.status !== "accepted") {
|
|
229
|
+
return resultStep(
|
|
230
|
+
changedUnavailable(
|
|
231
|
+
"baseline-partial",
|
|
232
|
+
"jscpd changed is unavailable because the accepted baseline has incomplete clone identities; no findings were acknowledged.",
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return Object.freeze({ status: "ready", config, ownedPaths, baseline });
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function scanCurrentChangesEffect(
|
|
241
|
+
capabilityService: JscpdCapabilityService,
|
|
242
|
+
service: JscpdService,
|
|
243
|
+
acknowledgements: JscpdAcknowledgementTracker,
|
|
244
|
+
context: Parameters<JscpdCommandExecutor["executeEffect"]>[1],
|
|
245
|
+
options: JscpdChangedExecutorOptions,
|
|
246
|
+
config: JscpdConfig,
|
|
247
|
+
scope: number,
|
|
248
|
+
): Effect.Effect<
|
|
249
|
+
JscpdScannedChanges | JscpdChangedResultStep,
|
|
250
|
+
never,
|
|
251
|
+
JscpdFileSystem | JscpdProcess
|
|
252
|
+
> {
|
|
253
|
+
return Effect.gen(function* () {
|
|
254
|
+
const cwd = yield* optionalCanonicalDirectoryEffect(context.cwd);
|
|
255
|
+
if (!cwd) return resultStep(unsupportedPathResult());
|
|
256
|
+
const capability = yield* capabilityProbeEffect(capabilityService, {
|
|
257
|
+
cwd,
|
|
258
|
+
path: options.path,
|
|
259
|
+
signal: context.signal,
|
|
260
|
+
});
|
|
261
|
+
if ((yield* acknowledgementScopeEffect(acknowledgements)) !== scope) {
|
|
262
|
+
return resultStep(lifecycleChanged());
|
|
263
|
+
}
|
|
264
|
+
if (capability.status !== "available") {
|
|
265
|
+
return resultStep(capabilityUnavailableResult(capability));
|
|
266
|
+
}
|
|
267
|
+
const run = yield* adapterRunEffect(service, {
|
|
268
|
+
executable: capability.executable,
|
|
269
|
+
cwd,
|
|
270
|
+
path: createJscpdExecutionPath(cwd, options.path, capability.source),
|
|
271
|
+
signal: context.signal,
|
|
272
|
+
timeoutMs: options.config ? config.timeoutMs : undefined,
|
|
273
|
+
reportExitCodes: JSCPD_CLONE_POSITIVE_EXIT_CODES,
|
|
274
|
+
createArguments: ({ directory }) => createJscpdScanArguments(directory, ["."]),
|
|
275
|
+
consumeReportEffect: (bytes) => consumeJscpdV5JsonReportEffect(bytes, cwd),
|
|
276
|
+
});
|
|
277
|
+
if ((yield* acknowledgementScopeEffect(acknowledgements)) !== scope) {
|
|
278
|
+
return resultStep(lifecycleChanged());
|
|
279
|
+
}
|
|
280
|
+
const report = reportFromRun(run);
|
|
281
|
+
return report
|
|
282
|
+
? Object.freeze({ status: "ready", cwd, report })
|
|
283
|
+
: resultStep(executionResult(run, config.maxFindings));
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function compareCurrentChangesEffect(
|
|
288
|
+
report: JscpdScanReport,
|
|
289
|
+
cwd: string,
|
|
290
|
+
baseline: Extract<JscpdBaselineState, { status: "accepted" }>,
|
|
291
|
+
ownedPaths: readonly string[],
|
|
292
|
+
acknowledgements: JscpdAcknowledgementTracker,
|
|
293
|
+
options: JscpdChangedExecutorOptions,
|
|
294
|
+
config: JscpdConfig,
|
|
295
|
+
scope: number,
|
|
296
|
+
verificationScope: number | undefined,
|
|
297
|
+
overlayFindingLimit: number | undefined,
|
|
298
|
+
): Effect.Effect<JscpdExecutionResult, never, JscpdFileSystem> {
|
|
299
|
+
return Effect.gen(function* () {
|
|
300
|
+
const revision = yield* acknowledgementRevisionEffect(acknowledgements);
|
|
301
|
+
const current = yield* indexJscpdCloneReportEffect(report, cwd);
|
|
302
|
+
if ((yield* acknowledgementScopeEffect(acknowledgements)) !== scope) return lifecycleChanged();
|
|
303
|
+
if (current.status !== "accepted") {
|
|
304
|
+
return changedUnavailable(
|
|
305
|
+
"identity-partial",
|
|
306
|
+
"jscpd changed could not derive complete identities from the current scan; no findings were acknowledged.",
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
const comparison = compareJscpdCloneSnapshots(baseline.snapshot, current);
|
|
310
|
+
const changedPathSet = new Set(ownedPaths);
|
|
311
|
+
const acknowledged = new Set(
|
|
312
|
+
(yield* acknowledgementFindingsEffect(acknowledgements)).map(
|
|
313
|
+
({ fingerprint }) => fingerprint,
|
|
314
|
+
),
|
|
315
|
+
);
|
|
316
|
+
const { active, candidates } = selectChangedCandidates(
|
|
317
|
+
current.groups,
|
|
318
|
+
comparison.new,
|
|
319
|
+
changedPathSet,
|
|
320
|
+
acknowledged,
|
|
321
|
+
options.prioritizeFindings ?? false,
|
|
322
|
+
);
|
|
323
|
+
const presented = presentJscpdChanged(
|
|
324
|
+
candidates.map(({ group }) => group.clone),
|
|
325
|
+
changedPathSet,
|
|
326
|
+
config.maxFindings,
|
|
327
|
+
comparison.ambiguous.length,
|
|
328
|
+
overlayFindingLimit,
|
|
329
|
+
);
|
|
330
|
+
const surfaced = candidates
|
|
331
|
+
.slice(0, presented.findings.length)
|
|
332
|
+
.map(({ acknowledgement }) => acknowledgement);
|
|
333
|
+
if (yield* acknowledgementReconcileEffect(acknowledgements, revision, active, surfaced)) {
|
|
334
|
+
options.stateChanged?.();
|
|
335
|
+
}
|
|
336
|
+
if (!options.verification || verificationScope === undefined) return presented;
|
|
337
|
+
const verification = yield* compareAndRememberJscpdVerificationEffect(
|
|
338
|
+
options.verification,
|
|
339
|
+
"changed",
|
|
340
|
+
".",
|
|
341
|
+
current,
|
|
342
|
+
verificationScope,
|
|
343
|
+
);
|
|
344
|
+
return withJscpdVerification(presented, verification);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function acknowledgementScopeEffect(service: JscpdAcknowledgementTracker): Effect.Effect<number> {
|
|
349
|
+
return service.scopeEffect ?? Effect.sync(() => service.scope());
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function acknowledgementRevisionEffect(
|
|
353
|
+
service: JscpdAcknowledgementTracker,
|
|
354
|
+
): Effect.Effect<number> {
|
|
355
|
+
return service.revisionEffect ?? Effect.sync(() => service.revision());
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function acknowledgementFindingsEffect(
|
|
359
|
+
service: JscpdAcknowledgementTracker,
|
|
360
|
+
): Effect.Effect<readonly JscpdAcknowledgedFinding[]> {
|
|
361
|
+
return service.findingsEffect ?? Effect.sync(() => service.findings());
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function acknowledgementReconcileEffect(
|
|
365
|
+
service: JscpdAcknowledgementTracker,
|
|
366
|
+
expectedRevision: number,
|
|
367
|
+
active: readonly JscpdAcknowledgedFinding[],
|
|
368
|
+
surfaced: readonly JscpdAcknowledgedFinding[],
|
|
369
|
+
): Effect.Effect<boolean> {
|
|
370
|
+
return (
|
|
371
|
+
service.reconcileEffect?.(expectedRevision, active, surfaced) ??
|
|
372
|
+
Effect.sync(() => service.reconcile(expectedRevision, active, surfaced))
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function changedFilesEffect(service: JscpdChangedFileTracker): Effect.Effect<readonly string[]> {
|
|
377
|
+
return service.filesEffect;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function safeBaselineEffect(service: JscpdBaselineService): Effect.Effect<JscpdBaselineState> {
|
|
381
|
+
return service.waitEffect;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function resultStep(result: JscpdExecutionResult): JscpdChangedResultStep {
|
|
385
|
+
return Object.freeze({ status: "result", result });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function disabledResult(): JscpdExecutionResult {
|
|
389
|
+
return Object.freeze({
|
|
390
|
+
status: "unavailable",
|
|
391
|
+
reason: "disabled",
|
|
392
|
+
message: "jscpd scanning is disabled for this session. Run /jscpd on to re-enable it.",
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function noTrackedFilesResult(): JscpdExecutionResult {
|
|
397
|
+
const message = "jscpd changed: no session-owned changed files are tracked; no scan ran.";
|
|
398
|
+
return Object.freeze({
|
|
399
|
+
status: "changed",
|
|
400
|
+
outcome: "clean",
|
|
401
|
+
scanPerformed: false,
|
|
402
|
+
message,
|
|
403
|
+
terminalMessage: message,
|
|
404
|
+
findings: Object.freeze([]),
|
|
405
|
+
omittedFindings: 0,
|
|
406
|
+
ambiguousFindings: 0,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function unsupportedPathResult(): JscpdExecutionResult {
|
|
411
|
+
return Object.freeze({
|
|
412
|
+
status: "failed",
|
|
413
|
+
reason: "unsupported-path",
|
|
414
|
+
message: "jscpd changed requires an available project working directory; no scan ran.",
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function reportFromRun(result: JscpdRunResult<JscpdScanReport>): JscpdScanReport | undefined {
|
|
419
|
+
if (result.status === "report") return result.value;
|
|
420
|
+
if (result.status === "no-findings") return result.value;
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function unavailableBaseline(state: JscpdBaselineState): JscpdExecutionResult {
|
|
425
|
+
switch (state.status) {
|
|
426
|
+
case "unstarted":
|
|
427
|
+
case "pending":
|
|
428
|
+
return changedUnavailable(
|
|
429
|
+
"baseline-pending",
|
|
430
|
+
"jscpd changed has no settled pre-session baseline yet; no current findings were classified or acknowledged.",
|
|
431
|
+
);
|
|
432
|
+
case "unavailable":
|
|
433
|
+
return changedUnavailable(
|
|
434
|
+
"baseline-unavailable",
|
|
435
|
+
"jscpd changed has no available pre-session baseline; no current findings were classified or acknowledged.",
|
|
436
|
+
);
|
|
437
|
+
case "partial":
|
|
438
|
+
return changedUnavailable(
|
|
439
|
+
"baseline-partial",
|
|
440
|
+
"jscpd changed cannot classify restored changes without the ephemeral pre-session baseline; run a full /jscpd scan for current repository results.",
|
|
441
|
+
);
|
|
442
|
+
case "cancelled":
|
|
443
|
+
return changedUnavailable(
|
|
444
|
+
"baseline-cancelled",
|
|
445
|
+
"The pre-session baseline was cancelled; no current findings were classified or acknowledged.",
|
|
446
|
+
);
|
|
447
|
+
case "timed-out":
|
|
448
|
+
return changedUnavailable(
|
|
449
|
+
"baseline-timed-out",
|
|
450
|
+
"The pre-session baseline timed out; no current findings were classified or acknowledged.",
|
|
451
|
+
);
|
|
452
|
+
case "failed":
|
|
453
|
+
return changedUnavailable(
|
|
454
|
+
"baseline-failed",
|
|
455
|
+
"The pre-session baseline failed safely; no current findings were classified or acknowledged.",
|
|
456
|
+
);
|
|
457
|
+
case "accepted":
|
|
458
|
+
return changedUnavailable(
|
|
459
|
+
"baseline-failed",
|
|
460
|
+
"The pre-session baseline could not be used; no findings were classified or acknowledged.",
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
interface JscpdChangedCandidate {
|
|
466
|
+
readonly group: JscpdIndexedCloneGroup;
|
|
467
|
+
readonly acknowledgement: JscpdAcknowledgedFinding;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function selectChangedCandidates(
|
|
471
|
+
groups: readonly JscpdIndexedCloneGroup[],
|
|
472
|
+
newClones: readonly JscpdScanReport["clonePairs"][number][],
|
|
473
|
+
changedPaths: ReadonlySet<string>,
|
|
474
|
+
acknowledged: ReadonlySet<string>,
|
|
475
|
+
prioritize: boolean,
|
|
476
|
+
): {
|
|
477
|
+
readonly active: readonly JscpdAcknowledgedFinding[];
|
|
478
|
+
readonly candidates: readonly JscpdChangedCandidate[];
|
|
479
|
+
} {
|
|
480
|
+
const newGroups = new Set(newClones);
|
|
481
|
+
const active: JscpdAcknowledgedFinding[] = [];
|
|
482
|
+
const candidates: JscpdChangedCandidate[] = [];
|
|
483
|
+
for (const group of groups) {
|
|
484
|
+
if (!group.fingerprint) continue;
|
|
485
|
+
const acknowledgement = acknowledgedFinding(group.fingerprint, group.clone);
|
|
486
|
+
active.push(acknowledgement);
|
|
487
|
+
if (!newGroups.has(group.clone)) continue;
|
|
488
|
+
if (!group.clone.occurrences.some(({ path }) => changedPaths.has(path))) continue;
|
|
489
|
+
if (acknowledged.has(group.fingerprint)) continue;
|
|
490
|
+
candidates.push({ group, acknowledgement });
|
|
491
|
+
}
|
|
492
|
+
if (prioritize)
|
|
493
|
+
candidates.sort((left, right) => compareChangedCandidate(left, right, changedPaths));
|
|
494
|
+
return Object.freeze({ active: Object.freeze(active), candidates: Object.freeze(candidates) });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function compareChangedCandidate(
|
|
498
|
+
left: JscpdChangedCandidate,
|
|
499
|
+
right: JscpdChangedCandidate,
|
|
500
|
+
changedPaths: ReadonlySet<string>,
|
|
501
|
+
): number {
|
|
502
|
+
const changedDifference =
|
|
503
|
+
changedOccurrenceCount(right.group.clone, changedPaths) -
|
|
504
|
+
changedOccurrenceCount(left.group.clone, changedPaths);
|
|
505
|
+
if (changedDifference !== 0) return changedDifference;
|
|
506
|
+
const lineDifference = right.group.clone.lines - left.group.clone.lines;
|
|
507
|
+
if (lineDifference !== 0) return lineDifference;
|
|
508
|
+
const tokenDifference = right.group.clone.tokens - left.group.clone.tokens;
|
|
509
|
+
if (tokenDifference !== 0) return tokenDifference;
|
|
510
|
+
return compareText(cloneLocationKey(left.group.clone), cloneLocationKey(right.group.clone));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function changedOccurrenceCount(
|
|
514
|
+
clone: JscpdScanReport["clonePairs"][number],
|
|
515
|
+
changedPaths: ReadonlySet<string>,
|
|
516
|
+
): number {
|
|
517
|
+
return clone.occurrences.filter(({ path }) => changedPaths.has(path)).length;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function cloneLocationKey(clone: JscpdScanReport["clonePairs"][number]): string {
|
|
521
|
+
return clone.occurrences
|
|
522
|
+
.map(
|
|
523
|
+
({ path, start, end }) => `${path}:${start.line}:${start.column}:${end.line}:${end.column}`,
|
|
524
|
+
)
|
|
525
|
+
.join("\0");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function acknowledgedFinding(
|
|
529
|
+
fingerprint: string,
|
|
530
|
+
clone: JscpdScanReport["clonePairs"][number],
|
|
531
|
+
): JscpdAcknowledgedFinding {
|
|
532
|
+
const paths = clone.occurrences.map(({ path }) => path).sort(compareText) as [string, string];
|
|
533
|
+
return Object.freeze({ fingerprint, paths: Object.freeze(paths) });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function lifecycleChanged(): JscpdExecutionResult {
|
|
537
|
+
return changedUnavailable(
|
|
538
|
+
"baseline-cancelled",
|
|
539
|
+
"The changed-duplication check was cancelled by a session branch transition; no findings were acknowledged.",
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function changedUnavailable(
|
|
544
|
+
reason: JscpdChangedUnavailableReason,
|
|
545
|
+
message: string,
|
|
546
|
+
): JscpdExecutionResult {
|
|
547
|
+
return Object.freeze({ status: "changed-unavailable", reason, message });
|
|
548
|
+
}
|