@dailephd/my-dev-kit-lab 0.2.2 → 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 (37) hide show
  1. package/README.md +3 -2
  2. package/dist/scripts/audits/runAudit.js +101 -0
  3. package/dist/src/audits/codeRot/codeRotDetectors.js +17 -0
  4. package/dist/src/audits/codeRot/detectors/architectureDriftDetector.js +271 -0
  5. package/dist/src/audits/codeRot/detectors/crossPlatformRotDetector.js +214 -0
  6. package/dist/src/audits/codeRot/detectors/deadCodeCandidateDetector.js +344 -0
  7. package/dist/src/audits/codeRot/detectors/dependencyEnvironmentRotDetector.js +244 -0
  8. package/dist/src/audits/codeRot/detectors/docsCodeMismatchDetector.js +219 -0
  9. package/dist/src/audits/codeRot/detectors/duplicateImplementationDetector.js +273 -0
  10. package/dist/src/audits/codeRot/detectors/packageReleaseRotDetector.js +325 -0
  11. package/dist/src/audits/codeRot/detectors/securityValidationAssumptionRotDetector.js +385 -0
  12. package/dist/src/audits/codeRot/detectors/staleCommandReferenceDetector.js +135 -0
  13. package/dist/src/audits/codeRot/detectors/testRotDetector.js +348 -0
  14. package/dist/src/audits/codeRot/utils/boundedRead.js +23 -0
  15. package/dist/src/audits/codeRot/utils/docClaimPatterns.js +62 -0
  16. package/dist/src/audits/codeRot/utils/docCommandReferences.js +64 -0
  17. package/dist/src/audits/codeRot/utils/filePatternUtils.js +47 -0
  18. package/dist/src/audits/codeRot/utils/issueFactories.js +38 -0
  19. package/dist/src/audits/codeRot/utils/textLines.js +12 -0
  20. package/dist/src/audits/core/auditConfig.js +125 -0
  21. package/dist/src/audits/core/auditExitCode.js +75 -0
  22. package/dist/src/audits/core/auditIssue.js +20 -0
  23. package/dist/src/audits/core/auditRegistry.js +40 -0
  24. package/dist/src/audits/core/auditRunner.js +71 -0
  25. package/dist/src/audits/core/auditTarget.js +19 -0
  26. package/dist/src/audits/core/auditTypes.js +30 -0
  27. package/dist/src/audits/core/projectInventory.js +308 -0
  28. package/dist/src/audits/core/sourceOfTruth.js +339 -0
  29. package/dist/src/audits/core/textLines.js +24 -0
  30. package/dist/src/audits/report/auditReportModel.js +241 -0
  31. package/dist/src/audits/report/auditReportPaths.js +13 -0
  32. package/dist/src/audits/report/renderAuditJsonReport.js +29 -0
  33. package/dist/src/audits/report/renderAuditTextReport.js +165 -0
  34. package/dist/src/audits/report/sanitizeAuditText.js +39 -0
  35. package/dist/src/audits/report/writeAuditReports.js +27 -0
  36. package/dist/src/core/resolveCommand.js +53 -2
  37. package/package.json +3 -2
@@ -0,0 +1,325 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { CURRENT_PACKAGE_VERSION_PATTERNS, PUBLISHED_OR_RELEASED_CLAIM_PATTERN, isHedgedNearby } 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 — package/release rot detector.
8
+ //
9
+ // Local-only: never runs npm pack, npm view, or any network/registry call
10
+ // (Batch 3 scope). Owns the publish/release-claim check and the doc-vs-
11
+ // package version-mismatch check (both also listed under
12
+ // docsCodeMismatchDetector.ts's Batch 3 spec section, but implemented here
13
+ // only, to avoid the same underlying signal producing two issues from two
14
+ // detectors -- see that file's header comment for the same note from the
15
+ // other side).
16
+ // ---------------------------------------------------------------------------
17
+ const DETECTOR_ID = "package-release-rot";
18
+ const MAX_DOC_READ_BYTES = 200_000;
19
+ // Paths that should never appear in package.json's "files" allowlist --
20
+ // each is either generated content or private/local state, per this
21
+ // project's own .gitignore/security-package-content conventions.
22
+ const RISKY_FILES_ENTRY_PATTERN = /^(reports|lab-output|\.my-dev-kit|\.env)/i;
23
+ export const PACKAGE_RELEASE_ROT_DETECTOR = {
24
+ id: DETECTOR_ID,
25
+ auditType: "code-rot",
26
+ title: "Package/release rot",
27
+ description: "Detects package.json/package-lock.json version or name drift, a missing CHANGELOG section for the current version, stale doc version mentions, unhedged publish/release claims, risky package.json files entries, and self-referencing script commands.",
28
+ supportedIncludeAreas: ["package", "docs", "cli"],
29
+ run: (ctx) => {
30
+ const issues = [];
31
+ const pkg = ctx.sourceOfTruth.package;
32
+ const lockfile = ctx.sourceOfTruth.lockfile;
33
+ if (ctx.config.include.includes("package") && pkg) {
34
+ if (lockfile.present && lockfile.packageManagerConsistent === false) {
35
+ if (lockfile.rootVersion !== null && lockfile.rootVersion !== pkg.version) {
36
+ issues.push(makeVersionMismatchIssue(pkg.version, lockfile.rootVersion));
37
+ }
38
+ else if (lockfile.rootName !== null && lockfile.rootName !== pkg.name) {
39
+ issues.push(makeNameMismatchIssue(pkg.name, lockfile.rootName));
40
+ }
41
+ }
42
+ if (pkg.files) {
43
+ for (const entry of pkg.files) {
44
+ if (RISKY_FILES_ENTRY_PATTERN.test(entry)) {
45
+ issues.push(makeRiskyFilesEntryIssue(entry));
46
+ }
47
+ }
48
+ }
49
+ for (const [scriptName, command] of Object.entries(pkg.scripts)) {
50
+ for (const match of command.matchAll(/npm run ([\w:.-]+)/g)) {
51
+ const referenced = match[1];
52
+ if (referenced !== scriptName && !(referenced in pkg.scripts)) {
53
+ issues.push(makeSelfReferenceIssue(scriptName, referenced));
54
+ }
55
+ }
56
+ }
57
+ }
58
+ if (ctx.config.include.includes("docs") && pkg) {
59
+ const changelogFile = ctx.inventory.docsFiles.find((f) => path.basename(f.relativePath).toLowerCase() === "changelog.md");
60
+ if (changelogFile) {
61
+ const content = readBoundedText(ctx.target.rootPath, changelogFile.relativePath, changelogFile.sizeBytes);
62
+ if (content !== null && pkg.version) {
63
+ const hasCurrentVersionSection = new RegExp(`^##\\s*\\[${escapeRegExp(pkg.version)}\\]`, "m").test(content);
64
+ if (!hasCurrentVersionSection) {
65
+ issues.push(makeMissingChangelogSectionIssue(changelogFile.relativePath, pkg.version));
66
+ }
67
+ }
68
+ }
69
+ for (const docFile of ctx.inventory.docsFiles) {
70
+ const content = readBoundedText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes);
71
+ if (content === null)
72
+ continue;
73
+ const lines = splitLines(content);
74
+ for (let i = 0; i < lines.length; i++) {
75
+ const line = lines[i];
76
+ if (pkg.version) {
77
+ for (const pattern of CURRENT_PACKAGE_VERSION_PATTERNS) {
78
+ const match = line.match(pattern);
79
+ if (match && match[1] !== pkg.version) {
80
+ issues.push(makeStaleDocVersionIssue(docFile.relativePath, i + 1, line, match[1], pkg.version));
81
+ }
82
+ }
83
+ }
84
+ if (PUBLISHED_OR_RELEASED_CLAIM_PATTERN.test(line)) {
85
+ const windowStart = Math.max(0, i - 1);
86
+ const window = lines.slice(windowStart, i + 1).join(" ");
87
+ if (!isHedgedNearby(window)) {
88
+ issues.push(makeUnsupportedPublishClaimIssue(docFile.relativePath, i + 1, line));
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ return deduplicateIssuesById(issues);
95
+ },
96
+ };
97
+ function makeVersionMismatchIssue(packageVersion, lockfileVersion) {
98
+ return makeCodeRotIssue({
99
+ auditType: "code-rot",
100
+ detectorId: DETECTOR_ID,
101
+ idCues: ["version-mismatch", packageVersion ?? "unknown", lockfileVersion],
102
+ title: "package.json and package-lock.json version mismatch",
103
+ description: `package.json version is "${packageVersion}" but package-lock.json's root version is "${lockfileVersion}".`,
104
+ severity: "blocker",
105
+ confidence: "high",
106
+ falsePositiveRisk: "low",
107
+ category: "package-release-rot",
108
+ recommendedAction: "Run npm install to regenerate package-lock.json so both versions match.",
109
+ suggestedFixStrategy: "npm install (lockfile-only update is sufficient if dependencies did not change).",
110
+ validationCommands: ["npm install --package-lock-only"],
111
+ releaseBlocking: true,
112
+ implementationBlocking: false,
113
+ autoFixEligible: false,
114
+ evidence: [
115
+ {
116
+ kind: "file",
117
+ message: "Version field mismatch between package.json and package-lock.json.",
118
+ filePath: "package-lock.json",
119
+ expected: packageVersion ?? undefined,
120
+ observed: lockfileVersion,
121
+ source: DETECTOR_ID,
122
+ confidence: "high",
123
+ },
124
+ ],
125
+ affectedFiles: ["package.json", "package-lock.json"],
126
+ });
127
+ }
128
+ function makeNameMismatchIssue(packageName, lockfileName) {
129
+ return makeCodeRotIssue({
130
+ auditType: "code-rot",
131
+ detectorId: DETECTOR_ID,
132
+ idCues: ["name-mismatch", packageName ?? "unknown", lockfileName],
133
+ title: "package.json and package-lock.json name mismatch",
134
+ description: `package.json name is "${packageName}" but package-lock.json's root name is "${lockfileName}".`,
135
+ severity: "high",
136
+ confidence: "high",
137
+ falsePositiveRisk: "low",
138
+ category: "package-release-rot",
139
+ recommendedAction: "Run npm install to regenerate package-lock.json so both names match.",
140
+ suggestedFixStrategy: "npm install (lockfile-only update is sufficient if dependencies did not change).",
141
+ validationCommands: ["npm install --package-lock-only"],
142
+ releaseBlocking: false,
143
+ implementationBlocking: false,
144
+ autoFixEligible: false,
145
+ evidence: [
146
+ {
147
+ kind: "file",
148
+ message: "Name field mismatch between package.json and package-lock.json.",
149
+ filePath: "package-lock.json",
150
+ expected: packageName ?? undefined,
151
+ observed: lockfileName,
152
+ source: DETECTOR_ID,
153
+ confidence: "high",
154
+ },
155
+ ],
156
+ affectedFiles: ["package.json", "package-lock.json"],
157
+ });
158
+ }
159
+ function makeRiskyFilesEntryIssue(entry) {
160
+ return makeCodeRotIssue({
161
+ auditType: "code-rot",
162
+ detectorId: DETECTOR_ID,
163
+ idCues: ["risky-files-entry", entry],
164
+ title: `package.json "files" field includes a risky-looking path: "${entry}"`,
165
+ description: `package.json's "files" allowlist includes "${entry}", which looks like generated or private content rather than intentional package content.`,
166
+ severity: "medium",
167
+ confidence: "medium",
168
+ falsePositiveRisk: "low",
169
+ category: "package-release-rot",
170
+ recommendedAction: `Remove "${entry}" from package.json's "files" field unless it is intentionally published.`,
171
+ suggestedFixStrategy: `Edit the "files" array in package.json to drop "${entry}".`,
172
+ validationCommands: ["npm pack --dry-run"],
173
+ releaseBlocking: false,
174
+ implementationBlocking: false,
175
+ autoFixEligible: false,
176
+ evidence: [
177
+ {
178
+ kind: "file",
179
+ message: `"files" entry matches a known risky pattern.`,
180
+ filePath: "package.json",
181
+ excerpt: entry,
182
+ source: DETECTOR_ID,
183
+ confidence: "medium",
184
+ },
185
+ ],
186
+ affectedFiles: ["package.json"],
187
+ });
188
+ }
189
+ function makeSelfReferenceIssue(scriptName, referenced) {
190
+ return makeCodeRotIssue({
191
+ auditType: "code-rot",
192
+ detectorId: DETECTOR_ID,
193
+ idCues: ["script-references-missing-script", scriptName, referenced],
194
+ title: `Script "${scriptName}" references undefined script "${referenced}"`,
195
+ description: `package.json script "${scriptName}" runs "npm run ${referenced}", but "${referenced}" is not itself a defined script.`,
196
+ severity: "high",
197
+ confidence: "high",
198
+ falsePositiveRisk: "low",
199
+ category: "package-release-rot",
200
+ recommendedAction: `Add the missing "${referenced}" script, or fix "${scriptName}" to reference an existing script.`,
201
+ suggestedFixStrategy: `Edit package.json's "${scriptName}" script.`,
202
+ validationCommands: [`npm run ${scriptName}`],
203
+ releaseBlocking: false,
204
+ implementationBlocking: false,
205
+ autoFixEligible: false,
206
+ evidence: [
207
+ {
208
+ kind: "file",
209
+ message: `Script command references a script name that does not exist.`,
210
+ filePath: "package.json",
211
+ excerpt: `"${scriptName}": "... npm run ${referenced} ..."`,
212
+ source: DETECTOR_ID,
213
+ confidence: "high",
214
+ },
215
+ ],
216
+ affectedFiles: ["package.json"],
217
+ });
218
+ }
219
+ function makeMissingChangelogSectionIssue(relativePath, currentVersion) {
220
+ return makeCodeRotIssue({
221
+ auditType: "code-rot",
222
+ detectorId: DETECTOR_ID,
223
+ idCues: [relativePath, "missing-current-version-section", currentVersion],
224
+ title: `${relativePath} has no section for the current package version (${currentVersion})`,
225
+ description: `package.json version is "${currentVersion}", but ${relativePath} has no "## [${currentVersion}]" heading.`,
226
+ severity: "high",
227
+ confidence: "high",
228
+ falsePositiveRisk: "low",
229
+ category: "package-release-rot",
230
+ recommendedAction: `Add a "## [${currentVersion}]" section to ${relativePath} describing this release.`,
231
+ suggestedFixStrategy: `Convert the top Unreleased/placeholder section in ${relativePath} into a "## [${currentVersion}]" entry.`,
232
+ validationCommands: ["npm run audit -- --types code-rot --include package,docs"],
233
+ releaseBlocking: false,
234
+ implementationBlocking: false,
235
+ autoFixEligible: false,
236
+ evidence: [
237
+ {
238
+ kind: "file",
239
+ message: `No "## [${currentVersion}]" heading found.`,
240
+ filePath: relativePath,
241
+ expected: `## [${currentVersion}]`,
242
+ source: DETECTOR_ID,
243
+ confidence: "high",
244
+ },
245
+ ],
246
+ affectedFiles: [relativePath],
247
+ });
248
+ }
249
+ function makeStaleDocVersionIssue(relativePath, lineNumber, excerpt, docVersion, currentVersion) {
250
+ return makeCodeRotIssue({
251
+ auditType: "code-rot",
252
+ detectorId: DETECTOR_ID,
253
+ idCues: [relativePath, "stale-doc-version", docVersion],
254
+ title: `${relativePath} states an outdated current version (${docVersion})`,
255
+ description: `${relativePath}:${lineNumber} describes "${docVersion}" as the current package version, but package.json says "${currentVersion}".`,
256
+ severity: "medium",
257
+ confidence: "medium",
258
+ falsePositiveRisk: "medium",
259
+ category: "package-release-rot",
260
+ recommendedAction: `Update ${relativePath} to reference the current version (${currentVersion}).`,
261
+ suggestedFixStrategy: `Replace "${docVersion}" with "${currentVersion}" in ${relativePath}.`,
262
+ validationCommands: ["npm run audit -- --types code-rot --include package,docs"],
263
+ releaseBlocking: false,
264
+ implementationBlocking: false,
265
+ autoFixEligible: false,
266
+ evidence: [
267
+ {
268
+ kind: "file",
269
+ message: `Doc-stated current version does not match package.json.`,
270
+ filePath: relativePath,
271
+ line: lineNumber,
272
+ excerpt: excerpt.trim().slice(0, 200),
273
+ expected: currentVersion,
274
+ observed: docVersion,
275
+ source: DETECTOR_ID,
276
+ confidence: "medium",
277
+ },
278
+ ],
279
+ affectedFiles: [relativePath],
280
+ });
281
+ }
282
+ function makeUnsupportedPublishClaimIssue(relativePath, lineNumber, excerpt) {
283
+ return makeCodeRotIssue({
284
+ auditType: "code-rot",
285
+ detectorId: DETECTOR_ID,
286
+ idCues: [relativePath, "unsupported-publish-claim", String(lineNumber)],
287
+ title: `${relativePath} claims publication/release/tag without local support`,
288
+ description: `${relativePath}:${lineNumber} contains an unhedged publish/release/tag claim. This is a local-only tool with no npm registry or GitHub API access, so this claim cannot be verified and is flagged for manual confirmation.`,
289
+ severity: "blocker",
290
+ confidence: "medium",
291
+ falsePositiveRisk: "medium",
292
+ category: "package-release-rot",
293
+ recommendedAction: "Confirm whether publication has actually happened; if not, correct the claim to say it has not yet been published.",
294
+ suggestedFixStrategy: `Rephrase the claim in ${relativePath} to accurately reflect current release state.`,
295
+ validationCommands: ["npm run audit -- --types code-rot --include package,docs"],
296
+ releaseBlocking: true,
297
+ implementationBlocking: false,
298
+ autoFixEligible: false,
299
+ evidence: [
300
+ {
301
+ kind: "file",
302
+ message: "Matched an unhedged publish/release/tag claim pattern with no nearby hedge word.",
303
+ filePath: relativePath,
304
+ line: lineNumber,
305
+ excerpt: excerpt.trim().slice(0, 200),
306
+ source: DETECTOR_ID,
307
+ confidence: "medium",
308
+ },
309
+ ],
310
+ affectedFiles: [relativePath],
311
+ });
312
+ }
313
+ function escapeRegExp(value) {
314
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
315
+ }
316
+ function readBoundedText(root, relativePath, sizeBytes) {
317
+ if (sizeBytes > MAX_DOC_READ_BYTES)
318
+ return null;
319
+ try {
320
+ return fs.readFileSync(path.join(root, relativePath), "utf8");
321
+ }
322
+ catch {
323
+ return null;
324
+ }
325
+ }