@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,348 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { readBoundedFileText } from "../utils/boundedRead.js";
4
+ import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
5
+ import { splitLines } from "../utils/textLines.js";
6
+ // ---------------------------------------------------------------------------
7
+ // v0.3.0 Batch 4 — test-rot detector.
8
+ //
9
+ // Bounded (per-file MAX_READ_BYTES skip), regex/structural only.
10
+ //
11
+ // Two spec bullets are intentionally SKIPPED here to avoid duplicate
12
+ // reporting with other detectors (documented in the Batch 4 spec itself):
13
+ // - "fixtures not referenced by tests" is implemented once, in
14
+ // deadCodeCandidateDetector.ts's findUnreferencedFixtures(), not here.
15
+ // - "test commands documented in docs but missing from package.json
16
+ // scripts" overlaps with staleCommandReferenceDetector.ts's existing
17
+ // doc-command check and is intentionally not re-implemented here.
18
+ // ---------------------------------------------------------------------------
19
+ const DETECTOR_ID = "test-rot";
20
+ const MAX_READ_BYTES = 200_000;
21
+ const RELATIVE_IMPORT_PATTERN = /(?:from|require\()\s*["'](\.\.?\/[^"']+)["']/g;
22
+ const NPM_RUN_PATTERN = /npm run ([\w:.-]+)/g;
23
+ const SKIP_PATTERN = /\b(describe|it|test)\.skip\s*\(/g;
24
+ const ONLY_PATTERN = /\b(describe|it|test)\.only\s*\(/g;
25
+ const TEST_TITLE_VERSION_PATTERN = /\b(describe|it|test)\s*\(\s*["'`][^"'`]*\bv(\d+\.\d+\.\d+)\b/;
26
+ const HISTORICAL_HEDGE_PATTERN = /\b(regression|historical|legacy)\b/i;
27
+ const RESOLVE_EXTENSIONS = ["", ".ts", ".tsx", ".js", ".mjs", ".cjs"];
28
+ const VITEST_RUN_ARG_PATTERN = /vitest run ([^\s&|]+)/g;
29
+ export const TEST_ROT_DETECTOR = {
30
+ id: DETECTOR_ID,
31
+ auditType: "code-rot",
32
+ title: "Test rot",
33
+ description: "Detects test files importing missing source files, stale npm-run references, committed .skip/.only usage, stale version mentions in test titles, and package scripts referencing missing test paths.",
34
+ supportedIncludeAreas: ["tests", "cli", "package"],
35
+ shouldSkip: (ctx) => {
36
+ if (!ctx.config.include.includes("tests")) {
37
+ return { skip: true, reason: "--include does not select tests; this detector's checks are test-file-based." };
38
+ }
39
+ return { skip: false };
40
+ },
41
+ run: (ctx) => {
42
+ const issues = [];
43
+ const scriptNames = new Set(ctx.sourceOfTruth.commands.allScriptNames);
44
+ const currentVersion = ctx.sourceOfTruth.package?.version ?? null;
45
+ for (const testFile of ctx.inventory.testFiles) {
46
+ const content = readBoundedFileText(ctx.target.rootPath, testFile.relativePath, testFile.sizeBytes, MAX_READ_BYTES);
47
+ if (content === null)
48
+ continue;
49
+ // tests/audits/codeRot/ is this project's own audit-detector test
50
+ // suite. Its whole purpose is to construct FIXTURE content (via
51
+ // writeFile() string arguments) that mimics the exact textual
52
+ // patterns this file's checks look for -- fake relative imports,
53
+ // fake "npm run <command>" references, "describe.skip("/"it.only(",
54
+ // and version-numbered test titles like `it("... v0.2.1 ...")`.
55
+ // Scanning those fixture strings as if they were real test-file
56
+ // content produces false positives against this repo's own accurate
57
+ // tests (confirmed via self-testing on every one of the checks
58
+ // below). This whole subdirectory is therefore excluded from this
59
+ // detector's per-file checks -- a project without an audit-detector
60
+ // test suite of its own would never hit this exclusion.
61
+ if (testFile.relativePath.startsWith("tests/audits/codeRot/"))
62
+ continue;
63
+ issues.push(...findMissingSourceImports(ctx, testFile.relativePath, content));
64
+ issues.push(...findStaleNpmRunReferences(testFile.relativePath, content, scriptNames));
65
+ issues.push(...findSkipUsage(testFile.relativePath, content));
66
+ issues.push(...findOnlyUsage(testFile.relativePath, content));
67
+ if (currentVersion)
68
+ issues.push(...findStaleVersionMentions(testFile.relativePath, content, currentVersion));
69
+ }
70
+ if (ctx.config.include.includes("package") && ctx.sourceOfTruth.package) {
71
+ issues.push(...findScriptsReferencingMissingTestPaths(ctx));
72
+ }
73
+ return deduplicateIssuesById(issues);
74
+ },
75
+ };
76
+ function findMissingSourceImports(ctx, testRelativePath, content) {
77
+ const issues = [];
78
+ const testDir = path.dirname(testRelativePath);
79
+ RELATIVE_IMPORT_PATTERN.lastIndex = 0;
80
+ for (const match of content.matchAll(RELATIVE_IMPORT_PATTERN)) {
81
+ const specifier = match[1];
82
+ const resolvedBase = path.join(testDir, specifier).replace(/\\/g, "/");
83
+ const strippedBase = resolvedBase.replace(/\.[cm]?[jt]sx?$/, "");
84
+ const resolved = RESOLVE_EXTENSIONS.some((ext) => fs.existsSync(path.join(ctx.target.rootPath, `${strippedBase}${ext}`)));
85
+ if (resolved)
86
+ continue;
87
+ issues.push(makeCodeRotIssue({
88
+ auditType: "code-rot",
89
+ detectorId: DETECTOR_ID,
90
+ idCues: [testRelativePath, "missing-source-import", specifier],
91
+ title: `Test "${testRelativePath}" imports a missing source file: "${specifier}"`,
92
+ description: `${testRelativePath} imports "${specifier}", which does not resolve to an existing file on disk (checked common TS/JS extensions relative to the test file).`,
93
+ severity: "medium",
94
+ confidence: "high",
95
+ falsePositiveRisk: "low",
96
+ category: "test-rot",
97
+ recommendedAction: "Fix or remove the stale import.",
98
+ suggestedFixStrategy: `Update the import specifier "${specifier}" in ${testRelativePath} to point to an existing file.`,
99
+ validationCommands: [`npx vitest run ${testRelativePath}`],
100
+ releaseBlocking: false,
101
+ implementationBlocking: false,
102
+ autoFixEligible: false,
103
+ evidence: [
104
+ {
105
+ kind: "file",
106
+ message: "Relative import specifier does not resolve to an existing file.",
107
+ filePath: testRelativePath,
108
+ excerpt: specifier,
109
+ source: DETECTOR_ID,
110
+ confidence: "high",
111
+ },
112
+ ],
113
+ affectedFiles: [testRelativePath],
114
+ }));
115
+ }
116
+ return issues;
117
+ }
118
+ function findStaleNpmRunReferences(testRelativePath, content, scriptNames) {
119
+ const issues = [];
120
+ const seen = new Set();
121
+ // Scanned per-line (not whole-content) so a line can be cheaply excluded
122
+ // when it's clearly constructing FIXTURE content for another detector's
123
+ // test (e.g. `writeFile(root, "README.md", "Run \`npm run fake-cmd\`
124
+ // ...")`) rather than a real reference to an npm command. This is the
125
+ // exact shape used throughout this repo's own audit-detector test files
126
+ // (see tests/audits/codeRot/*.test.ts) -- without this exclusion, this
127
+ // detector flags this repo's own accurate test suite as if its
128
+ // synthetic fixture strings were real stale command references, a false
129
+ // positive confirmed via self-testing.
130
+ const lines = splitLines(content);
131
+ for (const line of lines) {
132
+ if (/\bwriteFile\s*\(/.test(line))
133
+ continue;
134
+ NPM_RUN_PATTERN.lastIndex = 0;
135
+ for (const match of line.matchAll(NPM_RUN_PATTERN)) {
136
+ const scriptName = match[1];
137
+ if (scriptNames.has(scriptName) || seen.has(scriptName))
138
+ continue;
139
+ seen.add(scriptName);
140
+ issues.push(makeCodeRotIssue({
141
+ auditType: "code-rot",
142
+ detectorId: DETECTOR_ID,
143
+ idCues: [testRelativePath, "stale-npm-run-reference", scriptName],
144
+ title: `Test "${testRelativePath}" references "npm run ${scriptName}", which is not a current package.json script`,
145
+ description: `${testRelativePath} references "npm run ${scriptName}" but no such script exists in package.json.`,
146
+ severity: "medium",
147
+ confidence: "medium",
148
+ falsePositiveRisk: "medium",
149
+ category: "test-rot",
150
+ recommendedAction: "Update the test to reference a current script name, or add the missing script.",
151
+ suggestedFixStrategy: `Correct the "npm run ${scriptName}" reference in ${testRelativePath}.`,
152
+ validationCommands: [`npx vitest run ${testRelativePath}`],
153
+ releaseBlocking: false,
154
+ implementationBlocking: false,
155
+ autoFixEligible: false,
156
+ evidence: [
157
+ {
158
+ kind: "file",
159
+ message: `Referenced command "npm run ${scriptName}" not found in package.json scripts.`,
160
+ filePath: testRelativePath,
161
+ source: DETECTOR_ID,
162
+ confidence: "medium",
163
+ },
164
+ ],
165
+ affectedFiles: [testRelativePath],
166
+ }));
167
+ }
168
+ }
169
+ return issues;
170
+ }
171
+ function findSkipUsage(testRelativePath, content) {
172
+ const issues = [];
173
+ const lines = splitLines(content);
174
+ for (let i = 0; i < lines.length; i++) {
175
+ SKIP_PATTERN.lastIndex = 0;
176
+ const match = SKIP_PATTERN.exec(lines[i]);
177
+ if (!match)
178
+ continue;
179
+ issues.push(makeCodeRotIssue({
180
+ auditType: "code-rot",
181
+ detectorId: DETECTOR_ID,
182
+ idCues: [testRelativePath, "skip-usage", String(i + 1)],
183
+ title: `Committed "${match[1]}.skip(" in "${testRelativePath}"`,
184
+ description: `${testRelativePath}:${i + 1} uses "${match[1]}.skip(" -- confirm this is intentional and not accidentally committed.`,
185
+ severity: "medium",
186
+ confidence: "high",
187
+ falsePositiveRisk: "low",
188
+ category: "test-rot",
189
+ recommendedAction: "Remove .skip if the test should run, or add a comment explaining why it is skipped.",
190
+ suggestedFixStrategy: `Remove "${match[1]}.skip(" at ${testRelativePath}:${i + 1} or document why it is intentionally skipped.`,
191
+ validationCommands: [`npx vitest run ${testRelativePath}`],
192
+ releaseBlocking: false,
193
+ implementationBlocking: false,
194
+ autoFixEligible: false,
195
+ evidence: [
196
+ {
197
+ kind: "file",
198
+ message: `Found "${match[1]}.skip(" usage.`,
199
+ filePath: testRelativePath,
200
+ line: i + 1,
201
+ excerpt: lines[i].trim().slice(0, 200),
202
+ source: DETECTOR_ID,
203
+ confidence: "high",
204
+ },
205
+ ],
206
+ affectedFiles: [testRelativePath],
207
+ }));
208
+ }
209
+ return issues;
210
+ }
211
+ function findOnlyUsage(testRelativePath, content) {
212
+ const issues = [];
213
+ const lines = splitLines(content);
214
+ for (let i = 0; i < lines.length; i++) {
215
+ ONLY_PATTERN.lastIndex = 0;
216
+ const match = ONLY_PATTERN.exec(lines[i]);
217
+ if (!match)
218
+ continue;
219
+ issues.push(makeCodeRotIssue({
220
+ auditType: "code-rot",
221
+ detectorId: DETECTOR_ID,
222
+ idCues: [testRelativePath, "only-usage", String(i + 1)],
223
+ title: `Committed "${match[1]}.only(" in "${testRelativePath}"`,
224
+ description: `${testRelativePath}:${i + 1} uses "${match[1]}.only(" -- this silently excludes every other test in a CI run and is almost always accidental.`,
225
+ severity: "high",
226
+ confidence: "high",
227
+ falsePositiveRisk: "low",
228
+ category: "test-rot",
229
+ recommendedAction: "Remove .only before committing/merging.",
230
+ suggestedFixStrategy: `Remove "${match[1]}.only(" at ${testRelativePath}:${i + 1}.`,
231
+ validationCommands: [`npx vitest run ${testRelativePath}`],
232
+ releaseBlocking: true,
233
+ implementationBlocking: false,
234
+ autoFixEligible: false,
235
+ evidence: [
236
+ {
237
+ kind: "file",
238
+ message: `Found "${match[1]}.only(" usage.`,
239
+ filePath: testRelativePath,
240
+ line: i + 1,
241
+ excerpt: lines[i].trim().slice(0, 200),
242
+ source: DETECTOR_ID,
243
+ confidence: "high",
244
+ },
245
+ ],
246
+ affectedFiles: [testRelativePath],
247
+ }));
248
+ }
249
+ return issues;
250
+ }
251
+ function findStaleVersionMentions(testRelativePath, content, currentVersion) {
252
+ const issues = [];
253
+ const lines = splitLines(content);
254
+ for (let i = 0; i < lines.length; i++) {
255
+ const line = lines[i];
256
+ const match = line.match(TEST_TITLE_VERSION_PATTERN);
257
+ if (!match)
258
+ continue;
259
+ const mentionedVersion = match[2];
260
+ if (mentionedVersion === currentVersion)
261
+ continue;
262
+ if (HISTORICAL_HEDGE_PATTERN.test(line))
263
+ continue;
264
+ issues.push(makeCodeRotIssue({
265
+ auditType: "code-rot",
266
+ detectorId: DETECTOR_ID,
267
+ idCues: [testRelativePath, "stale-version-mention", String(i + 1), mentionedVersion],
268
+ title: `Test title in "${testRelativePath}" mentions version v${mentionedVersion}, not the current package version`,
269
+ description: `${testRelativePath}:${i + 1} mentions "v${mentionedVersion}" in a test title, but the current package version is "${currentVersion}". If this is intentionally a historical/regression reference, mention that in the title.`,
270
+ severity: "low",
271
+ confidence: "low",
272
+ falsePositiveRisk: "medium",
273
+ category: "test-rot",
274
+ recommendedAction: "Confirm whether the version mention should be updated or is intentionally historical.",
275
+ suggestedFixStrategy: `Update the test title in ${testRelativePath} or add "regression"/"historical" wording if intentional.`,
276
+ validationCommands: [`npx vitest run ${testRelativePath}`],
277
+ releaseBlocking: false,
278
+ implementationBlocking: false,
279
+ autoFixEligible: false,
280
+ evidence: [
281
+ {
282
+ kind: "file",
283
+ message: `Test title mentions "v${mentionedVersion}", current package version is "${currentVersion}".`,
284
+ filePath: testRelativePath,
285
+ line: i + 1,
286
+ excerpt: line.trim().slice(0, 200),
287
+ source: DETECTOR_ID,
288
+ confidence: "low",
289
+ },
290
+ ],
291
+ affectedFiles: [testRelativePath],
292
+ }));
293
+ }
294
+ return issues;
295
+ }
296
+ function findScriptsReferencingMissingTestPaths(ctx) {
297
+ const issues = [];
298
+ const pkg = ctx.sourceOfTruth.package;
299
+ if (!pkg)
300
+ return issues;
301
+ const knownPaths = new Set(ctx.inventory.files.map((f) => f.relativePath));
302
+ const knownDirPrefixes = new Set(ctx.inventory.files.flatMap((f) => {
303
+ const parts = f.relativePath.split("/");
304
+ const prefixes = [];
305
+ for (let i = 1; i < parts.length; i++)
306
+ prefixes.push(parts.slice(0, i).join("/"));
307
+ return prefixes;
308
+ }));
309
+ for (const [scriptName, command] of Object.entries(pkg.scripts)) {
310
+ VITEST_RUN_ARG_PATTERN.lastIndex = 0;
311
+ for (const match of command.matchAll(VITEST_RUN_ARG_PATTERN)) {
312
+ const referencedPath = match[1].replace(/^["']|["']$/g, "");
313
+ if (knownPaths.has(referencedPath) || knownDirPrefixes.has(referencedPath))
314
+ continue;
315
+ if (fs.existsSync(path.join(ctx.target.rootPath, referencedPath)))
316
+ continue;
317
+ issues.push(makeCodeRotIssue({
318
+ auditType: "code-rot",
319
+ detectorId: DETECTOR_ID,
320
+ idCues: ["script-references-missing-test-path", scriptName, referencedPath],
321
+ title: `Script "${scriptName}" references a test path that does not exist: "${referencedPath}"`,
322
+ description: `package.json script "${scriptName}" runs "vitest run ${referencedPath}", but "${referencedPath}" does not exist in the project.`,
323
+ severity: "medium",
324
+ confidence: "high",
325
+ falsePositiveRisk: "low",
326
+ category: "test-rot",
327
+ recommendedAction: "Fix the script's test path, or restore/remove the referenced tests.",
328
+ suggestedFixStrategy: `Edit package.json's "${scriptName}" script to reference an existing test path.`,
329
+ validationCommands: [`npm run ${scriptName}`],
330
+ releaseBlocking: false,
331
+ implementationBlocking: false,
332
+ autoFixEligible: false,
333
+ evidence: [
334
+ {
335
+ kind: "file",
336
+ message: `Script command references a test path that does not exist.`,
337
+ filePath: "package.json",
338
+ excerpt: `"${scriptName}": "... vitest run ${referencedPath} ..."`,
339
+ source: DETECTOR_ID,
340
+ confidence: "high",
341
+ },
342
+ ],
343
+ affectedFiles: ["package.json"],
344
+ }));
345
+ }
346
+ }
347
+ return issues;
348
+ }
@@ -0,0 +1,23 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ // ---------------------------------------------------------------------------
4
+ // v0.3.0 Batch 4 — shared bounded file-read helper.
5
+ //
6
+ // Every Batch 3 detector duplicated its own local readBoundedText() helper.
7
+ // Batch 4 factors the identical logic out once so the seven new detectors
8
+ // don't each re-implement it. Behavior is unchanged: returns null (never
9
+ // throws) for an oversized file or an unreadable path.
10
+ // ---------------------------------------------------------------------------
11
+ export function readBoundedFileText(root, relativePath, sizeBytes, maxBytes) {
12
+ if (sizeBytes > maxBytes)
13
+ return null;
14
+ try {
15
+ return fs.readFileSync(path.join(root, relativePath), "utf8");
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ export function escapeRegExp(value) {
22
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23
+ }
@@ -0,0 +1,62 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.3.0 Batch 3 — deterministic doc-claim regex patterns.
3
+ //
4
+ // Shared by docsCodeMismatchDetector.ts and packageReleaseRotDetector.ts.
5
+ // Every pattern here matches a narrow, explicit phrase rather than inferring
6
+ // meaning from prose -- per Batch 3 scope, no LLM and no broad semantic
7
+ // interpretation. Ambiguous or ISF-style hedged wording ("not yet
8
+ // published", "planned for a later version") deliberately does not match
9
+ // these "unhedged claim" patterns.
10
+ // ---------------------------------------------------------------------------
11
+ // An unhedged claim that publication/release/tagging has already happened.
12
+ // A nearby hedge word ("not yet", "has not", "no") should be checked by the
13
+ // caller before treating a match as a real claim -- see
14
+ // isHedgedNearby() below.
15
+ export const PUBLISHED_OR_RELEASED_CLAIM_PATTERN = /\b(is now published|has been published to npm|npm publish (?:has |)(?:completed|succeeded)|published to npm|github release (?:has been created|exists|created)|release has been created|has been tagged|git tag (?:has been created|created))\b/i;
16
+ // Common hedge words that, if present in the same line/window, indicate the
17
+ // author is explicitly describing something as NOT having happened yet --
18
+ // used to suppress a false match on "is now published" when the actual
19
+ // sentence is "... has not been published to npm" etc.
20
+ const HEDGE_PATTERN = /\b(not yet|has not|have not|hasn't|haven't|no(?:t)?\b.{0,20}\byet\b|never)\b/i;
21
+ export function isHedgedNearby(windowText) {
22
+ return HEDGE_PATTERN.test(windowText);
23
+ }
24
+ // Matches "current package is v1.2.3" / "current version is `1.2.3`" /
25
+ // "package is version 1.2.3" style phrasing used across this project's own
26
+ // docs (README.md, docs/CURRENT_STATE.md, docs/PROJECT_OVERVIEW.md).
27
+ export const CURRENT_PACKAGE_VERSION_PATTERNS = [
28
+ /current package is `?v?(\d+\.\d+\.\d+)`?/i,
29
+ /current package (?:version )?(?:is|remains) `?v?(\d+\.\d+\.\d+)`?/i,
30
+ /package is version `?(\d+\.\d+\.\d+)`?/i,
31
+ ];
32
+ // A scoped (non-full) --checks run being described as if it were a complete
33
+ // release gate. Matches the bare phrase; callers must additionally check
34
+ // isNegatedNearby() before treating a match as a real (mistaken) claim --
35
+ // this project's own docs correctly say things like "rather than described
36
+ // as a full release gate" and "it is not the same as a full release gate",
37
+ // which contain the phrase precisely BECAUSE they are correctly denying it.
38
+ export const FULL_RELEASE_GATE_FOR_SCOPED_CHECKS_PATTERN = /scoped[^.\n]{0,80}(full release gate|full audit coverage|complete release gate)/i;
39
+ // Negation cues that, appearing in the same sentence as a
40
+ // FULL_RELEASE_GATE_FOR_SCOPED_CHECKS_PATTERN match, indicate the sentence
41
+ // is correctly denying the claim rather than making it.
42
+ const NEGATION_PATTERN = /\b(not|rather than|isn't|is not|n't)\b/i;
43
+ export function isNegatedNearby(windowText) {
44
+ return NEGATION_PATTERN.test(windowText);
45
+ }
46
+ // Feature-implementation claim phrasing: "<subject> is implemented" /
47
+ // "<subject> is current" vs "<subject> is planned" / "<subject> is future".
48
+ //
49
+ // Deliberately narrow (subject-anchored, not a freestanding scan for
50
+ // "current"/"planned" anywhere in a line) -- this project's own docs
51
+ // legitimately use both words in the same sentence when correctly
52
+ // describing a roadmap (e.g. "separates the implemented baseline from
53
+ // planned work"), and a broad co-occurrence scan would flag that as
54
+ // ambiguous noise. Anchoring both patterns to the same feature subject
55
+ // keeps false positives near zero: a real ambiguous match requires the
56
+ // SAME subject to be described as both implemented and planned.
57
+ export function currentClaimPatternFor(subjectPattern) {
58
+ return new RegExp(`${subjectPattern}[^.\\n]{0,40}\\b(is implemented|is current|is now implemented)\\b`, "i");
59
+ }
60
+ export function plannedClaimPatternFor(subjectPattern) {
61
+ return new RegExp(`${subjectPattern}[^.\\n]{0,40}\\b(is planned|is future|is not yet implemented|not implemented)\\b`, "i");
62
+ }
@@ -0,0 +1,64 @@
1
+ import { splitLines } from "./textLines.js";
2
+ // Skips optional leading "npm run" flags (e.g. "npm run --silent <script>")
3
+ // before capturing the actual script name -- without this, "--silent" itself
4
+ // gets captured as if it were the script name.
5
+ const NPM_RUN_PATTERN = /npm run (?:--\S+\s+)*([\w:.-]+)/g;
6
+ // Cues that mark a nearby command reference as planned/future/intentionally-
7
+ // unimplemented rather than a current, working example. Deliberately
8
+ // conservative wording match (whole word/phrase), not broad semantic
9
+ // inference. "avoid" covers this project's own "commands to avoid unless
10
+ // truly necessary" roadmap convention for commands that are deliberately
11
+ // never meant to exist.
12
+ const PLANNED_LABEL_PATTERN = /\b(planned|future|roadmap|not implemented|not yet implemented|later version|coming soon|avoid)\b/i;
13
+ // How many lines above the reference line to include when checking for a
14
+ // planned-label cue, in addition to the enclosing-heading check below.
15
+ // Covers a preceding sentence or list-intro line that isn't itself a
16
+ // markdown heading (e.g. "Commands to avoid unless truly necessary:") and
17
+ // may be more than a few lines above the last item in a long bullet list.
18
+ const PLANNED_LABEL_WINDOW_LINES = 15;
19
+ const HEADING_PATTERN = /^(#{1,6})\s+(.*)$/;
20
+ export function extractDocCommandReferences(content) {
21
+ const lines = splitLines(content);
22
+ const refs = [];
23
+ const seenOnLine = new Set();
24
+ // Tracks the most recently seen heading text at each markdown heading
25
+ // depth (1-6) as we scan top to bottom. A reference nested arbitrarily
26
+ // deep under a "## Planned ..." section inherits that section's framing
27
+ // even when the nearest local text (a sibling "### v0.4.0 ..." heading,
28
+ // say) doesn't itself contain a planned-label word -- this is what makes
29
+ // the check robust to roadmap documents with many nested version
30
+ // subsections, without an unbounded backward text scan.
31
+ const headingStack = new Array(7).fill(undefined);
32
+ for (let i = 0; i < lines.length; i++) {
33
+ const line = lines[i];
34
+ const headingMatch = line.match(HEADING_PATTERN);
35
+ if (headingMatch) {
36
+ const depth = headingMatch[1].length;
37
+ headingStack[depth] = headingMatch[2];
38
+ // A new heading at depth N implicitly starts a fresh subsection for
39
+ // every deeper level -- clear them so a later, unrelated deep
40
+ // heading under a different parent doesn't leak stale context.
41
+ for (let d = depth + 1; d < headingStack.length; d++)
42
+ headingStack[d] = undefined;
43
+ }
44
+ NPM_RUN_PATTERN.lastIndex = 0;
45
+ for (const match of line.matchAll(NPM_RUN_PATTERN)) {
46
+ const command = match[1];
47
+ const key = `${i}:${command}`;
48
+ if (seenOnLine.has(key))
49
+ continue;
50
+ seenOnLine.add(key);
51
+ const windowStart = Math.max(0, i - PLANNED_LABEL_WINDOW_LINES);
52
+ const localWindow = lines.slice(windowStart, i + 1).join(" ");
53
+ const headingContext = headingStack.filter((h) => h !== undefined).join(" ");
54
+ const combined = `${headingContext} ${localWindow}`;
55
+ refs.push({
56
+ command,
57
+ lineNumber: i + 1,
58
+ excerpt: line.trim().slice(0, 200),
59
+ isLabeledPlanned: PLANNED_LABEL_PATTERN.test(combined),
60
+ });
61
+ }
62
+ }
63
+ return refs;
64
+ }
@@ -0,0 +1,47 @@
1
+ import path from "node:path";
2
+ // ---------------------------------------------------------------------------
3
+ // v0.3.0 Batch 4 — shared file-path pattern helpers.
4
+ //
5
+ // Small, deterministic path helpers reused by the Batch 4 candidate-based
6
+ // detectors (duplicate implementation, dead code, architecture drift).
7
+ // Deliberately forward-slash only -- inventory relativePath values are
8
+ // already forward-slash normalized by projectInventory.ts.
9
+ // ---------------------------------------------------------------------------
10
+ // Returns the first path segment (e.g. "src/audits/foo.ts" -> "src") or null
11
+ // for a root-level file with no directory segment.
12
+ export function firstPathSegment(relativePath) {
13
+ const idx = relativePath.indexOf("/");
14
+ return idx === -1 ? null : relativePath.slice(0, idx);
15
+ }
16
+ // Returns the first two path segments joined (e.g. "src/audits/foo.ts" ->
17
+ // "src/audits"), or the single segment if there is no second one.
18
+ export function firstTwoPathSegments(relativePath) {
19
+ const parts = relativePath.split("/");
20
+ return parts.length <= 2 ? (parts[0] ?? relativePath) : `${parts[0]}/${parts[1]}`;
21
+ }
22
+ export function baseNameNoExt(relativePath) {
23
+ const basename = path.basename(relativePath);
24
+ const ext = path.extname(basename);
25
+ return ext ? basename.slice(0, -ext.length) : basename;
26
+ }
27
+ // Common, generic infra-ish basenames that are expected to repeat across
28
+ // unrelated directories legitimately (barrels, shared type files, etc.) --
29
+ // excluded from the "same basename across unrelated roots" duplicate-
30
+ // candidate heuristic to keep false positives near zero.
31
+ export const GENERIC_INFRA_BASENAMES = new Set([
32
+ "index",
33
+ "types",
34
+ "type",
35
+ "config",
36
+ "utils",
37
+ "util",
38
+ "helpers",
39
+ "helper",
40
+ "constants",
41
+ "constant",
42
+ "setup",
43
+ "fixtures",
44
+ "fixture",
45
+ "errors",
46
+ "error",
47
+ ]);
@@ -0,0 +1,38 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.3.0 Batch 3 — shared code-rot issue construction helpers.
3
+ //
4
+ // Issue ids are content-addressed (detectorId + a stable, relative-path-only
5
+ // cue) rather than counter-based, so the same underlying problem always
6
+ // produces the same id across runs -- this is what makes de-duplication and
7
+ // "stable issue ids" (Batch 3 spec 3.8) trivial: two candidate issues with
8
+ // the same id are the same issue.
9
+ // ---------------------------------------------------------------------------
10
+ // Builds a deterministic id from a detector id and one or more relative-path
11
+ // or short-string cues. Never embeds an absolute path -- callers must pass
12
+ // already-relative paths (e.g. from InventoryFileEntry.relativePath).
13
+ export function makeIssueId(detectorId, ...cues) {
14
+ const sanitizedCues = cues.map((cue) => cue.replace(/\\/g, "/"));
15
+ return [detectorId, ...sanitizedCues].join(":");
16
+ }
17
+ export function makeCodeRotIssue(input) {
18
+ const { idCues, evidence, affectedFiles, ...rest } = input;
19
+ return {
20
+ ...rest,
21
+ id: makeIssueId(rest.detectorId, ...idCues),
22
+ evidence: evidence ?? [],
23
+ affectedFiles: affectedFiles ?? [],
24
+ };
25
+ }
26
+ // Deduplicates a list of already-built issues by id, keeping the first
27
+ // occurrence and preserving overall array order (stable de-duplication).
28
+ export function deduplicateIssuesById(issues) {
29
+ const seen = new Set();
30
+ const result = [];
31
+ for (const issue of issues) {
32
+ if (seen.has(issue.id))
33
+ continue;
34
+ seen.add(issue.id);
35
+ result.push(issue);
36
+ }
37
+ return result;
38
+ }
@@ -0,0 +1,12 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.3.0 Batch 3 (moved in Batch 6) — shared line-splitting helper.
3
+ //
4
+ // The canonical implementation now lives in src/audits/core/textLines.ts
5
+ // (core is lower-level than codeRot, and src/audits/core/sourceOfTruth.ts
6
+ // needed the same helper -- see that file's header comment). This module is
7
+ // kept as a transparent re-export so every existing codeRot detector/util
8
+ // that imports `splitLines` from "../utils/textLines.js" (or
9
+ // "./textLines.js") keeps working unchanged -- named-export re-exports are
10
+ // transparent to importers, there is no behavior difference.
11
+ // ---------------------------------------------------------------------------
12
+ export { splitLines } from "../../core/textLines.js";