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

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.
@@ -0,0 +1,106 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const GRADLE_BUILD_FILE_NAMES = new Set(["build.gradle", "build.gradle.kts"]);
4
+ const GRADLE_SETTINGS_FILE_NAMES = new Set(["settings.gradle", "settings.gradle.kts"]);
5
+ const GRADLE_WRAPPER_FILE_NAMES = new Set(["gradlew", "gradlew.bat"]);
6
+ // Conservative, single-line extraction: `rootProject.name = "foo"` (Groovy
7
+ // or Kotlin DSL both use this exact call-and-assign shape). Does not handle
8
+ // multi-line expressions, string concatenation, or computed values.
9
+ const GRADLE_SETTINGS_PROJECT_NAME_PATTERN = /rootProject\.name\s*=\s*["']([^"']+)["']/;
10
+ // Conservative, single-line/non-nested extraction: the first top-level
11
+ // `<artifactId>...</artifactId>` in the file. Real pom.xml can nest an
12
+ // `<artifactId>` inside `<parent>` or `<dependencies>`, which this naive
13
+ // first-match scan cannot distinguish -- so this is intentionally treated as
14
+ // best-effort only, matching this module's "leave null rather than guess"
15
+ // policy for anything structurally ambiguous. A `<parent>` block, if present
16
+ // before the project's own `<artifactId>`, is skipped over so the far more
17
+ // common case (parent declared first, then the project's own coordinates)
18
+ // resolves to the right value instead of the parent's.
19
+ const POM_ARTIFACT_ID_PATTERN = /<artifactId>\s*([^<\s][^<]*?)\s*<\/artifactId>/;
20
+ function findSourceSetDirectories(inventory, segment, language) {
21
+ const dirName = `src/${segment}/${language}`;
22
+ const prefix = `${dirName}/`;
23
+ const found = inventory.files.some((f) => f.relativePath === dirName || f.relativePath.startsWith(prefix));
24
+ return found ? [dirName] : [];
25
+ }
26
+ export function collectJvmProjectMetadata(targetRoot, inventory) {
27
+ const warnings = [];
28
+ const knownPaths = new Set(inventory.files.map((f) => f.relativePath));
29
+ const recognizedBuildFiles = inventory.files
30
+ .map((f) => f.relativePath)
31
+ .filter((relativePath) => {
32
+ const basename = path.posix.basename(relativePath);
33
+ return (GRADLE_BUILD_FILE_NAMES.has(basename) ||
34
+ GRADLE_SETTINGS_FILE_NAMES.has(basename) ||
35
+ GRADLE_WRAPPER_FILE_NAMES.has(basename) ||
36
+ basename === "pom.xml" ||
37
+ basename === "gradle.properties");
38
+ })
39
+ .sort((a, b) => a.localeCompare(b));
40
+ const hasGradleBuild = recognizedBuildFiles.some((p) => GRADLE_BUILD_FILE_NAMES.has(path.posix.basename(p)));
41
+ const hasGradleSettings = recognizedBuildFiles.some((p) => GRADLE_SETTINGS_FILE_NAMES.has(path.posix.basename(p)));
42
+ const hasMavenPom = knownPaths.has("pom.xml");
43
+ const hasGradleWrapper = recognizedBuildFiles.some((p) => GRADLE_WRAPPER_FILE_NAMES.has(path.posix.basename(p)));
44
+ const javaSourceDirs = findSourceSetDirectories(inventory, "main", "java");
45
+ const kotlinSourceDirs = findSourceSetDirectories(inventory, "main", "kotlin");
46
+ const javaTestDirs = findSourceSetDirectories(inventory, "test", "java");
47
+ const kotlinTestDirs = findSourceSetDirectories(inventory, "test", "kotlin");
48
+ // Source-set directory presence is a conservative signal only -- a
49
+ // project can validly keep Java/Kotlin files outside the conventional
50
+ // src/main|test/{java,kotlin} layout, so this does not attempt to prove
51
+ // "has no Java/Kotlin source"; it only reports the conventional-layout
52
+ // signal explicitly. hasJavaSource/hasKotlinSource below fall back to any
53
+ // scanned file of that language so they stay meaningful even for
54
+ // non-conventional layouts.
55
+ const hasJavaSource = javaSourceDirs.length > 0 || inventory.filesByLanguage.java > 0;
56
+ const hasKotlinSource = kotlinSourceDirs.length > 0 || inventory.filesByLanguage.kotlin > 0;
57
+ const hasJavaTests = javaTestDirs.length > 0;
58
+ const hasKotlinTests = kotlinTestDirs.length > 0;
59
+ const recognizedSourceDirectories = [...new Set([...javaSourceDirs, ...kotlinSourceDirs])].sort((a, b) => a.localeCompare(b));
60
+ const recognizedTestDirectories = [...new Set([...javaTestDirs, ...kotlinTestDirs])].sort((a, b) => a.localeCompare(b));
61
+ let projectName = null;
62
+ if (hasGradleSettings) {
63
+ const settingsPath = recognizedBuildFiles.find((p) => GRADLE_SETTINGS_FILE_NAMES.has(path.posix.basename(p)));
64
+ if (settingsPath) {
65
+ const content = readSafely(targetRoot, settingsPath, warnings);
66
+ if (content !== null) {
67
+ const match = content.match(GRADLE_SETTINGS_PROJECT_NAME_PATTERN);
68
+ if (match)
69
+ projectName = match[1];
70
+ }
71
+ }
72
+ }
73
+ if (projectName === null && hasMavenPom) {
74
+ const content = readSafely(targetRoot, "pom.xml", warnings);
75
+ if (content !== null) {
76
+ const withoutParentBlock = content.replace(/<parent>[\s\S]*?<\/parent>/, "");
77
+ const match = withoutParentBlock.match(POM_ARTIFACT_ID_PATTERN);
78
+ if (match)
79
+ projectName = match[1];
80
+ }
81
+ }
82
+ return {
83
+ hasGradleBuild,
84
+ hasGradleSettings,
85
+ hasMavenPom,
86
+ hasGradleWrapper,
87
+ hasJavaSource,
88
+ hasKotlinSource,
89
+ hasJavaTests,
90
+ hasKotlinTests,
91
+ recognizedBuildFiles,
92
+ recognizedSourceDirectories,
93
+ recognizedTestDirectories,
94
+ projectName,
95
+ warnings,
96
+ };
97
+ }
98
+ function readSafely(targetRoot, relativePath, warnings) {
99
+ try {
100
+ return fs.readFileSync(path.join(targetRoot, relativePath), "utf8");
101
+ }
102
+ catch (err) {
103
+ warnings.push(`Could not read ${relativePath}: ${err instanceof Error ? err.message : String(err)}`);
104
+ return null;
105
+ }
106
+ }
@@ -0,0 +1,280 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.3.3 Batch 1 -- conservative, dependency-free Kotlin source analyzer.
3
+ //
4
+ // There is no Kotlin compiler/parser available in this Node/TypeScript
5
+ // project, and per this batch's scope this analyzer must not add a new
6
+ // parser dependency. Facts are extracted with the same style of
7
+ // brace-depth-tracking line scan as javaAnalyzer.ts -- not a real
8
+ // tokenizer, not an AST, and not a syntax validator.
9
+ //
10
+ // What this DOES extract:
11
+ // - a single `package foo.bar` declaration (no trailing `;` required)
12
+ // - `import foo.bar.Baz` and `import foo.bar.Baz as Alias` (recorded via
13
+ // the existing generic ImportFactKind "static" -- "resolved through a
14
+ // static, syntactic import statement", the same meaning already used
15
+ // by every other analyzer in this project)
16
+ // - class, data class, interface, and object declarations (data class ->
17
+ // DeclarationFactKind "class"; object -> "class", the closest safe
18
+ // existing kind for a singleton declaration -- there is no dedicated
19
+ // object kind in the shared model)
20
+ // - `enum class Foo` -> DeclarationFactKind "enum"
21
+ // - top-level function declarations (`fun foo(...)`) -> kind "function"
22
+ // - member function declarations directly inside a class/interface/
23
+ // object body -> kind "method"
24
+ // - visibility (public/internal/protected/private), folded into the
25
+ // existing `exported` boolean -- see "Visibility mapping" below
26
+ // - line counts, parse/analyze status, and diagnostics for degraded
27
+ // parsing
28
+ //
29
+ // What this explicitly does NOT do: real Kotlin compilation, type
30
+ // inference, extension-function semantic resolution (an extension
31
+ // function's receiver type is not modeled -- only the function's own
32
+ // identifier is recorded as its name), coroutine behavior, multiplatform
33
+ // source-set semantics, Android/Compose lifecycle behavior, runtime
34
+ // behavior, semantic duplicate-implementation proof, or dead-code proof.
35
+ // Property (`val`/`var`) declarations are not extracted (out of this
36
+ // batch's required scope, and harder to distinguish conservatively from
37
+ // local variable declarations via regex alone).
38
+ //
39
+ // Visibility mapping (no SourceFacts schema change):
40
+ // Same reasoning as javaAnalyzer.ts. Kotlin's default (no modifier) is
41
+ // `public`, unlike Java's package-private default -- so `exported` is
42
+ // true unless an explicit `internal`, `protected`, or `private` modifier
43
+ // is present. This intentionally collapses internal/protected/private
44
+ // into a single "not exported" bucket; callers needing that distinction
45
+ // must re-derive it from source.
46
+ // ---------------------------------------------------------------------------
47
+ export const KOTLIN_ANALYZER_ID = "kotlin-analyzer";
48
+ const SUPPORTED_EXTENSIONS = [".kt", ".kts"];
49
+ // Mirrors typescriptJavaScriptAnalyzer.ts/pythonAnalyzer.ts/javaAnalyzer.ts's
50
+ // bound.
51
+ const MAX_ANALYZABLE_BYTES = 1_000_000;
52
+ const PACKAGE_LINE_PATTERN = /^\s*package\s+([\w.]+)\s*;?\s*$/;
53
+ const IMPORT_LINE_PATTERN = /^\s*import\s+([\w.]+(?:\.\*)?)(?:\s+as\s+(\w+))?\s*;?\s*$/;
54
+ const VISIBILITY_WORDS = "(?:public|internal|protected|private)";
55
+ const OTHER_TYPE_MODIFIER_WORDS = "(?:open|final|abstract|sealed|inner|data|annotation|enum|companion|value|actual|expect)";
56
+ const TYPE_DECL_PATTERN = new RegExp(`^\\s*(${VISIBILITY_WORDS}\\s+)?((?:${OTHER_TYPE_MODIFIER_WORDS}\\s+)*)(class|interface|object)\\s+(\\w+)`);
57
+ // Optional generic receiver/type parameters and an optional extension-
58
+ // function receiver (`ReceiverType.`) before the function name -- the
59
+ // receiver itself is discarded, only the final identifier is kept as the
60
+ // function's name (see header comment).
61
+ const FUN_LINE_PATTERN = new RegExp(`^\\s*(${VISIBILITY_WORDS}\\s+)?(?:(?:override|open|final|abstract|inline|suspend|operator|infix|tailrec|external)\\s+)*fun(?:\\s*<[^>]*>)?\\s+(?:[\\w<>?.]+\\.)?(\\w+)\\s*\\(`);
62
+ export const KOTLIN_ANALYZER = {
63
+ id: KOTLIN_ANALYZER_ID,
64
+ supportedLanguages: ["kotlin"],
65
+ supportedExtensions: SUPPORTED_EXTENSIONS,
66
+ analyzeFile,
67
+ };
68
+ function analyzeFile(input) {
69
+ const { relativePath, language, role, content, inventoryEntry } = input;
70
+ if (Buffer.byteLength(content, "utf8") > MAX_ANALYZABLE_BYTES) {
71
+ return {
72
+ relativePath,
73
+ language,
74
+ role,
75
+ parseStatus: "file-level-only",
76
+ analyzerId: KOTLIN_ANALYZER_ID,
77
+ lineCount: inventoryEntry.lineCount,
78
+ imports: [],
79
+ exports: [],
80
+ declarations: [],
81
+ references: [],
82
+ diagnostics: [
83
+ {
84
+ severity: "info",
85
+ code: "file-too-large-for-analysis",
86
+ message: `File exceeds the ${MAX_ANALYZABLE_BYTES}-byte analyzer bound; only inventory-level facts are available.`,
87
+ path: relativePath,
88
+ analyzerId: KOTLIN_ANALYZER_ID,
89
+ },
90
+ ],
91
+ };
92
+ }
93
+ const diagnostics = [];
94
+ const { logical, hadUnterminatedBlockComment, hadUnterminatedRawString } = buildLogicalLines(content);
95
+ const imports = [];
96
+ const declarations = [];
97
+ const scopeStack = [];
98
+ let depth = 0;
99
+ for (let i = 0; i < logical.length; i++) {
100
+ const line = logical[i];
101
+ const lineNumber = i + 1;
102
+ if (line.trim().length === 0) {
103
+ continue;
104
+ }
105
+ const importMatch = line.match(IMPORT_LINE_PATTERN);
106
+ if (importMatch) {
107
+ imports.push({
108
+ source: importMatch[1],
109
+ kind: "static",
110
+ line: lineNumber,
111
+ ...(importMatch[2] ? { importedNames: [importMatch[2]] } : {}),
112
+ });
113
+ }
114
+ const typeMatch = line.match(TYPE_DECL_PATTERN);
115
+ let isEnumClass = false;
116
+ if (typeMatch) {
117
+ isEnumClass = /(?:^|\s)enum(?:\s|$)/.test(typeMatch[2]);
118
+ }
119
+ if (typeMatch) {
120
+ const rawKeyword = typeMatch[3]; // "class" | "interface" | "object"
121
+ const name = typeMatch[4];
122
+ const exported = typeMatch[1] === undefined || typeMatch[1].trim() === "public";
123
+ declarations.push({
124
+ name,
125
+ kind: isEnumClass ? "enum" : rawKeyword === "interface" ? "interface" : "class",
126
+ exported,
127
+ line: lineNumber,
128
+ });
129
+ }
130
+ else if (scopeStack.length > 0 && depth === scopeStack[scopeStack.length - 1].bodyDepth) {
131
+ const funMatch = line.match(FUN_LINE_PATTERN);
132
+ if (funMatch) {
133
+ const exported = funMatch[1] === undefined || funMatch[1].trim() === "public";
134
+ declarations.push({ name: funMatch[2], kind: "method", exported, line: lineNumber });
135
+ }
136
+ }
137
+ else if (scopeStack.length === 0) {
138
+ const funMatch = line.match(FUN_LINE_PATTERN);
139
+ if (funMatch) {
140
+ const exported = funMatch[1] === undefined || funMatch[1].trim() === "public";
141
+ declarations.push({ name: funMatch[2], kind: "function", exported, line: lineNumber });
142
+ }
143
+ }
144
+ const depthBefore = depth;
145
+ depth += countChar(line, "{") - countChar(line, "}");
146
+ if (typeMatch) {
147
+ const rawKeyword = typeMatch[3];
148
+ // class/object and interface both get a scope frame ("object" is
149
+ // tracked as a "class"-kind scope for member-function purposes);
150
+ // whether the opening `{` lands on this line or a later one, the
151
+ // eventual `{` bumps depth to exactly depthBefore + 1 either way --
152
+ // same reasoning as javaAnalyzer.ts's scope push.
153
+ const scopeKind = rawKeyword === "interface" ? "interface" : "class";
154
+ scopeStack.push({ kind: scopeKind, bodyDepth: depthBefore + 1 });
155
+ }
156
+ while (scopeStack.length > 0 && depth < scopeStack[scopeStack.length - 1].bodyDepth) {
157
+ scopeStack.pop();
158
+ }
159
+ }
160
+ if (hadUnterminatedRawString) {
161
+ diagnostics.push({
162
+ severity: "error",
163
+ code: "unterminated-raw-string",
164
+ message: 'File appears to contain an unterminated """ raw string; facts from this best-effort scan may be incomplete.',
165
+ path: relativePath,
166
+ analyzerId: KOTLIN_ANALYZER_ID,
167
+ });
168
+ }
169
+ if (hadUnterminatedBlockComment) {
170
+ diagnostics.push({
171
+ severity: "error",
172
+ code: "unterminated-block-comment",
173
+ message: "File appears to contain an unterminated /* block comment; facts from this best-effort scan may be incomplete.",
174
+ path: relativePath,
175
+ analyzerId: KOTLIN_ANALYZER_ID,
176
+ });
177
+ }
178
+ const parseStatus = hadUnterminatedRawString || hadUnterminatedBlockComment ? "parse-error" : "parsed";
179
+ return {
180
+ relativePath,
181
+ language,
182
+ role,
183
+ parseStatus,
184
+ analyzerId: KOTLIN_ANALYZER_ID,
185
+ lineCount: inventoryEntry.lineCount,
186
+ imports,
187
+ exports: [],
188
+ declarations,
189
+ references: [],
190
+ diagnostics,
191
+ };
192
+ }
193
+ function countChar(line, ch) {
194
+ let count = 0;
195
+ for (let i = 0; i < line.length; i++) {
196
+ if (line[i] === ch)
197
+ count += 1;
198
+ }
199
+ return count;
200
+ }
201
+ // ---------------------------------------------------------------------------
202
+ // Comment/string stripping (heuristic, not a tokenizer). Strips `//` line
203
+ // comments and `/* ... */` block comments (which may span multiple lines),
204
+ // blanks out `"..."`/`'...'` single-line literal contents (escape-aware),
205
+ // and blanks out `"""..."""` raw strings (which may span multiple lines,
206
+ // no escape processing inside -- mirrors pythonAnalyzer.ts's triple-quote
207
+ // handling, since Kotlin raw strings share the same "arbitrary content
208
+ // until the next matching triple-quote marker" shape).
209
+ // ---------------------------------------------------------------------------
210
+ function buildLogicalLines(content) {
211
+ const rawLines = content.split(/\r\n|\r|\n/);
212
+ const logical = [];
213
+ let inBlockComment = false;
214
+ let inRawString = false;
215
+ for (const rawLine of rawLines) {
216
+ if (inRawString) {
217
+ const closeIdx = rawLine.indexOf('"""');
218
+ if (closeIdx === -1) {
219
+ logical.push("");
220
+ continue;
221
+ }
222
+ inRawString = false;
223
+ logical.push(stripLine(rawLine.slice(closeIdx + 3)));
224
+ continue;
225
+ }
226
+ logical.push(stripLine(rawLine));
227
+ }
228
+ return { logical, hadUnterminatedBlockComment: inBlockComment, hadUnterminatedRawString: inRawString };
229
+ function stripLine(line) {
230
+ let result = "";
231
+ let i = 0;
232
+ while (i < line.length) {
233
+ if (inBlockComment) {
234
+ const closeIdx = line.indexOf("*/", i);
235
+ if (closeIdx === -1)
236
+ return result;
237
+ inBlockComment = false;
238
+ i = closeIdx + 2;
239
+ continue;
240
+ }
241
+ const ch = line[i];
242
+ if (ch === "/" && line[i + 1] === "/")
243
+ return result;
244
+ if (ch === "/" && line[i + 1] === "*") {
245
+ inBlockComment = true;
246
+ i += 2;
247
+ continue;
248
+ }
249
+ if (line.slice(i, i + 3) === '"""') {
250
+ const closeIdx = line.indexOf('"""', i + 3);
251
+ if (closeIdx === -1) {
252
+ inRawString = true;
253
+ return result;
254
+ }
255
+ i = closeIdx + 3;
256
+ continue;
257
+ }
258
+ if (ch === '"' || ch === "'") {
259
+ const quote = ch;
260
+ let j = i + 1;
261
+ while (j < line.length) {
262
+ if (line[j] === "\\") {
263
+ j += 2;
264
+ continue;
265
+ }
266
+ if (line[j] === quote) {
267
+ j += 1;
268
+ break;
269
+ }
270
+ j += 1;
271
+ }
272
+ i = j;
273
+ continue;
274
+ }
275
+ result += ch;
276
+ i += 1;
277
+ }
278
+ return result;
279
+ }
280
+ }
@@ -1,5 +1,7 @@
1
1
  import { TYPESCRIPT_JAVASCRIPT_ANALYZER } from "./typescriptJavaScriptAnalyzer.js";
2
2
  import { PYTHON_ANALYZER } from "./pythonAnalyzer.js";
3
+ import { JAVA_ANALYZER } from "./javaAnalyzer.js";
4
+ import { KOTLIN_ANALYZER } from "./kotlinAnalyzer.js";
3
5
  // Validates a list has no duplicate analyzer ids. Later batches call this
4
6
  // when assembling their own registries; Batch 2's empty default trivially
5
7
  // passes.
@@ -27,10 +29,14 @@ export function selectLanguageAnalyzer(registry, language, extension) {
27
29
  return null;
28
30
  }
29
31
  // 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.
32
+ // Batch 1 adds the Python analyzer alongside it. v0.3.3 Batch 1 adds Java
33
+ // and Kotlin (see javaAnalyzer.ts/kotlinAnalyzer.ts) -- no other language
34
+ // remains fallback-only by design change here; any language not in this
35
+ // list still degrades through collectSourceFacts.ts's existing fallback
36
+ // policy unchanged.
33
37
  export const DEFAULT_LANGUAGE_ANALYZER_REGISTRY = createLanguageAnalyzerRegistry([
34
38
  TYPESCRIPT_JAVASCRIPT_ANALYZER,
35
39
  PYTHON_ANALYZER,
40
+ JAVA_ANALYZER,
41
+ KOTLIN_ANALYZER,
36
42
  ]);
@@ -384,6 +384,25 @@ function isPythonTestFile(relativePath, basename) {
384
384
  return true;
385
385
  return /^test_.*\.py$/.test(basename) || /^.*_test\.py$/.test(basename);
386
386
  }
387
+ // v0.3.3 Batch 1 -- conservative JVM (Java/Kotlin) test naming/source-set
388
+ // conventions. Gated to .java/.kt so this can never reclassify an unrelated
389
+ // file. "tests/**/*.java" and "tests/**/*.kt" are already covered by the
390
+ // generic, extension-agnostic check above; this adds the Gradle/Maven
391
+ // standard "src/test/{java,kotlin}" source-set layout, the singular "test/"
392
+ // directory convention (same style as isPythonTestFile's "test/" check
393
+ // above), and the "*Test(s).java" / "*Test(s).kt" / "*Spec.kt" filename
394
+ // conventions.
395
+ function isJvmTestFile(relativePath, basename, extension) {
396
+ if (relativePath.includes("src/test/java/") || relativePath.includes("src/test/kotlin/"))
397
+ return true;
398
+ if (relativePath.startsWith("test/") || relativePath.includes("/test/"))
399
+ return true;
400
+ if (extension === ".java")
401
+ return /Tests?\.java$/.test(basename);
402
+ if (extension === ".kt")
403
+ return /Tests?\.kt$/.test(basename) || /Spec\.kt$/.test(basename);
404
+ return false;
405
+ }
387
406
  function isTestFile(relativePath, basename, extension) {
388
407
  if (relativePath.startsWith("tests/") || relativePath.includes("/tests/"))
389
408
  return true;
@@ -391,6 +410,8 @@ function isTestFile(relativePath, basename, extension) {
391
410
  return true;
392
411
  if (extension === ".py" && isPythonTestFile(relativePath, basename))
393
412
  return true;
413
+ if ((extension === ".java" || extension === ".kt") && isJvmTestFile(relativePath, basename, extension))
414
+ return true;
394
415
  return false;
395
416
  }
396
417
  // v0.3.2 Batch 1 -- recognized Python project/config metadata filenames.
@@ -405,6 +426,24 @@ const PYTHON_PROJECT_METADATA_FILE_NAMES = new Set([
405
426
  "tox.ini",
406
427
  "pytest.ini",
407
428
  ]);
429
+ // v0.3.3 Batch 1 -- recognized JVM (Gradle/Maven) project/build metadata
430
+ // filenames. Same narrow, basename-only convention as
431
+ // PYTHON_PROJECT_METADATA_FILE_NAMES above: does not match arbitrary *.xml/
432
+ // *.properties files, only these exact names. `build.gradle.kts` and
433
+ // `settings.gradle.kts` matter in particular: without this, a Gradle Kotlin
434
+ // DSL build script would otherwise fall through to the generic
435
+ // KNOWN_SOURCE_EXTENSIONS ".kts" -> "source" classification below and be fed
436
+ // to the Kotlin source-facts analyzer as if it were application code.
437
+ const JVM_PROJECT_METADATA_FILE_NAMES = new Set([
438
+ "build.gradle",
439
+ "build.gradle.kts",
440
+ "settings.gradle",
441
+ "settings.gradle.kts",
442
+ "pom.xml",
443
+ "gradle.properties",
444
+ "gradlew",
445
+ "gradlew.bat",
446
+ ]);
408
447
  function isConfigFile(relativePath, basename, extension) {
409
448
  if (/^tsconfig(\..+)?\.json$/.test(basename))
410
449
  return true;
@@ -420,6 +459,8 @@ function isConfigFile(relativePath, basename, extension) {
420
459
  // here for the same "project metadata, not source" reasoning.
421
460
  if (basename === "setup.py" || PYTHON_PROJECT_METADATA_FILE_NAMES.has(basename))
422
461
  return true;
462
+ if (JVM_PROJECT_METADATA_FILE_NAMES.has(basename))
463
+ return true;
423
464
  // Root-level dotfile configs (e.g. .prettierrc, .babelrc) not already
424
465
  // covered above -- deliberately conservative (root-only, not recursive)
425
466
  // to avoid misclassifying dotfiles inside fixture/benchmark projects.
@@ -1,9 +1,8 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
3
2
  import { runSecurityValidation } from "../../securityValidation/validate/runSecurityValidation.js";
4
3
  import { reportFilenamePrefix } from "../../securityValidation/validate/resolveTarget.js";
5
- import { renderJsonReport, renderTextReport } from "../../securityValidation/report/renderSecurityReport.js";
6
4
  import { buildSecurityReportFromSummary } from "../../securityValidation/report/buildSecurityReport.js";
5
+ import { writeSecurityReportFiles } from "../../securityValidation/report/writeSecurityReportFiles.js";
7
6
  import { DEFAULT_SECURITY_CHECKS, DEFAULT_SECURITY_PROFILE } from "../../securityValidation/validate/cliOptions.js";
8
7
  import { verdictToHumanLabel } from "../../securityValidation/validate/verdict.js";
9
8
  import { mapSecurityFindingToAuditIssue } from "./mapSecurityFindingToAuditIssue.js";
@@ -23,23 +22,21 @@ export async function runSecurityAuditAdapter(options) {
23
22
  selectedChecks: [...DEFAULT_SECURITY_CHECKS],
24
23
  formats: ["text", "json"],
25
24
  });
26
- // Always under my-dev-kit-lab's own reports/ tree (toolRoot), never under
27
- // the audited target's root — matches the target-safety guarantee
28
- // AuditTarget.safeReportOutputRoot already documents for code-rot reports.
29
25
  const reportDir = path.join(toolRoot, "reports", "security");
30
- if (!fs.existsSync(reportDir)) {
31
- fs.mkdirSync(reportDir, { recursive: true });
32
- }
33
26
  const prefix = reportFilenamePrefix({
34
27
  isSelf: summary.isSelf,
35
28
  packageName: summary.packageName,
36
29
  packageVersion: summary.packageVersion,
37
30
  targetRoot: summary.targetRoot,
38
31
  });
39
- const textPath = path.join(reportDir, `${prefix}-security-validation.txt`);
40
- const jsonPath = path.join(reportDir, `${prefix}-security-validation.json`);
41
- fs.writeFileSync(textPath, renderTextReport(report), "utf8");
42
- fs.writeFileSync(jsonPath, renderJsonReport(report), "utf8");
32
+ const runSuffix = buildRunScopedReportSuffix(summary.startedAt, summary.finishedAt);
33
+ const { textPath, jsonPath } = writeSecurityReportFiles({
34
+ outDir: reportDir,
35
+ prefix,
36
+ report,
37
+ formats: ["text", "json"],
38
+ reportPathSuffix: runSuffix,
39
+ });
43
40
  const findingCounts = {
44
41
  blocker: summary.findings.filter((f) => f.severity === "blocker").length,
45
42
  major: summary.findings.filter((f) => f.severity === "major").length,
@@ -65,3 +62,9 @@ export async function runSecurityAuditAdapter(options) {
65
62
  };
66
63
  return { issues, summary: securitySummary };
67
64
  }
65
+ function buildRunScopedReportSuffix(startedAt, finishedAt) {
66
+ return `${sanitizeTimestamp(startedAt)}-${sanitizeTimestamp(finishedAt)}-${process.pid}`;
67
+ }
68
+ function sanitizeTimestamp(value) {
69
+ return value.replace(/[^0-9]/g, "").slice(0, 14) || "run";
70
+ }
@@ -0,0 +1,28 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { renderJsonReport, renderTextReport } from "./renderSecurityReport.js";
4
+ export function writeSecurityReportFiles(options) {
5
+ const reportDir = options.outDir;
6
+ if (!fs.existsSync(reportDir)) {
7
+ fs.mkdirSync(reportDir, { recursive: true });
8
+ }
9
+ const basename = options.reportPathSuffix
10
+ ? `${options.prefix}-${options.reportPathSuffix}-security-validation`
11
+ : `${options.prefix}-security-validation`;
12
+ const textPath = options.formats.includes("text")
13
+ ? path.join(reportDir, `${basename}.txt`)
14
+ : null;
15
+ const jsonPath = options.formats.includes("json")
16
+ ? path.join(reportDir, `${basename}.json`)
17
+ : null;
18
+ const writtenPaths = [];
19
+ if (textPath) {
20
+ fs.writeFileSync(textPath, renderTextReport(options.report), "utf8");
21
+ writtenPaths.push(textPath);
22
+ }
23
+ if (jsonPath) {
24
+ fs.writeFileSync(jsonPath, renderJsonReport(options.report), "utf8");
25
+ writtenPaths.push(jsonPath);
26
+ }
27
+ return { textPath, jsonPath, writtenPaths };
28
+ }