dd-trace 6.8.0 → 6.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/ci/diagnose.js +27 -25
  2. package/ci/init.js +6 -6
  3. package/ci/runbook.md +114 -20
  4. package/ci/test-optimization-validation/approval-artifacts.js +36 -3
  5. package/ci/test-optimization-validation/approval.js +78 -22
  6. package/ci/test-optimization-validation/blocker-category.js +24 -0
  7. package/ci/test-optimization-validation/ci-discovery.js +23 -17
  8. package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
  9. package/ci/test-optimization-validation/ci-remediation.js +1 -1
  10. package/ci/test-optimization-validation/cli.js +280 -52
  11. package/ci/test-optimization-validation/command-blocker.js +159 -20
  12. package/ci/test-optimization-validation/command-output-policy.js +9 -3
  13. package/ci/test-optimization-validation/command-runner.js +9 -7
  14. package/ci/test-optimization-validation/environment.js +4 -2
  15. package/ci/test-optimization-validation/executable.js +15 -13
  16. package/ci/test-optimization-validation/execution-lock.js +70 -0
  17. package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
  18. package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
  19. package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
  20. package/ci/test-optimization-validation/generated-files.js +38 -28
  21. package/ci/test-optimization-validation/generated-test-contract.js +8 -4
  22. package/ci/test-optimization-validation/generated-verifier.js +24 -17
  23. package/ci/test-optimization-validation/literal-glob.js +52 -0
  24. package/ci/test-optimization-validation/manifest-loader.js +3 -0
  25. package/ci/test-optimization-validation/manifest-scaffold.js +300 -50
  26. package/ci/test-optimization-validation/manifest-schema.js +80 -17
  27. package/ci/test-optimization-validation/offline-fixtures.js +13 -4
  28. package/ci/test-optimization-validation/package-check.js +94 -0
  29. package/ci/test-optimization-validation/plan-writer.js +183 -10
  30. package/ci/test-optimization-validation/preflight-runner.js +110 -31
  31. package/ci/test-optimization-validation/project-build-artifact.js +31 -0
  32. package/ci/test-optimization-validation/redaction.js +18 -27
  33. package/ci/test-optimization-validation/report-writer.js +182 -48
  34. package/ci/test-optimization-validation/result-semantics.js +20 -3
  35. package/ci/test-optimization-validation/runner-command.js +40 -20
  36. package/ci/test-optimization-validation/runner-contract.js +79 -43
  37. package/ci/test-optimization-validation/scenarios/basic-reporting.js +59 -5
  38. package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
  39. package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
  40. package/ci/test-optimization-validation/static-diagnosis.js +10 -3
  41. package/ci/test-optimization-validation/test-output.js +2 -1
  42. package/ci/test-optimization-validation/validation-blocker.js +21 -0
  43. package/ci/vitest-no-worker-init-setup.mjs +30 -34
  44. package/index.d.ts +98 -1
  45. package/init.js +1 -17
  46. package/initialize.mjs +11 -0
  47. package/loader-hook.mjs +6 -4
  48. package/package.json +5 -3
  49. package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
  50. package/packages/datadog-esbuild/src/utils.js +16 -1
  51. package/packages/datadog-instrumentations/src/ai.js +61 -34
  52. package/packages/datadog-instrumentations/src/bluebird.js +6 -6
  53. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +81 -5
  54. package/packages/datadog-instrumentations/src/cucumber.js +124 -47
  55. package/packages/datadog-instrumentations/src/dns.js +21 -2
  56. package/packages/datadog-instrumentations/src/electron.js +1 -0
  57. package/packages/datadog-instrumentations/src/fastify.js +3 -1
  58. package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
  59. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
  60. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  61. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +33 -9
  62. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +118 -0
  63. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
  64. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
  65. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +61 -0
  66. package/packages/datadog-instrumentations/src/helpers/router-helper.js +37 -6
  67. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
  68. package/packages/datadog-instrumentations/src/jest.js +979 -233
  69. package/packages/datadog-instrumentations/src/mocha/main.js +107 -9
  70. package/packages/datadog-instrumentations/src/mocha/utils.js +64 -42
  71. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +25 -0
  72. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -18
  73. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  74. package/packages/datadog-instrumentations/src/next.js +155 -23
  75. package/packages/datadog-instrumentations/src/openai.js +14 -0
  76. package/packages/datadog-instrumentations/src/pg.js +180 -6
  77. package/packages/datadog-instrumentations/src/playwright.js +173 -79
  78. package/packages/datadog-instrumentations/src/promise-js.js +4 -4
  79. package/packages/datadog-instrumentations/src/promise.js +3 -3
  80. package/packages/datadog-instrumentations/src/router.js +195 -19
  81. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -10
  82. package/packages/datadog-instrumentations/src/vitest-main.js +360 -56
  83. package/packages/datadog-instrumentations/src/vitest-util.js +42 -7
  84. package/packages/datadog-instrumentations/src/vitest-worker.js +234 -17
  85. package/packages/datadog-instrumentations/src/webdriverio.js +194 -18
  86. package/packages/datadog-instrumentations/src/when.js +3 -3
  87. package/packages/datadog-instrumentations/src/ws.js +5 -1
  88. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
  89. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -3
  90. package/packages/datadog-plugin-cucumber/src/index.js +5 -1
  91. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +50 -46
  92. package/packages/datadog-plugin-cypress/src/support.js +7 -4
  93. package/packages/datadog-plugin-electron/src/net.js +10 -4
  94. package/packages/datadog-plugin-http/src/client.js +1 -1
  95. package/packages/datadog-plugin-http2/src/client.js +1 -1
  96. package/packages/datadog-plugin-jest/src/index.js +1 -18
  97. package/packages/datadog-plugin-jest/src/util.js +2 -0
  98. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -1
  99. package/packages/datadog-plugin-mocha/src/index.js +318 -1
  100. package/packages/datadog-plugin-next/src/index.js +57 -9
  101. package/packages/datadog-plugin-openai/src/tracing.js +4 -0
  102. package/packages/datadog-plugin-pg/src/index.js +65 -1
  103. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  104. package/packages/datadog-plugin-vitest/src/index.js +95 -7
  105. package/packages/datadog-shimmer/src/shimmer.js +6 -2
  106. package/packages/dd-trace/src/aiguard/sdk.js +3 -0
  107. package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
  108. package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
  109. package/packages/dd-trace/src/appsec/handlers/http-request.js +215 -0
  110. package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
  111. package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
  112. package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
  113. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
  114. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  115. package/packages/dd-trace/src/appsec/index.js +48 -476
  116. package/packages/dd-trace/src/appsec/sdk/track_event.js +9 -0
  117. package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
  118. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
  119. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +27 -13
  120. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -0
  121. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +21 -1
  122. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
  123. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
  124. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
  125. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
  126. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  127. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  128. package/packages/dd-trace/src/config/helper.js +2 -0
  129. package/packages/dd-trace/src/config/index.js +16 -5
  130. package/packages/dd-trace/src/config/remote_config.js +1 -1
  131. package/packages/dd-trace/src/config/supported-configurations.json +8 -0
  132. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  133. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
  134. package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
  135. package/packages/dd-trace/src/guardrails/log.js +1 -10
  136. package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
  137. package/packages/dd-trace/src/llmobs/experiments/index.js +33 -25
  138. package/packages/dd-trace/src/llmobs/experiments/util.js +4 -2
  139. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  140. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
  141. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  142. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
  143. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
  144. package/packages/dd-trace/src/llmobs/sdk.js +149 -68
  145. package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
  146. package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
  147. package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
  148. package/packages/dd-trace/src/log/channels.js +1 -9
  149. package/packages/dd-trace/src/log/levels.js +12 -0
  150. package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
  151. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  152. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
  153. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
  154. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
  155. package/packages/dd-trace/src/opentelemetry/span.js +4 -0
  156. package/packages/dd-trace/src/plugins/ci_plugin.js +21 -5
  157. package/packages/dd-trace/src/plugins/index.js +1 -0
  158. package/packages/dd-trace/src/plugins/util/ci.js +6 -0
  159. package/packages/dd-trace/src/plugins/util/git.js +6 -4
  160. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +2 -2
  161. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  162. package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
  163. package/packages/dd-trace/src/plugins/util/test.js +319 -75
  164. package/packages/dd-trace/src/plugins/util/url.js +1 -1
  165. package/packages/dd-trace/src/plugins/util/web.js +23 -1
  166. package/packages/dd-trace/src/profiler.js +1 -1
  167. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
  168. package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
  169. package/packages/dd-trace/src/proxy.js +1 -0
  170. package/packages/dd-trace/src/ritm.js +5 -0
  171. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
  172. package/packages/dd-trace/src/sampling_rule.js +4 -2
  173. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
  174. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
  175. package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
  176. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  177. package/vendor/dist/pprof-format/index.js +1 -1
  178. package/version.js +10 -8
  179. /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
@@ -4,9 +4,12 @@ const fs = require('node:fs')
4
4
  const path = require('node:path')
5
5
 
6
6
  const { runDiagnosis } = require('../diagnose')
7
+ const { BLOCKER_CATEGORIES } = require('./blocker-category')
7
8
  const cucumber = require('./framework-adapters/cucumber')
8
9
  const cypress = require('./framework-adapters/cypress')
9
10
  const playwright = require('./framework-adapters/playwright')
11
+ const vitest = require('./framework-adapters/vitest')
12
+ const { isProjectBuildArtifactPath } = require('./project-build-artifact')
10
13
  const {
11
14
  GENERATED_SCENARIOS,
12
15
  getGeneratedRetryStatePath,
@@ -15,6 +18,7 @@ const {
15
18
  const { validateManifest } = require('./manifest-schema')
16
19
  const {
17
20
  getProjectNodeRunner,
21
+ getRunnerConfigurationContract,
18
22
  getRunnerContract,
19
23
  getRunnerSearchRoots,
20
24
  } = require('./runner-contract')
@@ -54,7 +58,7 @@ const CONFIG_PATTERNS = {
54
58
  jest: /^(?:jest|config-jest)\.config\.(?:[cm]?[jt]s|json)$/,
55
59
  mocha: /^\.mocharc\.(?:json|ya?ml|[cm]?js)$/,
56
60
  playwright: playwright.CONFIG_PATTERN,
57
- vitest: /^(?:vite|vitest)\.config\.[cm]?[jt]s$/,
61
+ vitest: /^(?:vite\.config|vitest\.(?:config|workspace))\.[cm]?[jt]s$/,
58
62
  }
59
63
  const JS_CONFIG_EXTENSIONS = ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts']
60
64
  const IMPLICIT_CONFIG_FILENAMES = {
@@ -72,14 +76,20 @@ const IMPLICIT_CONFIG_FILENAMES = {
72
76
  vitest: JS_CONFIG_EXTENSIONS.flatMap(extension => [
73
77
  `vite.config.${extension}`,
74
78
  `vitest.config.${extension}`,
79
+ `vitest.workspace.${extension}`,
75
80
  ]),
76
81
  }
77
82
  const TEST_FILE_PATTERN = /^.+[._-](?:test|spec)\.[cm]?[jt]sx?$/
78
83
  const BARE_TEST_FILE_PATTERN = /^test\.[cm]?[jt]sx?$/
79
84
  const TYPE_ONLY_TEST_PATTERN = /\.(?:test|spec)-d\.[cm]?tsx?$|\.d\.[cm]?ts$/i
80
- const TYPE_ONLY_DIRECTORY_PATTERN = /(?:^|\/)(?:type[-_]?tests?|typetests?|test-dts?)(?:\/|$)/i
85
+ const TYPE_ONLY_DIRECTORY_PATTERN = /(?:^|\/)(?:type[-_]?tests?|test-dts?)(?:\/|$)/i
81
86
  const LOCAL_SOCKET_PATTERN =
82
- /(?:\bcreateServer\s*\(|\.listen\s*\(|\b(?:localhost|127\.0\.0\.1)\b|\bcy\.(?:visit|request)\s*\()/
87
+ /\bcreateServer\s*\(|\.listen\s*\(|\b(?:localhost|127\.0\.0\.1|supertest)\b|\bcy\.(?:visit|request)\s*\(/
88
+ const IMPORT_SPECIFIER_PATTERN = /(?:from\s+|require\s*\(\s*|import\s*\(\s*)(['"])([^'"]+)\1/g
89
+ const CUCUMBER_BROWSER_SUPPORT_DIRECTORY_PATTERN =
90
+ /(?:^|\/)(?:features?|helpers?|step_definitions|steps?|support)(?:\/|$)/i
91
+ const CUCUMBER_BROWSER_DRIVER_PATTERN =
92
+ /(?:from\s+|(?:import|require)\s*\(\s*)['"](?:@playwright\/test|nightwatch|playwright(?:-core)?|puppeteer(?:-core)?|selenium-webdriver|webdriverio)['"]/
83
93
  const CYPRESS_LOCAL_ORIGIN_PATTERN =
84
94
  /\bcy\.(?:visit|request)\s*\([\s\S]{0,512}\b(?:https?:\/\/)?(?:localhost|127\.0\.0\.1)(?=[:/'"`\s}])/i
85
95
 
@@ -121,7 +131,11 @@ function createManifestScaffold ({ root, frameworks = new Set() }) {
121
131
  ciDiscovery,
122
132
  frameworks: [
123
133
  ...eligible.map(detection => buildFramework(repositoryRoot, detection, ciDiscovery)),
124
- ...detectedOnly.map(detection => buildDetectedOnlyFramework(repositoryRoot, detection, ciDiscovery)),
134
+ ...detectedOnly.map(detection => {
135
+ return detection.supportedVersion && SUPPORTED_FRAMEWORKS.has(detection.id)
136
+ ? buildFramework(repositoryRoot, detection, ciDiscovery)
137
+ : buildDetectedOnlyFramework(repositoryRoot, detection, ciDiscovery)
138
+ }),
125
139
  ...unsupported.map(detection => buildUnsupportedFramework(repositoryRoot, detection, ciDiscovery)),
126
140
  ],
127
141
  omitted: [],
@@ -144,7 +158,15 @@ function createManifestScaffold ({ root, frameworks = new Set() }) {
144
158
  */
145
159
  function buildFramework (repositoryRoot, detection, ciDiscovery) {
146
160
  const framework = detection.id
147
- const packageJson = getDetectionPackageJson(repositoryRoot, detection.commandLocation)
161
+ const packageJson = getFrameworkPackageJson(repositoryRoot, detection)
162
+ if (!packageJson) {
163
+ return buildDetectedOnlyFramework(
164
+ repositoryRoot,
165
+ detection,
166
+ ciDiscovery,
167
+ `The package that owns ${detection.name} could not be identified from the bounded detection evidence.`
168
+ )
169
+ }
148
170
  const projectRoot = path.dirname(packageJson.path)
149
171
  const base = getFrameworkBase({
150
172
  ciDiscovery,
@@ -158,16 +180,32 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
158
180
  if (!SUPPORTED_FRAMEWORKS.has(framework)) {
159
181
  return {
160
182
  ...base,
183
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
161
184
  status: 'unsupported_by_validator',
162
185
  notes: [`${detection.name} is supported by dd-trace, but this validator has no direct-runner adapter.`],
163
186
  }
164
187
  }
165
188
 
189
+ if (framework === 'cucumber' && !cucumber.supportsConfigIsolation(base.frameworkVersion)) {
190
+ return {
191
+ ...base,
192
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
193
+ status: 'requires_manual_setup',
194
+ notes: [
195
+ `${detection.name} ${base.frameworkVersion || 'with an unknown version'} is supported by dd-trace, but its ` +
196
+ 'CLI cannot bypass customer profiles with a validator-owned config. Live validation is unavailable because ' +
197
+ 'changing the working directory or loading the profile dynamically would weaken isolation. The static CI ' +
198
+ 'audit can still run.',
199
+ ],
200
+ }
201
+ }
202
+
166
203
  const projectRunner = getProjectNodeRunner(detection.command, projectRoot, repositoryRoot)
167
204
  const runner = projectRunner || resolveRunner(framework, projectRoot, repositoryRoot)
168
205
  if (!runner) {
169
206
  return {
170
207
  ...base,
208
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
171
209
  status: 'requires_manual_setup',
172
210
  notes: [
173
211
  `${detection.name} is detected, but its repository-contained executable is unavailable. Install this ` +
@@ -176,10 +214,13 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
176
214
  }
177
215
  }
178
216
 
179
- const runnerContract = getRunnerContract(framework, detection.command, projectRoot, repositoryRoot)
217
+ const projectFiles = collectProjectFiles(projectRoot)
218
+ const commandRoots = getRunnerSearchRoots(framework, detection.command, projectRoot, repositoryRoot)
219
+ let runnerContract = getRunnerContract(framework, detection.command, projectRoot, repositoryRoot)
180
220
  if (runnerContract.error) {
181
221
  return {
182
222
  ...base,
223
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
183
224
  status: 'requires_manual_setup',
184
225
  notes: [
185
226
  `${detection.name} runner configuration was not retained because ${runnerContract.error}. ` +
@@ -187,23 +228,91 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
187
228
  ],
188
229
  }
189
230
  }
190
- const commandRoots = getRunnerSearchRoots(framework, detection.command, projectRoot, repositoryRoot)
191
- const representativeRoot = commandRoots[0] || findPreferredRepresentativeRoot(projectRoot, repositoryRoot)
231
+ const implicitConfigFiles = [...new Set([
232
+ ...getImplicitConfigFiles(framework, projectRoot, repositoryRoot),
233
+ ...(framework === 'vitest' && commandRoots[0]
234
+ ? getImplicitConfigFiles(framework, commandRoots[0], repositoryRoot)
235
+ : []),
236
+ ])]
237
+ if (framework === 'cucumber') {
238
+ const expanded = cucumber.expandProfiles({
239
+ configFiles: [...new Set([...runnerContract.inputFiles, ...implicitConfigFiles])],
240
+ projectFiles,
241
+ projectRoot,
242
+ runnerArgs: runnerContract.runnerArgs,
243
+ })
244
+ if (expanded.error) {
245
+ return {
246
+ ...base,
247
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
248
+ status: 'requires_manual_setup',
249
+ notes: [
250
+ `${detection.name} runner configuration was not retained because ${expanded.error}. ` +
251
+ 'Use a literal Cucumber profile or a direct command with bounded support-code options, then create a ' +
252
+ 'fresh plan.',
253
+ ],
254
+ }
255
+ }
256
+ const expandedContract = getRunnerConfigurationContract(
257
+ framework,
258
+ expanded.runnerArgs,
259
+ runnerContract.environment,
260
+ projectRoot,
261
+ repositoryRoot
262
+ )
263
+ if (expandedContract.error) {
264
+ return {
265
+ ...base,
266
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
267
+ status: 'requires_manual_setup',
268
+ notes: [
269
+ `${detection.name} profile configuration was not retained because ${expandedContract.error}. ` +
270
+ 'Use repository-contained support-code inputs, then create a fresh plan.',
271
+ ],
272
+ }
273
+ }
274
+ runnerContract = {
275
+ ...expandedContract,
276
+ omittedOptions: runnerContract.omittedOptions,
277
+ }
278
+ }
279
+ const vitestProject = framework === 'vitest'
280
+ ? vitest.bindLiteralProject({
281
+ configFiles: [...new Set([...runnerContract.inputFiles, ...implicitConfigFiles])],
282
+ projectFiles,
283
+ projectRoot,
284
+ runnerArgs: runnerContract.runnerArgs,
285
+ })
286
+ : undefined
287
+ if (vitestProject?.error) {
288
+ return {
289
+ ...base,
290
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
291
+ status: 'requires_manual_setup',
292
+ notes: [
293
+ `${detection.name} runner configuration was not retained because ${vitestProject.error}. ` +
294
+ 'Use one literal, uniquely named Vitest project with static configuration, then create a fresh plan.',
295
+ ],
296
+ }
297
+ }
298
+ const representativeRoot = vitestProject?.root ||
299
+ commandRoots[0] ||
300
+ findPreferredRepresentativeRoot(projectRoot, repositoryRoot)
192
301
  const representativePackage = readJson(path.join(representativeRoot, 'package.json')) || packageJson.json
193
- const projectFiles = collectProjectFiles(projectRoot)
194
- const candidateFiles = representativeRoot === projectRoot
195
- ? projectFiles
196
- : collectProjectFiles(representativeRoot)
197
- const candidate = selectRepresentativeTest(
302
+ const candidateFiles = vitestProject?.files ||
303
+ (representativeRoot === projectRoot ? projectFiles : collectProjectFiles(representativeRoot))
304
+ const candidates = selectRepresentativeTests(
198
305
  candidateFiles,
199
306
  framework,
200
307
  representativeRoot,
201
308
  representativePackage.name,
202
- commandRoots.length > 0
309
+ commandRoots.length > 0 || vitestProject !== undefined
203
310
  )
311
+ const candidate = candidates[0]
204
312
  if (!candidate) {
205
313
  return {
206
314
  ...base,
315
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
207
316
  status: 'requires_manual_setup',
208
317
  notes: [
209
318
  `No single ${detection.name} test file could be selected confidently. Choose a normal framework-owned ` +
@@ -214,6 +323,7 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
214
323
  if (candidate.requiresExternalService) {
215
324
  return {
216
325
  ...base,
326
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
217
327
  status: 'requires_manual_setup',
218
328
  notes: [
219
329
  `No self-contained ${detection.name} spec could be selected. The available spec accesses a localhost ` +
@@ -223,14 +333,23 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
223
333
  }
224
334
  }
225
335
 
226
- const generatedTestStrategy = buildGeneratedTestStrategy({
336
+ const plannedGeneratedTestStrategy = buildGeneratedTestStrategy({
227
337
  framework,
228
- projectRoot,
338
+ projectRoot: vitestProject?.root || projectRoot,
229
339
  representative: candidate.path,
230
340
  })
341
+ const generatedTestStrategy = vitestProject &&
342
+ !vitest.supportsGeneratedFiles(vitestProject, plannedGeneratedTestStrategy)
343
+ ? {
344
+ reason: 'The selected Vitest project include or exclude patterns do not collect ' +
345
+ 'validator-generated test files.',
346
+ status: 'not_possible',
347
+ }
348
+ : plannedGeneratedTestStrategy
231
349
  const configFiles = [...new Set([
232
350
  ...runnerContract.inputFiles,
233
- ...getImplicitConfigFiles(framework, projectRoot, repositoryRoot),
351
+ ...(vitestProject?.configFile ? [vitestProject.configFile] : []),
352
+ ...implicitConfigFiles,
234
353
  ...(representativeRoot === projectRoot
235
354
  ? []
236
355
  : getImplicitConfigFiles(framework, representativeRoot, repositoryRoot)),
@@ -239,6 +358,7 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
239
358
  if (configFiles.length > 20) {
240
359
  return {
241
360
  ...base,
361
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
242
362
  status: 'requires_manual_setup',
243
363
  notes: [
244
364
  `${detection.name} loads more configuration files than the validator can approval-bind safely. ` +
@@ -247,13 +367,38 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
247
367
  }
248
368
  }
249
369
  const runnerDescription = projectRunner ? 'repository test wrapper' : `installed ${framework} runner`
370
+ const runnerConfigurationNote = detection.command
371
+ ? 'The validator retained only allowlisted runner configuration from the detected package script.'
372
+ : 'The validator uses the installed runner with repository-contained configuration files; the unresolved ' +
373
+ 'package wrapper is not executed.'
374
+ const cucumberBrowserRequired = framework === 'cucumber' &&
375
+ hasCucumberBrowserSupport(candidateFiles, runnerContract.inputFiles, representativeRoot)
250
376
  const browserRequired = framework === 'cypress' ||
251
377
  framework === 'playwright' ||
378
+ cucumberBrowserRequired ||
252
379
  (framework === 'vitest' && runnerContract.runnerArgs.includes('--browser'))
380
+ const fallbackCandidates = candidates
381
+ .slice(1)
382
+ .filter(candidate => hasGeneratedTestContract(
383
+ candidate.path,
384
+ framework,
385
+ vitestProject?.root || projectRoot,
386
+ plannedGeneratedTestStrategy
387
+ ))
388
+ const fallbackTests = fallbackCandidates
389
+ .map(candidate => ({
390
+ buildArtifactRequired: candidate.requiresBuildArtifact,
391
+ localSocketRequired: candidate.requiresLocalSocket,
392
+ testFile: candidate.path,
393
+ }))
394
+ const allCandidatesRequireLocalSocket =
395
+ [candidate, ...fallbackCandidates].every(candidate => candidate.requiresLocalSocket)
253
396
 
254
397
  return {
255
398
  ...base,
399
+ allCandidatesRequireLocalSocket,
256
400
  browserRequired,
401
+ buildArtifactRequired: candidate.requiresBuildArtifact,
257
402
  language: /\.[cm]?tsx?$/.test(candidate.path) ? 'typescript' : 'javascript',
258
403
  localSocketRequired: candidate.requiresLocalSocket,
259
404
  status: 'runnable',
@@ -272,6 +417,7 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
272
417
  runnerArgs: runnerContract.runnerArgs,
273
418
  selectorScope: projectRunner ? 'instrumented_event_identity' : 'bounded_direct_runner',
274
419
  testFile: candidate.path,
420
+ ...(fallbackTests.length > 0 ? { fallbackTests } : {}),
275
421
  timeoutMs: browserRequired ? 300_000 : 180_000,
276
422
  },
277
423
  preflight: { status: 'pending' },
@@ -280,7 +426,7 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
280
426
  `Basic Reporting will invoke the ${runnerDescription} ` +
281
427
  'directly for ' +
282
428
  `${path.relative(repositoryRoot, candidate.path)}.`,
283
- 'The validator retained only allowlisted runner configuration from the detected package script.',
429
+ runnerConfigurationNote,
284
430
  ...(projectRunner
285
431
  ? [
286
432
  'Basic Reporting will remain incomplete unless captured test events identify only the approved ' +
@@ -289,8 +435,20 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
289
435
  : []),
290
436
  ...(candidate.requiresLocalSocket
291
437
  ? [
292
- 'The selected test appears to require localhost. A restricted sandbox may leave local validation ' +
293
- 'incomplete.',
438
+ `${allCandidatesRequireLocalSocket ? 'Every approved candidate appears' : 'The selected test appears'} ` +
439
+ 'to require localhost. A restricted execution environment may leave local validation incomplete.',
440
+ ]
441
+ : []),
442
+ ...(candidate.requiresBuildArtifact
443
+ ? [
444
+ 'The selected test appears to load a build or dist artifact. Complete the project\'s normal build before ' +
445
+ 'validation if that artifact is not already present.',
446
+ ]
447
+ : []),
448
+ ...(cucumberBrowserRequired
449
+ ? [
450
+ 'The selected Cucumber support code imports a browser driver. The approved validation command may launch ' +
451
+ 'the project browser.',
294
452
  ]
295
453
  : []),
296
454
  ],
@@ -299,15 +457,17 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
299
457
 
300
458
  function getImplicitConfigFiles (framework, projectRoot, repositoryRoot) {
301
459
  const files = []
302
- for (const basename of IMPLICIT_CONFIG_FILENAMES[framework] || []) {
303
- const filename = path.join(projectRoot, basename)
304
- try {
305
- const stat = fs.lstatSync(filename)
306
- const physical = fs.realpathSync(filename)
307
- if (stat.isFile() && !stat.isSymbolicLink() &&
308
- fs.statSync(physical).isFile() &&
309
- isPathInside(fs.realpathSync(repositoryRoot), physical)) files.push(physical)
310
- } catch {}
460
+ if (IMPLICIT_CONFIG_FILENAMES[framework]) {
461
+ for (const basename of IMPLICIT_CONFIG_FILENAMES[framework]) {
462
+ const filename = path.join(projectRoot, basename)
463
+ try {
464
+ const stat = fs.lstatSync(filename)
465
+ const physical = fs.realpathSync(filename)
466
+ if (stat.isFile() && !stat.isSymbolicLink() &&
467
+ fs.statSync(physical).isFile() &&
468
+ isPathInside(fs.realpathSync(repositoryRoot), physical)) files.push(physical)
469
+ } catch {}
470
+ }
311
471
  }
312
472
  return files
313
473
  }
@@ -318,14 +478,17 @@ function getImplicitConfigFiles (framework, projectRoot, repositoryRoot) {
318
478
  * @param {string} repositoryRoot repository root
319
479
  * @param {object} detection supported framework detection
320
480
  * @param {object} ciDiscovery bounded CI discovery result
481
+ * @param {string} [blockerNote] precise reason live validation is unavailable
321
482
  * @returns {object} framework manifest entry
322
483
  */
323
- function buildDetectedOnlyFramework (repositoryRoot, detection, ciDiscovery) {
324
- const packageJson = getDetectionPackageJson(repositoryRoot, detection.locations?.[0])
484
+ function buildDetectedOnlyFramework (repositoryRoot, detection, ciDiscovery, blockerNote) {
485
+ const packageJson = getDetectionPackageJson(repositoryRoot, getDetectionPackageLocation(detection))
325
486
  const projectRoot = path.dirname(packageJson.path)
326
487
  const version = detection.supportedVersion?.version ||
327
488
  detection.versionDetections?.[0]?.version ||
328
489
  detection.versionDetections?.[0]?.rawVersion
490
+ const unsupportedVersion = !detection.supportedVersion &&
491
+ (detection.versionDetections?.[0]?.version || detection.versionDetections?.[0]?.rawVersion)
329
492
  return {
330
493
  ...getFrameworkBase({
331
494
  ciDiscovery,
@@ -335,11 +498,18 @@ function buildDetectedOnlyFramework (repositoryRoot, detection, ciDiscovery) {
335
498
  projectRoot,
336
499
  repositoryRoot,
337
500
  }),
501
+ blockerCategory: unsupportedVersion
502
+ ? BLOCKER_CATEGORIES.UNSUPPORTED_VERSION
503
+ : BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
338
504
  status: 'detected_not_runnable',
339
505
  notes: [
340
- detection.supportedVersion
341
- ? `A supported ${detection.name} installation was detected, but no project-local validation target was found.`
342
- : `${detection.name} was detected, but no supported installed version was confirmed.`,
506
+ blockerNote || (unsupportedVersion
507
+ ? `${detection.name} ${unsupportedVersion} was detected, but live validation requires ` +
508
+ `${detection.supportedRange}. Upgrade ${detection.name} through the project's normal dependency workflow ` +
509
+ 'before creating a fresh validation plan.'
510
+ : detection.supportedVersion
511
+ ? `A supported ${detection.name} installation was detected, but no project-local validation target was found.`
512
+ : `${detection.name} was detected, but no supported installed version was confirmed.`),
343
513
  ],
344
514
  }
345
515
  }
@@ -365,6 +535,7 @@ function buildUnsupportedFramework (repositoryRoot, detection, ciDiscovery) {
365
535
  projectRoot,
366
536
  repositoryRoot,
367
537
  }),
538
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
368
539
  status: 'unsupported_by_validator',
369
540
  notes: [`${detection.name} is not supported by this Test Optimization validator.`],
370
541
  }
@@ -498,17 +669,29 @@ function buildGeneratedTestStrategy ({ framework, projectRoot, representative })
498
669
  }
499
670
  }
500
671
 
672
+ function hasGeneratedTestContract (representative, framework, projectRoot, strategy) {
673
+ const convention = getGeneratedTestConvention(framework, representative, projectRoot)
674
+ return convention.fileExtension === strategy.fileExtension &&
675
+ convention.testDirectory === strategy.testDirectory &&
676
+ getModuleSystem(representative, projectRoot) === strategy.moduleSystem
677
+ }
678
+
501
679
  /**
502
- * Selects one representative framework-owned test file.
680
+ * Selects up to three representative framework-owned test files.
503
681
  *
504
682
  * @param {string[]} files bounded project files
505
683
  * @param {string} framework framework name
506
684
  * @param {string} projectRoot project root
507
685
  * @param {string} packageName project package name
508
- * @param {boolean} allowDirectoryConvention whether a literal runner selector chose this root
509
- * @returns {{path: string, requiresExternalService: boolean, requiresLocalSocket: boolean}|undefined} selected test
686
+ * @param {boolean} allowDirectoryConvention whether a literal runner selector owns this root
687
+ * @returns {Array<{
688
+ * path: string,
689
+ * requiresBuildArtifact: boolean,
690
+ * requiresExternalService: boolean,
691
+ * requiresLocalSocket: boolean
692
+ * }>} selected tests
510
693
  */
511
- function selectRepresentativeTest (files, framework, projectRoot, packageName, allowDirectoryConvention) {
694
+ function selectRepresentativeTests (files, framework, projectRoot, packageName, allowDirectoryConvention) {
512
695
  const candidates = []
513
696
  for (const filename of files) {
514
697
  const source = readText(filename)
@@ -520,18 +703,39 @@ function selectRepresentativeTest (files, framework, projectRoot, packageName, a
520
703
  : getStaticTestCount(source) === 0) continue
521
704
  if (!hasFrameworkOwnership(filename, source, framework, packageName)) continue
522
705
  candidates.push({
706
+ explicitFilename: TEST_FILE_PATTERN.test(path.basename(filename)),
523
707
  path: filename,
524
708
  rank: getTestRank(filename, source, projectRoot),
709
+ requiresBuildArtifact: requiresProjectBuildArtifact(source, projectRoot, path.dirname(filename)),
525
710
  requiresExternalService: framework === 'cypress' && CYPRESS_LOCAL_ORIGIN_PATTERN.test(source),
526
711
  requiresLocalSocket: LOCAL_SOCKET_PATTERN.test(source),
527
712
  })
528
713
  }
529
714
  candidates.sort((left, right) => {
530
715
  return Number(left.requiresExternalService) - Number(right.requiresExternalService) ||
716
+ Number(right.explicitFilename) - Number(left.explicitFilename) ||
531
717
  left.rank - right.rank ||
532
718
  left.path.localeCompare(right.path)
533
719
  })
534
- return candidates[0]
720
+ return candidates.slice(0, 3)
721
+ }
722
+
723
+ function requiresProjectBuildArtifact (source, projectRoot, sourceDirectory) {
724
+ for (const match of source.matchAll(IMPORT_SPECIFIER_PATTERN)) {
725
+ if (isProjectBuildArtifactPath(match[2], projectRoot, sourceDirectory)) return true
726
+ }
727
+ return false
728
+ }
729
+
730
+ function hasCucumberBrowserSupport (files, retainedInputs, projectRoot) {
731
+ const retained = new Set(retainedInputs)
732
+ for (const filename of new Set([...files, ...retained])) {
733
+ const relative = path.relative(projectRoot, filename).replaceAll('\\', '/')
734
+ if (!retained.has(filename) && !CUCUMBER_BROWSER_SUPPORT_DIRECTORY_PATTERN.test(relative)) continue
735
+ const source = readText(filename)
736
+ if (source !== undefined && CUCUMBER_BROWSER_DRIVER_PATTERN.test(source)) return true
737
+ }
738
+ return false
535
739
  }
536
740
 
537
741
  /**
@@ -541,7 +745,7 @@ function selectRepresentativeTest (files, framework, projectRoot, packageName, a
541
745
  * @param {string} source candidate source
542
746
  * @param {string} framework framework name
543
747
  * @param {string} projectRoot project root
544
- * @param {boolean} allowDirectoryConvention whether a literal runner selector chose this root
748
+ * @param {boolean} allowDirectoryConvention whether a literal runner selector owns this root
545
749
  * @returns {boolean} whether the file is a candidate
546
750
  */
547
751
  function isTestFile (filename, source, framework, projectRoot, allowDirectoryConvention) {
@@ -561,7 +765,7 @@ function isTestFile (filename, source, framework, projectRoot, allowDirectoryCon
561
765
  return ['__tests__', 'spec', 'test', 'tests'].includes(directory)
562
766
  })
563
767
  if (BARE_TEST_FILE_PATTERN.test(basename)) {
564
- return inTestDirectory || hasExplicitFrameworkImport(source, framework)
768
+ return (allowDirectoryConvention && inTestDirectory) || hasExplicitFrameworkImport(source, framework)
565
769
  }
566
770
  return allowDirectoryConvention && inTestDirectory && /\.[cm]?[jt]sx?$/.test(basename)
567
771
  }
@@ -597,14 +801,14 @@ function hasExplicitFrameworkImport (source, framework) {
597
801
  */
598
802
  function hasFrameworkOwnership (filename, source, framework, packageName) {
599
803
  const normalized = filename.replaceAll('\\', '/').toLowerCase()
600
- if (framework === 'cucumber') return /^\s*(?:Feature|Rule):/m.test(source)
804
+ if (framework === 'cucumber') return /^[ \t]*(?:Feature|Rule):/m.test(source)
601
805
  if (framework === 'cypress') return /\.cy\./.test(filename) || normalized.includes('/cypress/')
602
806
  if (framework === 'playwright') {
603
807
  return /@playwright\/test/.test(source) || normalized.includes('/playwright/') ||
604
808
  normalized.includes('/e2e/')
605
809
  }
606
- if (framework === 'vitest') return /\bvitest\b/.test(source) || /\b(?:describe|it|test)\s*\(/.test(source)
607
- if (framework === 'jest') return /\bjest\b/.test(source) || /\b(?:describe|it|test)\s*\(/.test(source)
810
+ if (framework === 'vitest') return /\bvitest\b/.test(source) || getStaticTestCount(source) > 0
811
+ if (framework === 'jest') return /\bjest\b/.test(source) || getStaticTestCount(source) > 0
608
812
  if (framework === 'mocha') {
609
813
  return /\bmocha\b/.test(source) || /\bdescribe\s*\(/.test(source) ||
610
814
  packageName === 'mocha'
@@ -643,7 +847,7 @@ function getTestRank (filename, source, projectRoot) {
643
847
  let rank = relative.split('/').length
644
848
  if (/(?:^|\/)(?:test|tests|__tests__|spec)\//.test(relative)) rank -= 20
645
849
  if (/(?:^|[._/-])unit(?:[._/-]|$)/.test(relative)) rank -= 15
646
- if (/(?:e2e|integration|acceptance|browser|conformance|fixtures?)/.test(relative)) rank += 20
850
+ if (/e2e|integration|acceptance|browser|conformance|fixtures?/.test(relative)) rank += 20
647
851
  if (LOCAL_SOCKET_PATTERN.test(source)) rank += 30
648
852
  rank += Math.min(getStaticTestCount(source), 50)
649
853
  return rank
@@ -656,9 +860,12 @@ function getTestRank (filename, source, projectRoot) {
656
860
  * @returns {number} approximate test declaration count
657
861
  */
658
862
  function getStaticTestCount (source) {
659
- return [...source.matchAll(/\b(?:it|test)((?:\.[A-Za-z]+)*)\s*\(/g)]
863
+ const direct = [...source.matchAll(/\b(?:it|test)((?:\.[A-Za-z]+)*)\s*\(\s*(['"`])/g)]
660
864
  .filter(match => !/\.(?:skip|todo)\b/.test(match[1]))
661
- .length
865
+ const parameterized = [...source.matchAll(
866
+ /\b(?:it|test)((?:\.[A-Za-z]+)*)\.each\s*(?:\([^()]*\)|`(?:\\[\s\S]|[^\\`])*`)((?:\.[A-Za-z]+)*)\s*\(\s*(['"`])/g
867
+ )].filter(match => !/\.(?:skip|todo)\b/.test(`${match[1]}${match[2]}`))
868
+ return direct.length + parameterized.length
662
869
  }
663
870
 
664
871
  /**
@@ -823,9 +1030,9 @@ function getTestExtension (framework, representative) {
823
1030
  if (framework === 'cucumber') return '.feature'
824
1031
  if (framework === 'cypress') return cypress.getTestExtension(representative)
825
1032
  if (framework === 'playwright') return playwright.getTestExtension(representative)
826
- const match = /((?:[.-](?:test|spec))\.[cm]?[jt]sx?)$/.exec(representative)
1033
+ const match = /([.-](?:test|spec)\.[cm]?[jt]sx?)$/.exec(representative)
827
1034
  if (match) return match[1]
828
- const moduleExtension = /\.((?:[cm]js|[cm]ts))$/.exec(representative)?.[1]
1035
+ const moduleExtension = /\.([cm]js|[cm]ts)$/.exec(representative)?.[1]
829
1036
  return moduleExtension ? `.test.${moduleExtension}` : '.test.js'
830
1037
  }
831
1038
 
@@ -899,7 +1106,7 @@ function rankCiReviewTargets (files) {
899
1106
  */
900
1107
  function getCiRank (filename) {
901
1108
  const value = filename.toLowerCase()
902
- if (/(?:test|ci|build|unit|integration)/.test(value)) return 0
1109
+ if (/test|ci|build|unit|integration/.test(value)) return 0
903
1110
  return 10
904
1111
  }
905
1112
 
@@ -922,6 +1129,49 @@ function getDetectionPackageJson (repositoryRoot, location) {
922
1129
  }
923
1130
  }
924
1131
 
1132
+ function getFrameworkPackageJson (repositoryRoot, detection) {
1133
+ const preciseLocation = detection.commandLocation ||
1134
+ detection.supportedVersion?.relativePath ||
1135
+ detection.eligibleCommand?.relativePath ||
1136
+ detection.versionLocation
1137
+ if (preciseLocation) return findOwningPackageJson(repositoryRoot, preciseLocation)
1138
+
1139
+ const owners = new Map()
1140
+ if (detection.locations) {
1141
+ for (const location of detection.locations) {
1142
+ const owner = findOwningPackageJson(repositoryRoot, location)
1143
+ if (owner) owners.set(owner.path, owner)
1144
+ }
1145
+ }
1146
+ if (owners.size === 1) return [...owners.values()][0]
1147
+ }
1148
+
1149
+ function findOwningPackageJson (repositoryRoot, location) {
1150
+ let candidate = path.resolve(repositoryRoot, location)
1151
+ if (!isPathInside(repositoryRoot, candidate)) return
1152
+ try {
1153
+ if (!fs.statSync(candidate).isDirectory()) candidate = path.dirname(candidate)
1154
+ } catch {
1155
+ candidate = path.dirname(candidate)
1156
+ }
1157
+
1158
+ while (isPathInside(repositoryRoot, candidate)) {
1159
+ const packageJson = path.join(candidate, 'package.json')
1160
+ const json = readJson(packageJson)
1161
+ if (json) return { json, path: packageJson }
1162
+ if (candidate === repositoryRoot) return
1163
+ candidate = path.dirname(candidate)
1164
+ }
1165
+ }
1166
+
1167
+ function getDetectionPackageLocation (detection) {
1168
+ return detection.commandLocation ||
1169
+ detection.supportedVersion?.relativePath ||
1170
+ detection.eligibleCommand?.relativePath ||
1171
+ detection.versionLocation ||
1172
+ detection.locations?.[0]
1173
+ }
1174
+
925
1175
  /**
926
1176
  * Safely reads a bounded directory.
927
1177
  *