claudeos-core 2.3.1 → 2.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/CHANGELOG.md +725 -0
- package/README.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.hi.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +2 -2
- package/README.ru.md +1 -1
- package/README.vi.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/commands/init.js +378 -227
- package/content-validator/index.js +89 -9
- package/package.json +1 -1
- package/pass-prompts/templates/common/claude-md-scaffold.md +122 -20
- package/pass-prompts/templates/common/pass3-footer.md +193 -56
- package/pass-prompts/templates/common/pass3a-facts.md +48 -3
- package/pass-prompts/templates/common/pass4.md +78 -40
- package/pass-prompts/templates/java-spring/pass1.md +54 -0
- package/pass-prompts/templates/java-spring/pass3.md +1 -1
- package/pass-prompts/templates/kotlin-spring/pass1.md +45 -0
- package/pass-prompts/templates/kotlin-spring/pass3.md +1 -1
- package/plan-installer/index.js +23 -0
- package/plan-installer/pass3-context-builder.js +14 -0
- package/plan-installer/scanners/scan-frontend.js +2 -1
- package/plan-installer/source-paths.js +242 -0
- package/plan-installer/stack-detector.js +478 -42
|
@@ -13,6 +13,20 @@ const { readStackEnvInfo } = require("../lib/env-parser");
|
|
|
13
13
|
|
|
14
14
|
// ─── Lookup tables ──────────────────────────────────────────────
|
|
15
15
|
|
|
16
|
+
// iBatis detection — ONLY matches Apache iBatis (EOL 2010) or
|
|
17
|
+
// Spring iBatis (`spring-ibatis`, `ibatis-sqlmap`, `ibatis-core`).
|
|
18
|
+
// This pattern intentionally avoids matching MyBatis coords
|
|
19
|
+
// (`org.mybatis:mybatis`, `mybatis-spring-boot-starter`) — MyBatis
|
|
20
|
+
// evolved out of iBatis but is a separate library with different
|
|
21
|
+
// XML namespace and SqlSessionFactory architecture.
|
|
22
|
+
//
|
|
23
|
+
// Why separate from the ORM_RULES tables: the other entries match on
|
|
24
|
+
// substring include() calls, which would produce false positives for
|
|
25
|
+
// iBatis (MyBatis coords contain "mybatis" which does NOT include
|
|
26
|
+
// "ibatis" — but legacy Spring projects may have both). We use a
|
|
27
|
+
// precise regex on specific library coords instead.
|
|
28
|
+
const IBATIS_REGEX = /\borg\.apache\.ibatis\b|\bspring-ibatis\b|\bibatis-sqlmap\b|\bibatis-core\b|\bibatis-common\b/i;
|
|
29
|
+
|
|
16
30
|
// ORM detection rules: [keyword, ormName] (order = priority, first match wins)
|
|
17
31
|
const GRADLE_ORM_RULES = [
|
|
18
32
|
["mybatis", "mybatis"],
|
|
@@ -41,6 +55,7 @@ const NODE_ORM_RULES = [
|
|
|
41
55
|
// DB detection rules: [keyword, dbName]
|
|
42
56
|
const DB_KEYWORD_RULES = [
|
|
43
57
|
["postgresql", "postgresql"], ["postgres", "postgresql"],
|
|
58
|
+
["mariadb", "mariadb"], // v2.3.2+: MariaDB is a distinct DB, not a MySQL alias
|
|
44
59
|
["mysql", "mysql"],
|
|
45
60
|
["oracle", "oracle"],
|
|
46
61
|
["mongodb", "mongodb"],
|
|
@@ -63,19 +78,151 @@ function detectFirst(stack, field, content, rules) {
|
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
|
|
81
|
+
// Logging framework rules: [regex, frameworkName]
|
|
82
|
+
// Order matters only for identification — all matches are collected.
|
|
83
|
+
// We use regexes (not plain includes()) because "log4j" is a substring
|
|
84
|
+
// of "log4jdbc" and "log4j-to-slf4j", which are JDBC adapters /
|
|
85
|
+
// bridges, not Log4j2 as the primary logging framework.
|
|
86
|
+
const LOGGING_RULES = [
|
|
87
|
+
// Log4j2 — the current Apache Logging project.
|
|
88
|
+
// Matches artifact coords in two forms:
|
|
89
|
+
// - Gradle coord string: `org.apache.logging.log4j:log4j-core`
|
|
90
|
+
// (or `org.apache.logging.log4j.log4j-core` in some catalogs).
|
|
91
|
+
// - Maven XML: `<groupId>org.apache.logging.log4j</groupId>` paired
|
|
92
|
+
// with `<artifactId>log4j-core</artifactId>`. The two tags are
|
|
93
|
+
// matched on the same content (after comment stripping, so the
|
|
94
|
+
// order and proximity within pom.xml is what matters — both must
|
|
95
|
+
// be present somewhere in the non-commented dependency region).
|
|
96
|
+
// Does NOT match `log4j-to-slf4j` / `log4j-api` alone (which bridge
|
|
97
|
+
// Log4j API to SLF4J and are usually paired with Logback).
|
|
98
|
+
[/org\.apache\.logging\.log4j[.:]log4j-core/i, "log4j2"],
|
|
99
|
+
[/<groupId>\s*org\.apache\.logging\.log4j\s*<\/groupId>[\s\S]{0,300}?<artifactId>\s*log4j-core\s*<\/artifactId>/i, "log4j2"],
|
|
100
|
+
// Log4j2 via config file patterns
|
|
101
|
+
[/\blog4j2[\w.-]*\.(?:xml|properties|yaml|yml|json)\b/i, "log4j2"],
|
|
102
|
+
|
|
103
|
+
// Logback — Spring Boot's default. Matches the dependency in two forms:
|
|
104
|
+
// - Gradle coord string: `ch.qos.logback:logback-classic`
|
|
105
|
+
// - Maven XML: `<groupId>ch.qos.logback</groupId>` paired with
|
|
106
|
+
// `<artifactId>logback-classic</artifactId>` (or logback-core).
|
|
107
|
+
// Also matches config file references `logback-*.xml` / `logback*.groovy`.
|
|
108
|
+
[/ch\.qos\.logback[.:]logback-classic|logback[\w.-]*\.xml|logback[\w.-]*\.groovy/i, "logback"],
|
|
109
|
+
[/<groupId>\s*ch\.qos\.logback\s*<\/groupId>[\s\S]{0,300}?<artifactId>\s*logback-(?:classic|core)\s*<\/artifactId>/i, "logback"],
|
|
110
|
+
|
|
111
|
+
// log4jdbc — JDBC logging adapter. Not a primary logging framework
|
|
112
|
+
// but useful metadata because CLAUDE.md / logging standards commonly
|
|
113
|
+
// describe both the primary framework AND JDBC adapters.
|
|
114
|
+
[/log4jdbc/i, "log4jdbc"],
|
|
115
|
+
|
|
116
|
+
// Log4j 1.x (EOL 2015) — still appears in legacy projects.
|
|
117
|
+
// The challenge is distinguishing it from Log4j2 adapters such as
|
|
118
|
+
// `log4j-to-slf4j`, `log4j-api`, `log4j-core` (all Log4j2 ecosystem).
|
|
119
|
+
//
|
|
120
|
+
// The groupId for Log4j 1.x is literally `log4j` (not
|
|
121
|
+
// `org.apache.logging.log4j`), so we anchor on the coord form
|
|
122
|
+
// `log4j:log4j` with surrounding quotes/whitespace boundaries so
|
|
123
|
+
// that `org.apache.logging.log4j:log4j-to-slf4j` does NOT match
|
|
124
|
+
// (word boundary alone isn't enough — `log4j:log4j` appears as a
|
|
125
|
+
// substring in `...log4j:log4j-to-slf4j`).
|
|
126
|
+
//
|
|
127
|
+
// Also matches:
|
|
128
|
+
// <groupId>log4j</groupId> (Maven XML form)
|
|
129
|
+
// log4j.properties / log4j.xml (config files, not log4j2.*)
|
|
130
|
+
[
|
|
131
|
+
/(?:['":\s]log4j:log4j(?:[:'"]|\s|$))|<groupId>\s*log4j\s*<\/groupId>|\blog4j(?!2)\.(?:properties|xml)\b/im,
|
|
132
|
+
"log4j",
|
|
133
|
+
],
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// Comment stripping for dependency/config content before regex scanning.
|
|
137
|
+
// Used by detectLogging and the Maven DB scan to ensure commented-out
|
|
138
|
+
// dependencies are never interpreted as "in use".
|
|
139
|
+
//
|
|
140
|
+
// Three comment styles are handled:
|
|
141
|
+
// 1. Line-level `//` (Gradle Kotlin/Groovy DSL).
|
|
142
|
+
// 2. Line-level `#` (yml, properties, shell).
|
|
143
|
+
// 3. Block-level `<!-- ... -->` (Maven pom.xml, XML config). Commonly
|
|
144
|
+
// used to disable a whole `<dependency>` block during migration
|
|
145
|
+
// (e.g., commenting out an old log4j 1.x dep after switching to
|
|
146
|
+
// Spring Boot's managed Logback). Block stripping is non-greedy
|
|
147
|
+
// and multi-line so nested blocks and `<dependency>` blocks
|
|
148
|
+
// spanning many lines are handled correctly.
|
|
149
|
+
//
|
|
150
|
+
// Returns a new string with the commented regions removed (replaced
|
|
151
|
+
// with a newline to preserve approximate line counts for any
|
|
152
|
+
// downstream logic that cares about position). Content outside
|
|
153
|
+
// comments is preserved byte-for-byte.
|
|
154
|
+
function stripComments(content) {
|
|
155
|
+
// Step 1: remove XML block comments first (can span multiple lines).
|
|
156
|
+
// Non-greedy `[\s\S]*?` matches newlines; the `/g` flag removes every
|
|
157
|
+
// occurrence. We intentionally do NOT handle nested `<!-- ... -->`
|
|
158
|
+
// because XML spec forbids nesting — any well-formed `<!-- ... -->`
|
|
159
|
+
// is flat.
|
|
160
|
+
const withoutBlock = content.replace(/<!--[\s\S]*?-->/g, "\n");
|
|
161
|
+
// Step 2: drop lines that are entirely a line-comment.
|
|
162
|
+
return withoutBlock
|
|
163
|
+
.split(/\r?\n/)
|
|
164
|
+
.filter(line => {
|
|
165
|
+
const trimmed = line.trimStart();
|
|
166
|
+
return !trimmed.startsWith("//") && !trimmed.startsWith("#");
|
|
167
|
+
})
|
|
168
|
+
.join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function detectLogging(stack, content) {
|
|
172
|
+
// Collect every matching logging framework. Uses stack.loggingFrameworks
|
|
173
|
+
// array (multi-valued) because it is common for a project to declare
|
|
174
|
+
// two: Logback as primary + log4jdbc as JDBC adapter.
|
|
175
|
+
//
|
|
176
|
+
// Comment stripping: commented-out lines in Gradle (`//`),
|
|
177
|
+
// yml/properties/shell-style (`#`), and XML block comments
|
|
178
|
+
// (`<!-- ... -->`) must not match. Without this, a line like
|
|
179
|
+
// `// implementation 'ch.qos.logback:logback-classic'` (commented
|
|
180
|
+
// out because the project switched away from explicit version
|
|
181
|
+
// pinning to Spring Boot's managed version) or a pom.xml
|
|
182
|
+
// `<!-- <dependency>log4j:log4j:1.2.17</dependency> -->` block
|
|
183
|
+
// (commented out during migration to Logback) is mistakenly
|
|
184
|
+
// reported as in use. We preserve the classic Logback detection
|
|
185
|
+
// path via the `logging.config:` yml reference or the logback
|
|
186
|
+
// config file glob elsewhere.
|
|
187
|
+
const stripped = stripComments(content);
|
|
188
|
+
for (const [regex, name] of LOGGING_RULES) {
|
|
189
|
+
if (regex.test(stripped) && !stack.loggingFrameworks.includes(name)) {
|
|
190
|
+
stack.loggingFrameworks.push(name);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
66
195
|
function detectDb(stack, content, rules) {
|
|
67
|
-
|
|
196
|
+
// Iterate every rule and record every DB keyword present in `content`.
|
|
197
|
+
// Two outputs:
|
|
198
|
+
// (a) stack.database — primary DB (first match, legacy semantics).
|
|
199
|
+
// Skipped once set, so earlier-called detectDb invocations
|
|
200
|
+
// (Gradle build.gradle → application.yml → pom.xml) establish
|
|
201
|
+
// the primary DB in source-file order.
|
|
202
|
+
// (b) stack.databases — every DB keyword detected across all sources.
|
|
203
|
+
// Deduped and order-preserved. Fills in multi-dialect projects.
|
|
68
204
|
for (const [keyword, value] of rules) {
|
|
69
205
|
if (content.includes(keyword)) {
|
|
70
|
-
stack.database
|
|
71
|
-
|
|
72
|
-
|
|
206
|
+
if (!stack.database) {
|
|
207
|
+
stack.database = value;
|
|
208
|
+
stack.detected.push(value);
|
|
209
|
+
}
|
|
210
|
+
if (!stack.databases.includes(value)) {
|
|
211
|
+
stack.databases.push(value);
|
|
212
|
+
}
|
|
73
213
|
}
|
|
74
214
|
}
|
|
75
|
-
// h2 with word boundary
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
215
|
+
// h2 with word boundary — same pattern, separate from the keyword
|
|
216
|
+
// rules table because it needs regex (to avoid false positives from
|
|
217
|
+
// oauth2, cache2k, etc.).
|
|
218
|
+
if (H2_REGEX.test(content)) {
|
|
219
|
+
if (!stack.database) {
|
|
220
|
+
stack.database = "h2";
|
|
221
|
+
stack.detected.push("h2");
|
|
222
|
+
}
|
|
223
|
+
if (!stack.databases.includes("h2")) {
|
|
224
|
+
stack.databases.push("h2");
|
|
225
|
+
}
|
|
79
226
|
}
|
|
80
227
|
}
|
|
81
228
|
|
|
@@ -90,6 +237,26 @@ async function detectStack(ROOT) {
|
|
|
90
237
|
language: null, languageVersion: null,
|
|
91
238
|
framework: null, frameworkVersion: null,
|
|
92
239
|
buildTool: null, database: null, orm: null,
|
|
240
|
+
// databases: multi-dialect projects declare more than one DB
|
|
241
|
+
// driver (e.g., PostgreSQL + MariaDB + Oracle for dialect-switchable
|
|
242
|
+
// backends). `database` keeps its legacy semantics of "the primary
|
|
243
|
+
// DB that wins the first-match race" for backward compatibility
|
|
244
|
+
// with v2.x consumers; `databases` is the full ordered list of
|
|
245
|
+
// every DB keyword detected across all config sources. Consumers
|
|
246
|
+
// that care about multi-dialect support (Pass 1 prompts, Pass 3
|
|
247
|
+
// standard files for database-schema docs) should prefer this
|
|
248
|
+
// field. Empty array, not null, when no DB is detected — makes
|
|
249
|
+
// the array-comprehension in prompts simpler.
|
|
250
|
+
databases: [],
|
|
251
|
+
orm: null,
|
|
252
|
+
// loggingFrameworks: detected JVM logging frameworks (Logback,
|
|
253
|
+
// Log4j2, SLF4J-only, etc.). Like `databases`, this is an
|
|
254
|
+
// informational list for Pass 1 prompts — the LLM can use it to
|
|
255
|
+
// ground logging-related standard/rule content. Empty array when
|
|
256
|
+
// no JVM logging evidence is found (e.g., Node.js projects).
|
|
257
|
+
// Mainly populated from Gradle/Maven dependency keywords and from
|
|
258
|
+
// `logging.config` references in application.yml.
|
|
259
|
+
loggingFrameworks: [],
|
|
93
260
|
frontend: null, frontendVersion: null,
|
|
94
261
|
packageManager: null, monorepo: null, workspaces: null,
|
|
95
262
|
detected: [],
|
|
@@ -111,16 +278,105 @@ async function detectStack(ROOT) {
|
|
|
111
278
|
];
|
|
112
279
|
for (const pattern of svPatterns) {
|
|
113
280
|
const sv = g.match(pattern);
|
|
114
|
-
if (sv) {
|
|
281
|
+
if (sv) {
|
|
282
|
+
// Reject captures that are variable references like `${var}`
|
|
283
|
+
// — those need resolution via the fallback block below.
|
|
284
|
+
if (/^\$\{/.test(sv[1])) continue;
|
|
285
|
+
stack.frameworkVersion = sv[1];
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Fallback: some projects centralize the Spring Boot version in
|
|
290
|
+
// an `ext { springBootVersion = '3.5.5' }` block and reference
|
|
291
|
+
// it from the plugin declaration (`version "${springBootVersion}"`).
|
|
292
|
+
// If none of the above patterns matched but we can find a
|
|
293
|
+
// variable-reference form, resolve the variable inside the same
|
|
294
|
+
// build.gradle.
|
|
295
|
+
if (!stack.frameworkVersion) {
|
|
296
|
+
const svVarRef = g.match(/springframework\.boot[^\n]*version\s*['"]\$\{?(\w+)\}?['"]/);
|
|
297
|
+
if (svVarRef) {
|
|
298
|
+
const varName = svVarRef[1];
|
|
299
|
+
const escapedVar = varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
300
|
+
const varDef = new RegExp(`${escapedVar}\\s*=\\s*['"]([\\d.]+)['"]`);
|
|
301
|
+
const varVal = g.match(varDef);
|
|
302
|
+
if (varVal) stack.frameworkVersion = varVal[1];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Java version — Gradle writes this in several forms. Try each
|
|
306
|
+
// pattern until one matches. Earlier patterns take precedence.
|
|
307
|
+
//
|
|
308
|
+
// Pattern 1: direct numeric literal (most common, v1.x era)
|
|
309
|
+
// sourceCompatibility = 21
|
|
310
|
+
// sourceCompatibility = '21'
|
|
311
|
+
// sourceCompatibility = "21"
|
|
312
|
+
//
|
|
313
|
+
// Pattern 2: JavaVersion enum (common with Spring Initializr)
|
|
314
|
+
// sourceCompatibility = JavaVersion.VERSION_21
|
|
315
|
+
// sourceCompatibility = JavaVersion.VERSION_1_8 (Java 8)
|
|
316
|
+
//
|
|
317
|
+
// Pattern 3: Gradle toolchain block (modern, Gradle 6.7+)
|
|
318
|
+
// java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
|
|
319
|
+
//
|
|
320
|
+
// Pattern 4: ext variable reference (common in team/enterprise
|
|
321
|
+
// projects that centralize versions)
|
|
322
|
+
// ext { javaVersion = '21' }
|
|
323
|
+
// java { sourceCompatibility = "${javaVersion}" }
|
|
324
|
+
//
|
|
325
|
+
// Without pattern 4, an ext-variable-reference-only build.gradle
|
|
326
|
+
// produces languageVersion=null, leaving the LLM to guess (e.g.
|
|
327
|
+
// "Java 17+" for a Spring Boot 3.x project that actually targets
|
|
328
|
+
// Java 21).
|
|
329
|
+
const javaVersionPatterns = [
|
|
330
|
+
// (1) numeric literal on sourceCompatibility or targetCompatibility
|
|
331
|
+
/sourceCompatibility\s*=\s*['"]?(\d+)['"]?/,
|
|
332
|
+
/targetCompatibility\s*=\s*['"]?(\d+)['"]?/,
|
|
333
|
+
// (2) JavaVersion enum — supports both VERSION_21 and VERSION_1_8
|
|
334
|
+
/JavaVersion\.VERSION_(?:1_)?(\d+)/,
|
|
335
|
+
// (3) toolchain block
|
|
336
|
+
/JavaLanguageVersion\.of\s*\(\s*(\d+)\s*\)/,
|
|
337
|
+
];
|
|
338
|
+
for (const pattern of javaVersionPatterns) {
|
|
339
|
+
const m = g.match(pattern);
|
|
340
|
+
if (m) { stack.languageVersion = m[1]; break; }
|
|
341
|
+
}
|
|
342
|
+
// (4) ext variable reference fallback — if the Compatibility
|
|
343
|
+
// assignment used "${varName}" we now resolve varName inside the
|
|
344
|
+
// same file's ext block. We only run this if the numeric patterns
|
|
345
|
+
// above did not already find a value.
|
|
346
|
+
if (!stack.languageVersion) {
|
|
347
|
+
const varRefMatch = g.match(/(?:source|target)Compatibility\s*=\s*["']?\$\{?(\w+)\}?["']?/);
|
|
348
|
+
if (varRefMatch) {
|
|
349
|
+
const varName = varRefMatch[1];
|
|
350
|
+
// Escape the variable name for use in a RegExp. In practice
|
|
351
|
+
// Gradle variable names are [A-Za-z0-9_], so escaping is a
|
|
352
|
+
// defensive guard against unexpected characters, not a
|
|
353
|
+
// practical necessity for today's inputs.
|
|
354
|
+
const escapedVarName = varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
355
|
+
const extAssign = new RegExp(`${escapedVarName}\\s*=\\s*['"]?(\\d+)['"]?`);
|
|
356
|
+
const extVal = g.match(extAssign);
|
|
357
|
+
if (extVal) stack.languageVersion = extVal[1];
|
|
358
|
+
}
|
|
115
359
|
}
|
|
116
|
-
const jv = g.match(/sourceCompatibility\s*=\s*['"]?(\d+)['"]?/);
|
|
117
|
-
if (jv) stack.languageVersion = jv[1];
|
|
118
360
|
|
|
119
|
-
|
|
361
|
+
// iBatis detection: runs BEFORE generic ORM_RULES because the
|
|
362
|
+
// generic table's "mybatis" keyword (substring match) would
|
|
363
|
+
// happily match Apache iBatis coords like `ibatis-core` if the
|
|
364
|
+
// order were reversed — "mybatis" is not a substring of
|
|
365
|
+
// "ibatis", but a future maintainer adding "ibatis" to the
|
|
366
|
+
// generic table would create ambiguity. Explicit precedence here.
|
|
367
|
+
if (IBATIS_REGEX.test(g)) {
|
|
368
|
+
stack.orm = "ibatis";
|
|
369
|
+
stack.detected.push("ibatis");
|
|
370
|
+
} else {
|
|
371
|
+
detectFirst(stack, "orm", g, GRADLE_ORM_RULES);
|
|
372
|
+
}
|
|
120
373
|
// Exclude "postgres" (substring of postgresql — false positive on r2dbc-postgres) and "sqlite" (rare in Gradle deps)
|
|
121
374
|
// "postgresql" is still matched via DB_KEYWORD_RULES; h2 uses word-boundary check separately
|
|
122
375
|
detectDb(stack, g, DB_KEYWORD_RULES.filter(([kw]) => !["postgres", "sqlite"].includes(kw)));
|
|
123
376
|
|
|
377
|
+
// Logging framework detection from Gradle dependencies.
|
|
378
|
+
detectLogging(stack, g);
|
|
379
|
+
|
|
124
380
|
// Kotlin detection: override language if Kotlin plugin found
|
|
125
381
|
if (g.includes("kotlin") || g.includes("org.jetbrains.kotlin")) {
|
|
126
382
|
stack.language = "kotlin"; stack.detected.push("kotlin");
|
|
@@ -155,9 +411,18 @@ async function detectStack(ROOT) {
|
|
|
155
411
|
if (!stack.orm && vc.includes("exposed")) { stack.orm = "exposed"; stack.detected.push("exposed (catalog)"); }
|
|
156
412
|
else if (!stack.orm && vc.includes("jooq")) { stack.orm = "jooq"; stack.detected.push("jooq (catalog)"); }
|
|
157
413
|
else if (!stack.orm && (vc.includes("jpa") || vc.includes("hibernate"))) { stack.orm = "jpa"; stack.detected.push("jpa (catalog)"); }
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
414
|
+
// DBs from version catalog — dual output (primary + array)
|
|
415
|
+
const catalogDbs = [
|
|
416
|
+
["postgresql", "postgresql"],
|
|
417
|
+
["mysql", "mysql"],
|
|
418
|
+
["mongodb", "mongodb"],
|
|
419
|
+
];
|
|
420
|
+
for (const [keyword, value] of catalogDbs) {
|
|
421
|
+
if (vc.includes(keyword)) {
|
|
422
|
+
if (!stack.database) stack.database = value;
|
|
423
|
+
if (!stack.databases.includes(value)) stack.databases.push(value);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
161
426
|
if (!stack.language && vc.includes("kotlin")) {
|
|
162
427
|
stack.language = "kotlin"; stack.detected.push("kotlin (catalog)");
|
|
163
428
|
}
|
|
@@ -220,17 +485,91 @@ async function detectStack(ROOT) {
|
|
|
220
485
|
if (!stack.buildTool) { stack.buildTool = "maven"; stack.language = "java"; stack.detected.push("pom.xml"); }
|
|
221
486
|
const sv = pom.match(/<spring-boot[^>]*version>([^<]+)/);
|
|
222
487
|
if (sv) stack.frameworkVersion = sv[1];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
488
|
+
// Java version — Maven commonly uses three patterns:
|
|
489
|
+
//
|
|
490
|
+
// Pattern 1: direct <java.version>21</java.version>
|
|
491
|
+
//
|
|
492
|
+
// Pattern 2: <maven.compiler.source>21</maven.compiler.source>
|
|
493
|
+
// (and matching <maven.compiler.target>), used when the project
|
|
494
|
+
// avoids the Spring Boot parent's `java.version` property.
|
|
495
|
+
//
|
|
496
|
+
// Pattern 3: property reference — `<java.version>${project.javaVersion}</java.version>`
|
|
497
|
+
// where `<project.javaVersion>21</project.javaVersion>` is
|
|
498
|
+
// declared earlier in <properties>. Enterprise projects
|
|
499
|
+
// centralize versions this way so all child modules pick up the
|
|
500
|
+
// same value.
|
|
501
|
+
//
|
|
502
|
+
// We try patterns in order (1) → (2) → (3). Pattern 3 is a
|
|
503
|
+
// fallback that resolves the referenced property within the same
|
|
504
|
+
// pom.xml (cross-file resolution — parent pom, BOM — is out of
|
|
505
|
+
// scope; the resulting null falls through to LLM-side analysis).
|
|
506
|
+
const mvnJavaPatterns = [
|
|
507
|
+
/<java\.version>\s*(\d+)\s*<\/java\.version>/,
|
|
508
|
+
/<maven\.compiler\.source>\s*(\d+)\s*<\/maven\.compiler\.source>/,
|
|
509
|
+
/<maven\.compiler\.target>\s*(\d+)\s*<\/maven\.compiler\.target>/,
|
|
510
|
+
];
|
|
511
|
+
for (const pattern of mvnJavaPatterns) {
|
|
512
|
+
const m = pom.match(pattern);
|
|
513
|
+
if (m) { stack.languageVersion = m[1]; break; }
|
|
514
|
+
}
|
|
515
|
+
// Pattern 3 fallback: if <java.version> references a property,
|
|
516
|
+
// resolve it inside the same pom.
|
|
517
|
+
if (!stack.languageVersion) {
|
|
518
|
+
const propRef = pom.match(/<java\.version>\s*\$\{([^}]+)\}\s*<\/java\.version>/);
|
|
519
|
+
if (propRef) {
|
|
520
|
+
const propName = propRef[1].trim();
|
|
521
|
+
const escapedProp = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
522
|
+
const propDef = new RegExp(`<${escapedProp}>\\s*(\\d+)\\s*</${escapedProp}>`);
|
|
523
|
+
const propVal = pom.match(propDef);
|
|
524
|
+
if (propVal) stack.languageVersion = propVal[1];
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// For dependency detection (framework, ORM, DB, logging), strip
|
|
528
|
+
// XML block comments first. A `<!-- <dependency>...</dependency> -->`
|
|
529
|
+
// block is a standard Maven pattern for disabling a dep during
|
|
530
|
+
// migration (e.g., commenting out a legacy log4j 1.x dep after
|
|
531
|
+
// switching to Spring Boot's managed Logback); without stripping,
|
|
532
|
+
// those deps are counted as "in use". The `<properties>` scan
|
|
533
|
+
// above stays on the raw `pom` because (a) commented-out property
|
|
534
|
+
// definitions are rare in practice and (b) the property-reference
|
|
535
|
+
// resolution already scopes itself to the declared property name.
|
|
536
|
+
const pomClean = stripComments(pom);
|
|
537
|
+
if (pomClean.includes("spring-boot") && !stack.framework) { stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
|
|
538
|
+
if (IBATIS_REGEX.test(pomClean)) {
|
|
539
|
+
stack.orm = "ibatis";
|
|
540
|
+
stack.detected.push("ibatis");
|
|
541
|
+
} else {
|
|
542
|
+
detectFirst(stack, "orm", pomClean, MAVEN_ORM_RULES);
|
|
543
|
+
}
|
|
227
544
|
// Maven DB: original does not push to detected (unlike Gradle)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
545
|
+
// DB keyword scan: reuses detectDb's dual-output semantics
|
|
546
|
+
// (primary first-match → stack.database; every match →
|
|
547
|
+
// stack.databases). Maven original did not push to `detected`
|
|
548
|
+
// (unlike Gradle), so we preserve that omission here.
|
|
549
|
+
const mvnDbRules = [
|
|
550
|
+
["postgresql", "postgresql"],
|
|
551
|
+
["mariadb", "mariadb"],
|
|
552
|
+
["mysql", "mysql"],
|
|
553
|
+
["oracle", "oracle"],
|
|
554
|
+
["mongodb", "mongodb"],
|
|
555
|
+
["sqlite", "sqlite"],
|
|
556
|
+
];
|
|
557
|
+
for (const [keyword, value] of mvnDbRules) {
|
|
558
|
+
if (pomClean.includes(keyword)) {
|
|
559
|
+
if (!stack.database) stack.database = value;
|
|
560
|
+
if (!stack.databases.includes(value)) stack.databases.push(value);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (H2_REGEX.test(pomClean)) {
|
|
564
|
+
if (!stack.database) stack.database = "h2";
|
|
565
|
+
if (!stack.databases.includes("h2")) stack.databases.push("h2");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Logging framework detection from Maven dependencies. detectLogging
|
|
569
|
+
// does its own comment stripping internally, so passing raw `pom`
|
|
570
|
+
// works correctly — but we pass `pomClean` for consistency with
|
|
571
|
+
// the other Maven dependency scans in this block.
|
|
572
|
+
detectLogging(stack, pomClean);
|
|
234
573
|
}
|
|
235
574
|
}
|
|
236
575
|
|
|
@@ -329,7 +668,12 @@ async function detectStack(ROOT) {
|
|
|
329
668
|
stack.orm = ormName; stack.detected.push(ormName);
|
|
330
669
|
}
|
|
331
670
|
}
|
|
332
|
-
if (deps.mongoose) {
|
|
671
|
+
if (deps.mongoose) {
|
|
672
|
+
if (!stack.database) stack.database = "mongodb";
|
|
673
|
+
if (!stack.databases.includes("mongodb")) stack.databases.push("mongodb");
|
|
674
|
+
if (!stack.orm) stack.orm = "mongoose";
|
|
675
|
+
stack.detected.push("mongoose");
|
|
676
|
+
}
|
|
333
677
|
|
|
334
678
|
// DB
|
|
335
679
|
const nodeDbRules = [["pg", "postgresql"], ["mysql2", "mysql"], ["mongodb", "mongodb"]];
|
|
@@ -383,8 +727,16 @@ async function detectStack(ROOT) {
|
|
|
383
727
|
for (const [kw, name] of pyOrmRules) {
|
|
384
728
|
if (r.includes(kw) && !stack.orm) { stack.orm = name; break; }
|
|
385
729
|
}
|
|
386
|
-
|
|
387
|
-
|
|
730
|
+
const pyDbs = [
|
|
731
|
+
["psycopg", "postgresql"],
|
|
732
|
+
["mysqlclient", "mysql"],
|
|
733
|
+
];
|
|
734
|
+
for (const [kw, value] of pyDbs) {
|
|
735
|
+
if (r.includes(kw)) {
|
|
736
|
+
if (!stack.database) stack.database = value;
|
|
737
|
+
if (!stack.databases.includes(value)) stack.databases.push(value);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
388
740
|
}
|
|
389
741
|
}
|
|
390
742
|
|
|
@@ -395,20 +747,94 @@ async function detectStack(ROOT) {
|
|
|
395
747
|
}
|
|
396
748
|
|
|
397
749
|
// ── DB from config files ──
|
|
398
|
-
|
|
750
|
+
//
|
|
751
|
+
// Glob covers Spring Boot's full configuration-file naming space:
|
|
752
|
+
// - `application.{yml,yaml,properties}` (Spring Initializr default;
|
|
753
|
+
// .yml is most common, .yaml is spec-official, .properties is the
|
|
754
|
+
// framework default when nothing specifies)
|
|
755
|
+
// - `application-*.{yml,yaml,properties}` profile variants
|
|
756
|
+
// (e.g. application-local.yml, application-dev.properties)
|
|
757
|
+
// - `bootstrap.{yml,yaml,properties}` + profile variants
|
|
758
|
+
// (Spring Cloud Config / Consul / Eureka; loaded before `application.*`
|
|
759
|
+
// so must be part of the same scan)
|
|
760
|
+
//
|
|
761
|
+
// The regexes inside the loop are format-agnostic: the `port` regex
|
|
762
|
+
// set covers both yml `server:\n port: N` syntax and .properties
|
|
763
|
+
// `server.port=N` flat-key syntax. DB keyword detection is
|
|
764
|
+
// substring-based and works identically across all three formats.
|
|
765
|
+
const configGlob = "**/{application,bootstrap}*.{yml,yaml,properties}";
|
|
766
|
+
const ymls = await glob(configGlob, {
|
|
767
|
+
cwd: ROOT, absolute: true,
|
|
768
|
+
ignore: ["**/node_modules/**", "**/build/**", "**/target/**", "**/.gradle/**"],
|
|
769
|
+
});
|
|
399
770
|
for (const y of ymls) {
|
|
400
771
|
const c = readFileSafe(y);
|
|
401
772
|
if (!c) continue;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
773
|
+
// DB detection — dual output per source file.
|
|
774
|
+
//
|
|
775
|
+
// NOTE on mariadb: earlier versions of this detector deliberately
|
|
776
|
+
// OMITTED mariadb from yml-side keyword matching because some
|
|
777
|
+
// projects mention mariadb in commented-out profile sections or
|
|
778
|
+
// as fallback drivers. With multi-dialect support (`stack.databases`
|
|
779
|
+
// array in v2.3.2+), the cost of over-reporting is lower than the
|
|
780
|
+
// cost of under-reporting — `databases` is an informational list
|
|
781
|
+
// for the LLM, and a false positive is easier to filter out in the
|
|
782
|
+
// prompt than a miss is to detect. So mariadb is now included.
|
|
783
|
+
const ymlDbs = [
|
|
784
|
+
["postgresql", "postgresql"],
|
|
785
|
+
["mariadb", "mariadb"],
|
|
786
|
+
["mysql", "mysql"],
|
|
787
|
+
["oracle", "oracle"],
|
|
788
|
+
["mongodb", "mongodb"],
|
|
789
|
+
];
|
|
790
|
+
for (const [keyword, value] of ymlDbs) {
|
|
791
|
+
if (c.includes(keyword)) {
|
|
792
|
+
if (!stack.database) stack.database = value;
|
|
793
|
+
if (!stack.databases.includes(value)) stack.databases.push(value);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (H2_REGEX.test(c)) {
|
|
797
|
+
if (!stack.database) stack.database = "h2";
|
|
798
|
+
if (!stack.databases.includes("h2")) stack.databases.push("h2");
|
|
799
|
+
}
|
|
800
|
+
if (c.includes("sqlite")) {
|
|
801
|
+
if (!stack.database) stack.database = "sqlite";
|
|
802
|
+
if (!stack.databases.includes("sqlite")) stack.databases.push("sqlite");
|
|
803
|
+
}
|
|
408
804
|
if (!stack.port) {
|
|
409
|
-
|
|
410
|
-
|
|
805
|
+
// Port — Spring Boot accepts both plain numeric values and
|
|
806
|
+
// property placeholders with a default:
|
|
807
|
+
// port: 8090
|
|
808
|
+
// port: ${APP_PORT:8090} ← Spring placeholder, default 8090
|
|
809
|
+
// server.port=8090 ← .properties-style, yml-inline
|
|
810
|
+
// server.port=${SERVER_PORT:8090}
|
|
811
|
+
//
|
|
812
|
+
// Without the placeholder patterns (3)/(4), a Spring Boot yml
|
|
813
|
+
// using `port: ${APP_PORT:8090}` produces stack.port=null, leaving
|
|
814
|
+
// the LLM to guess (e.g. assuming the "port 8080" Spring Boot
|
|
815
|
+
// framework default).
|
|
816
|
+
const portPatterns = [
|
|
817
|
+
// (1) direct numeric literal in yml `server:\n port: N`
|
|
818
|
+
/server:\s*\n\s*port:\s*(\d+)/,
|
|
819
|
+
// (2) flat-key style `server.port=N` or `server.port: N`
|
|
820
|
+
/server\.port\s*[=:]\s*(\d+)/,
|
|
821
|
+
// (3) placeholder-with-default in yml `server:\n port: ${VAR:N}`
|
|
822
|
+
// — capture the default value, which is what the app falls back
|
|
823
|
+
// to when VAR is unset (the most common dev/local scenario)
|
|
824
|
+
/server:\s*\n\s*port:\s*\$\{[^}:]+:(\d+)\}/,
|
|
825
|
+
// (4) placeholder-with-default in flat-key form
|
|
826
|
+
/server\.port\s*[=:]\s*\$\{[^}:]+:(\d+)\}/,
|
|
827
|
+
];
|
|
828
|
+
for (const re of portPatterns) {
|
|
829
|
+
const pm = c.match(re);
|
|
830
|
+
if (pm) { stack.port = parseInt(pm[1]); break; }
|
|
831
|
+
}
|
|
411
832
|
}
|
|
833
|
+
|
|
834
|
+
// Logging framework detection from yml. `logging.config:
|
|
835
|
+
// classpath:logback-app.xml` tells us Logback is primary; bare
|
|
836
|
+
// mentions of log4jdbc in the doc mean the adapter is in use.
|
|
837
|
+
detectLogging(stack, c);
|
|
412
838
|
}
|
|
413
839
|
|
|
414
840
|
// .env
|
|
@@ -417,11 +843,21 @@ async function detectStack(ROOT) {
|
|
|
417
843
|
if (existsSafe(ep)) {
|
|
418
844
|
const ec = readFileSafe(ep);
|
|
419
845
|
if (ec && ec.includes("DATABASE_URL")) {
|
|
420
|
-
// .env: original checks postgres (not postgresql), no oracle/h2
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
846
|
+
// .env: original checks postgres (not postgresql), no oracle/h2.
|
|
847
|
+
// Preserve that semantics, but update both primary and array
|
|
848
|
+
// outputs together.
|
|
849
|
+
const envDbs = [
|
|
850
|
+
["postgres", "postgresql"],
|
|
851
|
+
["mysql", "mysql"],
|
|
852
|
+
["mongodb", "mongodb"],
|
|
853
|
+
["sqlite", "sqlite"],
|
|
854
|
+
];
|
|
855
|
+
for (const [keyword, value] of envDbs) {
|
|
856
|
+
if (ec.includes(keyword)) {
|
|
857
|
+
if (!stack.database) stack.database = value;
|
|
858
|
+
if (!stack.databases.includes(value)) stack.databases.push(value);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
425
861
|
}
|
|
426
862
|
}
|
|
427
863
|
}
|