@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
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  my-dev-kit-lab is the experiment, evidence, reporting, security-validation, and future audit companion for my-dev-kit. It runs reproducible experiments that test whether my-dev-kit's graph-guided retrieval helps coding-agent workflows, collects metrics, renders reports, generates plots, captures screenshots, builds gallery outputs, and performs automated CLI/package security validation.
4
4
 
5
- The current package is v0.2.2, release-prepared but not yet published to npm. Its generic experiment-plugin framework was introduced in v0.2.0. The first plugin is `context-strategy-comparison`, which preserves the existing raw-full-file vs my-dev-kit-guided workflow through the plugin runner.
5
+ The current package version is `v0.3.0` (release-prepared; not yet published to npm, tagged, or released on GitHub). The latest published npm baseline is `v0.2.2`. Its generic experiment-plugin framework was introduced in v0.2.0. The first plugin is `context-strategy-comparison`, which preserves the existing raw-full-file vs my-dev-kit-guided workflow through the plugin runner.
6
6
 
7
7
  my-dev-kit is most useful when the repository is larger than the task. It helps coding agents work with large codebases through reusable structural indexing, graph-guided retrieval, targeted source slices, and auditable context selection. Results are scoped evidence; the project does not claim that my-dev-kit always saves tokens.
8
8
 
@@ -32,6 +32,7 @@ my-dev-kit is most useful when the repository is larger than the task. It helps
32
32
  - Target-aware experiment execution for local projects via `experiment:run -- --target <path>`
33
33
  - Plugin-aware JSON and HTML reports with plugin, target, variant, metric, artifact, warning, skip, and failure metadata
34
34
  - Security validation framework: dependency audit, package tarball inspection, CLI adversarial tests, static scans (CodeQL/Semgrep), bounded fuzz smoke, attack-scenario checks, and structured verdict/report output — runnable against any local project via `security:validate --target <path>`
35
+ - Generic audit framework (package.json now specifies version `v0.3.0`; release-prepared but not yet published to npm, tagged, or released on GitHub): `npm run audit` runs heuristic, conservative code-rot detectors and writes text/JSON reports under `reports/audits/code-rot/` by default. Only the `code-rot` audit type is currently implemented; `quality`, `security`, `project`, and `all` audit types are planned and fail cleanly instead of running. `npm run audit` supports `--target`, `--types code-rot`, and other flags — see [docs/COMMANDS.md](docs/COMMANDS.md). Audit does not modify target files, does not auto-fix issues, does not run `security:validate`, and does not perform release-readiness determination.
35
36
 
36
37
  ---
37
38
 
@@ -202,7 +203,7 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
202
203
 
203
204
  ## Current baseline release positioning
204
205
 
205
- my-dev-kit-lab is at a working baseline. The raw-vs-indexed experiment pipeline is fully implemented and produces reproducible artifacts. Real-agent campaign support exists for Codex and Claude. The current package version is `v0.2.2`, which implements the automated security-validation fortification work described below and has passed pre-release readiness and cross-platform validation. `v0.2.2` has not yet been published to npm.
206
+ my-dev-kit-lab is at a working baseline. The raw-vs-indexed experiment pipeline is fully implemented and produces reproducible artifacts. Real-agent campaign support exists for Codex and Claude. The latest published npm baseline is `v0.2.2`, which implements the automated security-validation fortification work described below and has passed pre-release readiness and cross-platform validation. package.json now specifies version `v0.3.0`, which adds the generic audit framework described below; `v0.3.0` is release-prepared but has not yet been published to npm, tagged, or released on GitHub.
206
207
 
207
208
  ---
208
209
 
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import { parseAuditArgs, normalizeAuditConfig } from "../../src/audits/core/auditConfig.js";
3
+ import { resolveAuditTarget } from "../../src/audits/core/auditTarget.js";
4
+ import { runAudit } from "../../src/audits/core/auditRunner.js";
5
+ import { AUDIT_EXIT_CODES } from "../../src/audits/core/auditExitCode.js";
6
+ import { resolveToolRoot } from "../security/resolveToolRoot.js";
7
+ import { buildAuditReportModel } from "../../src/audits/report/auditReportModel.js";
8
+ import { writeAuditReports } from "../../src/audits/report/writeAuditReports.js";
9
+ // ---------------------------------------------------------------------------
10
+ // v0.3.0 Batch 5 -- npm run audit entrypoint.
11
+ //
12
+ // Thin wiring only: parse args -> normalize config -> resolve target ->
13
+ // runAudit() -> buildAuditReportModel() -> writeAuditReports() -> print a
14
+ // concise console summary -> set process.exitCode. All report-shaping logic
15
+ // (the former inline buildJsonReport()/renderMinimalTextSummary()/
16
+ // summarizeInventory()/summarizeSourceOfTruth() functions from Batch 1/2)
17
+ // now lives in src/audits/report/ -- see auditReportModel.ts,
18
+ // renderAuditJsonReport.ts, renderAuditTextReport.ts, writeAuditReports.ts.
19
+ // Mirrors the structure of scripts/security/validate.ts.
20
+ // ---------------------------------------------------------------------------
21
+ const USAGE = "Usage: npm run audit -- [--target <path>] [--types code-rot] " +
22
+ "[--include docs,tests,package,architecture,cli] [--format text,json] " +
23
+ "[--fail-on blocker|high|medium|low|none] [--out <path>]";
24
+ const rawArgs = process.argv.slice(2);
25
+ const toolRoot = resolveToolRoot(import.meta.url);
26
+ let config;
27
+ try {
28
+ const args = parseAuditArgs(rawArgs);
29
+ config = normalizeAuditConfig(args, toolRoot);
30
+ }
31
+ catch (err) {
32
+ const msg = err instanceof Error ? err.message : String(err);
33
+ console.error(`\nERROR: ${msg}`);
34
+ console.error(USAGE);
35
+ process.exitCode = AUDIT_EXIT_CODES.FATAL_ERROR;
36
+ process.exit(AUDIT_EXIT_CODES.FATAL_ERROR);
37
+ }
38
+ let target;
39
+ try {
40
+ target = resolveAuditTarget(config.targetPathArg, toolRoot);
41
+ }
42
+ catch (err) {
43
+ const msg = err instanceof Error ? err.message : String(err);
44
+ console.error(`\nERROR: ${msg}`);
45
+ console.error(USAGE);
46
+ process.exitCode = AUDIT_EXIT_CODES.FATAL_ERROR;
47
+ process.exit(AUDIT_EXIT_CODES.FATAL_ERROR);
48
+ }
49
+ console.log("=".repeat(60));
50
+ console.log("my-dev-kit-lab audit");
51
+ console.log("=".repeat(60));
52
+ console.log(`Tool root : ${toolRoot}`);
53
+ if (!target.isSelf) {
54
+ console.log(`Target : ${target.rootPath}`);
55
+ }
56
+ else {
57
+ console.log(`Mode : self-audit`);
58
+ }
59
+ console.log(`Types : ${config.types.join(", ")}`);
60
+ console.log(`Include : ${config.include.join(", ")}`);
61
+ console.log("");
62
+ let result;
63
+ try {
64
+ result = await runAudit({ config, toolRoot, target });
65
+ }
66
+ catch (err) {
67
+ const msg = err instanceof Error ? err.message : String(err);
68
+ console.error(`\nERROR: audit runtime failure: ${msg}`);
69
+ process.exitCode = AUDIT_EXIT_CODES.FATAL_ERROR;
70
+ process.exit(AUDIT_EXIT_CODES.FATAL_ERROR);
71
+ }
72
+ const model = buildAuditReportModel(result, { target });
73
+ const { writtenPaths } = writeAuditReports({ model, config, outDir: config.out });
74
+ console.log(`Issues found : ${model.summary.totalIssues}`);
75
+ console.log(` blocker=${model.summary.issuesBySeverity.blocker} high=${model.summary.issuesBySeverity.high} medium=${model.summary.issuesBySeverity.medium} low=${model.summary.issuesBySeverity.low} info=${model.summary.issuesBySeverity.info}`);
76
+ console.log(`Skipped detectors : ${model.summary.skippedDetectorCount}`);
77
+ if (model.summary.detectorErrorCount > 0) {
78
+ console.log(`Detector errors : ${model.summary.detectorErrorCount}`);
79
+ for (const e of model.detectorErrors) {
80
+ console.log(` [${e.id}] ${e.message}`);
81
+ }
82
+ }
83
+ if (model.summary.noDetectorsRegistered) {
84
+ console.log(`Note: no code-rot detectors are registered for this run's --types/--include selection -- this run collected inventory/source-of-truth data only.`);
85
+ }
86
+ console.log(`\nInventory: ${model.inventory.totalScannedFileCount} file(s) scanned, ${model.inventory.skippedFileCount} skipped`);
87
+ console.log(` source=${model.inventory.filesByCategory.source} tests=${model.inventory.filesByCategory.tests} docs=${model.inventory.filesByCategory.docs} package=${model.inventory.filesByCategory.package} config=${model.inventory.filesByCategory.config} scripts=${model.inventory.filesByCategory.scripts} ci=${model.inventory.filesByCategory.ci}`);
88
+ console.log(`Source of truth: package=${model.sourceOfTruth.packageName ?? "(none)"}@${model.sourceOfTruth.packageVersion ?? "?"} readme=${model.sourceOfTruth.hasReadme} changelog=${model.sourceOfTruth.hasChangelog} ciWorkflows=${model.sourceOfTruth.ciWorkflowCount}`);
89
+ if (writtenPaths.length > 0) {
90
+ console.log(`\nReports written:`);
91
+ for (const p of writtenPaths) {
92
+ console.log(` ${p}`);
93
+ }
94
+ }
95
+ console.log(`\nHighest severity: ${model.summary.highestSeverity ?? "(none)"}`);
96
+ console.log(`Verdict: ${model.summary.finalVerdictLabel}`);
97
+ console.log(`Exit reason: ${model.exit.reason}`);
98
+ console.log(model.exit.code === AUDIT_EXIT_CODES.SUCCESS
99
+ ? `Exit 0 -- audit completed.`
100
+ : `Exit 1 -- fail-on threshold breached.`);
101
+ process.exitCode = model.exit.code;
@@ -0,0 +1,17 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.3.0 Batch 3 — code-rot detector aggregator.
3
+ //
4
+ // Single import surface for src/audits/core/auditRegistry.ts's
5
+ // DEFAULT_AUDIT_REGISTRY. Keeps the registry file from needing to know each
6
+ // detector's individual file path.
7
+ // ---------------------------------------------------------------------------
8
+ export { STALE_COMMAND_REFERENCE_DETECTOR } from "./detectors/staleCommandReferenceDetector.js";
9
+ export { DOCS_CODE_MISMATCH_DETECTOR } from "./detectors/docsCodeMismatchDetector.js";
10
+ export { PACKAGE_RELEASE_ROT_DETECTOR } from "./detectors/packageReleaseRotDetector.js";
11
+ export { DUPLICATE_IMPLEMENTATION_DETECTOR } from "./detectors/duplicateImplementationDetector.js";
12
+ export { DEAD_CODE_CANDIDATE_DETECTOR } from "./detectors/deadCodeCandidateDetector.js";
13
+ export { TEST_ROT_DETECTOR } from "./detectors/testRotDetector.js";
14
+ export { ARCHITECTURE_DRIFT_DETECTOR } from "./detectors/architectureDriftDetector.js";
15
+ export { DEPENDENCY_ENVIRONMENT_ROT_DETECTOR } from "./detectors/dependencyEnvironmentRotDetector.js";
16
+ export { CROSS_PLATFORM_ROT_DETECTOR } from "./detectors/crossPlatformRotDetector.js";
17
+ export { SECURITY_VALIDATION_ASSUMPTION_ROT_DETECTOR } from "./detectors/securityValidationAssumptionRotDetector.js";
@@ -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
+ }