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

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.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # my-dev-kit-lab
2
2
 
3
- my-dev-kit-lab is the experiment, evidence, reporting, security-validation, and 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, performs automated CLI/package security validation, and runs the generic audit framework.
4
-
5
- The current published baseline is `v0.3.0`. `v0.3.0` introduced the generic audit framework and the first implemented code-rot detector family. The 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
-
7
- The checked-out package is release-prepared for `v0.3.1` on top of the published baseline: normalized language/file-role inventory, source-facts collection, a TypeScript/JavaScript analyzer, source-facts-aware code-rot signals, and source-facts summaries in text/JSON audit reports. `v0.3.1` is not published yet; Python, Java, Kotlin, Android validation, framework-aware profiles, and broader audit types remain future roadmap work.
3
+ my-dev-kit-lab is the experiment, evidence, reporting, security-validation, and 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, performs automated CLI/package security validation, and runs the generic audit framework.
4
+
5
+ The current published baseline is `v0.3.1`. `v0.3.0` introduced the generic audit framework and the first implemented code-rot detector family; `v0.3.1` added the language-aware code-rot substrate plus TypeScript/JavaScript support. The 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
+
7
+ The checked-out package state is `v0.3.2` release-prepared on top of the published `v0.3.1` baseline: Python-aware code-rot support (a Python source-facts analyzer, Python project metadata, and Python-aware dead-code/duplicate-implementation/test-rot detector signals) and a security-validation audit adapter (`npm run audit -- --types security`, mapped security findings, and a `securitySummary` audit report field). Package metadata is bumped for release preparation, but `v0.3.2` is not published yet; Java, Kotlin, cross-language stability, Android validation, framework-aware profiles, and the `quality`/`project`/`all` audit types remain future roadmap work.
8
8
 
9
9
  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.
10
10
 
@@ -33,8 +33,10 @@ my-dev-kit is most useful when the repository is larger than the task. It helps
33
33
  - First experiment plugin: `context-strategy-comparison`
34
34
  - Target-aware experiment execution for local projects via `experiment:run -- --target <path>`
35
35
  - Plugin-aware JSON and HTML reports with plugin, target, variant, metric, artifact, warning, skip, and failure metadata
36
- - 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>`
37
- - Generic audit framework in the current published `v0.3.0` baseline: `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.
36
+ - 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>`. `security:validate` remains the standalone, focused security-validation command; it is not replaced by the audit adapter below.
37
+ - Generic audit framework, current published baseline: `npm run audit` runs heuristic, conservative code-rot detectors and writes text/JSON reports under `reports/audits/code-rot/` by default. `code-rot` and `security` are the currently implemented audit types (`--types code-rot`, `--types security`, or combined `--types code-rot,security`); `quality`, `project`, and `all` audit types are planned and fail cleanly instead of running. `npm run audit` supports `--target`, `--types`, and other flags — see [docs/COMMANDS.md](docs/COMMANDS.md). Audit does not modify target files and does not auto-fix issues.
38
+ - Language-aware code-rot support, current checked-out package state: normalized language/file-role inventory, a source-facts model and collector, a TypeScript/JavaScript analyzer, and a Python analyzer. Source facts feed the `dead-code-candidate`, `duplicate-implementation-candidate`, and `test-rot` detectors with parsed-import/export/declaration evidence for both languages. Python project metadata (presence of `pyproject.toml`, `requirements.txt`, `setup.py`/`setup.cfg`, `tox.ini`, `pytest.ini`, and a best-effort project name) is collected and reported as its own audit report field. Java and Kotlin remain inventory-classified but fallback-only; no analyzer is registered for them yet.
39
+ - Security-validation audit adapter, current checked-out package state: `npm run audit -- --types security` runs security validation through the shared audit/report surface by calling the same `runSecurityValidation()` internals `security:validate` uses, maps security findings into audit-issue-shaped entries, and adds a `securitySummary` field (verdict, check counts, finding counts, and links to the original `reports/security/*.txt`/`*.json` report) to audit JSON/text reports. `npm run audit -- --types code-rot,security` runs both audit types together. The original `security:validate` command and its `reports/security/` output are unchanged.
38
40
 
39
41
  ---
40
42
 
@@ -203,16 +205,16 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
203
205
 
204
206
  ---
205
207
 
206
- ## Current baseline release positioning
207
-
208
- 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 published npm baseline is `v0.3.0`, which includes the generic audit framework and the first implemented code-rot detector family on top of the earlier experiment and automated security-validation work.
209
-
210
- Planned roadmap direction after the published baseline:
211
-
212
- - `v0.3.1`: language-aware code-rot substrate plus TypeScript/JavaScript support, release-prepared and not yet published
213
- - `v0.3.2` through `v0.3.4`: Python, Java/Kotlin, and cross-language stability for the language-aware code-rot track
214
- - `v0.4.0` through `v0.4.2`: Android automated security validation
215
- - manual pentest: post-v1 / version TBD
208
+ ## Current baseline release positioning
209
+
210
+ 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 published npm baseline is `v0.3.1`, which includes the generic audit framework, the code-rot detector family, and the language-aware code-rot substrate with TypeScript/JavaScript support, on top of the earlier experiment and automated security-validation work.
211
+
212
+ Planned roadmap direction after the published baseline:
213
+
214
+ - `v0.3.2`: Python code-rot support plus a first security-validation audit adapter, release-prepared and not yet published
215
+ - `v0.3.3` through `v0.3.4`: Java/Kotlin and cross-language stability for the language-aware code-rot track
216
+ - `v0.4.0` through `v0.4.2`: Android automated security validation, including an Android-specific extension of the security audit adapter
217
+ - manual pentest: post-v1 / version TBD
216
218
 
217
219
  ---
218
220
 
@@ -220,6 +222,8 @@ Planned roadmap direction after the published baseline:
220
222
 
221
223
  my-dev-kit-lab owns a release-security validation track for **my-dev-kit**. This work is separate from the experiment pipeline and does not replace the generic experiment-plugin roadmap. Its purpose is to generate release-validation evidence for the local CLI/package before release preparation.
222
224
 
225
+ `security:validate` remains the standalone, focused command for this validation and is unaffected by the current checked-out state. `npm run audit -- --types security` additionally adapts the same `security:validate` internals into the shared audit/report surface (mapped issues, a `securitySummary` field, and links to the original `reports/security/` output) — it does not replace or duplicate `security:validate`'s own checks or reports.
226
+
223
227
  This is not a web application pentest framework. **my-dev-kit** is a local CLI/package, so the validation model is CLI/package adversarial testing focused on whether it remains:
224
228
 
225
229
  - local-first
@@ -250,7 +254,7 @@ Each security command can validate my-dev-kit-lab itself or another local projec
250
254
 
251
255
  `security:validate` supports `--checks`, `--profile`, `--format`, `--fail-on`, `--out`, and `--report-prefix`. The no-flag path remains backward compatible and still runs the classic implemented check groups: `deps`, `package`, `static`, `cli-adversarial`, and `fuzz`. Supplying `--profile` without `--checks` swaps in that profile's default checks; supplying explicit `--checks` always wins over profile defaults.
252
256
 
253
- The current attack-scenario checks cover boundary, subprocess, secrets, and network assumptions. They are automated adversarial checks, not a manual pentest. Manual pentest is deferred until after `v1.0.0`. `verdictImpact` metadata from each registered scenario drives blocker categorization in report reasoning, and narrowed `--checks` runs are labeled as scoped rather than described as a full release gate.
257
+ The current attack-scenario checks cover boundary, subprocess, secrets, and network assumptions. They are automated adversarial checks, not a manual pentest. Manual pentest is deferred until after `v1.0.0`. `verdictImpact` metadata from each registered scenario drives blocker categorization in report reasoning, and narrowed `--checks` runs are labeled as scoped rather than described as a full release gate.
254
258
 
255
259
  JSON report poisoning/config-injection checks use a baseline-diff schema guard so legitimate additive JSON fields do not fail the guard while payload-created trusted top-level fields still do. Text reports are sanitized to strip ANSI/control-byte payloads before rendering.
256
260
 
@@ -18,7 +18,7 @@ import { writeAuditReports } from "../../src/audits/report/writeAuditReports.js"
18
18
  // renderAuditJsonReport.ts, renderAuditTextReport.ts, writeAuditReports.ts.
19
19
  // Mirrors the structure of scripts/security/validate.ts.
20
20
  // ---------------------------------------------------------------------------
21
- const USAGE = "Usage: npm run audit -- [--target <path>] [--types code-rot] " +
21
+ const USAGE = "Usage: npm run audit -- [--target <path>] [--types code-rot,security] " +
22
22
  "[--include docs,tests,package,architecture,cli] [--format text,json] " +
23
23
  "[--fail-on blocker|high|medium|low|none] [--out <path>]";
24
24
  const rawArgs = process.argv.slice(2);
@@ -80,9 +80,15 @@ if (model.summary.detectorErrorCount > 0) {
80
80
  console.log(` [${e.id}] ${e.message}`);
81
81
  }
82
82
  }
83
- if (model.summary.noDetectorsRegistered) {
83
+ if (model.summary.noDetectorsRegistered && !model.securitySummary.ran) {
84
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
85
  }
86
+ if (model.securitySummary.ran) {
87
+ console.log(`\nSecurity validation: verdict=${model.securitySummary.verdictLabel} checks=${model.securitySummary.totalChecks} findings(blocker=${model.securitySummary.findingCounts.blocker} major=${model.securitySummary.findingCounts.major} minor=${model.securitySummary.findingCounts.minor} info=${model.securitySummary.findingCounts.informational})`);
88
+ if (model.securitySummary.reportPaths.text) {
89
+ console.log(` Full report: ${model.securitySummary.reportPaths.text}`);
90
+ }
91
+ }
86
92
  console.log(`\nInventory: ${model.inventory.totalScannedFileCount} file(s) scanned, ${model.inventory.skippedFileCount} skipped`);
87
93
  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
94
  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}`);
@@ -4,6 +4,7 @@ import fs from "node:fs";
4
4
  import { runSecurityValidation } from "../../src/securityValidation/validate/runSecurityValidation.js";
5
5
  import { resolveValidationTarget, reportFilenamePrefix } from "../../src/securityValidation/validate/resolveTarget.js";
6
6
  import { renderTextReport, renderJsonReport } from "../../src/securityValidation/report/renderSecurityReport.js";
7
+ import { buildSecurityReportFromSummary } from "../../src/securityValidation/report/buildSecurityReport.js";
7
8
  import { parseSecurityValidateArgs, normalizeSecurityValidateConfig, applyProfileDefaultChecksIfApplicable, } from "../../src/securityValidation/validate/cliOptions.js";
8
9
  import { resolveAttackProfile } from "../../src/securityValidation/attackScenarios/attackProfile.js";
9
10
  import { findingsBreachFailOnThreshold } from "../../src/securityValidation/validate/verdict.js";
@@ -81,35 +82,13 @@ const summary = await runSecurityValidation({
81
82
  // downgrade an existing blocker.
82
83
  const failOnBreached = findingsBreachFailOnThreshold(summary.findings, config.failOnThreshold);
83
84
  // Build report object
84
- const report = {
85
- metadata: {
86
- toolRoot: summary.toolRoot,
87
- toolPackageName: summary.toolPackageName,
88
- toolPackageVersion: summary.toolPackageVersion,
89
- targetRoot: summary.targetRoot,
90
- targetDescription: summary.targetDescription,
91
- packageName: summary.packageName,
92
- packageVersion: summary.packageVersion,
93
- branch: summary.auditedBranch,
94
- commit: summary.auditedCommit,
95
- isSelf: summary.isSelf,
96
- generatedAt: summary.finishedAt,
97
- totalDurationMs: new Date(summary.finishedAt).getTime() - new Date(summary.startedAt).getTime(),
98
- profile: config.profile,
99
- selectedChecks: config.checks,
100
- failOnThreshold: config.failOnThreshold,
101
- formats: config.formats,
102
- failOnBreached,
103
- isFullReleaseGate: summary.isFullReleaseGate,
104
- },
105
- sections: [],
106
- allChecks: summary.checks,
107
- allFindings: summary.findings,
108
- verdict: summary.verdict,
109
- recommendedNextStep: summary.recommendedNextStep,
110
- attackResults: summary.attackResults,
111
- verdictReasonSummary: summary.verdictReasonSummary,
112
- };
85
+ const report = buildSecurityReportFromSummary(summary, {
86
+ profile: config.profile,
87
+ selectedChecks: config.checks,
88
+ failOnThreshold: config.failOnThreshold,
89
+ formats: config.formats,
90
+ failOnBreached,
91
+ });
113
92
  const textReport = renderTextReport(report);
114
93
  const jsonReport = renderJsonReport(report);
115
94
  // Determine output directory
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { readBoundedFileText } from "../utils/boundedRead.js";
3
- import { baseNameNoExt } from "../utils/filePatternUtils.js";
3
+ import { baseNameNoExt, isGenericOrTestPrefixedDeclarationName } from "../utils/filePatternUtils.js";
4
4
  import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
5
5
  import { collectRelativeImportBasenames, getParsedSourceFacts, indexSourceFactsByPath } from "../utils/sourceFactsLookup.js";
6
6
  // ---------------------------------------------------------------------------
@@ -50,6 +50,9 @@ export const DEAD_CODE_CANDIDATE_DETECTOR = {
50
50
  if (ctx.config.include.includes("tests")) {
51
51
  issues.push(...findUnreferencedFixtures(ctx));
52
52
  }
53
+ if (ctx.config.include.includes("cli") || ctx.config.include.includes("architecture")) {
54
+ issues.push(...findPossiblyUnreferencedPythonDeclarations(ctx));
55
+ }
53
56
  return deduplicateIssuesById(issues);
54
57
  },
55
58
  };
@@ -250,6 +253,97 @@ function buildSourceFactsEvidence(facts) {
250
253
  confidence: "medium",
251
254
  };
252
255
  }
256
+ // v0.3.2 Batch 2 -- conservative, declaration-level Python dead-code
257
+ // candidate check. TS/JS module-level "unused exported symbol" detection is
258
+ // explicitly SKIPPED in this file's own header comment (no safe/cheap way
259
+ // without a real type-checker) -- this Python-only check is narrower and
260
+ // safer than that skipped TS/JS idea would be, because Python source facts
261
+ // already carry declarations *and* a repo-wide cross-file "imported name"
262
+ // index (unlike anything comparable for TS/JS bare specifiers), so the
263
+ // false-positive surface is much smaller. Every one of the skip conditions
264
+ // below is a deliberate, spec-required conservatism -- see the requirements
265
+ // list in this batch's task description. Findings stay at the lowest
266
+ // severity/confidence tier in this detector's own schema (matches
267
+ // findUnreferencedFixtures()'s tier) and use "candidate"/"may indicate"
268
+ // wording throughout, never "unused" or "dead".
269
+ function findPossiblyUnreferencedPythonDeclarations(ctx) {
270
+ if (!ctx.sourceFacts)
271
+ return [];
272
+ const index = indexSourceFactsByPath(ctx.sourceFacts);
273
+ // Cross-file "this name was imported somewhere" signal -- a declaration
274
+ // named here is referenced by at least one `from module import name`
275
+ // elsewhere in the scanned Python file set, which is the strongest signal
276
+ // this conservative scan can offer (Python source facts carry no
277
+ // call/identifier reference tracking -- see pythonAnalyzer.ts's header
278
+ // comment on why `references` stays empty by design).
279
+ const importedNamesAcrossProject = new Set();
280
+ for (const facts of index.values()) {
281
+ if (facts.language !== "python" || facts.parseStatus !== "parsed")
282
+ continue;
283
+ for (const imp of facts.imports) {
284
+ for (const name of imp.importedNames ?? [])
285
+ importedNamesAcrossProject.add(name);
286
+ }
287
+ }
288
+ const issues = [];
289
+ for (const facts of index.values()) {
290
+ if (facts.language !== "python" || facts.role !== "source" || facts.parseStatus !== "parsed")
291
+ continue;
292
+ // __init__.py commonly exists only to re-export a package's public
293
+ // surface or to mark a directory as a package -- its declarations (if
294
+ // any) are conservatively excluded entirely rather than guessed at.
295
+ if (path.basename(facts.relativePath) === "__init__.py")
296
+ continue;
297
+ for (const decl of facts.declarations) {
298
+ // Methods are excluded entirely -- this detector has no existing
299
+ // method-level dead-code evidence model for any language (TS/JS
300
+ // methods are never flagged either), and Python method visibility is
301
+ // even harder to infer safely (inherited/overridden/dispatched via
302
+ // duck typing).
303
+ if (decl.kind === "method")
304
+ continue;
305
+ if (!decl.exported)
306
+ continue; // leading-underscore convention already applied by pythonAnalyzer.ts
307
+ if (isGenericOrTestPrefixedDeclarationName(decl.name))
308
+ continue; // main/run/setup/teardown/__init__/__call__/test_*
309
+ // A name explicitly listed in a real `__all__` is the file's own
310
+ // strongest "this is public API" declaration -- skip rather than
311
+ // second-guess it.
312
+ if (facts.exports.some((e) => e.name === decl.name))
313
+ continue;
314
+ if (importedNamesAcrossProject.has(decl.name))
315
+ continue;
316
+ issues.push(makeCodeRotIssue({
317
+ auditType: "code-rot",
318
+ detectorId: DETECTOR_ID,
319
+ idCues: ["unreferenced-python-declaration", facts.relativePath, decl.kind, decl.name],
320
+ title: `Python ${decl.kind} "${decl.name}" in "${facts.relativePath}" has no detected cross-file import (weak signal)`,
321
+ description: `"${decl.name}" (${decl.kind}) is declared in "${facts.relativePath}" and does not appear as an imported name in any other scanned Python file. This is a deterministic source-facts signal from a conservative, non-semantic scan -- it does not resolve dynamic imports, framework/plugin discovery, string-based dispatch, or usage from outside the scanned project, so it may indicate dead code but is not proof. Confirm manually before removing.`,
322
+ severity: "info",
323
+ confidence: "low",
324
+ falsePositiveRisk: "high",
325
+ category: "dead-code-candidate",
326
+ recommendedAction: "Confirm the declaration is genuinely unused before removing it.",
327
+ suggestedFixStrategy: `Remove "${decl.name}" from "${facts.relativePath}" if confirmed unused, or reference it from another module.`,
328
+ validationCommands: ["npm run audit -- --types code-rot --include cli"],
329
+ releaseBlocking: false,
330
+ implementationBlocking: false,
331
+ autoFixEligible: false,
332
+ evidence: [
333
+ {
334
+ kind: "reference",
335
+ message: `Source facts: the Python analyzer parsed this file and found no other scanned Python file importing the name "${decl.name}". Static-analysis limitation -- confidence reduced because this scan cannot see dynamic imports or usage outside the scanned file set.`,
336
+ filePath: facts.relativePath,
337
+ source: DETECTOR_ID,
338
+ confidence: "low",
339
+ },
340
+ ],
341
+ affectedFiles: [facts.relativePath],
342
+ }));
343
+ }
344
+ }
345
+ return issues;
346
+ }
253
347
  function findOldOrDeprecatedFilesNotInDocs(ctx, docsConcat) {
254
348
  const issues = [];
255
349
  for (const file of ctx.inventory.files) {
@@ -1,5 +1,5 @@
1
1
  import { readBoundedFileText } from "../utils/boundedRead.js";
2
- import { firstPathSegment, GENERIC_DECLARATION_NAMES, GENERIC_INFRA_BASENAMES, baseNameNoExt } from "../utils/filePatternUtils.js";
2
+ import { firstPathSegment, GENERIC_INFRA_BASENAMES, baseNameNoExt, isGenericOrTestPrefixedDeclarationName, } from "../utils/filePatternUtils.js";
3
3
  import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
4
4
  import { indexSourceFactsByPath } from "../utils/sourceFactsLookup.js";
5
5
  // ---------------------------------------------------------------------------
@@ -225,12 +225,12 @@ function firstTopLevelUnderSrc(relativePath) {
225
225
  // (function/class/interface/type/enum) -- "variable"/"constant"/"method"/
226
226
  // "unknown" are excluded as too noisy (methods repeat across unrelated
227
227
  // classes by convention; module-scope variables/constants repeat even more
228
- // often). Generic declaration names (run, init, handler, ...) are excluded
229
- // via GENERIC_DECLARATION_NAMES for the same reason
230
- // GENERIC_INFRA_BASENAMES excludes generic file basenames above. Requires
231
- // ctx.sourceFacts -- returns no issues when it is absent (e.g. existing
232
- // detector unit tests that build a literal AuditDetectorContext without
233
- // it), so this check is purely additive.
228
+ // often). Generic/lifecycle declaration names (run, init, handler, __init__,
229
+ // test_*, ...) are excluded via isGenericOrTestPrefixedDeclarationName() for
230
+ // the same reason GENERIC_INFRA_BASENAMES excludes generic file basenames
231
+ // above. Requires ctx.sourceFacts -- returns no issues when it is absent
232
+ // (e.g. existing detector unit tests that build a literal
233
+ // AuditDetectorContext without it), so this check is purely additive.
234
234
  const NOISY_DECLARATION_KINDS = new Set(["variable", "constant", "method", "unknown"]);
235
235
  function findDuplicateDeclarationCandidates(ctx) {
236
236
  if (!ctx.sourceFacts)
@@ -248,9 +248,24 @@ function findDuplicateDeclarationCandidates(ctx) {
248
248
  if (NOISY_DECLARATION_KINDS.has(decl.kind))
249
249
  continue;
250
250
  const nameLower = decl.name.toLowerCase();
251
- if (GENERIC_DECLARATION_NAMES.has(nameLower))
251
+ if (isGenericOrTestPrefixedDeclarationName(nameLower))
252
252
  continue;
253
- const key = `${decl.kind}:${nameLower}`;
253
+ // v0.3.2 Batch 2 -- the key is scoped by `facts.analyzerId` (not
254
+ // `facts.language`) so a Python declaration is only ever compared
255
+ // against other Python declarations, never against a same-named
256
+ // TypeScript/JavaScript one. `analyzerId`, not `language`, is the
257
+ // right scope here: TypeScript and JavaScript are two different
258
+ // NormalizedLanguage values but share one analyzer
259
+ // (typescript-javascript-analyzer) and were already compared together
260
+ // before this batch -- scoping by `language` instead would have
261
+ // silently split that existing, still-desired TS/JS pairing in two
262
+ // (each side then below the "2+ files" threshold, so both would be
263
+ // dropped entirely), which is exactly the "preserve existing
264
+ // TypeScript/JavaScript duplicate behavior" regression this batch
265
+ // must not introduce. Scoping by `analyzerId` fixes the real
266
+ // cross-language bug (Python vs TS/JS) without touching TS/JS's own
267
+ // existing behavior at all.
268
+ const key = `${facts.analyzerId}:${decl.kind}:${nameLower}`;
254
269
  const entry = byKey.get(key) ?? { name: decl.name, kind: decl.kind, paths: new Set() };
255
270
  entry.paths.add(facts.relativePath);
256
271
  byKey.set(key, entry);
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { collectPythonProjectMetadata } from "../../core/pythonProjectMetadata.js";
3
4
  import { readBoundedFileText } from "../utils/boundedRead.js";
4
5
  import { deduplicateIssuesById, makeCodeRotIssue } from "../utils/issueFactories.js";
5
6
  import { getParsedSourceFacts, indexSourceFactsByPath } from "../utils/sourceFactsLookup.js";
@@ -44,6 +45,12 @@ export const TEST_ROT_DETECTOR = {
44
45
  const scriptNames = new Set(ctx.sourceOfTruth.commands.allScriptNames);
45
46
  const currentVersion = ctx.sourceOfTruth.package?.version ?? null;
46
47
  const sourceFactsIndex = indexSourceFactsByPath(ctx.sourceFacts);
48
+ // v0.3.2 Batch 2 -- computed once per run, not per test file. Presence-
49
+ // only/simple-text-extraction (see pythonProjectMetadata.ts); used below
50
+ // purely as weak supporting context on Python findings, never as a
51
+ // required condition -- its absence must not change whether a finding
52
+ // is emitted, only add or omit one extra evidence line.
53
+ const pythonMetadata = collectPythonProjectMetadata(ctx.target.rootPath, ctx.inventory);
47
54
  for (const testFile of ctx.inventory.testFiles) {
48
55
  const content = readBoundedFileText(ctx.target.rootPath, testFile.relativePath, testFile.sizeBytes, MAX_READ_BYTES);
49
56
  if (content === null)
@@ -71,7 +78,16 @@ export const TEST_ROT_DETECTOR = {
71
78
  // (testRelativePath, "missing-source-import", specifier), so an
72
79
  // identical id is deduplicated by deduplicateIssuesById() below.
73
80
  const parsedFacts = getParsedSourceFacts(sourceFactsIndex, testFile.relativePath);
74
- if (parsedFacts) {
81
+ if (parsedFacts?.language === "python") {
82
+ // v0.3.2 Batch 2 -- Python relative imports use dotted-module
83
+ // notation (".module", "..pkg.module"), not TS/JS's path-like
84
+ // "./module" syntax. Routing Python facts through the TS/JS
85
+ // resolver below would misresolve every relative Python import and
86
+ // flag it as missing regardless of whether it actually exists --
87
+ // this guard is the fix for that latent cross-language bug.
88
+ issues.push(...findMissingPythonSourceImports(ctx, testFile.relativePath, parsedFacts, pythonMetadata));
89
+ }
90
+ else if (parsedFacts) {
75
91
  issues.push(...findMissingSourceImportsFromSourceFacts(ctx, testFile.relativePath, parsedFacts));
76
92
  }
77
93
  issues.push(...findStaleNpmRunReferences(testFile.relativePath, content, scriptNames));
@@ -177,6 +193,104 @@ function findMissingSourceImportsFromSourceFacts(ctx, testRelativePath, facts) {
177
193
  }
178
194
  return issues;
179
195
  }
196
+ // v0.3.2 Batch 2 -- resolves a Python relative-import source (".module",
197
+ // "..pkg.sub", or bare "."/".." paired with `from . import name`-style
198
+ // imported names) to candidate local file paths, using Python's own
199
+ // relative-import level semantics (one leading dot = the importing file's
200
+ // own directory, each additional dot = one directory further up). This is a
201
+ // conservative, path-based approximation -- it does not resolve namespace
202
+ // packages, `sys.path` manipulation, editable installs, or anything beyond
203
+ // simple on-disk file/package presence, so returning zero matches is a
204
+ // candidate "may indicate a missing module" signal, not proof.
205
+ function resolvePythonRelativeImportCandidatePaths(testDir, importSource, importedNames) {
206
+ if (!importSource.startsWith("."))
207
+ return [];
208
+ let dotCount = 0;
209
+ while (dotCount < importSource.length && importSource[dotCount] === ".")
210
+ dotCount += 1;
211
+ const remainder = importSource.slice(dotCount);
212
+ const upLevels = dotCount - 1;
213
+ let baseDir = testDir;
214
+ for (let i = 0; i < upLevels; i++)
215
+ baseDir = path.dirname(baseDir);
216
+ const candidates = [];
217
+ const pushCandidatesFor = (moduleSegment) => {
218
+ candidates.push(path.join(baseDir, `${moduleSegment}.py`).replace(/\\/g, "/"));
219
+ candidates.push(path.join(baseDir, moduleSegment, "__init__.py").replace(/\\/g, "/"));
220
+ };
221
+ if (remainder) {
222
+ pushCandidatesFor(remainder.split(".").join("/"));
223
+ }
224
+ else {
225
+ // Bare "." / ".." (e.g. `from . import sibling`) -- the module lives
226
+ // in `importedNames`, not in `importSource` itself.
227
+ for (const name of importedNames ?? [])
228
+ pushCandidatesFor(name);
229
+ }
230
+ return candidates;
231
+ }
232
+ // v0.3.2 Batch 2 -- Python-specific counterpart to
233
+ // findMissingSourceImportsFromSourceFacts() above. Only ever called for
234
+ // facts with `language === "python"` (see caller) -- the two functions are
235
+ // kept fully separate rather than parameterized by language, because their
236
+ // resolution algorithms are genuinely different (dotted-module relative-
237
+ // import-level semantics vs. path-like specifier semantics), and conflating
238
+ // them risked silently misresolving one language or the other. Deliberately
239
+ // lower severity/confidence than the TS/JS counterpart: this scan has no
240
+ // real package/sys.path awareness, so it is weaker evidence than the
241
+ // TS/JS analyzer's near-exact relative-path resolution.
242
+ function findMissingPythonSourceImports(ctx, testRelativePath, facts, pythonMetadata) {
243
+ const issues = [];
244
+ const testDir = path.dirname(testRelativePath);
245
+ for (const imp of facts.imports) {
246
+ if (!imp.source.startsWith("."))
247
+ continue; // conservative: only relative Python imports are checked
248
+ const candidates = resolvePythonRelativeImportCandidatePaths(testDir, imp.source, imp.importedNames);
249
+ if (candidates.length === 0)
250
+ continue; // nothing checkable (e.g. bare "." with no imported names)
251
+ const resolved = candidates.some((candidate) => fs.existsSync(path.join(ctx.target.rootPath, candidate)));
252
+ if (resolved)
253
+ continue;
254
+ const evidence = [
255
+ {
256
+ kind: "file",
257
+ message: "Analyzer-recorded relative Python import does not resolve to a candidate local module/package file. This is a best-effort, path-based check -- it does not resolve Python's real package/sys.path rules, so confirm manually before treating it as stale.",
258
+ filePath: testRelativePath,
259
+ excerpt: imp.source,
260
+ source: DETECTOR_ID,
261
+ confidence: "low",
262
+ },
263
+ ];
264
+ if (pythonMetadata.hasPytestConfiguration) {
265
+ evidence.push({
266
+ kind: "observation",
267
+ message: "Weak supporting context: this project has a detected pytest configuration (pytest.ini, or a pyproject.toml/setup.cfg pytest section), consistent with pytest-style test conventions -- not a requirement for this finding.",
268
+ source: DETECTOR_ID,
269
+ confidence: "low",
270
+ });
271
+ }
272
+ issues.push(makeCodeRotIssue({
273
+ auditType: "code-rot",
274
+ detectorId: DETECTOR_ID,
275
+ idCues: [testRelativePath, "missing-python-source-import", imp.source],
276
+ title: `Test "${testRelativePath}" imports a Python module that may not exist: "${imp.source}"`,
277
+ description: `${testRelativePath} imports "${imp.source}" (source-facts-derived via the Python analyzer). No candidate local module or package file was found relative to the test file. This is a deterministic source-facts signal from a conservative, non-semantic scan -- it does not resolve Python's real package/sys.path rules, namespace packages, or editable installs, so this may indicate a stale import but is not proof.`,
278
+ severity: "low",
279
+ confidence: "low",
280
+ falsePositiveRisk: "medium",
281
+ category: "test-rot",
282
+ recommendedAction: "Confirm the imported Python module exists (it may resolve via a path this scan cannot see) before treating this as stale.",
283
+ suggestedFixStrategy: `Update or remove the import "${imp.source}" in ${testRelativePath} if confirmed stale.`,
284
+ validationCommands: [`npx vitest run ${testRelativePath}`],
285
+ releaseBlocking: false,
286
+ implementationBlocking: false,
287
+ autoFixEligible: false,
288
+ evidence,
289
+ affectedFiles: [testRelativePath],
290
+ }));
291
+ }
292
+ return issues;
293
+ }
180
294
  function findStaleNpmRunReferences(testRelativePath, content, scriptNames) {
181
295
  const issues = [];
182
296
  const seen = new Set();
@@ -49,13 +49,17 @@ export const GENERIC_INFRA_BASENAMES = new Set([
49
49
  // expected to repeat across unrelated files legitimately (CLI entrypoints,
50
50
  // handler/lifecycle conventions, etc.) -- excluded from the source-facts
51
51
  // duplicate-declaration-candidate heuristic in duplicateImplementationDetector.ts
52
- // to keep false positives near zero.
52
+ // to keep false positives near zero. v0.3.2 Batch 2 adds Python lifecycle/
53
+ // dunder names ("teardown", "__init__", "__call__") for the same reason,
54
+ // reused by both duplicateImplementationDetector.ts and
55
+ // deadCodeCandidateDetector.ts's new Python-aware checks.
53
56
  export const GENERIC_DECLARATION_NAMES = new Set([
54
57
  "run",
55
58
  "main",
56
59
  "init",
57
60
  "initialize",
58
61
  "setup",
62
+ "teardown",
59
63
  "handler",
60
64
  "handle",
61
65
  "execute",
@@ -76,4 +80,18 @@ export const GENERIC_DECLARATION_NAMES = new Set([
76
80
  "get",
77
81
  "set",
78
82
  "default",
83
+ "__init__",
84
+ "__call__",
79
85
  ]);
86
+ // v0.3.2 Batch 2 -- true for a generic/lifecycle declaration name (see
87
+ // GENERIC_DECLARATION_NAMES above) or a pytest-style `test_*` name. Shared by
88
+ // the Python-aware dead-code and duplicate-implementation checks so a
89
+ // `test_*` helper/fixture function is never flagged as a candidate dead-code
90
+ // symbol or a duplicate-implementation candidate merely for repeating across
91
+ // unrelated test modules, a legitimate and extremely common pytest
92
+ // convention. Comparison is case-insensitive; callers pass the already-
93
+ // lowercased name where convenient.
94
+ export function isGenericOrTestPrefixedDeclarationName(name) {
95
+ const lower = name.toLowerCase();
96
+ return GENERIC_DECLARATION_NAMES.has(lower) || lower.startsWith("test_");
97
+ }
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { AUDIT_FAIL_ON_THRESHOLDS, AUDIT_INCLUDE_AREAS, AUDIT_OUTPUT_FORMATS, AUDIT_TYPES, DEFAULT_AUDIT_FAIL_ON_THRESHOLD, DEFAULT_AUDIT_INCLUDE_AREAS, DEFAULT_AUDIT_OUTPUT_FORMATS, DEFAULT_AUDIT_TYPES, PLANNED_AUDIT_TYPES, } from "./auditTypes.js";
2
+ import { AUDIT_FAIL_ON_THRESHOLDS, AUDIT_INCLUDE_AREAS, AUDIT_OUTPUT_FORMATS, AUDIT_TYPES, DEFAULT_AUDIT_FAIL_ON_THRESHOLD, DEFAULT_AUDIT_INCLUDE_AREAS, DEFAULT_AUDIT_OUTPUT_FORMATS, DEFAULT_AUDIT_TYPES, IMPLEMENTED_AUDIT_TYPES, PLANNED_AUDIT_TYPES, } from "./auditTypes.js";
3
3
  const FLAGS_WITH_VALUE = {
4
4
  "--target": "target",
5
5
  "--types": "types",
@@ -67,7 +67,7 @@ function parseTypesOption(raw) {
67
67
  }
68
68
  const planned = parts.filter((p) => PLANNED_AUDIT_TYPES.includes(p));
69
69
  if (planned.length > 0) {
70
- throw new Error(`--types value(s) planned but not implemented: ${planned.join(", ")}. Only "code-rot" is implemented in this version.`);
70
+ throw new Error(`--types value(s) planned but not implemented: ${planned.join(", ")}. Implemented audit types: ${IMPLEMENTED_AUDIT_TYPES.join(", ")}.`);
71
71
  }
72
72
  const unique = new Set(parts);
73
73
  const ordered = AUDIT_TYPES.filter((id) => unique.has(id));
@@ -4,6 +4,9 @@ import { resolveAuditTarget } from "./auditTarget.js";
4
4
  import { scanProjectInventory } from "./projectInventory.js";
5
5
  import { collectSourceOfTruth } from "./sourceOfTruth.js";
6
6
  import { collectSourceFacts } from "./collectSourceFacts.js";
7
+ import { collectPythonProjectMetadata } from "./pythonProjectMetadata.js";
8
+ import { runSecurityAuditAdapter } from "../security/securityAuditAdapter.js";
9
+ import { SECURITY_AUDIT_NOT_RUN_SUMMARY } from "../security/securityAuditTypes.js";
7
10
  export async function runAudit(options) {
8
11
  const { config, toolRoot } = options;
9
12
  const registry = options.registry ?? DEFAULT_AUDIT_REGISTRY;
@@ -11,6 +14,7 @@ export async function runAudit(options) {
11
14
  const inventory = scanProjectInventory(target.rootPath);
12
15
  const sourceOfTruth = collectSourceOfTruth(target.rootPath, inventory);
13
16
  const sourceFacts = await collectSourceFacts(target.rootPath, inventory);
17
+ const pythonProjectMetadata = collectPythonProjectMetadata(target.rootPath, inventory);
14
18
  const selected = selectDetectors(registry, config.types, config.include);
15
19
  const ctx = { target, config, inventory, sourceOfTruth, sourceFacts };
16
20
  const issues = [];
@@ -37,6 +41,19 @@ export async function runAudit(options) {
37
41
  });
38
42
  }
39
43
  }
44
+ // v0.3.2 Batch 4 -- security audit adapter. Conditional (see the
45
+ // AuditResult.securitySummary field comment above): only runs when --types
46
+ // explicitly includes "security", never as part of a default/code-rot-only
47
+ // run. Appended after the code-rot detector loop above, so combined
48
+ // `--types code-rot,security` runs keep a deterministic issue order:
49
+ // code-rot detector issues (registry order) first, then security findings
50
+ // (security-validation check-execution order).
51
+ let securitySummary = SECURITY_AUDIT_NOT_RUN_SUMMARY;
52
+ if (config.types.includes("security")) {
53
+ const securityAudit = await runSecurityAuditAdapter({ toolRoot, config });
54
+ issues.push(...securityAudit.issues);
55
+ securitySummary = securityAudit.summary;
56
+ }
40
57
  const issueCounts = countIssuesBySeverity(issues);
41
58
  const { exitCode, reason } = calculateAuditExitCode(issues, config.failOn);
42
59
  return {
@@ -62,6 +79,8 @@ export async function runAudit(options) {
62
79
  inventory,
63
80
  sourceOfTruth,
64
81
  sourceFacts,
82
+ pythonProjectMetadata,
83
+ securitySummary,
65
84
  noDetectorsRegistered: selected.length === 0,
66
85
  };
67
86
  }
@@ -7,14 +7,21 @@
7
7
  // this batch keeps the audit issue schema independent even where a
8
8
  // vocabulary word (e.g. "blocker") happens to be shared.
9
9
  // ---------------------------------------------------------------------------
10
- // Recognized --types values. Only "code-rot" has real detector coverage in
11
- // Batch 1 — the rest are known, named future audit types that must fail
12
- // cleanly with a "planned but not implemented" message rather than being
13
- // silently accepted or treated as an unknown/garbage value.
10
+ // Recognized --types values. "code-rot" (Batch 1) and "security" (v0.3.2
11
+ // Batch 4, via the security-validation audit adapter) have real coverage —
12
+ // the rest are known, named future audit types that must fail cleanly with a
13
+ // "planned but not implemented" message rather than being silently accepted
14
+ // or treated as an unknown/garbage value.
14
15
  export const AUDIT_TYPES = ["code-rot", "quality", "security", "project", "all"];
15
- export const IMPLEMENTED_AUDIT_TYPES = ["code-rot"];
16
- export const PLANNED_AUDIT_TYPES = ["quality", "security", "project", "all"];
17
- export const DEFAULT_AUDIT_TYPES = IMPLEMENTED_AUDIT_TYPES;
16
+ export const IMPLEMENTED_AUDIT_TYPES = ["code-rot", "security"];
17
+ export const PLANNED_AUDIT_TYPES = ["quality", "project", "all"];
18
+ // v0.3.2 Batch 4 — deliberately NOT derived from IMPLEMENTED_AUDIT_TYPES.
19
+ // "security" becomes selectable via --types security (and --types
20
+ // code-rot,security) but must never join the *default* (no --types flag)
21
+ // audit run, since it shells out to npm audit/CodeQL/Semgrep/fuzz and is far
22
+ // more expensive than the always-on code-rot detectors — a default,
23
+ // unqualified `npm run audit` must keep its existing code-rot-only behavior.
24
+ export const DEFAULT_AUDIT_TYPES = ["code-rot"];
18
25
  export const AUDIT_INCLUDE_AREAS = ["docs", "tests", "package", "architecture", "cli"];
19
26
  export const DEFAULT_AUDIT_INCLUDE_AREAS = AUDIT_INCLUDE_AREAS;
20
27
  export const AUDIT_OUTPUT_FORMATS = ["text", "json"];
@@ -1,4 +1,5 @@
1
1
  import { TYPESCRIPT_JAVASCRIPT_ANALYZER } from "./typescriptJavaScriptAnalyzer.js";
2
+ import { PYTHON_ANALYZER } from "./pythonAnalyzer.js";
2
3
  // Validates a list has no duplicate analyzer ids. Later batches call this
3
4
  // when assembling their own registries; Batch 2's empty default trivially
4
5
  // passes.
@@ -25,9 +26,11 @@ export function selectLanguageAnalyzer(registry, language, extension) {
25
26
  }
26
27
  return null;
27
28
  }
28
- // v0.3.1 Batch 3 -- registers the TypeScript/JavaScript analyzer. Python,
29
- // Java, and Kotlin remain fallback-only (no analyzer registered for them) --
30
- // see collectSourceFacts.ts's fallback policy.
29
+ // v0.3.1 Batch 3 -- registers the TypeScript/JavaScript analyzer. v0.3.2
30
+ // Batch 1 adds the Python analyzer alongside it. Java and Kotlin remain
31
+ // fallback-only (no analyzer registered for them) -- see
32
+ // collectSourceFacts.ts's fallback policy.
31
33
  export const DEFAULT_LANGUAGE_ANALYZER_REGISTRY = createLanguageAnalyzerRegistry([
32
34
  TYPESCRIPT_JAVASCRIPT_ANALYZER,
35
+ PYTHON_ANALYZER,
33
36
  ]);