dd-trace 6.4.0 → 6.5.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 (178) hide show
  1. package/README.electron.md +7 -0
  2. package/README.md +17 -0
  3. package/ci/diagnose.js +2100 -0
  4. package/ci/init.js +23 -2
  5. package/ci/runbook.md +198 -0
  6. package/ci/test-optimization-validation/approval-artifacts.js +143 -0
  7. package/ci/test-optimization-validation/approval.js +299 -0
  8. package/ci/test-optimization-validation/artifact-id.js +20 -0
  9. package/ci/test-optimization-validation/bounded-json.js +121 -0
  10. package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
  11. package/ci/test-optimization-validation/ci-discovery.js +181 -0
  12. package/ci/test-optimization-validation/ci-remediation.js +210 -0
  13. package/ci/test-optimization-validation/cli.js +903 -0
  14. package/ci/test-optimization-validation/command-blocker.js +81 -0
  15. package/ci/test-optimization-validation/command-output-policy.js +206 -0
  16. package/ci/test-optimization-validation/command-runner.js +707 -0
  17. package/ci/test-optimization-validation/command-suitability.js +471 -0
  18. package/ci/test-optimization-validation/executable-approval.js +48 -0
  19. package/ci/test-optimization-validation/executable.js +480 -0
  20. package/ci/test-optimization-validation/generated-file-policy.js +63 -0
  21. package/ci/test-optimization-validation/generated-files.js +351 -0
  22. package/ci/test-optimization-validation/generated-verifier.js +196 -0
  23. package/ci/test-optimization-validation/init-probe-preload.js +163 -0
  24. package/ci/test-optimization-validation/init-probe.js +246 -0
  25. package/ci/test-optimization-validation/late-initialization.js +63 -0
  26. package/ci/test-optimization-validation/local-command.js +163 -0
  27. package/ci/test-optimization-validation/manifest-loader.js +133 -0
  28. package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
  29. package/ci/test-optimization-validation/manifest-schema.js +862 -0
  30. package/ci/test-optimization-validation/offline-fixtures.js +327 -0
  31. package/ci/test-optimization-validation/offline-output.js +406 -0
  32. package/ci/test-optimization-validation/payload-normalizer.js +72 -0
  33. package/ci/test-optimization-validation/plan-writer.js +1120 -0
  34. package/ci/test-optimization-validation/preflight-runner.js +114 -0
  35. package/ci/test-optimization-validation/redaction.js +331 -0
  36. package/ci/test-optimization-validation/report-writer.js +1508 -0
  37. package/ci/test-optimization-validation/safe-files.js +203 -0
  38. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
  39. package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
  40. package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
  41. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
  42. package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
  43. package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
  44. package/ci/test-optimization-validation/setup-runner.js +97 -0
  45. package/ci/test-optimization-validation/static-diagnosis.js +159 -0
  46. package/ci/test-optimization-validation/test-output.js +129 -0
  47. package/ci/test-optimization-validation-manifest.schema.json +1 -0
  48. package/ci/validate-test-optimization.js +3 -0
  49. package/ext/exporters.js +1 -0
  50. package/index.d.ts +121 -8
  51. package/init.js +18 -0
  52. package/initialize.mjs +1 -1
  53. package/loader-hook.mjs +28 -18
  54. package/openfeature.d.ts +1 -0
  55. package/openfeature.js +4 -0
  56. package/package.json +10 -7
  57. package/packages/datadog-instrumentations/src/ai.js +8 -2
  58. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  59. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  60. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  61. package/packages/datadog-instrumentations/src/cucumber.js +20 -7
  62. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  63. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  64. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  65. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  66. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  67. package/packages/datadog-instrumentations/src/jest.js +26 -0
  68. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  69. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  70. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  71. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  72. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  73. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  74. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  75. package/packages/datadog-instrumentations/src/next.js +24 -0
  76. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  77. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  78. package/packages/datadog-instrumentations/src/pg.js +2 -2
  79. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  80. package/packages/datadog-instrumentations/src/process.js +3 -0
  81. package/packages/datadog-instrumentations/src/router.js +18 -8
  82. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  83. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  84. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  85. package/packages/datadog-instrumentations/src/ws.js +2 -1
  86. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  87. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  88. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
  89. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  90. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
  91. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  92. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  93. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  94. package/packages/datadog-plugin-graphql/src/execute.js +87 -45
  95. package/packages/datadog-plugin-graphql/src/validate.js +2 -2
  96. package/packages/datadog-plugin-http2/src/client.js +1 -1
  97. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  98. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  99. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  100. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  101. package/packages/datadog-plugin-router/src/index.js +11 -2
  102. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  103. package/packages/datadog-plugin-ws/src/close.js +2 -1
  104. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  105. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  106. package/packages/datadog-plugin-ws/src/server.js +2 -1
  107. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  108. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  109. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  110. package/packages/dd-trace/src/azure_metadata.js +2 -1
  111. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  112. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  113. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  114. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  115. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  116. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  117. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  118. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
  119. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
  120. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
  121. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  122. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  123. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  124. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  125. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  126. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
  127. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  128. package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
  129. package/packages/dd-trace/src/config/helper.js +1 -0
  130. package/packages/dd-trace/src/config/index.js +10 -0
  131. package/packages/dd-trace/src/config/supported-configurations.json +66 -0
  132. package/packages/dd-trace/src/exporter.js +2 -0
  133. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  134. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  135. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  136. package/packages/dd-trace/src/feature-registry.js +10 -3
  137. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  138. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  139. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  140. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  141. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  142. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  143. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  144. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  145. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  146. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  147. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  148. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  149. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  150. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  151. package/packages/dd-trace/src/llmobs/tagger.js +68 -0
  152. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  153. package/packages/dd-trace/src/llmobs/util.js +32 -0
  154. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  155. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  156. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  157. package/packages/dd-trace/src/openfeature/index.js +0 -2
  158. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  159. package/packages/dd-trace/src/openfeature/register.js +16 -27
  160. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  161. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  162. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  163. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  164. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  165. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  166. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  167. package/packages/dd-trace/src/plugins/util/test.js +56 -4
  168. package/packages/dd-trace/src/proxy.js +84 -7
  169. package/packages/dd-trace/src/span_processor.js +5 -0
  170. package/packages/dd-trace/src/standalone/index.js +0 -14
  171. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  172. package/packages/dd-trace/src/util.js +26 -0
  173. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  174. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  175. package/vendor/dist/protobufjs/index.js +1 -1
  176. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  177. package/vendor/dist/shell-quote/index.js +1 -1
  178. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -0,0 +1,299 @@
1
+ 'use strict'
2
+
3
+ const crypto = require('node:crypto')
4
+ const fs = require('node:fs')
5
+ const path = require('node:path')
6
+
7
+ const { getCommandOutputPaths } = require('./command-output-policy')
8
+ const { getCommandExecutionSettings } = require('./command-runner')
9
+ const { bindManifestExecutables, getManifestCommands } = require('./executable')
10
+ const { getFixtureRecipeDigests } = require('./offline-fixtures')
11
+ const { sanitizeForReport } = require('./redaction')
12
+
13
+ const APPROVAL_DIGEST_PATTERN = /^[a-f0-9]{64}$/
14
+ const OFFLINE_FIXTURE_NONCE_PATTERN = /^[a-f0-9]{32}$/
15
+ const PACKAGE_SNAPSHOT_EXCLUDED_NAMES = new Set(['.git', '.nyc_output', 'node_modules'])
16
+
17
+ /**
18
+ * Binds an approval to the exact manifest bytes and live validator options.
19
+ *
20
+ * @param {object} input approval inputs
21
+ * @param {object} input.manifest loaded manifest
22
+ * @param {string} input.out validation output directory
23
+ * @param {string[]} [input.selectedFrameworkIds] selected framework ids
24
+ * @param {string|null} [input.requestedScenario] selected scenario
25
+ * @param {string} input.offlineFixtureNonce random fixture-root nonce shown in the execution plan
26
+ * @param {boolean} [input.keepTempFiles] whether generated files are retained
27
+ * @param {boolean} [input.verbose] whether command progress is printed
28
+ * @returns {string} SHA-256 approval digest
29
+ */
30
+ function getApprovalDigest ({
31
+ manifest,
32
+ out,
33
+ selectedFrameworkIds = [],
34
+ requestedScenario = null,
35
+ offlineFixtureNonce,
36
+ keepTempFiles = false,
37
+ verbose = false,
38
+ }) {
39
+ const approvalJson = serializeApprovalMaterial({
40
+ manifest,
41
+ out,
42
+ selectedFrameworkIds,
43
+ requestedScenario,
44
+ offlineFixtureNonce,
45
+ keepTempFiles,
46
+ verbose,
47
+ })
48
+ return crypto.createHash('sha256').update(approvalJson).digest('hex')
49
+ }
50
+
51
+ /**
52
+ * Builds the complete, inspectable material covered by one approval fingerprint.
53
+ *
54
+ * Secret-like values are redacted for the artifact while the raw manifest digest still binds their exact bytes.
55
+ *
56
+ * @param {object} input approval inputs
57
+ * @param {object} input.manifest loaded validation manifest
58
+ * @param {string} input.out validation output directory
59
+ * @param {string[]} [input.selectedFrameworkIds] selected framework identifiers
60
+ * @param {string|null} [input.requestedScenario] selected validation scenario
61
+ * @param {string} input.offlineFixtureNonce private offline fixture nonce
62
+ * @param {boolean} [input.keepTempFiles] whether generated files remain after validation
63
+ * @param {boolean} [input.verbose] whether verbose validation output is enabled
64
+ * @returns {object} deterministic approval material
65
+ */
66
+ function getApprovalMaterial ({
67
+ manifest,
68
+ out,
69
+ selectedFrameworkIds = [],
70
+ requestedScenario = null,
71
+ offlineFixtureNonce,
72
+ keepTempFiles = false,
73
+ verbose = false,
74
+ }) {
75
+ if (!OFFLINE_FIXTURE_NONCE_PATTERN.test(String(offlineFixtureNonce || ''))) {
76
+ throw new Error('Invalid offline fixture nonce. Render a fresh plan with --print-plan.')
77
+ }
78
+
79
+ const validationDirectory = __dirname
80
+ const packageRoot = path.resolve(validationDirectory, '..', '..')
81
+ const packageJsonPath = path.join(packageRoot, 'package.json')
82
+ const packageMetadata = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
83
+ const packageFiles = getPackageFiles(packageRoot, [
84
+ manifest.__path,
85
+ out,
86
+ path.join(packageRoot, '.junit-tmp'),
87
+ ])
88
+ const executableIdentities = bindManifestExecutables(manifest)
89
+
90
+ return {
91
+ schemaVersion: 1,
92
+ sharingWarning: 'Internal diagnostic material. Review repository paths, commands, and CI metadata before sharing.',
93
+ validator: {
94
+ package: packageMetadata.name,
95
+ version: packageMetadata.version,
96
+ packageRoot,
97
+ coveredFiles: packageFiles.map(filename => ({
98
+ path: path.relative(packageRoot, filename).split(path.sep).join('/'),
99
+ sha256: getFileDigest(filename),
100
+ })),
101
+ },
102
+ manifest: {
103
+ path: path.resolve(manifest.__path),
104
+ sha256: getManifestDigest(manifest),
105
+ },
106
+ selection: {
107
+ frameworks: [...selectedFrameworkIds],
108
+ scenario: requestedScenario,
109
+ },
110
+ validation: {
111
+ outputDirectory: path.resolve(out),
112
+ offlineFixtureNonce,
113
+ keepTemporaryFiles: keepTempFiles,
114
+ verbose,
115
+ },
116
+ fixtureRecipeDigests: getFixtureRecipeDigests({
117
+ frameworks: manifest.frameworks || [],
118
+ selectedFrameworkIds,
119
+ requestedScenario,
120
+ }),
121
+ commands: getManifestCommands(manifest).map(([id, command]) => getApprovalCommand(id, command)),
122
+ generatedFiles: getGeneratedFileMaterial(manifest),
123
+ executables: executableIdentities,
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Serializes approval material using stable formatting suitable for independent SHA-256 tools.
129
+ *
130
+ * @param {object} input approval inputs
131
+ * @returns {string} UTF-8 JSON text ending in one newline
132
+ */
133
+ function serializeApprovalMaterial (input) {
134
+ return `${JSON.stringify(getApprovalMaterial(input), null, 2)}\n`
135
+ }
136
+
137
+ /**
138
+ * Returns every regular file owned by the installed dd-trace package.
139
+ *
140
+ * @param {string} packageRoot installed dd-trace package root
141
+ * @param {string[]} excludedPaths generated files or directories outside the package snapshot
142
+ * @returns {string[]} sorted absolute file paths
143
+ */
144
+ function getPackageFiles (packageRoot, excludedPaths) {
145
+ const files = []
146
+ collectPackageFiles(
147
+ fs.realpathSync(packageRoot),
148
+ excludedPaths.map(resolvePhysicalPath),
149
+ files
150
+ )
151
+ return files.sort()
152
+ }
153
+
154
+ /**
155
+ * Converts one manifest command into its sanitized, execution-relevant approval shape.
156
+ *
157
+ * @param {string} id stable command identifier
158
+ * @param {object} command structured command
159
+ * @returns {object} command approval material
160
+ */
161
+ function getApprovalCommand (id, command) {
162
+ const shape = {
163
+ id,
164
+ required: command.required !== false,
165
+ usesShell: command.usesShell === true,
166
+ cwd: path.resolve(command.cwd),
167
+ environmentMode: 'clean',
168
+ environment: command.env || {},
169
+ ...getCommandExecutionSettings(command),
170
+ outputPaths: getCommandOutputPaths(command),
171
+ }
172
+ if (command.usesShell) {
173
+ shape.shell = command.shell || null
174
+ shape.shellCommand = command.shellCommand
175
+ } else {
176
+ shape.argv = command.argv
177
+ }
178
+ return sanitizeForReport(shape)
179
+ }
180
+
181
+ /**
182
+ * Returns exact generated test source and cleanup policy covered by the manifest digest.
183
+ *
184
+ * @param {object} manifest loaded manifest
185
+ * @returns {object[]} generated file approval material
186
+ */
187
+ function getGeneratedFileMaterial (manifest) {
188
+ const files = []
189
+ for (const framework of manifest.frameworks || []) {
190
+ const strategy = framework.generatedTestStrategy
191
+ for (const file of strategy?.files || []) {
192
+ const content = `${file.contentLines.join('\n')}\n`
193
+ files.push(sanitizeForReport({
194
+ frameworkId: framework.id,
195
+ path: path.resolve(file.path),
196
+ sha256: crypto.createHash('sha256').update(content).digest('hex'),
197
+ content,
198
+ removeAfterValidation: (strategy.cleanupPaths || []).some(cleanupPath => {
199
+ return path.resolve(cleanupPath) === path.resolve(file.path)
200
+ }),
201
+ }))
202
+ }
203
+ }
204
+ return files
205
+ }
206
+
207
+ /**
208
+ * Hashes one covered regular file.
209
+ *
210
+ * @param {string} filename absolute filename
211
+ * @returns {string} lowercase SHA-256 digest
212
+ */
213
+ function getFileDigest (filename) {
214
+ return crypto.createHash('sha256').update(fs.readFileSync(filename)).digest('hex')
215
+ }
216
+
217
+ /**
218
+ * Collects regular package files without following package-internal symbolic links.
219
+ *
220
+ * @param {string} directory current package directory
221
+ * @param {string[]} excludedPaths generated paths omitted from the package snapshot
222
+ * @param {string[]} files collected files
223
+ */
224
+ function collectPackageFiles (directory, excludedPaths, files) {
225
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
226
+ const filename = path.join(directory, entry.name)
227
+ if (PACKAGE_SNAPSHOT_EXCLUDED_NAMES.has(entry.name) || isExcludedPackagePath(filename, excludedPaths)) continue
228
+ if (entry.isDirectory()) {
229
+ collectPackageFiles(filename, excludedPaths, files)
230
+ } else if (entry.isFile()) {
231
+ files.push(filename)
232
+ }
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Checks whether a package path belongs to a generated approval input or output.
238
+ *
239
+ * @param {string} filename package path
240
+ * @param {string[]} excludedPaths generated paths omitted from the package snapshot
241
+ * @returns {boolean} whether the path is excluded
242
+ */
243
+ function isExcludedPackagePath (filename, excludedPaths) {
244
+ return excludedPaths.some(excluded => filename === excluded || filename.startsWith(`${excluded}${path.sep}`))
245
+ }
246
+
247
+ /**
248
+ * Resolves an existing path or its nearest existing ancestor through filesystem aliases.
249
+ *
250
+ * @param {string} filename path that may not exist yet
251
+ * @returns {string} physical path
252
+ */
253
+ function resolvePhysicalPath (filename) {
254
+ const missingSegments = []
255
+ let existingPath = path.resolve(filename)
256
+ while (!fs.existsSync(existingPath)) {
257
+ missingSegments.unshift(path.basename(existingPath))
258
+ const parent = path.dirname(existingPath)
259
+ if (parent === existingPath) return path.resolve(filename)
260
+ existingPath = parent
261
+ }
262
+ return path.join(fs.realpathSync(existingPath), ...missingSegments)
263
+ }
264
+
265
+ /**
266
+ * Validates an approval digest before live validation executes project code.
267
+ *
268
+ * @param {string} digest supplied approval digest
269
+ * @param {object} input approval inputs
270
+ * @returns {void}
271
+ */
272
+ function assertApprovalDigest (digest, input) {
273
+ if (!APPROVAL_DIGEST_PATTERN.test(String(digest || ''))) {
274
+ throw new Error('Invalid approved plan SHA-256. Render a fresh plan with --print-plan.')
275
+ }
276
+
277
+ const expected = getApprovalDigest(input)
278
+ if (digest !== expected) {
279
+ throw new Error(
280
+ 'The validation manifest or execution options changed after approval. ' +
281
+ 'Render a fresh plan with --print-plan and approve that exact plan before live validation.'
282
+ )
283
+ }
284
+ }
285
+
286
+ function getManifestDigest (manifest) {
287
+ if (manifest.__sourceSha256) return manifest.__sourceSha256
288
+
289
+ const serializable = { ...manifest }
290
+ delete serializable.__path
291
+ return crypto.createHash('sha256').update(JSON.stringify(serializable)).digest('hex')
292
+ }
293
+
294
+ module.exports = {
295
+ assertApprovalDigest,
296
+ getApprovalDigest,
297
+ getApprovalMaterial,
298
+ serializeApprovalMaterial,
299
+ }
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const crypto = require('node:crypto')
4
+
5
+ const MAX_PREFIX_LENGTH = 72
6
+
7
+ /**
8
+ * Maps an arbitrary manifest identifier to a bounded, portable, collision-resistant path segment.
9
+ *
10
+ * @param {string} value original identifier
11
+ * @returns {string} artifact path segment
12
+ */
13
+ function getArtifactId (value) {
14
+ const source = String(value)
15
+ const prefix = source.replaceAll(/[^a-zA-Z0-9._-]+/g, '-').slice(0, MAX_PREFIX_LENGTH) || 'framework'
16
+ const digest = crypto.createHash('sha256').update(source).digest('hex').slice(0, 12)
17
+ return `${prefix}-${digest}`
18
+ }
19
+
20
+ module.exports = { getArtifactId }
@@ -0,0 +1,121 @@
1
+ 'use strict'
2
+
3
+ const DEFAULT_MAX_COLLECTION_ENTRIES = 100_000
4
+ const DEFAULT_MAX_NESTING_DEPTH = 128
5
+ const DEFAULT_MAX_STRING_BYTES = 64 * 1024
6
+
7
+ /**
8
+ * Parses JSON only after a no-allocation scan bounds nesting, strings, and collection cardinality.
9
+ *
10
+ * @param {Buffer|string} source encoded JSON
11
+ * @param {object} [options] parser limits
12
+ * @param {string} [options.label] value label used in errors
13
+ * @param {number} [options.maxCollectionEntries] aggregate array/object entries
14
+ * @param {number} [options.maxNestingDepth] maximum container nesting
15
+ * @param {number} [options.maxStringBytes] maximum encoded string bytes
16
+ * @returns {{collectionEntries: number, value: unknown}} parsed value and scanned cardinality
17
+ */
18
+ function parseBoundedJson (source, options = {}) {
19
+ const buffer = Buffer.isBuffer(source) ? source : Buffer.from(String(source))
20
+ const limits = {
21
+ label: options.label || 'JSON',
22
+ maxCollectionEntries: options.maxCollectionEntries || DEFAULT_MAX_COLLECTION_ENTRIES,
23
+ maxNestingDepth: options.maxNestingDepth || DEFAULT_MAX_NESTING_DEPTH,
24
+ maxStringBytes: options.maxStringBytes || DEFAULT_MAX_STRING_BYTES,
25
+ }
26
+ const collectionEntries = scanJson(buffer, limits)
27
+ return {
28
+ collectionEntries,
29
+ value: JSON.parse(buffer.toString('utf8')),
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Scans JSON bytes without creating one object per delimiter.
35
+ *
36
+ * @param {Buffer} buffer JSON bytes
37
+ * @param {object} limits parser limits
38
+ * @returns {number} aggregate collection entries
39
+ */
40
+ function scanJson (buffer, limits) {
41
+ const containerTypes = new Uint8Array(limits.maxNestingDepth)
42
+ const arrayHasEntry = new Uint8Array(limits.maxNestingDepth)
43
+ let collectionEntries = 0
44
+ let depth = 0
45
+ let escaped = false
46
+ let inString = false
47
+ let stringBytes = 0
48
+
49
+ for (const byte of buffer) {
50
+ if (inString) {
51
+ stringBytes++
52
+ if (stringBytes > limits.maxStringBytes) {
53
+ throw new Error(`${limits.label} contains a string larger than ${limits.maxStringBytes} bytes.`)
54
+ }
55
+ if (escaped) {
56
+ escaped = false
57
+ } else if (byte === 0x5C) {
58
+ escaped = true
59
+ } else if (byte === 0x22) {
60
+ inString = false
61
+ }
62
+ continue
63
+ }
64
+
65
+ if (byte === 0x22) {
66
+ inString = true
67
+ stringBytes = 0
68
+ markArrayEntry()
69
+ continue
70
+ }
71
+
72
+ const containerIndex = depth - 1
73
+ if (containerIndex >= 0 && containerTypes[containerIndex] === 1 &&
74
+ arrayHasEntry[containerIndex] === 0 && !isJsonWhitespace(byte) && byte !== 0x5D) {
75
+ arrayHasEntry[containerIndex] = 1
76
+ addEntry()
77
+ }
78
+
79
+ if (byte === 0x5B || byte === 0x7B) {
80
+ if (depth >= limits.maxNestingDepth) {
81
+ throw new Error(`${limits.label} nesting exceeds ${limits.maxNestingDepth}.`)
82
+ }
83
+ containerTypes[depth] = byte === 0x5B ? 1 : 2
84
+ arrayHasEntry[depth] = 0
85
+ depth++
86
+ } else if (byte === 0x5D || byte === 0x7D) {
87
+ if (depth > 0) depth--
88
+ } else if (byte === 0x2C && containerIndex >= 0 && containerTypes[containerIndex] === 1) {
89
+ arrayHasEntry[containerIndex] = 0
90
+ } else if (byte === 0x3A && containerIndex >= 0 && containerTypes[containerIndex] === 2) {
91
+ addEntry()
92
+ }
93
+ }
94
+
95
+ return collectionEntries
96
+
97
+ function markArrayEntry () {
98
+ const index = depth - 1
99
+ if (index < 0 || containerTypes[index] !== 1 || arrayHasEntry[index] !== 0) return
100
+ arrayHasEntry[index] = 1
101
+ addEntry()
102
+ }
103
+
104
+ function addEntry () {
105
+ collectionEntries++
106
+ if (collectionEntries > limits.maxCollectionEntries) {
107
+ throw new Error(`${limits.label} exceeds ${limits.maxCollectionEntries} aggregate collection entries.`)
108
+ }
109
+ }
110
+ }
111
+
112
+ function isJsonWhitespace (byte) {
113
+ return byte === 0x20 || byte === 0x09 || byte === 0x0A || byte === 0x0D
114
+ }
115
+
116
+ module.exports = {
117
+ DEFAULT_MAX_COLLECTION_ENTRIES,
118
+ DEFAULT_MAX_NESTING_DEPTH,
119
+ DEFAULT_MAX_STRING_BYTES,
120
+ parseBoundedJson,
121
+ }
@@ -0,0 +1,83 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ getCommandDetails,
5
+ serializeDisplayCommand,
6
+ } = require('./command-runner')
7
+ const { sanitizeEnv } = require('./redaction')
8
+
9
+ /**
10
+ * Builds the normalized CI command metadata shape shared by reports and UI payloads.
11
+ *
12
+ * @param {object} framework manifest framework entry
13
+ * @returns {object|undefined} CI command candidate context when available
14
+ */
15
+ function buildCiCommandCandidate (framework) {
16
+ const ciWiring = framework.ciWiring || {}
17
+ const command = framework.ciWiringCommand
18
+
19
+ if (!command && !hasCiWiringContext(ciWiring)) return
20
+
21
+ return removeUndefined({
22
+ provider: ciWiring.provider || undefined,
23
+ configFile: ciWiring.configFile || undefined,
24
+ workflow: ciWiring.workflow || undefined,
25
+ job: ciWiring.job || undefined,
26
+ step: ciWiring.step || undefined,
27
+ runner: ciWiring.runner || undefined,
28
+ shell: ciWiring.shell || undefined,
29
+ command: command ? serializeDisplayCommand(command) : ciWiring.command,
30
+ cwd: command?.cwd || ciWiring.workingDirectory,
31
+ whySelected: ciWiring.whySelected || ciWiring.selectionReason || ciWiring.diagnosis,
32
+ replayability: ciWiring.replayability,
33
+ replayBlocker: ciWiring.replayBlocker,
34
+ initialization: ciWiring.initialization,
35
+ env: buildCiEnvSummary(ciWiring, command),
36
+ packageScriptExpansionChain: getFirstArray(
37
+ ciWiring.packageScriptExpansionChain,
38
+ ciWiring.scriptExpansionChain,
39
+ ciWiring.commandExpansion
40
+ ),
41
+ runnerToolChain: getFirstArray(
42
+ ciWiring.runnerToolChain,
43
+ ciWiring.toolChain,
44
+ ciWiring.commandChain
45
+ ),
46
+ setupCommandIds: ciWiring.setupCommandIds,
47
+ unresolved: ciWiring.unresolved,
48
+ commandDetails: command && getCommandDetails(command),
49
+ })
50
+ }
51
+
52
+ function buildCiEnvSummary (ciWiring, command) {
53
+ const summary = removeUndefined({
54
+ workflow: sanitizeEnv(ciWiring.workflowEnv || ciWiring.env?.workflow),
55
+ job: sanitizeEnv(ciWiring.jobEnv || ciWiring.env?.job),
56
+ step: sanitizeEnv(ciWiring.stepEnv || command?.env || ciWiring.env?.step),
57
+ inherited: sanitizeEnv(ciWiring.inheritedEnv),
58
+ })
59
+
60
+ return Object.keys(summary).length > 0 ? summary : undefined
61
+ }
62
+
63
+ function hasCiWiringContext (ciWiring) {
64
+ return Object.keys(ciWiring).length > 0
65
+ }
66
+
67
+ function getFirstArray (...values) {
68
+ for (const value of values) {
69
+ if (Array.isArray(value) && value.length > 0) return value
70
+ }
71
+ }
72
+
73
+ function removeUndefined (object) {
74
+ const result = {}
75
+ for (const [key, value] of Object.entries(object)) {
76
+ if (value !== undefined) result[key] = value
77
+ }
78
+ return result
79
+ }
80
+
81
+ module.exports = {
82
+ buildCiCommandCandidate,
83
+ }
@@ -0,0 +1,181 @@
1
+ 'use strict'
2
+
3
+ const path = require('node:path')
4
+
5
+ const DEFAULT_CI_SEARCHES = [
6
+ '.github/workflows/*.yml',
7
+ '.github/workflows/*.yaml',
8
+ '.gitlab-ci.yml',
9
+ '.gitlab-ci.yaml',
10
+ '.circleci/config.yml',
11
+ '.circleci/config.yaml',
12
+ '.buildkite/pipeline.yml',
13
+ '.buildkite/pipeline.yaml',
14
+ 'bitbucket-pipelines.yml',
15
+ 'bitbucket-pipelines.yaml',
16
+ 'azure-pipelines.yml',
17
+ 'azure-pipelines.yaml',
18
+ '.azure-pipelines/*.yml',
19
+ '.azure-pipelines/*.yaml',
20
+ 'Jenkinsfile',
21
+ ]
22
+
23
+ function annotateCiDiscovery ({ manifest, diagnosis }) {
24
+ manifest.ciDiscovery = buildCiDiscovery({ manifest, diagnosis })
25
+
26
+ if (manifest.ciDiscovery.contradictions.length === 0) return
27
+
28
+ const warnings = Array.isArray(manifest.warnings) ? manifest.warnings : []
29
+ for (const contradiction of manifest.ciDiscovery.contradictions) {
30
+ const warning = `CI discovery contradiction: ${contradiction}`
31
+ if (!warnings.includes(warning)) warnings.push(warning)
32
+ }
33
+ manifest.warnings = warnings
34
+ }
35
+
36
+ function buildCiDiscovery ({ manifest, diagnosis }) {
37
+ const declared = isObject(manifest.ciDiscovery) ? manifest.ciDiscovery : {}
38
+ const staticFound = getStaticWorkflowLocations(diagnosis)
39
+ const declaredFound = normalizeStringArray(declared.found)
40
+ const candidateFound = getManifestWorkflowLocations(manifest)
41
+ const manifestFound = uniqueStrings([...declaredFound, ...candidateFound])
42
+ const searched = normalizeStringArray(declared.searched)
43
+ let method = 'validator-static-diagnosis'
44
+ if (candidateFound.length > 0) method = 'framework-ci-command'
45
+ if (declaredFound.length > 0) method = 'manifest'
46
+ if (typeof declared.method === 'string' && declared.method) method = declared.method
47
+ const found = manifestFound.length > 0 ? manifestFound : staticFound
48
+ const contradictions = getCiDiscoveryContradictions({ manifest, declaredFound: manifestFound, staticFound })
49
+
50
+ return {
51
+ searched: searched.length > 0 ? searched : DEFAULT_CI_SEARCHES,
52
+ found,
53
+ staticFound,
54
+ method,
55
+ warnings: normalizeStringArray(declared.warnings),
56
+ notes: normalizeStringArray(declared.notes),
57
+ contradictions,
58
+ }
59
+ }
60
+
61
+ function getManifestWorkflowLocations (manifest) {
62
+ const root = manifest.repository?.root
63
+ const locations = []
64
+ for (const framework of manifest.frameworks || []) {
65
+ const configFile = framework.ciWiring?.configFile
66
+ if (typeof configFile !== 'string') continue
67
+ if (!root || !path.isAbsolute(configFile)) {
68
+ locations.push(configFile)
69
+ continue
70
+ }
71
+
72
+ const relative = path.relative(root, configFile)
73
+ locations.push(relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)
74
+ ? relative.split(path.sep).join('/')
75
+ : configFile)
76
+ }
77
+ return uniqueStrings(locations)
78
+ }
79
+
80
+ function getFrameworkCiDiscoveryContradiction (framework, manifest) {
81
+ const ciDiscovery = manifest?.ciDiscovery
82
+ if (!ciDiscovery || !Array.isArray(ciDiscovery.staticFound) || ciDiscovery.staticFound.length === 0) return null
83
+ if (!frameworkClaimsNoCi(framework)) return null
84
+
85
+ return {
86
+ reason: 'CI workflow files were found by validator static diagnosis, but this manifest entry says no CI ' +
87
+ 'workflow was found. The manifest cannot support a "no CI workflow found" conclusion.',
88
+ recommendation: 'Inspect the discovered CI files with hidden-directory-aware discovery, then update ciWiring, ' +
89
+ 'ciWiringCommand, omittedTestCommands, notes, or unresolved blockers before rerunning live validation.',
90
+ ciDiscovery,
91
+ }
92
+ }
93
+
94
+ function manifestHasCiDiscoveryContradiction (manifest) {
95
+ return Array.isArray(manifest?.ciDiscovery?.contradictions) &&
96
+ manifest.ciDiscovery.contradictions.length > 0
97
+ }
98
+
99
+ function getCiDiscoveryContradictions ({ manifest, declaredFound, staticFound }) {
100
+ if (staticFound.length === 0) return []
101
+
102
+ const contradictions = []
103
+ if (isObject(manifest.ciDiscovery) && declaredFound.length === 0) {
104
+ contradictions.push(
105
+ `manifest ciDiscovery.found is empty, but static diagnosis found ${formatList(staticFound)}`
106
+ )
107
+ }
108
+
109
+ for (const framework of manifest.frameworks || []) {
110
+ if (!frameworkClaimsNoCi(framework)) continue
111
+ contradictions.push(
112
+ `framework ${framework.id || '<unknown>'} records no CI workflow, but static diagnosis found ` +
113
+ formatList(staticFound)
114
+ )
115
+ }
116
+
117
+ return contradictions
118
+ }
119
+
120
+ function frameworkClaimsNoCi (framework) {
121
+ const ciWiring = framework?.ciWiring
122
+ if (!isObject(ciWiring)) return false
123
+ if (ciWiring.provider === 'none') return true
124
+
125
+ return textClaimsNoCi([
126
+ ciWiring.diagnosis,
127
+ ...(Array.isArray(ciWiring.unresolved) ? ciWiring.unresolved : []),
128
+ ...(Array.isArray(framework.notes) ? framework.notes : []),
129
+ ])
130
+ }
131
+
132
+ function textClaimsNoCi (values) {
133
+ return values.some(value => {
134
+ if (typeof value !== 'string') return false
135
+ return /no .*ci .*workflow/i.test(value) ||
136
+ /no .*ci .*configuration/i.test(value) ||
137
+ /no github actions.*gitlab.*circleci.*jenkins/i.test(value)
138
+ })
139
+ }
140
+
141
+ function getStaticWorkflowLocations (diagnosis) {
142
+ const results = Array.isArray(diagnosis?.results) ? diagnosis.results : []
143
+ const locations = []
144
+ const seen = new Set()
145
+
146
+ for (const result of results) {
147
+ if (result.title !== 'CI workflow files found' || !Array.isArray(result.locations)) continue
148
+ for (const location of result.locations) {
149
+ if (typeof location !== 'string' || seen.has(location)) continue
150
+ seen.add(location)
151
+ locations.push(location)
152
+ }
153
+ }
154
+
155
+ return locations
156
+ }
157
+
158
+ function normalizeStringArray (value) {
159
+ if (!Array.isArray(value)) return []
160
+ return value.filter(item => typeof item === 'string')
161
+ }
162
+
163
+ function uniqueStrings (values) {
164
+ return [...new Set(values.filter(value => typeof value === 'string' && value !== ''))]
165
+ }
166
+
167
+ function formatList (values) {
168
+ return values.map(value => `"${value}"`).join(', ')
169
+ }
170
+
171
+ function isObject (value) {
172
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
173
+ }
174
+
175
+ module.exports = {
176
+ annotateCiDiscovery,
177
+ buildCiDiscovery,
178
+ getFrameworkCiDiscoveryContradiction,
179
+ getStaticWorkflowLocations,
180
+ manifestHasCiDiscoveryContradiction,
181
+ }