backend-skeleton 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -0
  3. package/bin/bskel.mjs +2384 -0
  4. package/contracts/completeness.mjs +176 -0
  5. package/contracts/emit.mjs +287 -0
  6. package/contracts/export.mjs +325 -0
  7. package/contracts/openapi.mjs +869 -0
  8. package/contracts/validate.mjs +147 -0
  9. package/handles/_engine.mjs +281 -0
  10. package/handles/codec.mjs +119 -0
  11. package/handles/conformance.mjs +74 -0
  12. package/handles/providers/java-spring/ast-bridge.mjs +59 -0
  13. package/handles/providers/java-spring/ast-helper/build.gradle +34 -0
  14. package/handles/providers/java-spring/ast-helper/gradle/wrapper/gradle-wrapper.jar +0 -0
  15. package/handles/providers/java-spring/ast-helper/gradle/wrapper/gradle-wrapper.properties +9 -0
  16. package/handles/providers/java-spring/ast-helper/gradlew +248 -0
  17. package/handles/providers/java-spring/ast-helper/gradlew.bat +82 -0
  18. package/handles/providers/java-spring/ast-helper/settings.gradle +1 -0
  19. package/handles/providers/java-spring/ast-helper/src/main/java/com/backendskeleton/asthelper/Main.java +178 -0
  20. package/handles/providers/java-spring/emit.mjs +232 -0
  21. package/handles/providers/java-spring/patch-strategy.mjs +229 -0
  22. package/handles/providers/java-spring/plan.mjs +377 -0
  23. package/handles/providers/java-spring/templates/HandleAspect.java.tmpl +125 -0
  24. package/handles/providers/java-spring/templates/HandleCodec.java.tmpl +150 -0
  25. package/handles/providers/java-spring/templates/HandleController.java.tmpl +177 -0
  26. package/handles/providers/java-spring/templates/HandleRegistry.java.tmpl +107 -0
  27. package/handles/providers/java-spring/templates/HandleRegistryRepository.java.tmpl +8 -0
  28. package/handles/providers/java-spring/templates/HandleService.java.tmpl +95 -0
  29. package/handles/providers/java-spring/templates/HandleSnapshot.java.tmpl +75 -0
  30. package/handles/providers/java-spring/templates/HandleSnapshotRepository.java.tmpl +20 -0
  31. package/handles/providers/java-spring/templates/RecordHandleSnapshot.java.tmpl +50 -0
  32. package/handles/providers/java-spring/templates/ResourceResolver.java.tmpl +50 -0
  33. package/handles/providers/java-spring/templates/ResourceResolverStub.java.tmpl +77 -0
  34. package/handles/providers/java-spring/templates/migration.sql.tmpl +34 -0
  35. package/handles/providers/java-spring.mjs +21 -0
  36. package/handles/providers/python-fastapi/emit.mjs +171 -0
  37. package/handles/providers/python-fastapi/plan.mjs +186 -0
  38. package/handles/providers/python-fastapi/templates/__init__.py.tmpl +1 -0
  39. package/handles/providers/python-fastapi/templates/codec.py.tmpl +122 -0
  40. package/handles/providers/python-fastapi/templates/handle_service.py.tmpl +96 -0
  41. package/handles/providers/python-fastapi/templates/migration.sql.tmpl +35 -0
  42. package/handles/providers/python-fastapi/templates/record_snapshot.py.tmpl +155 -0
  43. package/handles/providers/python-fastapi/templates/registry.py.tmpl +37 -0
  44. package/handles/providers/python-fastapi/templates/resolver.py.tmpl +59 -0
  45. package/handles/providers/python-fastapi/templates/resolvers_init.py.tmpl +13 -0
  46. package/handles/providers/python-fastapi/templates/router.py.tmpl +140 -0
  47. package/handles/providers/python-fastapi/templates/tables.py.tmpl +66 -0
  48. package/handles/providers/python-fastapi.mjs +22 -0
  49. package/handles/providers/typescript-express/emit.mjs +128 -0
  50. package/handles/providers/typescript-express/plan.mjs +234 -0
  51. package/handles/providers/typescript-express/templates/codec.ts.tmpl +116 -0
  52. package/handles/providers/typescript-express/templates/registry.ts.tmpl +39 -0
  53. package/handles/providers/typescript-express/templates/resolver.ts.tmpl +55 -0
  54. package/handles/providers/typescript-express/templates/resolvers_index.ts.tmpl +11 -0
  55. package/handles/providers/typescript-express/templates/router.ts.tmpl +122 -0
  56. package/handles/providers/typescript-express.mjs +20 -0
  57. package/handles/registry.mjs +90 -0
  58. package/lib/cli.mjs +430 -0
  59. package/lib/doctor.mjs +200 -0
  60. package/lib/exit-codes.mjs +67 -0
  61. package/lib/featureid.mjs +55 -0
  62. package/lib/featurelifecycle.mjs +205 -0
  63. package/lib/fsutil.mjs +50 -0
  64. package/lib/gate-definitions.mjs +293 -0
  65. package/lib/gates.mjs +263 -0
  66. package/lib/handles-manifest.mjs +92 -0
  67. package/lib/lock.mjs +68 -0
  68. package/lib/patch-approvals.mjs +56 -0
  69. package/lib/paths.mjs +21 -0
  70. package/lib/repo.mjs +44 -0
  71. package/lib/schema-validate.mjs +56 -0
  72. package/lib/state.mjs +124 -0
  73. package/lib/template.mjs +35 -0
  74. package/lib/verify.mjs +206 -0
  75. package/lib/workflow.mjs +142 -0
  76. package/new/fastapi.mjs +165 -0
  77. package/new/index.mjs +62 -0
  78. package/new/params.mjs +233 -0
  79. package/new/spring.mjs +198 -0
  80. package/new/templates/fastapi/README.md +26 -0
  81. package/new/templates/fastapi/app/__init__.py +0 -0
  82. package/new/templates/fastapi/app/main.py +8 -0
  83. package/new/templates/fastapi/gitignore +6 -0
  84. package/new/templates/fastapi/pyproject.toml +14 -0
  85. package/package.json +50 -0
  86. package/scanners/adapters/_express-shared.mjs +238 -0
  87. package/scanners/adapters/_java-spring-analyzer.mjs +273 -0
  88. package/scanners/adapters/generic-grep.mjs +128 -0
  89. package/scanners/adapters/java-spring.mjs +301 -0
  90. package/scanners/adapters/javascript-express.mjs +422 -0
  91. package/scanners/adapters/python-fastapi.mjs +348 -0
  92. package/scanners/adapters/typescript-express.mjs +299 -0
  93. package/scanners/capabilities.mjs +90 -0
  94. package/scanners/conformance.mjs +59 -0
  95. package/scanners/db/introspect.mjs +109 -0
  96. package/scanners/db/migrations.mjs +126 -0
  97. package/scanners/index.mjs +281 -0
  98. package/scanners/registry.mjs +130 -0
  99. package/scanners/render.mjs +136 -0
  100. package/scanners/text-util.mjs +8 -0
  101. package/schemas/adapter.schema.json +23 -0
  102. package/schemas/agent-envelope.schema.json +21 -0
  103. package/schemas/contract-resolution.schema.json +28 -0
  104. package/schemas/feature-contract.schema.json +78 -0
  105. package/schemas/feature-index.schema.json +25 -0
  106. package/schemas/feature.schema.json +17 -0
  107. package/schemas/gate-event.schema.json +19 -0
  108. package/schemas/handles-plan.schema.json +31 -0
  109. package/schemas/handles-provider.schema.json +26 -0
  110. package/schemas/patch-approvals.schema.json +28 -0
  111. package/schemas/scan-report.schema.json +102 -0
  112. package/schemas/stack-choice.schema.json +89 -0
  113. package/schemas/stack-record.schema.json +20 -0
  114. package/schemas/state.schema.json +43 -0
  115. package/scripts/preflight-base-ref.sh +226 -0
  116. package/stack/apply.mjs +159 -0
  117. package/stack/bootstrap/_lib.sh +73 -0
  118. package/stack/bootstrap/ngrok.sh +90 -0
  119. package/stack/catalog/ngrok.yml +63 -0
@@ -0,0 +1,301 @@
1
+ // Ripgrep-for-discovery + a masking/balanced-delimiter structural scan (A2 Phase 1,
2
+ // D-java-analyzer -- see scanners/adapters/_java-spring-analyzer.mjs) for structure. Deliberately
3
+ // still no real Java parser/AST (Phase 2, out of scope) -- good enough to find "does a related
4
+ // module already exist" without a compiler dependency, not a general-purpose Java analyzer.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { execFileSync } from 'node:child_process';
8
+ import { lineNumberAt } from '../text-util.mjs';
9
+ import { maskNonCode, findClassOrRecordDeclaration, findClassLevelMappingArgs, findMappingAnnotations } from './_java-spring-analyzer.mjs';
10
+
11
+ export function detectJavaSpringRoot(repoRoot) {
12
+ const buildFiles = ['build.gradle', 'build.gradle.kts', 'pom.xml'];
13
+ if (!buildFiles.some((f) => fs.existsSync(path.join(repoRoot, f)))) return null;
14
+ const srcRoot = path.join(repoRoot, 'src', 'main', 'java');
15
+ return fs.existsSync(srcRoot) ? srcRoot : null;
16
+ }
17
+
18
+ // O6: `rg --files` (no `--sort`) is explicitly unordered/parallel by ripgrep's own docs -- two
19
+ // runs against an unchanged repo can return files in a different order, which without this sort
20
+ // would propagate into non-deterministic controller/entity/module array order in every scan
21
+ // report and contract, causing spurious diffs even when nothing real changed.
22
+ function listJavaFiles(srcRoot) {
23
+ const out = execFileSync('rg', ['--files', '-g', '*.java', srcRoot], { encoding: 'utf8' });
24
+ return out.split('\n').filter(Boolean).sort();
25
+ }
26
+
27
+ function moduleOf(filePath, srcRoot) {
28
+ const parts = path.relative(srcRoot, filePath).split(path.sep);
29
+ const domainIdx = parts.indexOf('domain');
30
+ return domainIdx >= 0 && parts[domainIdx + 1] ? parts[domainIdx + 1] : null;
31
+ }
32
+
33
+ function extractQuotedOrValue(argsText) {
34
+ if (argsText == null) return null;
35
+ const named = argsText.match(/value\s*=\s*"([^"]*)"/);
36
+ if (named) return named[1];
37
+ const bare = argsText.match(/"([^"]*)"/);
38
+ return bare ? bare[1] : null;
39
+ }
40
+
41
+ function joinPath(base, segment) {
42
+ const b = (base || '').replace(/\/$/, '');
43
+ const s = (segment || '').replace(/^\//, '');
44
+ return s ? `${b}/${s}` : (b || '/');
45
+ }
46
+
47
+ // A2 Phase 1 (D-java-analyzer): finds every `operationId = "..."` occurrence's REAL value within
48
+ // `text[searchStart:searchEnd]`, safe from a comment/string phantom match (D-java-analyzer's
49
+ // headline example: a `//` comment mentioning `operationId = "..."` as prose used to appear in
50
+ // controller.operationIds). The POSITION is found on `masked` text (comments/strings blanked to
51
+ // spaces, so a phantom mention can never be found there at all); the VALUE is always re-read from
52
+ // the ORIGINAL text at that same offset, since a masked string body only ever contains spaces.
53
+ function findOperationIdValue(text, masked, searchStart, searchEnd) {
54
+ const posMatch = masked.slice(searchStart, searchEnd).match(/operationId\s*=\s*"/);
55
+ if (!posMatch) return null;
56
+ const real = text.slice(searchStart + posMatch.index).match(/operationId\s*=\s*"([^"]+)"/);
57
+ return real ? real[1] : null;
58
+ }
59
+
60
+ function findAllOperationIdValues(text, masked) {
61
+ const ids = [];
62
+ const re = /operationId\s*=\s*"/g;
63
+ let m;
64
+ while ((m = re.exec(masked))) {
65
+ const real = text.slice(m.index).match(/operationId\s*=\s*"([^"]+)"/);
66
+ if (real) ids.push(real[1]);
67
+ }
68
+ return ids;
69
+ }
70
+
71
+ function extractController(text, filePath) {
72
+ if (!/@RestController\b/.test(text)) return null;
73
+ const masked = maskNonCode(text);
74
+
75
+ // D3 (D-scanner-evidence): the class declaration's own line -- classLine is null only when
76
+ // there's no class/record declaration at all to point at.
77
+ const classDecl = findClassOrRecordDeclaration(masked);
78
+ const className = classDecl ? classDecl.name : path.basename(filePath, '.java');
79
+ const classLine = classDecl ? lineNumberAt(text, classDecl.index) : null;
80
+
81
+ // A2 Phase 1: class-level @RequestMapping detection now goes through the shared analyzer
82
+ // (balanced-paren args, masked-text lookahead for `class`/`record`) instead of a lazy-backtrack
83
+ // regex -- see D-java-analyzer for the lazy-backtrack misattribution bug this class of pattern
84
+ // used to cause.
85
+ const classMappingArgs = findClassLevelMappingArgs(text);
86
+ const basePath = classMappingArgs != null ? (extractQuotedOrValue(classMappingArgs) ?? '') : '';
87
+
88
+ // Document-order list of every operationId in the file -- this is what the acceptance
89
+ // oracle (10 operationIds for Team-IZ-Backend's OrganizationController) checks directly.
90
+ const operationIds = findAllOperationIdValues(text, masked);
91
+ const operationStarts = [...masked.matchAll(/@Operation\(/g)].map((m) => m.index);
92
+
93
+ const endpoints = [];
94
+ for (const mapping of findMappingAnnotations(text)) {
95
+ const segment = extractQuotedOrValue(mapping.argsText) ?? '';
96
+
97
+ // Correlate with the nearest PRECEDING @Operation( block, even when an @ApiResponses
98
+ // (or similar) annotation sits between it and this mapping -- heuristic, not a parser.
99
+ let operationId = null;
100
+ const preceding = operationStarts.filter((idx) => idx < mapping.index);
101
+ if (preceding.length > 0) {
102
+ operationId = findOperationIdValue(text, masked, preceding[preceding.length - 1], mapping.index);
103
+ }
104
+ // D3: line of the mapping annotation itself. A2 Phase 1: this is now the SAME position
105
+ // handles/providers/java-spring/plan.mjs derives too -- both consume
106
+ // findMappingAnnotations() from scanners/adapters/_java-spring-analyzer.mjs, the duplicate
107
+ // regex pass this comment used to point at is gone.
108
+ endpoints.push({ verb: mapping.verb, path: joinPath(basePath, segment), operationId, method: mapping.methodName, line: mapping.methodLine });
109
+ }
110
+
111
+ return { className, basePath, operationIds, endpoints, file: filePath, line: classLine };
112
+ }
113
+
114
+ function extractEntity(text, filePath) {
115
+ if (!/@Entity\b/.test(text)) return null;
116
+ const masked = maskNonCode(text);
117
+ const classDecl = findClassOrRecordDeclaration(masked);
118
+ const tableMatch = text.match(/@Table\(\s*name\s*=\s*"([^"]+)"/);
119
+ const idFieldMatch = text.match(/@Id\b[\s\S]{0,200}?private\s+\S+\s+(\w+)\s*;/);
120
+ return {
121
+ className: classDecl ? classDecl.name : path.basename(filePath, '.java'),
122
+ table: tableMatch ? tableMatch[1] : null,
123
+ idField: idFieldMatch ? idFieldMatch[1] : null,
124
+ file: filePath,
125
+ line: classDecl ? lineNumberAt(text, classDecl.index) : null,
126
+ };
127
+ }
128
+
129
+ function extractDomainEnum(text, filePath) {
130
+ const match = text.match(/public\s+enum\s+(\w+)\s*\{([\s\S]*?)\n\}/);
131
+ if (!match) return null;
132
+ const [, name, rawBody] = match;
133
+ const body = maskNonCode(rawBody);
134
+ // Each constant ends at the first `,` `;` (or the start of a constructor-arg `(` if present).
135
+ const constants = body
136
+ .split(/[,;]/)
137
+ .map((s) => s.trim().split('(')[0].trim())
138
+ .filter((s) => /^[A-Z][A-Z0-9_]*$/.test(s));
139
+ return { name, constants, file: filePath, line: lineNumberAt(text, match.index) };
140
+ }
141
+
142
+ const APPLICATION_CONFIG_GLOB = 'application*.{yml,yaml,properties}';
143
+
144
+ function findApplicationConfigFiles(repoRoot) {
145
+ try {
146
+ return execFileSync('rg', ['--files', '-g', APPLICATION_CONFIG_GLOB, repoRoot], { encoding: 'utf8' })
147
+ .split('\n').filter(Boolean).sort(); // O6: rg --files order isn't guaranteed -- see listJavaFiles.
148
+ } catch {
149
+ return []; // rg exits 1 on "no files matched" -- not an error, just nothing to report
150
+ }
151
+ }
152
+
153
+ // A1 §7: three independent Spring config signals for a GLOBAL path prefix the regex endpoint
154
+ // scanner structurally cannot see -- extractController() reads one file at a time and has no
155
+ // idea a WebMvcConfigurer or application.yaml elsewhere in the repo prepends something to every
156
+ // path it found. Team-IZ-Backend's ApiPathConfig.java (configurePathMatch + addPathPrefix) is
157
+ // exactly the real defect D-openapi-reconciliation documents: every contract path this tool
158
+ // emits without --openapi-file was silently wrong until A1 closed it with real-document
159
+ // reconciliation. This function can't fix that (only a real OpenAPI document can) -- it exists
160
+ // so a user who doesn't know --openapi-file exists gets told the defect is likely present, via
161
+ // scanners/index.mjs's `unknowns` note.
162
+ export function detectGlobalPathPrefixSignals(repoRoot) {
163
+ const signals = [];
164
+
165
+ // (1) WebMvcConfigurer.configurePathMatch + addPathPrefix("...", ...). Two-step, same style
166
+ // as listJavaFiles/extractController elsewhere in this file: `rg -l` finds candidate FILES
167
+ // mentioning configurePathMatch, then a content read confirms addPathPrefix( is actually
168
+ // present (a configurePathMatch override that does something else entirely -- a custom
169
+ // PathMatcher, a trailing-slash setting -- must not be reported as a prefix).
170
+ const srcRoot = path.join(repoRoot, 'src', 'main', 'java');
171
+ if (fs.existsSync(srcRoot)) {
172
+ let candidates = [];
173
+ try {
174
+ candidates = execFileSync('rg', ['-l', 'configurePathMatch', '-g', '*.java', srcRoot], { encoding: 'utf8' }).split('\n').filter(Boolean).sort();
175
+ } catch {
176
+ // no matches -- not an error
177
+ }
178
+ for (const file of candidates) {
179
+ const text = fs.readFileSync(file, 'utf8');
180
+ const prefixMatch = text.match(/addPathPrefix\s*\(\s*"([^"]+)"/);
181
+ if (!prefixMatch) continue;
182
+ signals.push({ kind: 'configurePathMatch', file: path.relative(repoRoot, file), prefix: prefixMatch[1] });
183
+ }
184
+ }
185
+
186
+ // (2)/(3) application.yml/.yaml/.properties -- server.servlet.context-path (Spring Boot's own
187
+ // built-in global-prefix mechanism, same blind spot as (1)) and springdoc.paths-to-match
188
+ // (doesn't itself apply a prefix, but a pattern narrower than "/**" is strong circumstantial
189
+ // evidence one is documented, even when (1)/(2) are what actually implement it). Same
190
+ // good-enough-regex-not-a-real-YAML-parser philosophy as the rest of this file -- matches
191
+ // either YAML (`key:`) or properties (`a.b.c=`) form.
192
+ for (const file of findApplicationConfigFiles(repoRoot)) {
193
+ const text = fs.readFileSync(file, 'utf8');
194
+ const contextPath = text.match(/(?:^|\n)\s*context-path\s*:\s*(\S+)/) ?? text.match(/server\.servlet\.context-path\s*=\s*(\S+)/);
195
+ if (contextPath) {
196
+ signals.push({ kind: 'context-path', file: path.relative(repoRoot, file), prefix: contextPath[1].replace(/^["']|["']$/g, '') });
197
+ }
198
+ const pathsToMatch = text.match(/(?:^|\n)\s*paths-to-match\s*:\s*(\S+)/) ?? text.match(/springdoc\.paths-to-match\s*=\s*(\S+)/);
199
+ if (pathsToMatch) {
200
+ signals.push({ kind: 'paths-to-match', file: path.relative(repoRoot, file), pattern: pathsToMatch[1].replace(/^["']|["']$/g, '') });
201
+ }
202
+ }
203
+
204
+ return signals;
205
+ }
206
+
207
+ export function scanJavaSpring(repoRoot) {
208
+ const srcRoot = detectJavaSpringRoot(repoRoot);
209
+ if (!srcRoot) return null;
210
+
211
+ const modules = new Map();
212
+ const moduleEntry = (name) => {
213
+ const key = name ?? '_unknown';
214
+ if (!modules.has(key)) modules.set(key, { module: key, controllers: [], entities: [], enums: [], dtos: [] });
215
+ return modules.get(key);
216
+ };
217
+
218
+ for (const file of listJavaFiles(srcRoot)) {
219
+ const text = fs.readFileSync(file, 'utf8');
220
+ const mod = moduleOf(file, srcRoot);
221
+
222
+ if (/@RestController\b/.test(text)) {
223
+ const controller = extractController(text, file);
224
+ if (controller) moduleEntry(mod).controllers.push(controller);
225
+ }
226
+ if (/@Entity\b/.test(text)) {
227
+ const entity = extractEntity(text, file);
228
+ if (entity) moduleEntry(mod).entities.push(entity);
229
+ }
230
+ if (mod && file.includes(`${path.sep}domain${path.sep}`) && /public\s+enum\s+\w+/.test(text)) {
231
+ const en = extractDomainEnum(text, file);
232
+ if (en) moduleEntry(mod).enums.push(en);
233
+ }
234
+ if (mod && file.includes(`${path.sep}presentation${path.sep}dto${path.sep}`)) {
235
+ moduleEntry(mod).dtos.push(path.basename(file, '.java'));
236
+ }
237
+ }
238
+
239
+ // S2 (D-gate-precision, continued): repo-relative (not srcRoot-relative like moduleOf's own
240
+ // paths) -- this is what lib/gate-definitions.mjs's `scan` gate hashes to detect real content
241
+ // drift, and every other manifest-shaped gate input in this codebase (stack's `applied_file:`)
242
+ // is repo-relative too.
243
+ const filesRead = listJavaFiles(srcRoot).map((f) => path.relative(repoRoot, f));
244
+ return { srcRoot, modules: [...modules.values()], pathPrefixSignals: detectGlobalPathPrefixSignals(repoRoot), filesRead };
245
+ }
246
+
247
+ // G1: adapter descriptor consumed by scanners/registry.mjs -- see D-adapter-registry in
248
+ // DECISIONS.md. Wraps the functions above without changing any of them; `id` must equal this
249
+ // file's stem ("java-spring"). specificity 100: this adapter only detects when a Spring Boot
250
+ // build file AND src/main/java both exist -- a build-file-plus-source-layout-confirmed match, the
251
+ // strongest signal any adapter in this codebase can give.
252
+ export const adapter = {
253
+ contract: 'sbf.adapter/1',
254
+ id: 'java-spring',
255
+ title: 'Java / Spring Boot',
256
+ specificity: 100,
257
+ confidence: 'high',
258
+ capabilities: {
259
+ 'api.operations': true,
260
+ 'api.request-shape': true,
261
+ 'resource.fetch': true,
262
+ 'codegen.handles': true,
263
+ },
264
+ detect: detectJavaSpringRoot,
265
+ scan(repoRoot, _detection) {
266
+ const result = scanJavaSpring(repoRoot);
267
+ return { modules: result.modules, pathPrefixSignals: result.pathPrefixSignals, filesRead: result.filesRead };
268
+ },
269
+ // S2 (D-gate-precision, continued): reuses the EXACT same listJavaFiles() call scan() itself
270
+ // makes -- no separate file-walking logic -- so the `scan` gate's staleness token can re-derive
271
+ // the CURRENT read-set fresh every time it's checked (not just re-hash whatever the last scan
272
+ // run happened to record), which is what lets it notice a brand-new file too, not just an edit
273
+ // to a previously-known one.
274
+ listReadSet(repoRoot) {
275
+ const srcRoot = detectJavaSpringRoot(repoRoot);
276
+ if (!srcRoot) return [];
277
+ return listJavaFiles(srcRoot).map((f) => path.relative(repoRoot, f));
278
+ },
279
+ diagnostics(repoRoot) {
280
+ const messages = [];
281
+ const buildFiles = ['build.gradle', 'build.gradle.kts', 'pom.xml'];
282
+ if (!buildFiles.some((f) => fs.existsSync(path.join(repoRoot, f)))) {
283
+ messages.push({ level: 'info', code: 'no-build-file', message: `none of ${buildFiles.join(', ')} found at repo root` });
284
+ }
285
+ const srcRoot = path.join(repoRoot, 'src', 'main', 'java');
286
+ if (!fs.existsSync(srcRoot)) {
287
+ messages.push({ level: 'info', code: 'no-src-main-java', message: 'src/main/java does not exist' });
288
+ } else {
289
+ let rgOk = true;
290
+ try {
291
+ execFileSync('rg', ['--version'], { stdio: 'pipe' });
292
+ } catch {
293
+ rgOk = false;
294
+ }
295
+ if (!rgOk) {
296
+ messages.push({ level: 'warn', code: 'rg-missing', message: 'ripgrep (rg) is not on PATH -- this adapter shells out to it and will throw, not degrade, if it is missing' });
297
+ }
298
+ }
299
+ return messages;
300
+ },
301
+ };