@dailephd/my-dev-kit-lab 0.2.2 → 0.3.1

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 (42) hide show
  1. package/README.md +17 -7
  2. package/dist/scripts/audits/runAudit.js +102 -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 +377 -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 +350 -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 +410 -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 +79 -0
  18. package/dist/src/audits/codeRot/utils/issueFactories.js +38 -0
  19. package/dist/src/audits/codeRot/utils/sourceFactsLookup.js +53 -0
  20. package/dist/src/audits/codeRot/utils/textLines.js +12 -0
  21. package/dist/src/audits/core/auditConfig.js +125 -0
  22. package/dist/src/audits/core/auditExitCode.js +75 -0
  23. package/dist/src/audits/core/auditIssue.js +20 -0
  24. package/dist/src/audits/core/auditRegistry.js +40 -0
  25. package/dist/src/audits/core/auditRunner.js +74 -0
  26. package/dist/src/audits/core/auditTarget.js +19 -0
  27. package/dist/src/audits/core/auditTypes.js +30 -0
  28. package/dist/src/audits/core/collectSourceFacts.js +142 -0
  29. package/dist/src/audits/core/languageAnalyzerRegistry.js +33 -0
  30. package/dist/src/audits/core/projectInventory.js +458 -0
  31. package/dist/src/audits/core/sourceFacts.js +17 -0
  32. package/dist/src/audits/core/sourceOfTruth.js +339 -0
  33. package/dist/src/audits/core/textLines.js +24 -0
  34. package/dist/src/audits/core/typescriptJavaScriptAnalyzer.js +299 -0
  35. package/dist/src/audits/report/auditReportModel.js +253 -0
  36. package/dist/src/audits/report/auditReportPaths.js +13 -0
  37. package/dist/src/audits/report/renderAuditJsonReport.js +30 -0
  38. package/dist/src/audits/report/renderAuditTextReport.js +181 -0
  39. package/dist/src/audits/report/sanitizeAuditText.js +39 -0
  40. package/dist/src/audits/report/writeAuditReports.js +27 -0
  41. package/dist/src/core/resolveCommand.js +53 -2
  42. package/package.json +10 -6
@@ -0,0 +1,385 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { currentClaimPatternFor, plannedClaimPatternFor } 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 — security/validation assumption-rot detector.
8
+ //
9
+ // Narrow regex checks on doc content only -- no LLM, no broad semantic
10
+ // judgment. This detector is deliberately conservative: it was run against
11
+ // this repo's own real docs during development and every sub-check below
12
+ // was verified to produce zero findings against this repo's current,
13
+ // hedged docs (see the detector's own test file for the self-scan
14
+ // regression guard).
15
+ //
16
+ // Explicitly SKIPPED (spec-documented, not implemented in this batch):
17
+ // - "docs describing a scoped `--checks` run as a full release gate" is
18
+ // owned entirely by docsCodeMismatchDetector.ts's
19
+ // FULL_RELEASE_GATE_FOR_SCOPED_CHECKS_PATTERN check -- not duplicated
20
+ // here.
21
+ // - "stale security:validate --flag doc check" is SKIPPED: there is no
22
+ // cheap, deterministic source of the real current flag set for
23
+ // security:validate (would require parsing
24
+ // scripts/security/validate.ts's own CLI option parser), so this batch
25
+ // cannot verify a doc-mentioned flag against ground truth without
26
+ // re-implementing that parser here. Left for a future batch.
27
+ // - the optional "imprecise caveat" catch-all (absolute-sounding word
28
+ // with no hedge, outside the specific patterns below) is SKIPPED as
29
+ // off-by-default per the spec's own guidance -- it was too easy to
30
+ // produce false positives against legitimate, confident (but accurate)
31
+ // prose without a real semantic check, so it is not implemented.
32
+ // ---------------------------------------------------------------------------
33
+ const DETECTOR_ID = "security-validation-assumption-rot";
34
+ const MAX_READ_BYTES = 200_000;
35
+ const TOOL_PASSED_WITHOUT_HEDGE_PATTERN = /\b(codeql|semgrep|osv-scanner)\b[^.\n]{0,60}\bpassed\b/i;
36
+ const HEDGE_NEARBY_PATTERN = /\b(optional|skipped|if available|when available|unavailable|absent)\b/i;
37
+ const SKIPPED_EQUALS_PASSED_PATTERN = /\bskipped\b[^.\n]{0,60}\b(equivalent to|same as|counts as)\b[^.\n]{0,20}\bpassed\b/i;
38
+ const EXHAUSTIVE_SECRET_CLAIM_PATTERN = /\b(exhaustive|complete|comprehensive)\b[^.\n]{0,30}\bsecret\b/i;
39
+ const COMPLETE_NETWORK_ISOLATION_PATTERN = /\b(complete|full|guaranteed)\b[^.\n]{0,30}\bnetwork isolation\b|\bguarantees? no network\b/i;
40
+ const V021_SECURITY_CONTEXT_PATTERN = /\bv0\.2\.1\b/;
41
+ const SECURITY_CONTEXT_LINE_PATTERN = /\bsecurity\b/i;
42
+ // Negation/hedge words that, appearing near an "exhaustive/complete secret"
43
+ // or "complete network isolation" match, indicate the sentence is correctly
44
+ // DENYING the claim (e.g. "cannot prove exhaustive secret absence") rather
45
+ // than making it. Discovered via a real false positive against this repo's
46
+ // own docs/security-validation-framework.md, which correctly says "cannot
47
+ // prove exhaustive secret absence".
48
+ const CLAIM_NEGATION_NEARBY_PATTERN = /\b(cannot|can't|not|never|no|bounded|does not|doesn't)\b/i;
49
+ // Historical/superseded framing that demotes an old-version security
50
+ // mention from "stale current-state claim" to "correctly historical" --
51
+ // e.g. changelog entries and roadmap "previous baseline" sections.
52
+ const HISTORICAL_VERSION_FRAMING_PATTERN = /\b(previous|completed|historical|superseded|prior baseline)\b/i;
53
+ export const SECURITY_VALIDATION_ASSUMPTION_ROT_DETECTOR = {
54
+ id: DETECTOR_ID,
55
+ auditType: "code-rot",
56
+ title: "Security/validation assumption rot",
57
+ description: "Detects docs that describe optional/skipped security tools as passed, claim exhaustive secret detection or complete network isolation, claim manual pentest/Android validation as current, reference stale pre-current security versions, or describe attack scenarios as planned when they are already implemented.",
58
+ supportedIncludeAreas: ["docs", "cli", "package"],
59
+ shouldSkip: (ctx) => {
60
+ if (!ctx.config.include.includes("docs")) {
61
+ return { skip: true, reason: "--include does not select docs; this detector's checks are docs-based." };
62
+ }
63
+ return { skip: false };
64
+ },
65
+ run: (ctx) => {
66
+ const issues = [];
67
+ const pkgVersion = ctx.sourceOfTruth.package?.version ?? null;
68
+ const hasSecurityValidationSourceDir = ctx.sourceOfTruth.security.hasSecurityValidationSourceDir;
69
+ const hasAttackScenariosDir = fs.existsSync(path.join(ctx.target.rootPath, "src", "securityValidation", "attackScenarios"));
70
+ for (const docFile of ctx.inventory.docsFiles) {
71
+ const content = readBoundedFileText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes, MAX_READ_BYTES);
72
+ if (content === null)
73
+ continue;
74
+ issues.push(...findToolPassedWithoutHedge(docFile.relativePath, content));
75
+ issues.push(...findSkippedEqualsPassed(docFile.relativePath, content));
76
+ issues.push(...findExhaustiveSecretClaim(docFile.relativePath, content));
77
+ issues.push(...findCompleteNetworkIsolationClaim(docFile.relativePath, content));
78
+ issues.push(...findUnsupportedCurrentClaims(docFile.relativePath, content));
79
+ if (pkgVersion)
80
+ issues.push(...findStaleV021SecurityMention(docFile.relativePath, content, pkgVersion));
81
+ if (hasSecurityValidationSourceDir && hasAttackScenariosDir) {
82
+ issues.push(...findAttackScenariosDescribedAsPlanned(docFile.relativePath, content));
83
+ }
84
+ }
85
+ return deduplicateIssuesById(issues);
86
+ },
87
+ };
88
+ function findToolPassedWithoutHedge(relativePath, content) {
89
+ const match = TOOL_PASSED_WITHOUT_HEDGE_PATTERN.exec(content);
90
+ if (!match)
91
+ return [];
92
+ const matchIndex = match.index;
93
+ const window = content.slice(Math.max(0, matchIndex - 80), Math.min(content.length, matchIndex + match[0].length + 80));
94
+ if (HEDGE_NEARBY_PATTERN.test(window))
95
+ return [];
96
+ return [
97
+ makeCodeRotIssue({
98
+ auditType: "code-rot",
99
+ detectorId: DETECTOR_ID,
100
+ idCues: [relativePath, "tool-passed-without-hedge"],
101
+ title: `${relativePath} claims an optional security tool "passed" without a hedge word nearby`,
102
+ description: `${relativePath} says "${match[0]}" without a nearby "optional"/"skipped"/"if available" hedge. Optional tools (CodeQL/Semgrep/OSV-Scanner) can be absent and reported as skipped -- describing them as unconditionally "passed" is misleading.`,
103
+ severity: "high",
104
+ confidence: "medium",
105
+ falsePositiveRisk: "medium",
106
+ category: "security-validation-assumption-rot",
107
+ recommendedAction: `Add a hedge ("optional", "when available") near this claim in ${relativePath}, or clarify that a skip is not a pass.`,
108
+ suggestedFixStrategy: `Rephrase the claim in ${relativePath} to distinguish "passed" from "skipped (optional tool unavailable)".`,
109
+ validationCommands: ["npm run audit -- --types code-rot --include docs"],
110
+ releaseBlocking: false,
111
+ implementationBlocking: false,
112
+ autoFixEligible: false,
113
+ evidence: [
114
+ {
115
+ kind: "file",
116
+ message: "Matched an unhedged optional-tool-passed claim pattern.",
117
+ filePath: relativePath,
118
+ excerpt: match[0].slice(0, 200),
119
+ source: DETECTOR_ID,
120
+ confidence: "medium",
121
+ },
122
+ ],
123
+ affectedFiles: [relativePath],
124
+ }),
125
+ ];
126
+ }
127
+ function findSkippedEqualsPassed(relativePath, content) {
128
+ const match = SKIPPED_EQUALS_PASSED_PATTERN.exec(content);
129
+ if (!match)
130
+ return [];
131
+ return [
132
+ makeCodeRotIssue({
133
+ auditType: "code-rot",
134
+ detectorId: DETECTOR_ID,
135
+ idCues: [relativePath, "skipped-equals-passed"],
136
+ title: `${relativePath} equates a skipped check with a passed check`,
137
+ description: `${relativePath} says "${match[0]}" -- a skipped optional check must never be described as equivalent to a passed check.`,
138
+ severity: "high",
139
+ confidence: "high",
140
+ falsePositiveRisk: "low",
141
+ category: "security-validation-assumption-rot",
142
+ recommendedAction: `Correct ${relativePath} to clearly state that a skipped check is not the same as a passed check.`,
143
+ suggestedFixStrategy: `Remove or rephrase the "skipped ... equivalent to ... passed" wording in ${relativePath}.`,
144
+ validationCommands: ["npm run audit -- --types code-rot --include docs"],
145
+ releaseBlocking: false,
146
+ implementationBlocking: false,
147
+ autoFixEligible: false,
148
+ evidence: [
149
+ {
150
+ kind: "file",
151
+ message: "Matched a skipped-equals-passed phrasing pattern.",
152
+ filePath: relativePath,
153
+ excerpt: match[0].slice(0, 200),
154
+ source: DETECTOR_ID,
155
+ confidence: "high",
156
+ },
157
+ ],
158
+ affectedFiles: [relativePath],
159
+ }),
160
+ ];
161
+ }
162
+ function findExhaustiveSecretClaim(relativePath, content) {
163
+ const match = EXHAUSTIVE_SECRET_CLAIM_PATTERN.exec(content);
164
+ if (!match)
165
+ return [];
166
+ const matchIndex = match.index;
167
+ const window = content.slice(Math.max(0, matchIndex - 60), Math.min(content.length, matchIndex + match[0].length + 20));
168
+ if (CLAIM_NEGATION_NEARBY_PATTERN.test(window))
169
+ return [];
170
+ return [
171
+ makeCodeRotIssue({
172
+ auditType: "code-rot",
173
+ detectorId: DETECTOR_ID,
174
+ idCues: [relativePath, "exhaustive-secret-claim"],
175
+ title: `${relativePath} claims exhaustive/complete secret detection`,
176
+ description: `${relativePath} says "${match[0]}" -- secret scanning in this framework is bounded, not exhaustive, per its own documented limitations.`,
177
+ severity: "high",
178
+ confidence: "medium",
179
+ falsePositiveRisk: "medium",
180
+ category: "security-validation-assumption-rot",
181
+ recommendedAction: `Rephrase ${relativePath} to describe secret scanning as bounded rather than exhaustive/complete.`,
182
+ suggestedFixStrategy: `Replace the exhaustive/complete claim in ${relativePath} with hedged wording matching the framework's actual scope.`,
183
+ validationCommands: ["npm run audit -- --types code-rot --include docs"],
184
+ releaseBlocking: false,
185
+ implementationBlocking: false,
186
+ autoFixEligible: false,
187
+ evidence: [
188
+ {
189
+ kind: "file",
190
+ message: "Matched an exhaustive/complete secret-detection claim pattern.",
191
+ filePath: relativePath,
192
+ excerpt: match[0].slice(0, 200),
193
+ source: DETECTOR_ID,
194
+ confidence: "medium",
195
+ },
196
+ ],
197
+ affectedFiles: [relativePath],
198
+ }),
199
+ ];
200
+ }
201
+ function findCompleteNetworkIsolationClaim(relativePath, content) {
202
+ const match = COMPLETE_NETWORK_ISOLATION_PATTERN.exec(content);
203
+ if (!match)
204
+ return [];
205
+ const matchIndex = match.index;
206
+ const window = content.slice(Math.max(0, matchIndex - 60), Math.min(content.length, matchIndex + match[0].length + 20));
207
+ if (CLAIM_NEGATION_NEARBY_PATTERN.test(window))
208
+ return [];
209
+ return [
210
+ makeCodeRotIssue({
211
+ auditType: "code-rot",
212
+ detectorId: DETECTOR_ID,
213
+ idCues: [relativePath, "complete-network-isolation-claim"],
214
+ title: `${relativePath} claims complete/guaranteed network isolation`,
215
+ description: `${relativePath} says "${match[0]}" -- the network/local-first check is a bounded static assumption check, not proof of runtime network isolation.`,
216
+ severity: "high",
217
+ confidence: "medium",
218
+ falsePositiveRisk: "medium",
219
+ category: "security-validation-assumption-rot",
220
+ recommendedAction: `Rephrase ${relativePath} to describe the network check as bounded rather than a complete/guaranteed isolation proof.`,
221
+ suggestedFixStrategy: `Replace the unhedged network-isolation claim in ${relativePath} with accurate, bounded wording.`,
222
+ validationCommands: ["npm run audit -- --types code-rot --include docs"],
223
+ releaseBlocking: false,
224
+ implementationBlocking: false,
225
+ autoFixEligible: false,
226
+ evidence: [
227
+ {
228
+ kind: "file",
229
+ message: "Matched an unhedged complete-network-isolation claim pattern.",
230
+ filePath: relativePath,
231
+ excerpt: match[0].slice(0, 200),
232
+ source: DETECTOR_ID,
233
+ confidence: "medium",
234
+ },
235
+ ],
236
+ affectedFiles: [relativePath],
237
+ }),
238
+ ];
239
+ }
240
+ function findUnsupportedCurrentClaims(relativePath, content) {
241
+ const issues = [];
242
+ const subjects = [
243
+ { id: "manual-pentest", label: "manual pentest", pattern: "manual pentest" },
244
+ { id: "android-validation", label: "Android/mobile validation", pattern: "(?:android (?:validation|support)|mobile validation)" },
245
+ ];
246
+ for (const subject of subjects) {
247
+ const match = currentClaimPatternFor(subject.pattern).exec(content);
248
+ if (!match)
249
+ continue;
250
+ issues.push(makeCodeRotIssue({
251
+ auditType: "code-rot",
252
+ detectorId: DETECTOR_ID,
253
+ idCues: [relativePath, "unsupported-current-claim", subject.id],
254
+ title: `${relativePath} claims "${subject.label}" is implemented/current`,
255
+ description: `${relativePath} contains an unhedged "${subject.label} is implemented/current" claim, but there is no corresponding source signal for this repo.`,
256
+ severity: "high",
257
+ confidence: "medium",
258
+ falsePositiveRisk: "medium",
259
+ category: "security-validation-assumption-rot",
260
+ recommendedAction: `Confirm whether "${subject.label}" is actually implemented; if not, correct ${relativePath}.`,
261
+ suggestedFixStrategy: `Rephrase the "${subject.label}" claim in ${relativePath} to accurately reflect its planned status.`,
262
+ validationCommands: ["npm run audit -- --types code-rot --include docs"],
263
+ releaseBlocking: false,
264
+ implementationBlocking: false,
265
+ autoFixEligible: false,
266
+ evidence: [
267
+ {
268
+ kind: "file",
269
+ message: `Matched an unhedged "${subject.label} is implemented/current" claim pattern.`,
270
+ filePath: relativePath,
271
+ excerpt: match[0].slice(0, 200),
272
+ source: DETECTOR_ID,
273
+ confidence: "medium",
274
+ },
275
+ ],
276
+ affectedFiles: [relativePath],
277
+ }));
278
+ }
279
+ return issues;
280
+ }
281
+ function findStaleV021SecurityMention(relativePath, content, currentVersion) {
282
+ if (isVersionAtLeast(currentVersion, "0.2.2") !== true)
283
+ return [];
284
+ // CHANGELOG.md entries are definitionally historical records of past
285
+ // versions -- a "v0.2.1" mention there is never a stale current-state
286
+ // claim. Discovered via a real false positive during self-testing.
287
+ if (path.basename(relativePath).toLowerCase() === "changelog.md")
288
+ return [];
289
+ const lines = content.split(/\r\n|\n/);
290
+ for (let i = 0; i < lines.length; i++) {
291
+ if (!V021_SECURITY_CONTEXT_PATTERN.test(lines[i]))
292
+ continue;
293
+ const windowStart = Math.max(0, i - 3);
294
+ const windowEnd = Math.min(lines.length, i + 4);
295
+ const windowLines = lines.slice(windowStart, windowEnd);
296
+ const nearbySecurityContext = windowLines.some((l) => SECURITY_CONTEXT_LINE_PATTERN.test(l));
297
+ if (!nearbySecurityContext)
298
+ continue;
299
+ if (windowLines.some((l) => HISTORICAL_VERSION_FRAMING_PATTERN.test(l)))
300
+ continue;
301
+ return [
302
+ makeCodeRotIssue({
303
+ auditType: "code-rot",
304
+ detectorId: DETECTOR_ID,
305
+ idCues: [relativePath, "stale-v021-security-mention", String(i + 1)],
306
+ title: `${relativePath} mentions v0.2.1 security behavior near current package version ${currentVersion}`,
307
+ description: `${relativePath}:${i + 1} mentions "v0.2.1" in a security-context paragraph, but the current package version is "${currentVersion}" (>= 0.2.2). Confirm this is not describing stale pre-fortification security behavior as current.`,
308
+ severity: "medium",
309
+ confidence: "low",
310
+ falsePositiveRisk: "medium",
311
+ category: "security-validation-assumption-rot",
312
+ recommendedAction: "Confirm whether this v0.2.1 mention is intentionally historical; update if it describes current behavior.",
313
+ suggestedFixStrategy: `Update or clarify the v0.2.1 reference in ${relativePath}.`,
314
+ validationCommands: ["npm run audit -- --types code-rot --include docs"],
315
+ releaseBlocking: false,
316
+ implementationBlocking: false,
317
+ autoFixEligible: false,
318
+ evidence: [
319
+ {
320
+ kind: "file",
321
+ message: "Doc mentions v0.2.1 near security-context wording while the current package version is 0.2.2 or later.",
322
+ filePath: relativePath,
323
+ line: i + 1,
324
+ excerpt: lines[i].trim().slice(0, 200),
325
+ source: DETECTOR_ID,
326
+ confidence: "low",
327
+ },
328
+ ],
329
+ affectedFiles: [relativePath],
330
+ }),
331
+ ];
332
+ }
333
+ return [];
334
+ }
335
+ function findAttackScenariosDescribedAsPlanned(relativePath, content) {
336
+ const pattern = plannedClaimPatternFor("attack scenario");
337
+ const match = pattern.exec(content);
338
+ if (!match)
339
+ return [];
340
+ return [
341
+ makeCodeRotIssue({
342
+ auditType: "code-rot",
343
+ detectorId: DETECTOR_ID,
344
+ idCues: [relativePath, "attack-scenarios-planned-but-implemented"],
345
+ title: `${relativePath} describes attack scenarios as planned/future, but they are already implemented`,
346
+ description: `${relativePath} describes "attack scenario" work as planned/future, but src/securityValidation/attackScenarios already exists and the security-validation source directory is present.`,
347
+ severity: "medium",
348
+ confidence: "medium",
349
+ falsePositiveRisk: "medium",
350
+ category: "security-validation-assumption-rot",
351
+ recommendedAction: `Update ${relativePath} to describe attack scenarios as implemented/current.`,
352
+ suggestedFixStrategy: `Move the attack-scenario description in ${relativePath} out of the planned/roadmap section.`,
353
+ validationCommands: ["npm run audit -- --types code-rot --include docs"],
354
+ releaseBlocking: false,
355
+ implementationBlocking: false,
356
+ autoFixEligible: false,
357
+ evidence: [
358
+ {
359
+ kind: "file",
360
+ message: "Doc describes attack scenarios as planned/future while src/securityValidation/attackScenarios already exists.",
361
+ filePath: relativePath,
362
+ excerpt: match[0].slice(0, 200),
363
+ source: DETECTOR_ID,
364
+ confidence: "medium",
365
+ },
366
+ ],
367
+ affectedFiles: [relativePath],
368
+ }),
369
+ ];
370
+ }
371
+ // Minimal semver-ish "is A >= B" comparison for major.minor.patch strings.
372
+ // Returns null when either string doesn't parse cleanly.
373
+ function isVersionAtLeast(a, b) {
374
+ const pa = a.split(".").map(Number);
375
+ const pb = b.split(".").map(Number);
376
+ if (pa.length !== 3 || pb.length !== 3 || pa.some(Number.isNaN) || pb.some(Number.isNaN))
377
+ return null;
378
+ for (let i = 0; i < 3; i++) {
379
+ if (pa[i] > pb[i])
380
+ return true;
381
+ if (pa[i] < pb[i])
382
+ return false;
383
+ }
384
+ return true;
385
+ }
@@ -0,0 +1,135 @@
1
+ import { extractDocCommandReferences } from "../utils/docCommandReferences.js";
2
+ import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ // ---------------------------------------------------------------------------
6
+ // v0.3.0 Batch 3 — stale command/workflow reference detector.
7
+ //
8
+ // Two deterministic checks, both requiring "docs" in --include (see
9
+ // shouldSkip below):
10
+ // A. A doc references "npm run <script>" for a script that is not a
11
+ // current package.json script, and the surrounding text is not
12
+ // labeled planned/future/roadmap/not implemented/later version.
13
+ // B. An "important public" package.json script (the small, deliberately
14
+ // curated list below) is never mentioned anywhere in docs/COMMANDS.md.
15
+ //
16
+ // Reads doc text directly (via inventory.docsFiles + a bounded fs.readFile)
17
+ // rather than re-deriving it from sourceOfTruth.docs.files, because
18
+ // DocsFileTruth only carries boolean "mentions" flags, not the line/excerpt
19
+ // detail this detector's evidence needs (Batch 3 spec 3.4: "matched line or
20
+ // excerpt if cheaply available").
21
+ // ---------------------------------------------------------------------------
22
+ const DETECTOR_ID = "stale-command-reference";
23
+ // Deliberately small and curated -- these are the command surfaces a v0.3.0
24
+ // user is expected to discover via docs/COMMANDS.md. Internal/helper
25
+ // scripts are never flagged missing, per Batch 3 scope ("avoid flagging
26
+ // internal helper scripts unless docs claim comprehensive command
27
+ // coverage" -- Batch 3 does not implement that comprehensive-coverage-claim
28
+ // check, so this list stays fixed and small).
29
+ const IMPORTANT_PUBLIC_SCRIPTS = ["audit", "security:validate", "experiment:list", "experiment:describe", "experiment:run", "test:security"];
30
+ const MAX_DOC_READ_BYTES = 200_000;
31
+ export const STALE_COMMAND_REFERENCE_DETECTOR = {
32
+ id: DETECTOR_ID,
33
+ auditType: "code-rot",
34
+ title: "Stale command/workflow reference",
35
+ description: "Detects documentation references to npm scripts that no longer exist in package.json (and are not clearly labeled planned/future), and important public scripts that are undocumented.",
36
+ supportedIncludeAreas: ["docs", "cli", "package"],
37
+ shouldSkip: (ctx) => {
38
+ if (!ctx.config.include.includes("docs")) {
39
+ return { skip: true, reason: "--include does not select docs; this detector's checks are docs-based." };
40
+ }
41
+ return { skip: false };
42
+ },
43
+ run: (ctx) => {
44
+ const scriptNames = new Set(Object.keys(ctx.sourceOfTruth.package?.scripts ?? {}));
45
+ const issues = [];
46
+ for (const docFile of ctx.inventory.docsFiles) {
47
+ const content = readBoundedText(ctx.target.rootPath, docFile.relativePath, docFile.sizeBytes);
48
+ if (content === null)
49
+ continue;
50
+ for (const ref of extractDocCommandReferences(content)) {
51
+ if (scriptNames.has(ref.command) || ref.isLabeledPlanned)
52
+ continue;
53
+ issues.push(makeCodeRotIssue({
54
+ auditType: "code-rot",
55
+ detectorId: DETECTOR_ID,
56
+ idCues: [docFile.relativePath, "missing-script", ref.command],
57
+ title: `Documented command "npm run ${ref.command}" is not a current package.json script`,
58
+ description: `${docFile.relativePath}:${ref.lineNumber} references "npm run ${ref.command}" as a current command, but no such script exists in package.json.`,
59
+ severity: "high",
60
+ confidence: "high",
61
+ falsePositiveRisk: "low",
62
+ category: "stale-command-reference",
63
+ recommendedAction: "Either add the missing script to package.json, remove the stale example, or label it clearly as planned/future.",
64
+ suggestedFixStrategy: `Update ${docFile.relativePath} to remove or relabel the "npm run ${ref.command}" example.`,
65
+ validationCommands: ["npm run audit -- --types code-rot --include docs"],
66
+ releaseBlocking: false,
67
+ implementationBlocking: false,
68
+ autoFixEligible: false,
69
+ evidence: [
70
+ {
71
+ kind: "file",
72
+ message: `Referenced command not found in package.json scripts.`,
73
+ filePath: docFile.relativePath,
74
+ line: ref.lineNumber,
75
+ excerpt: ref.excerpt,
76
+ source: DETECTOR_ID,
77
+ confidence: "high",
78
+ },
79
+ ],
80
+ affectedFiles: [docFile.relativePath],
81
+ }));
82
+ }
83
+ }
84
+ const commandsDoc = ctx.inventory.docsFiles.find((f) => path.basename(f.relativePath).toLowerCase() === "commands.md");
85
+ const commandsDocContent = commandsDoc
86
+ ? readBoundedText(ctx.target.rootPath, commandsDoc.relativePath, commandsDoc.sizeBytes)
87
+ : null;
88
+ if (commandsDocContent !== null && commandsDoc) {
89
+ for (const scriptName of IMPORTANT_PUBLIC_SCRIPTS) {
90
+ if (!scriptNames.has(scriptName))
91
+ continue;
92
+ if (commandsDocContent.includes(scriptName))
93
+ continue;
94
+ issues.push(makeCodeRotIssue({
95
+ auditType: "code-rot",
96
+ detectorId: DETECTOR_ID,
97
+ idCues: [commandsDoc.relativePath, "undocumented-script", scriptName],
98
+ title: `Implemented script "${scriptName}" is not documented in ${commandsDoc.relativePath}`,
99
+ description: `package.json defines "${scriptName}" but ${commandsDoc.relativePath} never mentions it.`,
100
+ severity: "medium",
101
+ confidence: "medium",
102
+ falsePositiveRisk: "medium",
103
+ category: "undocumented-command",
104
+ recommendedAction: `Add the "${scriptName}" command to ${commandsDoc.relativePath}.`,
105
+ suggestedFixStrategy: `Document "npm run ${scriptName}" with a short usage example in ${commandsDoc.relativePath}.`,
106
+ validationCommands: ["npm run audit -- --types code-rot --include docs"],
107
+ releaseBlocking: false,
108
+ implementationBlocking: false,
109
+ autoFixEligible: false,
110
+ evidence: [
111
+ {
112
+ kind: "reference",
113
+ message: `Script "${scriptName}" present in package.json but not mentioned in ${commandsDoc.relativePath}.`,
114
+ filePath: commandsDoc.relativePath,
115
+ source: DETECTOR_ID,
116
+ confidence: "medium",
117
+ },
118
+ ],
119
+ affectedFiles: [commandsDoc.relativePath],
120
+ }));
121
+ }
122
+ }
123
+ return deduplicateIssuesById(issues);
124
+ },
125
+ };
126
+ function readBoundedText(root, relativePath, sizeBytes) {
127
+ if (sizeBytes > MAX_DOC_READ_BYTES)
128
+ return null;
129
+ try {
130
+ return fs.readFileSync(path.join(root, relativePath), "utf8");
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ }