@dailephd/my-dev-kit-lab 0.2.1 → 0.3.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 +36 -24
- package/dist/scripts/audits/runAudit.js +101 -0
- package/dist/scripts/security/validate.js +68 -29
- package/dist/src/audits/codeRot/codeRotDetectors.js +17 -0
- package/dist/src/audits/codeRot/detectors/architectureDriftDetector.js +271 -0
- package/dist/src/audits/codeRot/detectors/crossPlatformRotDetector.js +214 -0
- package/dist/src/audits/codeRot/detectors/deadCodeCandidateDetector.js +344 -0
- package/dist/src/audits/codeRot/detectors/dependencyEnvironmentRotDetector.js +244 -0
- package/dist/src/audits/codeRot/detectors/docsCodeMismatchDetector.js +219 -0
- package/dist/src/audits/codeRot/detectors/duplicateImplementationDetector.js +273 -0
- package/dist/src/audits/codeRot/detectors/packageReleaseRotDetector.js +325 -0
- package/dist/src/audits/codeRot/detectors/securityValidationAssumptionRotDetector.js +385 -0
- package/dist/src/audits/codeRot/detectors/staleCommandReferenceDetector.js +135 -0
- package/dist/src/audits/codeRot/detectors/testRotDetector.js +348 -0
- package/dist/src/audits/codeRot/utils/boundedRead.js +23 -0
- package/dist/src/audits/codeRot/utils/docClaimPatterns.js +62 -0
- package/dist/src/audits/codeRot/utils/docCommandReferences.js +64 -0
- package/dist/src/audits/codeRot/utils/filePatternUtils.js +47 -0
- package/dist/src/audits/codeRot/utils/issueFactories.js +38 -0
- package/dist/src/audits/codeRot/utils/textLines.js +12 -0
- package/dist/src/audits/core/auditConfig.js +125 -0
- package/dist/src/audits/core/auditExitCode.js +75 -0
- package/dist/src/audits/core/auditIssue.js +20 -0
- package/dist/src/audits/core/auditRegistry.js +40 -0
- package/dist/src/audits/core/auditRunner.js +71 -0
- package/dist/src/audits/core/auditTarget.js +19 -0
- package/dist/src/audits/core/auditTypes.js +30 -0
- package/dist/src/audits/core/projectInventory.js +308 -0
- package/dist/src/audits/core/sourceOfTruth.js +339 -0
- package/dist/src/audits/core/textLines.js +24 -0
- package/dist/src/audits/report/auditReportModel.js +241 -0
- package/dist/src/audits/report/auditReportPaths.js +13 -0
- package/dist/src/audits/report/renderAuditJsonReport.js +29 -0
- package/dist/src/audits/report/renderAuditTextReport.js +165 -0
- package/dist/src/audits/report/sanitizeAuditText.js +39 -0
- package/dist/src/audits/report/writeAuditReports.js +27 -0
- package/dist/src/core/resolveCommand.js +53 -2
- package/dist/src/securityValidation/attackScenarios/attackProfile.js +20 -0
- package/dist/src/securityValidation/attackScenarios/attackResult.js +37 -0
- package/dist/src/securityValidation/attackScenarios/attackRunner.js +153 -0
- package/dist/src/securityValidation/attackScenarios/attackScenario.js +21 -0
- package/dist/src/securityValidation/attackScenarios/boundedSourceScan.js +100 -0
- package/dist/src/securityValidation/attackScenarios/exploitEvidence.js +69 -0
- package/dist/src/securityValidation/attackScenarios/payloadCorpus.js +0 -0
- package/dist/src/securityValidation/attackScenarios/profiles/localToolProfile.js +10 -0
- package/dist/src/securityValidation/attackScenarios/profiles/nodeCliPackageProfile.js +20 -0
- package/dist/src/securityValidation/attackScenarios/profiles/npmPackageProfile.js +10 -0
- package/dist/src/securityValidation/attackScenarios/reportSchemaGuard.js +41 -0
- package/dist/src/securityValidation/attackScenarios/scenarios/configInjectionScenario.js +144 -0
- package/dist/src/securityValidation/attackScenarios/scenarios/networkAssumptionScenario.js +129 -0
- package/dist/src/securityValidation/attackScenarios/scenarios/outputBoundaryScenario.js +69 -0
- package/dist/src/securityValidation/attackScenarios/scenarios/packageBoundaryScenario.js +118 -0
- package/dist/src/securityValidation/attackScenarios/scenarios/pathTraversalScenario.js +119 -0
- package/dist/src/securityValidation/attackScenarios/scenarios/reportPoisoningScenario.js +176 -0
- package/dist/src/securityValidation/attackScenarios/scenarios/secretLeakageScenario.js +143 -0
- package/dist/src/securityValidation/attackScenarios/scenarios/subprocessInjectionScenario.js +117 -0
- package/dist/src/securityValidation/attackScenarios/scenarios/targetSandboxScenario.js +118 -0
- package/dist/src/securityValidation/attackScenarios/targetSnapshot.js +70 -0
- package/dist/src/securityValidation/packageChecks/forbiddenPackageContents.js +4 -1
- package/dist/src/securityValidation/report/renderSecurityReport.js +135 -13
- package/dist/src/securityValidation/types.js +7 -0
- package/dist/src/securityValidation/validate/cliOptions.js +182 -0
- package/dist/src/securityValidation/validate/runSecurityValidation.js +134 -35
- package/dist/src/securityValidation/validate/verdict.js +147 -0
- package/package.json +18 -17
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { currentClaimPatternFor, plannedClaimPatternFor, FULL_RELEASE_GATE_FOR_SCOPED_CHECKS_PATTERN, isNegatedNearby, } from "../utils/docClaimPatterns.js";
|
|
4
|
+
import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
|
|
5
|
+
import { splitLines } from "../utils/textLines.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// v0.3.0 Batch 3 — documentation/code mismatch detector.
|
|
8
|
+
//
|
|
9
|
+
// Deterministic, regex/structured-cue only (no LLM, no broad semantic
|
|
10
|
+
// interpretation, per Batch 3 scope). Publish/release claims and doc-vs-
|
|
11
|
+
// package version mismatches are deliberately owned by
|
|
12
|
+
// packageReleaseRotDetector.ts instead of here (both detectors' Batch 3
|
|
13
|
+
// spec sections list overlapping bullets for those two checks; this
|
|
14
|
+
// detector focuses on feature-implementation claims and the
|
|
15
|
+
// full-release-gate-for-scoped-checks wording pattern, so the same
|
|
16
|
+
// underlying signal is never reported twice by two different detectors).
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const DETECTOR_ID = "docs-code-mismatch";
|
|
19
|
+
const MAX_DOC_READ_BYTES = 200_000;
|
|
20
|
+
// Deliberately small and conservative -- only features with an unambiguous,
|
|
21
|
+
// directly-observable source-of-truth signal are checked. Audit's own
|
|
22
|
+
// implementation status is intentionally excluded to avoid a
|
|
23
|
+
// self-referential "is the audit tool auditing claims about itself"
|
|
24
|
+
// complication in this batch.
|
|
25
|
+
const FEATURE_CHECKS = [
|
|
26
|
+
{
|
|
27
|
+
featureId: "security-validation",
|
|
28
|
+
label: "security validation",
|
|
29
|
+
subjectPattern: "security validation",
|
|
30
|
+
isImplemented: (sot) => sot.security.hasSecurityValidationSourceDir && sot.security.hasSecurityValidateScript,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
featureId: "experiment-framework",
|
|
34
|
+
label: "experiment framework",
|
|
35
|
+
subjectPattern: "experiment (?:framework|plugin)",
|
|
36
|
+
isImplemented: (sot) => sot.experiment.hasExperimentsSourceDir && sot.experiment.hasExperimentListScript,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
export const DOCS_CODE_MISMATCH_DETECTOR = {
|
|
40
|
+
id: DETECTOR_ID,
|
|
41
|
+
auditType: "code-rot",
|
|
42
|
+
title: "Documentation/code mismatch",
|
|
43
|
+
description: "Detects deterministic mismatches between documentation claims and source-of-truth data: feature-implementation claims and full-release-gate-for-scoped-checks wording.",
|
|
44
|
+
supportedIncludeAreas: ["docs", "architecture", "cli", "package"],
|
|
45
|
+
shouldSkip: (ctx) => {
|
|
46
|
+
if (!ctx.config.include.includes("docs")) {
|
|
47
|
+
return { skip: true, reason: "--include does not select docs; this detector's checks are docs-based." };
|
|
48
|
+
}
|
|
49
|
+
return { skip: false };
|
|
50
|
+
},
|
|
51
|
+
run: (ctx) => {
|
|
52
|
+
const issues = [];
|
|
53
|
+
for (const docFile of ctx.inventory.docsFiles) {
|
|
54
|
+
const content = readBoundedText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes);
|
|
55
|
+
if (content === null)
|
|
56
|
+
continue;
|
|
57
|
+
const lines = splitLines(content);
|
|
58
|
+
for (let i = 0; i < lines.length; i++) {
|
|
59
|
+
const line = lines[i];
|
|
60
|
+
for (const feature of FEATURE_CHECKS) {
|
|
61
|
+
const currentMatch = currentClaimPatternFor(feature.subjectPattern).test(line);
|
|
62
|
+
const plannedMatch = plannedClaimPatternFor(feature.subjectPattern).test(line);
|
|
63
|
+
const implemented = feature.isImplemented(ctx.sourceOfTruth);
|
|
64
|
+
if (currentMatch && plannedMatch) {
|
|
65
|
+
// Same line claims both current and planned for the same
|
|
66
|
+
// subject -- genuinely ambiguous, reported as low-confidence
|
|
67
|
+
// info rather than skipped silently.
|
|
68
|
+
issues.push(makeAmbiguousIssue(docFile.relativePath, i + 1, line, feature.label));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (currentMatch && !implemented) {
|
|
72
|
+
issues.push(makeCurrentButMissingIssue(docFile.relativePath, i + 1, line, feature));
|
|
73
|
+
}
|
|
74
|
+
else if (plannedMatch && implemented) {
|
|
75
|
+
issues.push(makePlannedButImplementedIssue(docFile.relativePath, i + 1, line, feature));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (FULL_RELEASE_GATE_FOR_SCOPED_CHECKS_PATTERN.test(line) && !isNegatedNearby(line)) {
|
|
79
|
+
issues.push(makeFullReleaseGateIssue(docFile.relativePath, i + 1, line));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return deduplicateIssuesById(issues);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
function makeCurrentButMissingIssue(relativePath, lineNumber, excerpt, feature) {
|
|
87
|
+
return makeCodeRotIssue({
|
|
88
|
+
auditType: "code-rot",
|
|
89
|
+
detectorId: DETECTOR_ID,
|
|
90
|
+
idCues: [relativePath, "current-but-missing", feature.featureId],
|
|
91
|
+
title: `Docs claim "${feature.label}" is implemented/current, but source-of-truth shows it is missing`,
|
|
92
|
+
description: `${relativePath}:${lineNumber} claims "${feature.label}" is implemented/current, but the expected source directory/script is not present.`,
|
|
93
|
+
severity: "high",
|
|
94
|
+
confidence: "medium",
|
|
95
|
+
falsePositiveRisk: "medium",
|
|
96
|
+
category: "docs-feature-mismatch",
|
|
97
|
+
recommendedAction: `Either implement the missing "${feature.label}" pieces, or update ${relativePath} to describe it as planned.`,
|
|
98
|
+
suggestedFixStrategy: `Correct the "${feature.label}" claim in ${relativePath} to match its actual implementation state.`,
|
|
99
|
+
validationCommands: ["npm run audit -- --types code-rot --include docs"],
|
|
100
|
+
releaseBlocking: false,
|
|
101
|
+
implementationBlocking: false,
|
|
102
|
+
autoFixEligible: false,
|
|
103
|
+
evidence: [
|
|
104
|
+
{
|
|
105
|
+
kind: "file",
|
|
106
|
+
message: `Doc claims "${feature.label}" is current/implemented; source-of-truth signal indicates it is not.`,
|
|
107
|
+
filePath: relativePath,
|
|
108
|
+
line: lineNumber,
|
|
109
|
+
excerpt: excerpt.trim().slice(0, 200),
|
|
110
|
+
source: DETECTOR_ID,
|
|
111
|
+
confidence: "medium",
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
affectedFiles: [relativePath],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function makePlannedButImplementedIssue(relativePath, lineNumber, excerpt, feature) {
|
|
118
|
+
return makeCodeRotIssue({
|
|
119
|
+
auditType: "code-rot",
|
|
120
|
+
detectorId: DETECTOR_ID,
|
|
121
|
+
idCues: [relativePath, "planned-but-implemented", feature.featureId],
|
|
122
|
+
title: `Docs claim "${feature.label}" is planned/future, but it is already implemented`,
|
|
123
|
+
description: `${relativePath}:${lineNumber} describes "${feature.label}" as planned/future, but source-of-truth shows it is already present.`,
|
|
124
|
+
severity: "medium",
|
|
125
|
+
confidence: "medium",
|
|
126
|
+
falsePositiveRisk: "medium",
|
|
127
|
+
category: "docs-feature-mismatch",
|
|
128
|
+
recommendedAction: `Update ${relativePath} to describe "${feature.label}" as implemented/current.`,
|
|
129
|
+
suggestedFixStrategy: `Move the "${feature.label}" description out of the planned/roadmap section in ${relativePath}.`,
|
|
130
|
+
validationCommands: ["npm run audit -- --types code-rot --include docs"],
|
|
131
|
+
releaseBlocking: false,
|
|
132
|
+
implementationBlocking: false,
|
|
133
|
+
autoFixEligible: false,
|
|
134
|
+
evidence: [
|
|
135
|
+
{
|
|
136
|
+
kind: "file",
|
|
137
|
+
message: `Doc claims "${feature.label}" is planned/future; source-of-truth signal indicates it is already implemented.`,
|
|
138
|
+
filePath: relativePath,
|
|
139
|
+
line: lineNumber,
|
|
140
|
+
excerpt: excerpt.trim().slice(0, 200),
|
|
141
|
+
source: DETECTOR_ID,
|
|
142
|
+
confidence: "medium",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
affectedFiles: [relativePath],
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function makeAmbiguousIssue(relativePath, lineNumber, excerpt, featureLabel) {
|
|
149
|
+
return makeCodeRotIssue({
|
|
150
|
+
auditType: "code-rot",
|
|
151
|
+
detectorId: DETECTOR_ID,
|
|
152
|
+
idCues: [relativePath, "ambiguous", featureLabel, String(lineNumber)],
|
|
153
|
+
title: `Ambiguous implementation-status claim for "${featureLabel}"`,
|
|
154
|
+
description: `${relativePath}:${lineNumber} appears to describe "${featureLabel}" as both current/implemented and planned/future in the same line -- not automatically resolvable.`,
|
|
155
|
+
severity: "info",
|
|
156
|
+
confidence: "low",
|
|
157
|
+
falsePositiveRisk: "high",
|
|
158
|
+
category: "docs-feature-mismatch-ambiguous",
|
|
159
|
+
recommendedAction: "Manually review this line for clarity.",
|
|
160
|
+
suggestedFixStrategy: "Rewrite the sentence so the implementation status of the feature is unambiguous.",
|
|
161
|
+
validationCommands: ["npm run audit -- --types code-rot --include docs"],
|
|
162
|
+
releaseBlocking: false,
|
|
163
|
+
implementationBlocking: false,
|
|
164
|
+
autoFixEligible: false,
|
|
165
|
+
evidence: [
|
|
166
|
+
{
|
|
167
|
+
kind: "file",
|
|
168
|
+
message: "Both a current/implemented cue and a planned/future cue matched the same line for the same subject.",
|
|
169
|
+
filePath: relativePath,
|
|
170
|
+
line: lineNumber,
|
|
171
|
+
excerpt: excerpt.trim().slice(0, 200),
|
|
172
|
+
source: DETECTOR_ID,
|
|
173
|
+
confidence: "low",
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
affectedFiles: [relativePath],
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function makeFullReleaseGateIssue(relativePath, lineNumber, excerpt) {
|
|
180
|
+
return makeCodeRotIssue({
|
|
181
|
+
auditType: "code-rot",
|
|
182
|
+
detectorId: DETECTOR_ID,
|
|
183
|
+
idCues: [relativePath, "full-release-gate-scoped-claim", String(lineNumber)],
|
|
184
|
+
title: `Docs describe a scoped run as a full release gate`,
|
|
185
|
+
description: `${relativePath}:${lineNumber} appears to describe a scoped (non-full) run as if it were a complete release gate.`,
|
|
186
|
+
severity: "medium",
|
|
187
|
+
confidence: "medium",
|
|
188
|
+
falsePositiveRisk: "medium",
|
|
189
|
+
category: "docs-feature-mismatch",
|
|
190
|
+
recommendedAction: `Clarify in ${relativePath} that a scoped run is not a full release gate.`,
|
|
191
|
+
suggestedFixStrategy: `Add explicit "scoped run, not a full release gate" wording near this claim in ${relativePath}.`,
|
|
192
|
+
validationCommands: ["npm run audit -- --types code-rot --include docs"],
|
|
193
|
+
releaseBlocking: false,
|
|
194
|
+
implementationBlocking: false,
|
|
195
|
+
autoFixEligible: false,
|
|
196
|
+
evidence: [
|
|
197
|
+
{
|
|
198
|
+
kind: "file",
|
|
199
|
+
message: "Matched a scoped-run-described-as-full-release-gate phrasing pattern.",
|
|
200
|
+
filePath: relativePath,
|
|
201
|
+
line: lineNumber,
|
|
202
|
+
excerpt: excerpt.trim().slice(0, 200),
|
|
203
|
+
source: DETECTOR_ID,
|
|
204
|
+
confidence: "medium",
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
affectedFiles: [relativePath],
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function readBoundedText(root, relativePath, sizeBytes) {
|
|
211
|
+
if (sizeBytes > MAX_DOC_READ_BYTES)
|
|
212
|
+
return null;
|
|
213
|
+
try {
|
|
214
|
+
return fs.readFileSync(path.join(root, relativePath), "utf8");
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { readBoundedFileText } from "../utils/boundedRead.js";
|
|
2
|
+
import { firstPathSegment, GENERIC_INFRA_BASENAMES, baseNameNoExt } from "../utils/filePatternUtils.js";
|
|
3
|
+
import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// v0.3.0 Batch 4 — duplicate/parallel implementation candidate detector.
|
|
6
|
+
//
|
|
7
|
+
// Every check here is a *candidate* signal, never proof of duplication --
|
|
8
|
+
// title/description wording is deliberately hedged throughout. Bounded,
|
|
9
|
+
// deterministic, no fuzzy text similarity and no large file reads (only a
|
|
10
|
+
// cheap regex scan of already-small detector source files for the
|
|
11
|
+
// registry-id-duplicate sub-check).
|
|
12
|
+
//
|
|
13
|
+
// Simplifications made relative to the full Batch 4 spec bullet list (see
|
|
14
|
+
// docs handed to this batch for the original wording):
|
|
15
|
+
// - "multiple report renderers for same feature family" is folded into the
|
|
16
|
+
// generic same-basename-across-unrelated-roots check (below) rather than
|
|
17
|
+
// a separate report-specific check -- a dedicated report-directory scan
|
|
18
|
+
// would duplicate that same logic for one category of directory.
|
|
19
|
+
// - "multiple validation runners for same command surface" and "duplicated
|
|
20
|
+
// script families where only one is documented" are SKIPPED: both
|
|
21
|
+
// require inferring "same command surface"/"same family" from script
|
|
22
|
+
// naming, which risks false positives without semantic judgment (out of
|
|
23
|
+
// scope). Left for a future batch if a cheap deterministic signal is
|
|
24
|
+
// found.
|
|
25
|
+
// - The registry-duplicate-id check is intentionally narrow: it scans
|
|
26
|
+
// source text of files under src/audits/codeRot/detectors/ for
|
|
27
|
+
// `id: "..."` string literals via a cheap regex (not a semantic parse),
|
|
28
|
+
// per the spec's own suggested simplification for that bullet.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const DETECTOR_ID = "duplicate-implementation-candidate";
|
|
31
|
+
const MAX_READ_BYTES = 200_000;
|
|
32
|
+
// Directory-name pairs commonly conflated in real projects (singular vs.
|
|
33
|
+
// plural, or synonym pairs). Flagged only when BOTH variants exist as a
|
|
34
|
+
// first path segment among inventoried files.
|
|
35
|
+
const PARALLEL_DIR_PAIRS = [
|
|
36
|
+
["report", "reports"],
|
|
37
|
+
["test", "tests"],
|
|
38
|
+
["util", "utils"],
|
|
39
|
+
["script", "scripts"],
|
|
40
|
+
["doc", "docs"],
|
|
41
|
+
];
|
|
42
|
+
export const DUPLICATE_IMPLEMENTATION_DETECTOR = {
|
|
43
|
+
id: DETECTOR_ID,
|
|
44
|
+
auditType: "code-rot",
|
|
45
|
+
title: "Duplicate/parallel implementation candidate",
|
|
46
|
+
description: "Detects candidate (not proven) duplicate or parallel implementations: overlapping npm script entrypoints, parallel singular/plural directories, duplicate detector-registry ids, and repeated basenames across unrelated source roots.",
|
|
47
|
+
supportedIncludeAreas: ["cli", "package", "architecture"],
|
|
48
|
+
shouldSkip: (ctx) => {
|
|
49
|
+
if (!ctx.config.include.includes("package")) {
|
|
50
|
+
return { skip: true, reason: "--include does not select package; this detector's primary signal is package.json scripts." };
|
|
51
|
+
}
|
|
52
|
+
return { skip: false };
|
|
53
|
+
},
|
|
54
|
+
run: (ctx) => {
|
|
55
|
+
const issues = [];
|
|
56
|
+
const pkg = ctx.sourceOfTruth.package;
|
|
57
|
+
if (pkg) {
|
|
58
|
+
issues.push(...findDuplicateScriptEntrypoints(pkg.scripts));
|
|
59
|
+
}
|
|
60
|
+
if (ctx.config.include.includes("architecture")) {
|
|
61
|
+
issues.push(...findParallelDirectories(ctx.inventory.files.map((f) => f.relativePath)));
|
|
62
|
+
issues.push(...findDuplicateBasenamesAcrossRoots(ctx.inventory.sourceFiles.map((f) => f.relativePath)));
|
|
63
|
+
}
|
|
64
|
+
issues.push(...findDuplicateDetectorRegistryIds(ctx));
|
|
65
|
+
return deduplicateIssuesById(issues);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
// Groups scripts by the file path they invoke (first "scripts/..." token in
|
|
69
|
+
// the command) and flags when 2+ distinct script names invoke the exact
|
|
70
|
+
// same target file -- a strong, low-noise duplicate-entrypoint signal.
|
|
71
|
+
function findDuplicateScriptEntrypoints(scripts) {
|
|
72
|
+
const byTarget = new Map();
|
|
73
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
74
|
+
const match = command.match(/\b(scripts\/[\w./-]+\.[cm]?[jt]sx?)\b/);
|
|
75
|
+
if (!match)
|
|
76
|
+
continue;
|
|
77
|
+
const target = match[1];
|
|
78
|
+
const names = byTarget.get(target) ?? [];
|
|
79
|
+
names.push(name);
|
|
80
|
+
byTarget.set(target, names);
|
|
81
|
+
}
|
|
82
|
+
const issues = [];
|
|
83
|
+
for (const [target, names] of byTarget) {
|
|
84
|
+
if (names.length < 2)
|
|
85
|
+
continue;
|
|
86
|
+
const sortedNames = [...names].sort((a, b) => a.localeCompare(b));
|
|
87
|
+
issues.push(makeCodeRotIssue({
|
|
88
|
+
auditType: "code-rot",
|
|
89
|
+
detectorId: DETECTOR_ID,
|
|
90
|
+
idCues: ["script-entrypoint-candidate", target],
|
|
91
|
+
title: `Multiple npm scripts may be duplicate entrypoints for "${target}"`,
|
|
92
|
+
description: `Scripts ${sortedNames.map((n) => `"${n}"`).join(", ")} all invoke "${target}" -- this may indicate duplicate/parallel CLI entrypoints for the same feature, or may be intentional aliasing. Inspect manually before consolidating.`,
|
|
93
|
+
severity: "medium",
|
|
94
|
+
confidence: "medium",
|
|
95
|
+
falsePositiveRisk: "medium",
|
|
96
|
+
category: "duplicate-implementation-candidate",
|
|
97
|
+
recommendedAction: "Manually confirm whether these scripts are intentional aliases or accidental duplicates before consolidating or removing any of them.",
|
|
98
|
+
suggestedFixStrategy: "If duplicated unintentionally, keep one canonical script name and remove or redirect the others.",
|
|
99
|
+
validationCommands: ["npm run audit -- --types code-rot --include package"],
|
|
100
|
+
releaseBlocking: false,
|
|
101
|
+
implementationBlocking: false,
|
|
102
|
+
autoFixEligible: false,
|
|
103
|
+
evidence: [
|
|
104
|
+
{
|
|
105
|
+
kind: "reference",
|
|
106
|
+
message: `${sortedNames.length} package.json scripts invoke the same target file.`,
|
|
107
|
+
filePath: "package.json",
|
|
108
|
+
excerpt: sortedNames.join(", "),
|
|
109
|
+
source: DETECTOR_ID,
|
|
110
|
+
confidence: "medium",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
affectedFiles: ["package.json", target],
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
return issues;
|
|
117
|
+
}
|
|
118
|
+
function findParallelDirectories(relativePaths) {
|
|
119
|
+
const topSegments = new Set(relativePaths.map((p) => firstPathSegment(p)).filter((s) => s !== null));
|
|
120
|
+
// Also consider the file itself as a "segment" for root-level layout
|
|
121
|
+
// (rare, but keeps this check symmetric for flat projects).
|
|
122
|
+
for (const p of relativePaths) {
|
|
123
|
+
if (!p.includes("/"))
|
|
124
|
+
topSegments.add(p);
|
|
125
|
+
}
|
|
126
|
+
const issues = [];
|
|
127
|
+
for (const [a, b] of PARALLEL_DIR_PAIRS) {
|
|
128
|
+
if (topSegments.has(a) && topSegments.has(b)) {
|
|
129
|
+
issues.push(makeCodeRotIssue({
|
|
130
|
+
auditType: "code-rot",
|
|
131
|
+
detectorId: DETECTOR_ID,
|
|
132
|
+
idCues: ["parallel-directory-candidate", a, b],
|
|
133
|
+
title: `Both "${a}/" and "${b}/" exist as top-level directories`,
|
|
134
|
+
description: `Both "${a}/" and "${b}/" are present at the project root -- this may indicate parallel/overlapping directories for the same responsibility. Inspect manually before consolidating.`,
|
|
135
|
+
severity: "low",
|
|
136
|
+
confidence: "low",
|
|
137
|
+
falsePositiveRisk: "medium",
|
|
138
|
+
category: "duplicate-implementation-candidate",
|
|
139
|
+
recommendedAction: `Confirm whether "${a}/" and "${b}/" serve distinct purposes; if not, consolidate into one.`,
|
|
140
|
+
suggestedFixStrategy: `Merge "${a}/" and "${b}/" or rename one to make the distinction explicit.`,
|
|
141
|
+
validationCommands: ["npm run audit -- --types code-rot --include architecture"],
|
|
142
|
+
releaseBlocking: false,
|
|
143
|
+
implementationBlocking: false,
|
|
144
|
+
autoFixEligible: false,
|
|
145
|
+
evidence: [
|
|
146
|
+
{
|
|
147
|
+
kind: "observation",
|
|
148
|
+
message: `Both "${a}/" and "${b}/" exist as top-level project directories.`,
|
|
149
|
+
source: DETECTOR_ID,
|
|
150
|
+
confidence: "low",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
affectedFiles: [`${a}/`, `${b}/`],
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return issues;
|
|
158
|
+
}
|
|
159
|
+
// Weak/low-confidence signal: the same basename repeated across 2-3
|
|
160
|
+
// unrelated top-level source roots, excluding generic infra names (index,
|
|
161
|
+
// types, config, etc.) that legitimately repeat by convention.
|
|
162
|
+
function findDuplicateBasenamesAcrossRoots(sourceRelativePaths) {
|
|
163
|
+
const byBasename = new Map();
|
|
164
|
+
for (const relativePath of sourceRelativePaths) {
|
|
165
|
+
const base = baseNameNoExt(relativePath).toLowerCase();
|
|
166
|
+
if (GENERIC_INFRA_BASENAMES.has(base))
|
|
167
|
+
continue;
|
|
168
|
+
const top = firstTopLevelUnderSrc(relativePath);
|
|
169
|
+
if (top === null)
|
|
170
|
+
continue;
|
|
171
|
+
const roots = byBasename.get(base) ?? new Set();
|
|
172
|
+
roots.add(top);
|
|
173
|
+
byBasename.set(base, roots);
|
|
174
|
+
}
|
|
175
|
+
const issues = [];
|
|
176
|
+
for (const [base, roots] of byBasename) {
|
|
177
|
+
if (roots.size < 2 || roots.size > 3)
|
|
178
|
+
continue;
|
|
179
|
+
const sortedRoots = [...roots].sort((a, b) => a.localeCompare(b));
|
|
180
|
+
issues.push(makeCodeRotIssue({
|
|
181
|
+
auditType: "code-rot",
|
|
182
|
+
detectorId: DETECTOR_ID,
|
|
183
|
+
idCues: ["basename-repeat-candidate", base, ...sortedRoots],
|
|
184
|
+
title: `Basename "${base}" repeats across unrelated source roots (weak signal)`,
|
|
185
|
+
description: `A file named "${base}.*" exists under multiple unrelated top-level source directories: ${sortedRoots.join(", ")}. This is a weak candidate signal for parallel implementations -- inspect manually.`,
|
|
186
|
+
severity: "info",
|
|
187
|
+
confidence: "low",
|
|
188
|
+
falsePositiveRisk: "high",
|
|
189
|
+
category: "duplicate-implementation-candidate",
|
|
190
|
+
recommendedAction: "Manually confirm whether these files serve genuinely different purposes before taking any action.",
|
|
191
|
+
suggestedFixStrategy: "No automatic action -- rename or consolidate only after manual review.",
|
|
192
|
+
validationCommands: ["npm run audit -- --types code-rot --include architecture"],
|
|
193
|
+
releaseBlocking: false,
|
|
194
|
+
implementationBlocking: false,
|
|
195
|
+
autoFixEligible: false,
|
|
196
|
+
evidence: [
|
|
197
|
+
{
|
|
198
|
+
kind: "observation",
|
|
199
|
+
message: `Basename "${base}" appears under ${sortedRoots.length} different top-level source directories.`,
|
|
200
|
+
source: DETECTOR_ID,
|
|
201
|
+
confidence: "low",
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
affectedFiles: sortedRoots,
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
return issues;
|
|
208
|
+
}
|
|
209
|
+
function firstTopLevelUnderSrc(relativePath) {
|
|
210
|
+
const parts = relativePath.split("/");
|
|
211
|
+
if (parts.length < 3)
|
|
212
|
+
return null; // need at least src/<dir>/file
|
|
213
|
+
if (parts[0] !== "src" && parts[0] !== "scripts")
|
|
214
|
+
return null;
|
|
215
|
+
return `${parts[0]}/${parts[1]}`;
|
|
216
|
+
}
|
|
217
|
+
// Cheap regex scan (not a semantic parse) of already-small detector source
|
|
218
|
+
// files for exported `id: "..."` string literals. Flags duplicates only if
|
|
219
|
+
// genuinely found via this file-based check -- per the spec's own allowed
|
|
220
|
+
// simplification, this does not attempt to reconcile against
|
|
221
|
+
// DEFAULT_AUDIT_REGISTRY at runtime (that would be circular).
|
|
222
|
+
function findDuplicateDetectorRegistryIds(ctx) {
|
|
223
|
+
const detectorFiles = ctx.inventory.sourceFiles.filter((f) => /\/detectors\/[\w-]+\.ts$/.test(f.relativePath));
|
|
224
|
+
if (detectorFiles.length === 0)
|
|
225
|
+
return [];
|
|
226
|
+
const idsToFiles = new Map();
|
|
227
|
+
for (const file of detectorFiles) {
|
|
228
|
+
const content = readBoundedFileText(ctx.target.rootPath, file.relativePath, file.sizeBytes, MAX_READ_BYTES);
|
|
229
|
+
if (content === null)
|
|
230
|
+
continue;
|
|
231
|
+
const match = content.match(/\bid:\s*DETECTOR_ID\b/) ? content.match(/const DETECTOR_ID\s*=\s*"([^"]+)"/) : content.match(/\bid:\s*"([^"]+)"/);
|
|
232
|
+
if (!match)
|
|
233
|
+
continue;
|
|
234
|
+
const id = match[1];
|
|
235
|
+
const files = idsToFiles.get(id) ?? [];
|
|
236
|
+
files.push(file.relativePath);
|
|
237
|
+
idsToFiles.set(id, files);
|
|
238
|
+
}
|
|
239
|
+
const issues = [];
|
|
240
|
+
for (const [id, files] of idsToFiles) {
|
|
241
|
+
if (files.length < 2)
|
|
242
|
+
continue;
|
|
243
|
+
const sortedFiles = [...files].sort((a, b) => a.localeCompare(b));
|
|
244
|
+
issues.push(makeCodeRotIssue({
|
|
245
|
+
auditType: "code-rot",
|
|
246
|
+
detectorId: DETECTOR_ID,
|
|
247
|
+
idCues: ["duplicate-detector-id", id],
|
|
248
|
+
title: `Multiple detector files declare the same detector id "${id}"`,
|
|
249
|
+
description: `Files ${sortedFiles.map((f) => `"${f}"`).join(", ")} appear to declare the same detector id "${id}". Registry construction rejects true duplicate ids at runtime; this file-based scan is a defensive early signal.`,
|
|
250
|
+
severity: "medium",
|
|
251
|
+
confidence: "medium",
|
|
252
|
+
falsePositiveRisk: "low",
|
|
253
|
+
category: "duplicate-implementation-candidate",
|
|
254
|
+
recommendedAction: "Rename one of the conflicting detector ids.",
|
|
255
|
+
suggestedFixStrategy: `Give each detector a unique id string.`,
|
|
256
|
+
validationCommands: ["npm run audit -- --types code-rot --include architecture"],
|
|
257
|
+
releaseBlocking: false,
|
|
258
|
+
implementationBlocking: false,
|
|
259
|
+
autoFixEligible: false,
|
|
260
|
+
evidence: [
|
|
261
|
+
{
|
|
262
|
+
kind: "reference",
|
|
263
|
+
message: `Detector id "${id}" found in ${sortedFiles.length} files.`,
|
|
264
|
+
excerpt: sortedFiles.join(", "),
|
|
265
|
+
source: DETECTOR_ID,
|
|
266
|
+
confidence: "medium",
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
affectedFiles: sortedFiles,
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
return issues;
|
|
273
|
+
}
|