@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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -24
  3. package/dist/scripts/audits/runAudit.js +101 -0
  4. package/dist/scripts/security/validate.js +68 -29
  5. package/dist/src/audits/codeRot/codeRotDetectors.js +17 -0
  6. package/dist/src/audits/codeRot/detectors/architectureDriftDetector.js +271 -0
  7. package/dist/src/audits/codeRot/detectors/crossPlatformRotDetector.js +214 -0
  8. package/dist/src/audits/codeRot/detectors/deadCodeCandidateDetector.js +344 -0
  9. package/dist/src/audits/codeRot/detectors/dependencyEnvironmentRotDetector.js +244 -0
  10. package/dist/src/audits/codeRot/detectors/docsCodeMismatchDetector.js +219 -0
  11. package/dist/src/audits/codeRot/detectors/duplicateImplementationDetector.js +273 -0
  12. package/dist/src/audits/codeRot/detectors/packageReleaseRotDetector.js +325 -0
  13. package/dist/src/audits/codeRot/detectors/securityValidationAssumptionRotDetector.js +385 -0
  14. package/dist/src/audits/codeRot/detectors/staleCommandReferenceDetector.js +135 -0
  15. package/dist/src/audits/codeRot/detectors/testRotDetector.js +348 -0
  16. package/dist/src/audits/codeRot/utils/boundedRead.js +23 -0
  17. package/dist/src/audits/codeRot/utils/docClaimPatterns.js +62 -0
  18. package/dist/src/audits/codeRot/utils/docCommandReferences.js +64 -0
  19. package/dist/src/audits/codeRot/utils/filePatternUtils.js +47 -0
  20. package/dist/src/audits/codeRot/utils/issueFactories.js +38 -0
  21. package/dist/src/audits/codeRot/utils/textLines.js +12 -0
  22. package/dist/src/audits/core/auditConfig.js +125 -0
  23. package/dist/src/audits/core/auditExitCode.js +75 -0
  24. package/dist/src/audits/core/auditIssue.js +20 -0
  25. package/dist/src/audits/core/auditRegistry.js +40 -0
  26. package/dist/src/audits/core/auditRunner.js +71 -0
  27. package/dist/src/audits/core/auditTarget.js +19 -0
  28. package/dist/src/audits/core/auditTypes.js +30 -0
  29. package/dist/src/audits/core/projectInventory.js +308 -0
  30. package/dist/src/audits/core/sourceOfTruth.js +339 -0
  31. package/dist/src/audits/core/textLines.js +24 -0
  32. package/dist/src/audits/report/auditReportModel.js +241 -0
  33. package/dist/src/audits/report/auditReportPaths.js +13 -0
  34. package/dist/src/audits/report/renderAuditJsonReport.js +29 -0
  35. package/dist/src/audits/report/renderAuditTextReport.js +165 -0
  36. package/dist/src/audits/report/sanitizeAuditText.js +39 -0
  37. package/dist/src/audits/report/writeAuditReports.js +27 -0
  38. package/dist/src/core/resolveCommand.js +53 -2
  39. package/dist/src/securityValidation/attackScenarios/attackProfile.js +20 -0
  40. package/dist/src/securityValidation/attackScenarios/attackResult.js +37 -0
  41. package/dist/src/securityValidation/attackScenarios/attackRunner.js +153 -0
  42. package/dist/src/securityValidation/attackScenarios/attackScenario.js +21 -0
  43. package/dist/src/securityValidation/attackScenarios/boundedSourceScan.js +100 -0
  44. package/dist/src/securityValidation/attackScenarios/exploitEvidence.js +69 -0
  45. package/dist/src/securityValidation/attackScenarios/payloadCorpus.js +0 -0
  46. package/dist/src/securityValidation/attackScenarios/profiles/localToolProfile.js +10 -0
  47. package/dist/src/securityValidation/attackScenarios/profiles/nodeCliPackageProfile.js +20 -0
  48. package/dist/src/securityValidation/attackScenarios/profiles/npmPackageProfile.js +10 -0
  49. package/dist/src/securityValidation/attackScenarios/reportSchemaGuard.js +41 -0
  50. package/dist/src/securityValidation/attackScenarios/scenarios/configInjectionScenario.js +144 -0
  51. package/dist/src/securityValidation/attackScenarios/scenarios/networkAssumptionScenario.js +129 -0
  52. package/dist/src/securityValidation/attackScenarios/scenarios/outputBoundaryScenario.js +69 -0
  53. package/dist/src/securityValidation/attackScenarios/scenarios/packageBoundaryScenario.js +118 -0
  54. package/dist/src/securityValidation/attackScenarios/scenarios/pathTraversalScenario.js +119 -0
  55. package/dist/src/securityValidation/attackScenarios/scenarios/reportPoisoningScenario.js +176 -0
  56. package/dist/src/securityValidation/attackScenarios/scenarios/secretLeakageScenario.js +143 -0
  57. package/dist/src/securityValidation/attackScenarios/scenarios/subprocessInjectionScenario.js +117 -0
  58. package/dist/src/securityValidation/attackScenarios/scenarios/targetSandboxScenario.js +118 -0
  59. package/dist/src/securityValidation/attackScenarios/targetSnapshot.js +70 -0
  60. package/dist/src/securityValidation/packageChecks/forbiddenPackageContents.js +4 -1
  61. package/dist/src/securityValidation/report/renderSecurityReport.js +135 -13
  62. package/dist/src/securityValidation/types.js +7 -0
  63. package/dist/src/securityValidation/validate/cliOptions.js +182 -0
  64. package/dist/src/securityValidation/validate/runSecurityValidation.js +134 -35
  65. package/dist/src/securityValidation/validate/verdict.js +147 -0
  66. package/package.json +18 -17
@@ -0,0 +1,271 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { currentClaimPatternFor } from "../utils/docClaimPatterns.js";
4
+ import { readBoundedFileText } from "../utils/boundedRead.js";
5
+ import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
6
+ // ---------------------------------------------------------------------------
7
+ // v0.3.0 Batch 4 — architecture drift detector.
8
+ //
9
+ // Deterministic path/claim checks only -- no semantic judgment. Two of the
10
+ // checks below (stale/missing backtick path mentions, and the hardcoded
11
+ // "audit framework described as planned while implemented" check) are
12
+ // deliberately scoped to docs/ARCHITECTURE.md and docs/CURRENT_STATE.md
13
+ // only, not every doc file. Both files' own headers state their purpose is
14
+ // describing CURRENT implemented state ("This file is the concise source of
15
+ // truth for the checked-in implementation" / "Current implemented
16
+ // architecture"). docs/ROADMAP.md, by contrast, is full of legitimate
17
+ // backtick-quoted "Suggested architecture" paths for genuinely
18
+ // not-yet-built future work items -- scanning it with the same "does this
19
+ // path exist" check would produce many false positives against accurate,
20
+ // correctly-labeled-as-future roadmap content. Restricting scope keeps this
21
+ // detector's signal high-confidence.
22
+ // ---------------------------------------------------------------------------
23
+ const DETECTOR_ID = "architecture-drift";
24
+ const MAX_READ_BYTES = 200_000;
25
+ const CURRENT_STATE_DOC_BASENAMES = new Set(["architecture.md", "current_state.md"]);
26
+ const BACKTICK_SRC_PATH_PATTERN = /`(src\/[\w./-]+|scripts\/[\w./-]+)`/g;
27
+ const HISTORICAL_FRAMING_PATTERN = /\b(historical|previously|used to|no longer|removed|deprecated)\b/i;
28
+ const PLANNED_FRAMING_PATTERN = /\b(planned|future|roadmap|not (?:yet )?implemented)\b/i;
29
+ const AUDIT_FRAMEWORK_SUBJECT_PATTERN = /\baudit (?:framework|contracts)\b|\bcode rot detector\b/i;
30
+ const MIN_SOURCE_FILES_FOR_MAJOR_DIR = 3;
31
+ export const ARCHITECTURE_DRIFT_DETECTOR = {
32
+ id: DETECTOR_ID,
33
+ auditType: "code-rot",
34
+ title: "Architecture drift",
35
+ description: "Detects deterministic drift between documentation and actual source layout: doc-claimed paths that don't exist, the audit framework described as planned while implemented, major source directories missing from ARCHITECTURE.md, and unhedged manual-pentest/Android implementation claims.",
36
+ supportedIncludeAreas: ["architecture", "docs", "package", "cli"],
37
+ shouldSkip: (ctx) => {
38
+ if (!ctx.config.include.includes("docs") && !ctx.config.include.includes("architecture")) {
39
+ return { skip: true, reason: "--include selects neither docs nor architecture; this detector's checks are docs-based." };
40
+ }
41
+ return { skip: false };
42
+ },
43
+ run: (ctx) => {
44
+ const issues = [];
45
+ const currentStateDocs = ctx.inventory.docsFiles.filter((f) => CURRENT_STATE_DOC_BASENAMES.has(path.basename(f.relativePath).toLowerCase()));
46
+ for (const docFile of currentStateDocs) {
47
+ const content = readBoundedFileText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes, MAX_READ_BYTES);
48
+ if (content === null)
49
+ continue;
50
+ issues.push(...findStaleOrMissingBacktickPaths(ctx, docFile.relativePath, content));
51
+ }
52
+ issues.push(...findAuditFrameworkDescribedAsPlanned(ctx, currentStateDocs));
53
+ const architectureDoc = ctx.inventory.docsFiles.find((f) => path.basename(f.relativePath).toLowerCase() === "architecture.md");
54
+ if (architectureDoc) {
55
+ const content = readBoundedFileText(ctx.target.rootPath, architectureDoc.relativePath, architectureDoc.sizeBytes, MAX_READ_BYTES);
56
+ if (content !== null) {
57
+ issues.push(...findMajorDirsAbsentFromArchitectureDoc(ctx, architectureDoc.relativePath, content));
58
+ }
59
+ }
60
+ for (const docFile of ctx.inventory.docsFiles) {
61
+ const content = readBoundedFileText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes, MAX_READ_BYTES);
62
+ if (content === null)
63
+ continue;
64
+ issues.push(...findUnsupportedCurrentClaims(docFile.relativePath, content));
65
+ }
66
+ return deduplicateIssuesById(issues);
67
+ },
68
+ };
69
+ function pathExistsInInventoryOrDisk(ctx, claimedPath) {
70
+ const trimmed = claimedPath.replace(/\/$/, "");
71
+ const inventoryHit = ctx.inventory.files.some((f) => f.relativePath === trimmed || f.relativePath.startsWith(`${trimmed}/`));
72
+ if (inventoryHit)
73
+ return true;
74
+ try {
75
+ return fs.existsSync(path.join(ctx.target.rootPath, trimmed));
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ function findStaleOrMissingBacktickPaths(ctx, docRelativePath, content) {
82
+ const issues = [];
83
+ const seen = new Set();
84
+ BACKTICK_SRC_PATH_PATTERN.lastIndex = 0;
85
+ for (const match of content.matchAll(BACKTICK_SRC_PATH_PATTERN)) {
86
+ const claimedPath = match[1];
87
+ if (seen.has(claimedPath))
88
+ continue;
89
+ seen.add(claimedPath);
90
+ if (pathExistsInInventoryOrDisk(ctx, claimedPath))
91
+ continue;
92
+ const matchIndex = match.index ?? 0;
93
+ const windowStart = Math.max(0, matchIndex - 200);
94
+ const windowEnd = Math.min(content.length, matchIndex + claimedPath.length + 200);
95
+ const window = content.slice(windowStart, windowEnd);
96
+ const isHistorical = HISTORICAL_FRAMING_PATTERN.test(window);
97
+ issues.push(makeCodeRotIssue({
98
+ auditType: "code-rot",
99
+ detectorId: DETECTOR_ID,
100
+ idCues: [docRelativePath, isHistorical ? "stale-path-mention" : "claimed-path-missing", claimedPath],
101
+ title: isHistorical
102
+ ? `${docRelativePath} mentions a stale (historically-framed) path: "${claimedPath}"`
103
+ : `${docRelativePath} claims path "${claimedPath}" exists, but it does not`,
104
+ description: isHistorical
105
+ ? `${docRelativePath} mentions "${claimedPath}" with historical framing, and the path no longer exists on disk. Low-severity because it is explicitly framed as historical.`
106
+ : `${docRelativePath} references "${claimedPath}" as a current path (backtick code span), but no such file or directory exists in the project.`,
107
+ severity: isHistorical ? "low" : "medium",
108
+ confidence: isHistorical ? "medium" : "medium",
109
+ falsePositiveRisk: "medium",
110
+ category: "architecture-drift",
111
+ recommendedAction: isHistorical
112
+ ? `Confirm whether the historical reference to "${claimedPath}" is still useful context.`
113
+ : `Update ${docRelativePath} to reference an existing path, or restore/implement "${claimedPath}".`,
114
+ suggestedFixStrategy: `Correct or remove the "${claimedPath}" reference in ${docRelativePath}.`,
115
+ validationCommands: ["npm run audit -- --types code-rot --include architecture,docs"],
116
+ releaseBlocking: false,
117
+ implementationBlocking: false,
118
+ autoFixEligible: false,
119
+ evidence: [
120
+ {
121
+ kind: "file",
122
+ message: "Backtick-quoted path does not exist on disk.",
123
+ filePath: docRelativePath,
124
+ excerpt: claimedPath,
125
+ source: DETECTOR_ID,
126
+ confidence: "medium",
127
+ },
128
+ ],
129
+ affectedFiles: [docRelativePath],
130
+ }));
131
+ }
132
+ return issues;
133
+ }
134
+ function findAuditFrameworkDescribedAsPlanned(ctx, currentStateDocs) {
135
+ const auditImplemented = fs.existsSync(path.join(ctx.target.rootPath, "src", "audits")) && "audit" in (ctx.sourceOfTruth.package?.scripts ?? {});
136
+ if (!auditImplemented)
137
+ return [];
138
+ const issues = [];
139
+ for (const docFile of currentStateDocs) {
140
+ const content = readBoundedFileText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes, MAX_READ_BYTES);
141
+ if (content === null)
142
+ continue;
143
+ AUDIT_FRAMEWORK_SUBJECT_PATTERN.lastIndex = 0;
144
+ const subjectMatch = AUDIT_FRAMEWORK_SUBJECT_PATTERN.exec(content);
145
+ if (!subjectMatch)
146
+ continue;
147
+ const matchIndex = subjectMatch.index;
148
+ const windowStart = Math.max(0, matchIndex - 400);
149
+ const windowEnd = Math.min(content.length, matchIndex + subjectMatch[0].length + 400);
150
+ const window = content.slice(windowStart, windowEnd);
151
+ if (!PLANNED_FRAMING_PATTERN.test(window))
152
+ continue;
153
+ issues.push(makeCodeRotIssue({
154
+ auditType: "code-rot",
155
+ detectorId: DETECTOR_ID,
156
+ idCues: [docFile.relativePath, "audit-framework-planned-but-implemented"],
157
+ title: `${docFile.relativePath} describes the audit framework as planned/future, but it is already implemented`,
158
+ description: `${docFile.relativePath} mentions "${subjectMatch[0]}" near planned/future/not-implemented wording, but src/audits exists and package.json defines an "audit" script -- the audit framework is already implemented, not merely planned.`,
159
+ severity: "high",
160
+ confidence: "medium",
161
+ falsePositiveRisk: "medium",
162
+ category: "architecture-drift",
163
+ recommendedAction: `Update ${docFile.relativePath} to describe the audit framework's actual current implementation status.`,
164
+ suggestedFixStrategy: `Move the audit-framework description in ${docFile.relativePath} out of the planned/future section.`,
165
+ validationCommands: ["npm run audit -- --types code-rot --include architecture,docs"],
166
+ releaseBlocking: false,
167
+ implementationBlocking: false,
168
+ autoFixEligible: false,
169
+ evidence: [
170
+ {
171
+ kind: "file",
172
+ message: "Doc describes the audit framework as planned/future near this mention, but src/audits exists and an \"audit\" npm script is defined.",
173
+ filePath: docFile.relativePath,
174
+ excerpt: subjectMatch[0],
175
+ source: DETECTOR_ID,
176
+ confidence: "medium",
177
+ },
178
+ ],
179
+ affectedFiles: [docFile.relativePath],
180
+ }));
181
+ }
182
+ return issues;
183
+ }
184
+ function findMajorDirsAbsentFromArchitectureDoc(ctx, docRelativePath, content) {
185
+ const countsByDir = new Map();
186
+ for (const file of ctx.inventory.sourceFiles) {
187
+ const parts = file.relativePath.split("/");
188
+ if (parts[0] !== "src" || parts.length < 3)
189
+ continue;
190
+ const dirName = parts[1];
191
+ countsByDir.set(dirName, (countsByDir.get(dirName) ?? 0) + 1);
192
+ }
193
+ const issues = [];
194
+ const sortedDirs = [...countsByDir.entries()].sort((a, b) => a[0].localeCompare(b[0]));
195
+ for (const [dirName, count] of sortedDirs) {
196
+ if (count < MIN_SOURCE_FILES_FOR_MAJOR_DIR)
197
+ continue;
198
+ if (content.includes(dirName))
199
+ continue;
200
+ issues.push(makeCodeRotIssue({
201
+ auditType: "code-rot",
202
+ detectorId: DETECTOR_ID,
203
+ idCues: [docRelativePath, "major-dir-absent-from-architecture-doc", dirName],
204
+ title: `Major implemented directory "src/${dirName}" is not mentioned in ${docRelativePath}`,
205
+ description: `"src/${dirName}" contains ${count} source files but is never mentioned in ${docRelativePath}.`,
206
+ severity: "medium",
207
+ confidence: "medium",
208
+ falsePositiveRisk: "medium",
209
+ category: "architecture-drift",
210
+ recommendedAction: `Add "src/${dirName}" to ${docRelativePath}'s module map.`,
211
+ suggestedFixStrategy: `Document the responsibility of "src/${dirName}" in ${docRelativePath}.`,
212
+ validationCommands: ["npm run audit -- --types code-rot --include architecture,docs"],
213
+ releaseBlocking: false,
214
+ implementationBlocking: false,
215
+ autoFixEligible: false,
216
+ evidence: [
217
+ {
218
+ kind: "observation",
219
+ message: `"src/${dirName}" has ${count} source files and is not mentioned anywhere in ${docRelativePath}.`,
220
+ filePath: docRelativePath,
221
+ source: DETECTOR_ID,
222
+ confidence: "medium",
223
+ },
224
+ ],
225
+ affectedFiles: [docRelativePath],
226
+ }));
227
+ }
228
+ return issues;
229
+ }
230
+ function findUnsupportedCurrentClaims(docRelativePath, content) {
231
+ const issues = [];
232
+ const subjects = [
233
+ { id: "manual-pentest", label: "manual pentest", pattern: "manual pentest" },
234
+ { id: "android-validation", label: "Android/mobile validation", pattern: "(?:android (?:validation|support))" },
235
+ ];
236
+ for (const subject of subjects) {
237
+ const pattern = currentClaimPatternFor(subject.pattern);
238
+ const match = pattern.exec(content);
239
+ if (!match)
240
+ continue;
241
+ issues.push(makeCodeRotIssue({
242
+ auditType: "code-rot",
243
+ detectorId: DETECTOR_ID,
244
+ idCues: [docRelativePath, "unsupported-current-claim", subject.id],
245
+ title: `${docRelativePath} claims "${subject.label}" is implemented/current`,
246
+ description: `${docRelativePath} contains an unhedged "${subject.label} is implemented/current" claim, but there is no corresponding source signal for this repo.`,
247
+ severity: "high",
248
+ confidence: "medium",
249
+ falsePositiveRisk: "medium",
250
+ category: "architecture-drift",
251
+ recommendedAction: `Confirm whether "${subject.label}" is actually implemented; if not, correct ${docRelativePath}.`,
252
+ suggestedFixStrategy: `Rephrase the "${subject.label}" claim in ${docRelativePath} to accurately reflect its planned status.`,
253
+ validationCommands: ["npm run audit -- --types code-rot --include architecture,docs"],
254
+ releaseBlocking: false,
255
+ implementationBlocking: false,
256
+ autoFixEligible: false,
257
+ evidence: [
258
+ {
259
+ kind: "file",
260
+ message: `Matched an unhedged "${subject.label} is implemented/current" claim pattern.`,
261
+ filePath: docRelativePath,
262
+ excerpt: match[0].slice(0, 200),
263
+ source: DETECTOR_ID,
264
+ confidence: "medium",
265
+ },
266
+ ],
267
+ affectedFiles: [docRelativePath],
268
+ }));
269
+ }
270
+ return issues;
271
+ }
@@ -0,0 +1,214 @@
1
+ import { readBoundedFileText } from "../utils/boundedRead.js";
2
+ import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
3
+ // ---------------------------------------------------------------------------
4
+ // v0.3.0 Batch 4 — cross-platform rot detector.
5
+ //
6
+ // Read-only with respect to the target project throughout -- never
7
+ // modifies scripts or source. Bounded (per-file MAX_READ_BYTES skip).
8
+ //
9
+ // Simplification made after self-testing against this repo's own source:
10
+ // the spec's ".split(\"/\")" raw-path-splitting sub-check was DROPPED. This
11
+ // repo's own project inventory (src/audits/core/projectInventory.ts)
12
+ // guarantees relativePath values are always forward-slash normalized, so
13
+ // splitting an already-normalized relativePath on "/" (as several of this
14
+ // batch's own new detector files legitimately do, e.g.
15
+ // filePatternUtils.ts's firstTwoPathSegments()) is the correct, intentional
16
+ // pattern here, not a cross-platform bug -- a bare regex cannot distinguish
17
+ // that from a genuinely unsafe raw OS path split without semantic analysis.
18
+ // The much narrower, high-signal ".split(\"\\\\\")" (raw double-backslash
19
+ // Windows-style split) sub-check is kept, since that pattern has near-zero
20
+ // legitimate use in this codebase.
21
+ // ---------------------------------------------------------------------------
22
+ const DETECTOR_ID = "cross-platform-rot";
23
+ const MAX_READ_BYTES = 200_000;
24
+ const WINDOWS_MENTION_PATTERN = /\bWindows\b/;
25
+ const POSIX_ONLY_BINARY_PATTERN = /(^|&&)\s*(rm -rf|cp -r|mv |grep |sed |cat |find \.)/;
26
+ const SPLIT_NEWLINE_LITERAL_PATTERN = /\.split\(\s*["']\\n["']\s*\)/g;
27
+ const RAW_BACKSLASH_SPLIT_PATTERN = /\.split\(\s*["']\\\\["']\s*\)/g;
28
+ const SHELL_TRUE_PATTERN = /shell:\s*true/;
29
+ const EXEC_SPAWN_CALL_PATTERN = /\b(exec|spawn|execFile\w*|spawnSync|execSync|execFileSync)\s*\(/;
30
+ export const CROSS_PLATFORM_ROT_DETECTOR = {
31
+ id: DETECTOR_ID,
32
+ auditType: "code-rot",
33
+ title: "Cross-platform rot",
34
+ description: "Detects POSIX-only shell syntax in npm scripts when Windows support is claimed, raw .split(\"\\n\") usage outside the canonical splitLines() helper, raw backslash path splitting, and exec/spawn calls with shell: true.",
35
+ supportedIncludeAreas: ["cli", "package", "docs", "tests"],
36
+ shouldSkip: (ctx) => {
37
+ if (!ctx.config.include.includes("cli") && !ctx.config.include.includes("package")) {
38
+ return { skip: true, reason: "--include selects neither cli nor package; this detector's checks are script/source-based." };
39
+ }
40
+ return { skip: false };
41
+ },
42
+ run: (ctx) => {
43
+ const issues = [];
44
+ const pkg = ctx.sourceOfTruth.package;
45
+ let docsClaimWindowsSupport = false;
46
+ if (ctx.config.include.includes("docs")) {
47
+ docsClaimWindowsSupport = docsClaimWindows(ctx);
48
+ }
49
+ if (ctx.config.include.includes("package") && pkg) {
50
+ issues.push(...findPosixOnlyScriptSyntax(pkg.scripts, docsClaimWindowsSupport));
51
+ }
52
+ if (ctx.config.include.includes("cli")) {
53
+ issues.push(...scanSourceFiles(ctx));
54
+ }
55
+ return deduplicateIssuesById(issues);
56
+ },
57
+ };
58
+ function docsClaimWindows(ctx) {
59
+ for (const docFile of ctx.inventory.docsFiles) {
60
+ const content = readBoundedFileText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes, MAX_READ_BYTES);
61
+ if (content !== null && WINDOWS_MENTION_PATTERN.test(content))
62
+ return true;
63
+ }
64
+ return false;
65
+ }
66
+ function findPosixOnlyScriptSyntax(scripts, docsClaimWindowsSupport) {
67
+ const issues = [];
68
+ for (const [scriptName, command] of Object.entries(scripts)) {
69
+ const segments = command.split("&&").map((s) => s.trim());
70
+ for (const segment of segments) {
71
+ if (!POSIX_ONLY_BINARY_PATTERN.test(segment))
72
+ continue;
73
+ issues.push(makeCodeRotIssue({
74
+ auditType: "code-rot",
75
+ detectorId: DETECTOR_ID,
76
+ idCues: ["posix-only-script-syntax", scriptName],
77
+ title: `Script "${scriptName}" appears to use POSIX-only shell syntax`,
78
+ description: `package.json script "${scriptName}" contains a segment that looks like a bare POSIX shell command ("${segment.slice(0, 60)}"), which is not guaranteed to work on Windows without a POSIX shell shim.${docsClaimWindowsSupport ? " Docs explicitly claim Windows support." : ""}`,
79
+ severity: docsClaimWindowsSupport ? "high" : "medium",
80
+ confidence: "medium",
81
+ falsePositiveRisk: "medium",
82
+ category: "cross-platform-rot",
83
+ recommendedAction: "Replace the POSIX-only command with a cross-platform Node script or a guarded/portable equivalent.",
84
+ suggestedFixStrategy: `Rewrite the "${scriptName}" script segment to avoid relying on a POSIX-only shell binary.`,
85
+ validationCommands: [`npm run ${scriptName}`],
86
+ releaseBlocking: false,
87
+ implementationBlocking: false,
88
+ autoFixEligible: false,
89
+ evidence: [
90
+ {
91
+ kind: "reference",
92
+ message: "Script command segment matches a bare POSIX-only shell binary pattern.",
93
+ filePath: "package.json",
94
+ excerpt: segment.slice(0, 200),
95
+ source: DETECTOR_ID,
96
+ confidence: "medium",
97
+ },
98
+ ],
99
+ affectedFiles: ["package.json"],
100
+ }));
101
+ }
102
+ }
103
+ return issues;
104
+ }
105
+ function scanSourceFiles(ctx) {
106
+ const issues = [];
107
+ const auditSourceFiles = ctx.inventory.sourceFiles.filter((f) => f.relativePath.startsWith("src/audits/"));
108
+ for (const file of auditSourceFiles) {
109
+ if (path_basename(file.relativePath) === "textLines.ts")
110
+ continue; // canonical implementation, allowed
111
+ const content = readBoundedFileText(ctx.target.rootPath, file.relativePath, file.sizeBytes, MAX_READ_BYTES);
112
+ if (content === null)
113
+ continue;
114
+ SPLIT_NEWLINE_LITERAL_PATTERN.lastIndex = 0;
115
+ if (SPLIT_NEWLINE_LITERAL_PATTERN.test(content)) {
116
+ issues.push(makeCodeRotIssue({
117
+ auditType: "code-rot",
118
+ detectorId: DETECTOR_ID,
119
+ idCues: [file.relativePath, "raw-split-newline"],
120
+ title: `"${file.relativePath}" uses raw .split("\\n") instead of the canonical splitLines() helper`,
121
+ description: `"${file.relativePath}" contains a raw ".split(\\"\\n\\")"/".split('\\n')" call. This project's own convention (see src/audits/codeRot/utils/textLines.ts) requires splitLines() for all doc/text line-splitting, because a bare split leaves a trailing "\\r" on every line for CRLF-authored files.`,
122
+ severity: "medium",
123
+ confidence: "high",
124
+ falsePositiveRisk: "low",
125
+ category: "cross-platform-rot",
126
+ recommendedAction: `Replace the raw .split("\\n") in "${file.relativePath}" with splitLines() from ../utils/textLines.js.`,
127
+ suggestedFixStrategy: `Import and use splitLines() in "${file.relativePath}".`,
128
+ validationCommands: ["npm run audit -- --types code-rot --include cli"],
129
+ releaseBlocking: false,
130
+ implementationBlocking: false,
131
+ autoFixEligible: false,
132
+ evidence: [
133
+ {
134
+ kind: "file",
135
+ message: "Raw .split(\"\\n\") usage found outside the canonical splitLines() implementation.",
136
+ filePath: file.relativePath,
137
+ source: DETECTOR_ID,
138
+ confidence: "high",
139
+ },
140
+ ],
141
+ affectedFiles: [file.relativePath],
142
+ }));
143
+ }
144
+ RAW_BACKSLASH_SPLIT_PATTERN.lastIndex = 0;
145
+ if (RAW_BACKSLASH_SPLIT_PATTERN.test(content)) {
146
+ issues.push(makeCodeRotIssue({
147
+ auditType: "code-rot",
148
+ detectorId: DETECTOR_ID,
149
+ idCues: [file.relativePath, "raw-backslash-split"],
150
+ title: `"${file.relativePath}" raw-splits a path on a literal backslash`,
151
+ description: `"${file.relativePath}" contains a raw backslash .split() call, which assumes Windows-style backslash separators and will silently fail to split forward-slash paths (e.g. on POSIX or already-normalized relative paths).`,
152
+ severity: "medium",
153
+ confidence: "medium",
154
+ falsePositiveRisk: "medium",
155
+ category: "cross-platform-rot",
156
+ recommendedAction: `Use path.sep/path.normalize or a forward-slash-normalized path representation instead of a raw backslash split in "${file.relativePath}".`,
157
+ suggestedFixStrategy: `Replace the raw backslash split in "${file.relativePath}" with a portable path-handling helper.`,
158
+ validationCommands: ["npm run audit -- --types code-rot --include cli"],
159
+ releaseBlocking: false,
160
+ implementationBlocking: false,
161
+ autoFixEligible: false,
162
+ evidence: [
163
+ {
164
+ kind: "file",
165
+ message: "Raw backslash-splitting pattern found in source.",
166
+ filePath: file.relativePath,
167
+ source: DETECTOR_ID,
168
+ confidence: "medium",
169
+ },
170
+ ],
171
+ affectedFiles: [file.relativePath],
172
+ }));
173
+ }
174
+ SHELL_TRUE_PATTERN.lastIndex = 0;
175
+ const shellTrueMatch = SHELL_TRUE_PATTERN.exec(content);
176
+ if (shellTrueMatch && EXEC_SPAWN_CALL_PATTERN.test(nearbyWindow(content, shellTrueMatch.index))) {
177
+ issues.push(makeCodeRotIssue({
178
+ auditType: "code-rot",
179
+ detectorId: DETECTOR_ID,
180
+ idCues: [file.relativePath, "shell-true-usage"],
181
+ title: `"${file.relativePath}" appears to invoke a subprocess with shell: true`,
182
+ description: `"${file.relativePath}" contains a "shell: true" option near an exec/spawn call. This is a known risk pattern (shell metacharacter injection via unsanitized arguments).`,
183
+ severity: "high",
184
+ confidence: "medium",
185
+ falsePositiveRisk: "medium",
186
+ category: "cross-platform-rot",
187
+ recommendedAction: `Remove "shell: true" and pass arguments as an array instead, in "${file.relativePath}".`,
188
+ suggestedFixStrategy: `Refactor the subprocess call in "${file.relativePath}" to avoid shell: true.`,
189
+ validationCommands: ["npm run audit -- --types code-rot --include cli"],
190
+ releaseBlocking: true,
191
+ implementationBlocking: false,
192
+ autoFixEligible: false,
193
+ evidence: [
194
+ {
195
+ kind: "file",
196
+ message: "shell: true found near an exec/spawn-family call.",
197
+ filePath: file.relativePath,
198
+ source: DETECTOR_ID,
199
+ confidence: "medium",
200
+ },
201
+ ],
202
+ affectedFiles: [file.relativePath],
203
+ }));
204
+ }
205
+ }
206
+ return issues;
207
+ }
208
+ function nearbyWindow(content, index, radius = 150) {
209
+ return content.slice(Math.max(0, index - radius), Math.min(content.length, index + radius));
210
+ }
211
+ function path_basename(relativePath) {
212
+ const idx = relativePath.lastIndexOf("/");
213
+ return idx === -1 ? relativePath : relativePath.slice(idx + 1);
214
+ }