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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jscpd
|
|
3
|
+
description: Pi-native polyglot duplicate-code analysis powered by jscpd v5. Use when asked to find duplicate blocks, check whether current-session edits duplicate existing code, scan specific project paths for duplication, interpret jscpd findings, or verify duplication after a refactor.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Requires the pi-jscpd extension and its jscpd_run tool.
|
|
6
|
+
metadata:
|
|
7
|
+
author: Revaz Zakalashvili
|
|
8
|
+
version: 1.0.0
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# jscpd duplication guardrail
|
|
12
|
+
|
|
13
|
+
Use `pi-jscpd` for deterministic duplicate-code analysis across jscpd-supported
|
|
14
|
+
languages and embedded formats. jscpd remains authoritative for tokenization,
|
|
15
|
+
clone detection, configuration, supported formats, and statistics.
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
- Check whether code changed in the current Pi session duplicates existing code.
|
|
20
|
+
- Scan the project or specific in-project files/directories for duplicate blocks.
|
|
21
|
+
- Inspect both sides of a reported match before a refactor.
|
|
22
|
+
- Verify whether a previously reported duplicate was removed, remains, or was
|
|
23
|
+
replaced by another duplicate.
|
|
24
|
+
- Explain analyzer readiness, configuration source, or the previous check state.
|
|
25
|
+
|
|
26
|
+
Do not use this skill as a general dead-code, dependency, complexity, lint, or
|
|
27
|
+
security analyzer. Do not treat every duplicate as a defect.
|
|
28
|
+
|
|
29
|
+
## Preferred interface
|
|
30
|
+
|
|
31
|
+
Prefer the `jscpd_run` tool when it is available. Do not invoke `npx`, install an
|
|
32
|
+
analyzer, download anything, or bypass the extension's bounded process and
|
|
33
|
+
report handling.
|
|
34
|
+
|
|
35
|
+
| Intent | Tool call |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| Check tracked Pi edits against the session baseline | `{ "command": "changed" }` |
|
|
38
|
+
| Scan the current project | `{ "command": "scan" }` |
|
|
39
|
+
| Scan selected in-project paths | `{ "command": "scan", "args": ["src", "test"] }` |
|
|
40
|
+
| Check readiness and the last result | `{ "command": "status" }` |
|
|
41
|
+
| Show supported operations | `{ "command": "help" }` |
|
|
42
|
+
|
|
43
|
+
Use `changed` only when Pi's built-in write/edit tracking represents the relevant
|
|
44
|
+
changes. If files were modified manually, by shell commands, by another tool, or
|
|
45
|
+
before the session baseline, use `scan` with the narrowest useful target.
|
|
46
|
+
|
|
47
|
+
The interactive `/jscpd` command is for the human-facing overview and findings
|
|
48
|
+
navigator. Do not attempt to operate its TUI on the user's behalf.
|
|
49
|
+
|
|
50
|
+
## Finding workflow
|
|
51
|
+
|
|
52
|
+
1. Run the narrowest appropriate `changed` or `scan` operation.
|
|
53
|
+
2. Report both locations, line spans, line/token size, format, and whether a
|
|
54
|
+
location is `new in this session`, an `existing match`, or simply a current
|
|
55
|
+
project location.
|
|
56
|
+
3. Inspect both locations and surrounding behavior with ordinary read tools.
|
|
57
|
+
4. Decide whether the duplication represents behavior that should stay
|
|
58
|
+
synchronized or intentional separation across tests, generated boundaries,
|
|
59
|
+
protocols, or independently owned domains.
|
|
60
|
+
5. Propose changes through the normal agent workflow. Never refactor, delete, or
|
|
61
|
+
modify ignore/configuration policy merely because a duplicate exists.
|
|
62
|
+
6. After user-approved edits and relevant tests, rerun the same scan scope to
|
|
63
|
+
verify the result.
|
|
64
|
+
|
|
65
|
+
Treat omitted or ambiguous findings conservatively. A display limit does not
|
|
66
|
+
mean additional jscpd findings are clean, and an unavailable classification must
|
|
67
|
+
not be presented as newly introduced duplication.
|
|
68
|
+
|
|
69
|
+
## Configuration and coexistence
|
|
70
|
+
|
|
71
|
+
Respect the repository's normal jscpd configuration, including `.jscpd.json`,
|
|
72
|
+
package-level settings, ignore rules, thresholds, formats, and modes. For
|
|
73
|
+
intentional duplication, recommend a normal jscpd exclusion only after verifying
|
|
74
|
+
that retaining the duplicate is deliberate; never write configuration
|
|
75
|
+
automatically.
|
|
76
|
+
|
|
77
|
+
When status reports that Fallow duplication overlap moved automatic jscpd checks
|
|
78
|
+
to on-demand mode, avoid running both duplication analyzers for the same scope
|
|
79
|
+
unless the user explicitly requests comparison. Explicit jscpd scans remain
|
|
80
|
+
available for polyglot or jscpd-specific coverage.
|
|
81
|
+
|
|
82
|
+
## Safety rules
|
|
83
|
+
|
|
84
|
+
- Advisory only: findings do not authorize source changes.
|
|
85
|
+
- Never expose source fragments, raw analyzer output, temporary report paths,
|
|
86
|
+
environment values, or internal fingerprints.
|
|
87
|
+
- Never construct shell command strings from paths.
|
|
88
|
+
- Do not reinterpret or replace deterministic jscpd results with an LLM guess.
|
|
89
|
+
- If the analyzer is missing, incompatible, timed out, cancelled, or returns an
|
|
90
|
+
invalid report, explain the bounded failure and fail open.
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { Context, Effect, Layer, MutableRef } from "effect";
|
|
2
|
+
import { isSafeChangedFilePath } from "./changed-files.js";
|
|
3
|
+
import { compareText } from "./path-utils.js";
|
|
4
|
+
import { hasExactKeys } from "./value-utils.js";
|
|
5
|
+
|
|
6
|
+
const JSCPD_ACKNOWLEDGEMENT_IDENTITY_VERSION = 1;
|
|
7
|
+
export const MAX_ACKNOWLEDGED_FINDINGS = 1_000;
|
|
8
|
+
|
|
9
|
+
export interface JscpdAcknowledgedFinding {
|
|
10
|
+
/** Opaque issue-19 clone-group identity, scoped by identityVersion. */
|
|
11
|
+
readonly fingerprint: string;
|
|
12
|
+
readonly paths: readonly [string, string];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface JscpdPersistedAcknowledgements {
|
|
16
|
+
readonly identityVersion: typeof JSCPD_ACKNOWLEDGEMENT_IDENTITY_VERSION;
|
|
17
|
+
readonly findings: readonly JscpdAcknowledgedFinding[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface JscpdAcknowledgementTracker {
|
|
21
|
+
restore(value?: JscpdPersistedAcknowledgements): void;
|
|
22
|
+
reset(): void;
|
|
23
|
+
/** Lifecycle generation for active-branch isolation. */
|
|
24
|
+
scope(): number;
|
|
25
|
+
readonly scopeEffect?: Effect.Effect<number>;
|
|
26
|
+
revision(): number;
|
|
27
|
+
readonly revisionEffect?: Effect.Effect<number>;
|
|
28
|
+
findings(): readonly JscpdAcknowledgedFinding[];
|
|
29
|
+
readonly findingsEffect?: Effect.Effect<readonly JscpdAcknowledgedFinding[]>;
|
|
30
|
+
has(fingerprint: string): boolean;
|
|
31
|
+
/** Drop acknowledgements touched by a verified source mutation. */
|
|
32
|
+
invalidatePaths(paths: readonly string[]): boolean;
|
|
33
|
+
/** Retain only active findings and add findings actually surfaced by this check. */
|
|
34
|
+
reconcile(
|
|
35
|
+
expectedRevision: number,
|
|
36
|
+
active: readonly JscpdAcknowledgedFinding[],
|
|
37
|
+
surfaced: readonly JscpdAcknowledgedFinding[],
|
|
38
|
+
): boolean;
|
|
39
|
+
reconcileEffect?: (
|
|
40
|
+
expectedRevision: number,
|
|
41
|
+
active: readonly JscpdAcknowledgedFinding[],
|
|
42
|
+
surfaced: readonly JscpdAcknowledgedFinding[],
|
|
43
|
+
) => Effect.Effect<boolean>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface AcknowledgementState {
|
|
47
|
+
readonly scope: number;
|
|
48
|
+
readonly revision: number;
|
|
49
|
+
readonly findings: ReadonlyMap<string, JscpdAcknowledgedFinding>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface JscpdAcknowledgementEffectService {
|
|
53
|
+
readonly restore: (value?: JscpdPersistedAcknowledgements) => Effect.Effect<void>;
|
|
54
|
+
readonly reset: Effect.Effect<void>;
|
|
55
|
+
readonly scope: Effect.Effect<number>;
|
|
56
|
+
readonly revision: Effect.Effect<number>;
|
|
57
|
+
readonly findings: Effect.Effect<readonly JscpdAcknowledgedFinding[]>;
|
|
58
|
+
readonly has: (fingerprint: string) => Effect.Effect<boolean>;
|
|
59
|
+
readonly invalidatePaths: (paths: readonly string[]) => Effect.Effect<boolean>;
|
|
60
|
+
readonly reconcile: (
|
|
61
|
+
expectedRevision: number,
|
|
62
|
+
active: readonly JscpdAcknowledgedFinding[],
|
|
63
|
+
surfaced: readonly JscpdAcknowledgedFinding[],
|
|
64
|
+
) => Effect.Effect<boolean>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const JscpdAcknowledgements = Context.GenericTag<JscpdAcknowledgementEffectService>(
|
|
68
|
+
"pi-jscpd/effect/Acknowledgements",
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const EMPTY: JscpdPersistedAcknowledgements = Object.freeze({
|
|
72
|
+
identityVersion: JSCPD_ACKNOWLEDGEMENT_IDENTITY_VERSION,
|
|
73
|
+
findings: Object.freeze([]),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** Keep acknowledgement identities conservative, bounded, and owned by one active branch. */
|
|
77
|
+
export function createJscpdAcknowledgementTracker(): JscpdAcknowledgementTracker {
|
|
78
|
+
return acknowledgementTrackerFor(new AcknowledgementOwner());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createJscpdAcknowledgementLayer() {
|
|
82
|
+
const owner = new AcknowledgementOwner();
|
|
83
|
+
return Layer.succeed(JscpdAcknowledgements, acknowledgementEffectServiceFor(owner));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class AcknowledgementOwner {
|
|
87
|
+
readonly #state = MutableRef.make<AcknowledgementState>(initialAcknowledgementState());
|
|
88
|
+
|
|
89
|
+
restore(value: JscpdPersistedAcknowledgements = EMPTY): void {
|
|
90
|
+
const current = MutableRef.get(this.#state);
|
|
91
|
+
MutableRef.set(this.#state, {
|
|
92
|
+
scope: current.scope + 1,
|
|
93
|
+
revision: current.revision + 1,
|
|
94
|
+
findings: new Map(value.findings.map((finding) => [finding.fingerprint, finding])),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
reset(): void {
|
|
99
|
+
const current = MutableRef.get(this.#state);
|
|
100
|
+
MutableRef.set(this.#state, {
|
|
101
|
+
scope: current.scope + 1,
|
|
102
|
+
revision: current.revision + 1,
|
|
103
|
+
findings: new Map(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
scope(): number {
|
|
108
|
+
return MutableRef.get(this.#state).scope;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
revision(): number {
|
|
112
|
+
return MutableRef.get(this.#state).revision;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
findings(): readonly JscpdAcknowledgedFinding[] {
|
|
116
|
+
return Object.freeze([...MutableRef.get(this.#state).findings.values()].sort(compareFinding));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
has(fingerprint: string): boolean {
|
|
120
|
+
return MutableRef.get(this.#state).findings.has(fingerprint);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
invalidatePaths(paths: readonly string[]): boolean {
|
|
124
|
+
const current = MutableRef.get(this.#state);
|
|
125
|
+
const changed = new Set(paths);
|
|
126
|
+
const findings = new Map(
|
|
127
|
+
[...current.findings].filter(([, finding]) =>
|
|
128
|
+
finding.paths.every((path) => !changed.has(path)),
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
if (findings.size === current.findings.size) return false;
|
|
132
|
+
MutableRef.set(this.#state, { ...current, revision: current.revision + 1, findings });
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
reconcile(
|
|
137
|
+
expectedRevision: number,
|
|
138
|
+
active: readonly JscpdAcknowledgedFinding[],
|
|
139
|
+
surfaced: readonly JscpdAcknowledgedFinding[],
|
|
140
|
+
): boolean {
|
|
141
|
+
const current = MutableRef.get(this.#state);
|
|
142
|
+
if (current.revision !== expectedRevision) return false;
|
|
143
|
+
const findings = reconciledFindings(current.findings, active, surfaced);
|
|
144
|
+
MutableRef.set(this.#state, { ...current, revision: current.revision + 1, findings });
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function initialAcknowledgementState(): AcknowledgementState {
|
|
150
|
+
return { scope: 0, revision: 0, findings: new Map() };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function reconciledFindings(
|
|
154
|
+
retained: ReadonlyMap<string, JscpdAcknowledgedFinding>,
|
|
155
|
+
active: readonly JscpdAcknowledgedFinding[],
|
|
156
|
+
surfaced: readonly JscpdAcknowledgedFinding[],
|
|
157
|
+
): ReadonlyMap<string, JscpdAcknowledgedFinding> {
|
|
158
|
+
const surfacedFingerprints = new Set(
|
|
159
|
+
surfaced.slice(0, MAX_ACKNOWLEDGED_FINDINGS).map(({ fingerprint }) => fingerprint),
|
|
160
|
+
);
|
|
161
|
+
const wantedFingerprints = new Set([...surfacedFingerprints, ...retained.keys()]);
|
|
162
|
+
const activeByFingerprint = new Map(
|
|
163
|
+
active
|
|
164
|
+
.filter((finding) => wantedFingerprints.has(finding.fingerprint))
|
|
165
|
+
.map((finding) => [finding.fingerprint, finding]),
|
|
166
|
+
);
|
|
167
|
+
const next = new Map<string, JscpdAcknowledgedFinding>();
|
|
168
|
+
appendActiveFindings(next, surfacedFingerprints, activeByFingerprint);
|
|
169
|
+
appendActiveFindings(next, retained.keys(), activeByFingerprint);
|
|
170
|
+
return next;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function appendActiveFindings(
|
|
174
|
+
target: Map<string, JscpdAcknowledgedFinding>,
|
|
175
|
+
fingerprints: Iterable<string>,
|
|
176
|
+
active: ReadonlyMap<string, JscpdAcknowledgedFinding>,
|
|
177
|
+
): void {
|
|
178
|
+
for (const fingerprint of fingerprints) {
|
|
179
|
+
if (target.size >= MAX_ACKNOWLEDGED_FINDINGS) break;
|
|
180
|
+
const finding = active.get(fingerprint);
|
|
181
|
+
if (finding) target.set(fingerprint, finding);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function acknowledgementTrackerFor(owner: AcknowledgementOwner): JscpdAcknowledgementTracker {
|
|
186
|
+
return {
|
|
187
|
+
restore: (value) => owner.restore(value),
|
|
188
|
+
reset: () => owner.reset(),
|
|
189
|
+
scope: () => owner.scope(),
|
|
190
|
+
scopeEffect: Effect.sync(() => owner.scope()),
|
|
191
|
+
revision: () => owner.revision(),
|
|
192
|
+
revisionEffect: Effect.sync(() => owner.revision()),
|
|
193
|
+
findings: () => owner.findings(),
|
|
194
|
+
findingsEffect: Effect.sync(() => owner.findings()),
|
|
195
|
+
has: (fingerprint) => owner.has(fingerprint),
|
|
196
|
+
invalidatePaths: (paths) => owner.invalidatePaths(paths),
|
|
197
|
+
reconcile: (revision, active, surfaced) => owner.reconcile(revision, active, surfaced),
|
|
198
|
+
reconcileEffect: (revision, active, surfaced) =>
|
|
199
|
+
Effect.sync(() => owner.reconcile(revision, active, surfaced)),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function acknowledgementEffectServiceFor(
|
|
204
|
+
owner: AcknowledgementOwner,
|
|
205
|
+
): JscpdAcknowledgementEffectService {
|
|
206
|
+
return {
|
|
207
|
+
restore: (value) => Effect.sync(() => owner.restore(value)),
|
|
208
|
+
reset: Effect.sync(() => owner.reset()),
|
|
209
|
+
scope: Effect.sync(() => owner.scope()),
|
|
210
|
+
revision: Effect.sync(() => owner.revision()),
|
|
211
|
+
findings: Effect.sync(() => owner.findings()),
|
|
212
|
+
has: (fingerprint) => Effect.sync(() => owner.has(fingerprint)),
|
|
213
|
+
invalidatePaths: (paths) => Effect.sync(() => owner.invalidatePaths(paths)),
|
|
214
|
+
reconcile: (revision, active, surfaced) =>
|
|
215
|
+
Effect.sync(() => owner.reconcile(revision, active, surfaced)),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function emptyJscpdAcknowledgements(): JscpdPersistedAcknowledgements {
|
|
220
|
+
return EMPTY;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function snapshotJscpdAcknowledgements(
|
|
224
|
+
tracker: JscpdAcknowledgementTracker,
|
|
225
|
+
): JscpdPersistedAcknowledgements {
|
|
226
|
+
return Object.freeze({
|
|
227
|
+
identityVersion: JSCPD_ACKNOWLEDGEMENT_IDENTITY_VERSION,
|
|
228
|
+
findings: Object.freeze([...tracker.findings()]),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function parseJscpdAcknowledgements(
|
|
233
|
+
value: unknown,
|
|
234
|
+
): JscpdPersistedAcknowledgements | undefined {
|
|
235
|
+
if (!hasExactKeys(value, ["identityVersion", "findings"])) return undefined;
|
|
236
|
+
if (value.identityVersion !== JSCPD_ACKNOWLEDGEMENT_IDENTITY_VERSION) return undefined;
|
|
237
|
+
if (!Array.isArray(value.findings) || value.findings.length > MAX_ACKNOWLEDGED_FINDINGS) {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
const findings: JscpdAcknowledgedFinding[] = [];
|
|
241
|
+
const seen = new Set<string>();
|
|
242
|
+
for (const candidate of value.findings) {
|
|
243
|
+
const finding = parseFinding(candidate);
|
|
244
|
+
if (!finding || seen.has(finding.fingerprint)) return undefined;
|
|
245
|
+
seen.add(finding.fingerprint);
|
|
246
|
+
findings.push(finding);
|
|
247
|
+
}
|
|
248
|
+
return Object.freeze({
|
|
249
|
+
identityVersion: JSCPD_ACKNOWLEDGEMENT_IDENTITY_VERSION,
|
|
250
|
+
findings: Object.freeze(findings.sort(compareFinding)),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseFinding(value: unknown): JscpdAcknowledgedFinding | undefined {
|
|
255
|
+
if (!hasExactKeys(value, ["fingerprint", "paths"])) return undefined;
|
|
256
|
+
if (typeof value.fingerprint !== "string" || !/^[0-9a-f]{64}$/.test(value.fingerprint)) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
if (!Array.isArray(value.paths) || value.paths.length !== 2) return undefined;
|
|
260
|
+
const [first, second] = value.paths;
|
|
261
|
+
if (!isSafeChangedFilePath(first) || !isSafeChangedFilePath(second)) return undefined;
|
|
262
|
+
const paths = [first, second].sort(compareText) as [string, string];
|
|
263
|
+
return Object.freeze({ fingerprint: value.fingerprint, paths: Object.freeze(paths) });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function compareFinding(first: JscpdAcknowledgedFinding, second: JscpdAcknowledgedFinding): number {
|
|
267
|
+
return compareText(first.fingerprint, second.fingerprint);
|
|
268
|
+
}
|