sdl-mcp 0.12.2 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/README.md +10 -0
  2. package/config/sdlmcp.config.example.json +53 -8
  3. package/config/sdlmcp.config.schema.json +530 -106
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/benchmark/external-manifest.d.ts +162 -0
  6. package/dist/benchmark/external-manifest.d.ts.map +1 -0
  7. package/dist/benchmark/external-manifest.js +336 -0
  8. package/dist/benchmark/external-manifest.js.map +1 -0
  9. package/dist/benchmark/external-runner.d.ts +100 -0
  10. package/dist/benchmark/external-runner.d.ts.map +1 -0
  11. package/dist/benchmark/external-runner.js +1171 -0
  12. package/dist/benchmark/external-runner.js.map +1 -0
  13. package/dist/benchmark/output-file.d.ts +3 -0
  14. package/dist/benchmark/output-file.d.ts.map +1 -0
  15. package/dist/benchmark/output-file.js +8 -0
  16. package/dist/benchmark/output-file.js.map +1 -0
  17. package/dist/cli/argParsing.d.ts.map +1 -1
  18. package/dist/cli/argParsing.js +6 -0
  19. package/dist/cli/argParsing.js.map +1 -1
  20. package/dist/cli/commands/benchmark.d.ts.map +1 -1
  21. package/dist/cli/commands/benchmark.js +3 -2
  22. package/dist/cli/commands/benchmark.js.map +1 -1
  23. package/dist/cli/commands/index.d.ts +1 -0
  24. package/dist/cli/commands/index.d.ts.map +1 -1
  25. package/dist/cli/commands/index.js +13 -7
  26. package/dist/cli/commands/index.js.map +1 -1
  27. package/dist/cli/commands/init.d.ts +5 -1
  28. package/dist/cli/commands/init.d.ts.map +1 -1
  29. package/dist/cli/commands/init.js +73 -3
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/serve.d.ts.map +1 -1
  32. package/dist/cli/commands/serve.js +2 -0
  33. package/dist/cli/commands/serve.js.map +1 -1
  34. package/dist/cli/index.js +2 -0
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/transport/http.d.ts +1 -35
  37. package/dist/cli/transport/http.d.ts.map +1 -1
  38. package/dist/cli/transport/http.js +151 -175
  39. package/dist/cli/transport/http.js.map +1 -1
  40. package/dist/cli/types.d.ts +1 -0
  41. package/dist/cli/types.d.ts.map +1 -1
  42. package/dist/config/constants.d.ts +30 -0
  43. package/dist/config/constants.d.ts.map +1 -1
  44. package/dist/config/constants.js +30 -0
  45. package/dist/config/constants.js.map +1 -1
  46. package/dist/config/types.d.ts +53 -1
  47. package/dist/config/types.d.ts.map +1 -1
  48. package/dist/config/types.js +62 -14
  49. package/dist/config/types.js.map +1 -1
  50. package/dist/db/ladybug-batching.d.ts +1 -0
  51. package/dist/db/ladybug-batching.d.ts.map +1 -1
  52. package/dist/db/ladybug-batching.js +1 -0
  53. package/dist/db/ladybug-batching.js.map +1 -1
  54. package/dist/db/ladybug-core.d.ts +3 -0
  55. package/dist/db/ladybug-core.d.ts.map +1 -1
  56. package/dist/db/ladybug-core.js +72 -2
  57. package/dist/db/ladybug-core.js.map +1 -1
  58. package/dist/db/ladybug-embeddings.d.ts +6 -1
  59. package/dist/db/ladybug-embeddings.d.ts.map +1 -1
  60. package/dist/db/ladybug-embeddings.js +6 -1
  61. package/dist/db/ladybug-embeddings.js.map +1 -1
  62. package/dist/db/ladybug-file-summaries.d.ts.map +1 -1
  63. package/dist/db/ladybug-file-summaries.js +23 -0
  64. package/dist/db/ladybug-file-summaries.js.map +1 -1
  65. package/dist/db/ladybug-queries.js +1 -1
  66. package/dist/db/ladybug-queries.js.map +1 -1
  67. package/dist/db/migrations/index.d.ts.map +1 -1
  68. package/dist/db/migrations/index.js +2 -0
  69. package/dist/db/migrations/index.js.map +1 -1
  70. package/dist/db/migrations/m007-copy-embeddings-to-symbol.d.ts +5 -9
  71. package/dist/db/migrations/m007-copy-embeddings-to-symbol.d.ts.map +1 -1
  72. package/dist/db/migrations/m007-copy-embeddings-to-symbol.js +24 -126
  73. package/dist/db/migrations/m007-copy-embeddings-to-symbol.js.map +1 -1
  74. package/dist/db/migrations/m021-remediate-symbol-embeddings.d.ts +5 -0
  75. package/dist/db/migrations/m021-remediate-symbol-embeddings.d.ts.map +1 -0
  76. package/dist/db/migrations/m021-remediate-symbol-embeddings.js +7 -0
  77. package/dist/db/migrations/m021-remediate-symbol-embeddings.js.map +1 -0
  78. package/dist/db/migrations/symbol-embedding-remediation.d.ts +57 -0
  79. package/dist/db/migrations/symbol-embedding-remediation.d.ts.map +1 -0
  80. package/dist/db/migrations/symbol-embedding-remediation.js +335 -0
  81. package/dist/db/migrations/symbol-embedding-remediation.js.map +1 -0
  82. package/dist/graph/layout/force-layout.d.ts +4 -0
  83. package/dist/graph/layout/force-layout.d.ts.map +1 -0
  84. package/dist/graph/layout/force-layout.js +127 -0
  85. package/dist/graph/layout/force-layout.js.map +1 -0
  86. package/dist/graph/layout/layout-service.d.ts +37 -0
  87. package/dist/graph/layout/layout-service.d.ts.map +1 -0
  88. package/dist/graph/layout/layout-service.js +172 -0
  89. package/dist/graph/layout/layout-service.js.map +1 -0
  90. package/dist/graph/layout/native-engine.d.ts +9 -0
  91. package/dist/graph/layout/native-engine.d.ts.map +1 -0
  92. package/dist/graph/layout/native-engine.js +59 -0
  93. package/dist/graph/layout/native-engine.js.map +1 -0
  94. package/dist/graph/layout/prng.d.ts +3 -0
  95. package/dist/graph/layout/prng.d.ts.map +1 -0
  96. package/dist/graph/layout/prng.js +19 -0
  97. package/dist/graph/layout/prng.js.map +1 -0
  98. package/dist/graph/layout/serializer.d.ts +3 -0
  99. package/dist/graph/layout/serializer.d.ts.map +1 -0
  100. package/dist/graph/layout/serializer.js +20 -0
  101. package/dist/graph/layout/serializer.js.map +1 -0
  102. package/dist/graph/layout/types.d.ts +30 -0
  103. package/dist/graph/layout/types.d.ts.map +1 -0
  104. package/dist/graph/layout/types.js +2 -0
  105. package/dist/graph/layout/types.js.map +1 -0
  106. package/dist/indexer/embeddings.d.ts +7 -0
  107. package/dist/indexer/embeddings.d.ts.map +1 -1
  108. package/dist/indexer/embeddings.js +44 -4
  109. package/dist/indexer/embeddings.js.map +1 -1
  110. package/dist/indexer/file-summary-embeddings.d.ts +12 -0
  111. package/dist/indexer/file-summary-embeddings.d.ts.map +1 -1
  112. package/dist/indexer/file-summary-embeddings.js +83 -21
  113. package/dist/indexer/file-summary-embeddings.js.map +1 -1
  114. package/dist/indexer/fileWalker.d.ts.map +1 -1
  115. package/dist/indexer/fileWalker.js +2 -1
  116. package/dist/indexer/fileWalker.js.map +1 -1
  117. package/dist/indexer/metrics-updater.d.ts.map +1 -1
  118. package/dist/indexer/metrics-updater.js +27 -4
  119. package/dist/indexer/metrics-updater.js.map +1 -1
  120. package/dist/indexer/watcher.d.ts.map +1 -1
  121. package/dist/indexer/watcher.js +2 -1
  122. package/dist/indexer/watcher.js.map +1 -1
  123. package/dist/mcp/context-response-projection.d.ts.map +1 -1
  124. package/dist/mcp/context-response-projection.js +49 -13
  125. package/dist/mcp/context-response-projection.js.map +1 -1
  126. package/dist/mcp/tools/search-edit/planner.d.ts.map +1 -1
  127. package/dist/mcp/tools/search-edit/planner.js +10 -3
  128. package/dist/mcp/tools/search-edit/planner.js.map +1 -1
  129. package/dist/observability/event-tap.d.ts +2 -1
  130. package/dist/observability/event-tap.d.ts.map +1 -1
  131. package/dist/observability/event-tap.js.map +1 -1
  132. package/dist/observability/service.d.ts +11 -4
  133. package/dist/observability/service.d.ts.map +1 -1
  134. package/dist/observability/service.js +83 -13
  135. package/dist/observability/service.js.map +1 -1
  136. package/dist/observability/types.d.ts +48 -2
  137. package/dist/observability/types.d.ts.map +1 -1
  138. package/dist/ui/.tsbuildinfo +1 -0
  139. package/dist/ui/vendor/fflate.js +2692 -0
  140. package/dist/ui/vendor/jsm/controls/ArcballControls.js +3543 -0
  141. package/dist/ui/vendor/jsm/controls/DragControls.js +452 -0
  142. package/dist/ui/vendor/jsm/controls/FirstPersonControls.js +488 -0
  143. package/dist/ui/vendor/jsm/controls/FlyControls.js +384 -0
  144. package/dist/ui/vendor/jsm/controls/MapControls.js +116 -0
  145. package/dist/ui/vendor/jsm/controls/OrbitControls.js +1963 -0
  146. package/dist/ui/vendor/jsm/controls/PointerLockControls.js +264 -0
  147. package/dist/ui/vendor/jsm/controls/TrackballControls.js +1001 -0
  148. package/dist/ui/vendor/jsm/controls/TransformControls.js +2003 -0
  149. package/dist/ui/vendor/jsm/loaders/3DMLoader.js +1836 -0
  150. package/dist/ui/vendor/jsm/loaders/3MFLoader.js +1621 -0
  151. package/dist/ui/vendor/jsm/loaders/AMFLoader.js +541 -0
  152. package/dist/ui/vendor/jsm/loaders/BVHLoader.js +484 -0
  153. package/dist/ui/vendor/jsm/loaders/ColladaLoader.js +153 -0
  154. package/dist/ui/vendor/jsm/loaders/DDSLoader.js +385 -0
  155. package/dist/ui/vendor/jsm/loaders/DRACOLoader.js +772 -0
  156. package/dist/ui/vendor/jsm/loaders/EXRLoader.js +3395 -0
  157. package/dist/ui/vendor/jsm/loaders/FBXLoader.js +4580 -0
  158. package/dist/ui/vendor/jsm/loaders/FontLoader.js +260 -0
  159. package/dist/ui/vendor/jsm/loaders/GCodeLoader.js +318 -0
  160. package/dist/ui/vendor/jsm/loaders/GLTFLoader.js +4860 -0
  161. package/dist/ui/vendor/jsm/loaders/HDRCubeTextureLoader.js +164 -0
  162. package/dist/ui/vendor/jsm/loaders/HDRLoader.js +463 -0
  163. package/dist/ui/vendor/jsm/loaders/IESLoader.js +379 -0
  164. package/dist/ui/vendor/jsm/loaders/KMZLoader.js +163 -0
  165. package/dist/ui/vendor/jsm/loaders/KTX2Loader.js +1276 -0
  166. package/dist/ui/vendor/jsm/loaders/KTXLoader.js +197 -0
  167. package/dist/ui/vendor/jsm/loaders/LDrawLoader.js +2510 -0
  168. package/dist/ui/vendor/jsm/loaders/LUT3dlLoader.js +205 -0
  169. package/dist/ui/vendor/jsm/loaders/LUTCubeLoader.js +190 -0
  170. package/dist/ui/vendor/jsm/loaders/LUTImageLoader.js +190 -0
  171. package/dist/ui/vendor/jsm/loaders/LWOLoader.js +1051 -0
  172. package/dist/ui/vendor/jsm/loaders/LottieLoader.js +130 -0
  173. package/dist/ui/vendor/jsm/loaders/MD2Loader.js +435 -0
  174. package/dist/ui/vendor/jsm/loaders/MDDLoader.js +147 -0
  175. package/dist/ui/vendor/jsm/loaders/MTLLoader.js +593 -0
  176. package/dist/ui/vendor/jsm/loaders/MaterialXLoader.js +1126 -0
  177. package/dist/ui/vendor/jsm/loaders/NRRDLoader.js +718 -0
  178. package/dist/ui/vendor/jsm/loaders/OBJLoader.js +955 -0
  179. package/dist/ui/vendor/jsm/loaders/PCDLoader.js +620 -0
  180. package/dist/ui/vendor/jsm/loaders/PDBLoader.js +272 -0
  181. package/dist/ui/vendor/jsm/loaders/PLYLoader.js +968 -0
  182. package/dist/ui/vendor/jsm/loaders/PVRLoader.js +270 -0
  183. package/dist/ui/vendor/jsm/loaders/RGBELoader.js +18 -0
  184. package/dist/ui/vendor/jsm/loaders/STLLoader.js +421 -0
  185. package/dist/ui/vendor/jsm/loaders/SVGLoader.js +3319 -0
  186. package/dist/ui/vendor/jsm/loaders/TDSLoader.js +1142 -0
  187. package/dist/ui/vendor/jsm/loaders/TGALoader.js +538 -0
  188. package/dist/ui/vendor/jsm/loaders/TIFFLoader.js +59 -0
  189. package/dist/ui/vendor/jsm/loaders/TTFLoader.js +261 -0
  190. package/dist/ui/vendor/jsm/loaders/USDLoader.js +303 -0
  191. package/dist/ui/vendor/jsm/loaders/USDZLoader.js +16 -0
  192. package/dist/ui/vendor/jsm/loaders/UltraHDRLoader.js +755 -0
  193. package/dist/ui/vendor/jsm/loaders/VOXLoader.js +919 -0
  194. package/dist/ui/vendor/jsm/loaders/VRMLLoader.js +3646 -0
  195. package/dist/ui/vendor/jsm/loaders/VTKLoader.js +1293 -0
  196. package/dist/ui/vendor/jsm/loaders/XYZLoader.js +143 -0
  197. package/dist/ui/vendor/jsm/loaders/collada/ColladaComposer.js +3044 -0
  198. package/dist/ui/vendor/jsm/loaders/collada/ColladaParser.js +1977 -0
  199. package/dist/ui/vendor/jsm/loaders/lwo/IFFParser.js +1217 -0
  200. package/dist/ui/vendor/jsm/loaders/lwo/LWO2Parser.js +414 -0
  201. package/dist/ui/vendor/jsm/loaders/lwo/LWO3Parser.js +373 -0
  202. package/dist/ui/vendor/jsm/loaders/usd/USDAParser.js +901 -0
  203. package/dist/ui/vendor/jsm/loaders/usd/USDCParser.js +1878 -0
  204. package/dist/ui/vendor/jsm/loaders/usd/USDComposer.js +4627 -0
  205. package/dist/ui/vendor/jsm/utils/BufferGeometryUtils.js +1501 -0
  206. package/dist/ui/vendor/jsm/utils/CameraUtils.js +82 -0
  207. package/dist/ui/vendor/jsm/utils/ColorUtils.js +76 -0
  208. package/dist/ui/vendor/jsm/utils/GeometryCompressionUtils.js +547 -0
  209. package/dist/ui/vendor/jsm/utils/GeometryUtils.js +226 -0
  210. package/dist/ui/vendor/jsm/utils/LDrawUtils.js +211 -0
  211. package/dist/ui/vendor/jsm/utils/SceneOptimizer.js +458 -0
  212. package/dist/ui/vendor/jsm/utils/SceneUtils.js +363 -0
  213. package/dist/ui/vendor/jsm/utils/ShadowMapViewer.js +244 -0
  214. package/dist/ui/vendor/jsm/utils/ShadowMapViewerGPU.js +233 -0
  215. package/dist/ui/vendor/jsm/utils/SkeletonUtils.js +496 -0
  216. package/dist/ui/vendor/jsm/utils/SortUtils.js +175 -0
  217. package/dist/ui/vendor/jsm/utils/UVsDebug.js +173 -0
  218. package/dist/ui/vendor/jsm/utils/WebGLTextureUtils.js +115 -0
  219. package/dist/ui/vendor/jsm/utils/WebGPUTextureUtils.js +81 -0
  220. package/dist/ui/vendor/jsm/utils/WorkerPool.js +167 -0
  221. package/dist/ui/vendor/three.core.min.js +6 -0
  222. package/dist/ui/vendor/three.module.min.js +6 -0
  223. package/dist/ui/viewer/ambient.js +39 -0
  224. package/dist/ui/viewer/api.js +93 -0
  225. package/dist/ui/viewer/chrome.js +36 -0
  226. package/dist/ui/viewer/edges.js +33 -0
  227. package/dist/ui/viewer/index.html +32 -0
  228. package/dist/ui/viewer/inspector.js +23 -0
  229. package/dist/ui/viewer/lenses/activity.js +24 -0
  230. package/dist/ui/viewer/lenses/community.js +5 -0
  231. package/dist/ui/viewer/lenses/edges.js +4 -0
  232. package/dist/ui/viewer/lenses/impact.js +6 -0
  233. package/dist/ui/viewer/lenses/search.js +69 -0
  234. package/dist/ui/viewer/lod.js +100 -0
  235. package/dist/ui/viewer/main.js +92 -0
  236. package/dist/ui/viewer/picking.js +45 -0
  237. package/dist/ui/viewer/scene.js +85 -0
  238. package/dist/ui/viewer/skins/default-skin.js +15 -0
  239. package/dist/ui/viewer/skins/loader.js +44 -0
  240. package/dist/ui/viewer/skins/manifest-schema.js +30 -0
  241. package/dist/ui/viewer/state.js +44 -0
  242. package/dist/ui/viewer/theme.js +23 -0
  243. package/dist/ui/viewer/universe.js +151 -0
  244. package/dist/ui/viewer/viewer.css +85 -0
  245. package/dist/util/safeRegex.d.ts.map +1 -1
  246. package/dist/util/safeRegex.js +221 -68
  247. package/dist/util/safeRegex.js.map +1 -1
  248. package/dist/viewer/legacy-graph.d.ts +40 -0
  249. package/dist/viewer/legacy-graph.d.ts.map +1 -0
  250. package/dist/viewer/legacy-graph.js +152 -0
  251. package/dist/viewer/legacy-graph.js.map +1 -0
  252. package/dist/viewer/routes.d.ts +10 -0
  253. package/dist/viewer/routes.d.ts.map +1 -0
  254. package/dist/viewer/routes.js +221 -0
  255. package/dist/viewer/routes.js.map +1 -0
  256. package/dist/viewer/service.d.ts +44 -0
  257. package/dist/viewer/service.d.ts.map +1 -0
  258. package/dist/viewer/service.js +160 -0
  259. package/dist/viewer/service.js.map +1 -0
  260. package/dist/viewer/types.d.ts +63 -0
  261. package/dist/viewer/types.d.ts.map +1 -0
  262. package/dist/viewer/types.js +2 -0
  263. package/dist/viewer/types.js.map +1 -0
  264. package/dist/viewer/viewer-config.d.ts +12 -0
  265. package/dist/viewer/viewer-config.d.ts.map +1 -0
  266. package/dist/viewer/viewer-config.js +36 -0
  267. package/dist/viewer/viewer-config.js.map +1 -0
  268. package/package.json +18 -9
  269. package/templates/SDL.md +19 -0
  270. package/templates/claude-code.json +8 -43
  271. package/dist/ui/graph.css +0 -108
  272. package/dist/ui/graph.d.ts +0 -2
  273. package/dist/ui/graph.d.ts.map +0 -1
  274. package/dist/ui/graph.html +0 -52
  275. package/dist/ui/graph.js +0 -259
  276. package/dist/ui/graph.js.map +0 -1
@@ -0,0 +1,1171 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import fs, { existsSync, readdirSync } from "node:fs";
4
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { buildExternalBenchmarkResults, compareArtifactPath, fingerprintDirectory, fingerprintFiles, hashSerializedManifest, normalizeArtifactPath, serializeExternalBenchmarkResults, serializeRunManifest, sha256File, } from "./external-manifest.js";
7
+ import { writeUtf8Output } from "./output-file.js";
8
+ function familyEntries(primaryPath) {
9
+ const directory = dirname(primaryPath);
10
+ const primaryName = basename(primaryPath);
11
+ if (!existsSync(directory))
12
+ return [];
13
+ return readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.name === primaryName || entry.name.startsWith(primaryName + "."));
14
+ }
15
+ export function collectDbFamilyFiles(primaryPath) {
16
+ const directory = dirname(primaryPath);
17
+ return familyEntries(primaryPath)
18
+ .filter((entry) => entry.isFile())
19
+ .map((entry) => join(directory, entry.name))
20
+ .sort((left, right) => compareArtifactPath(left.replaceAll("\\", "/"), right.replaceAll("\\", "/")));
21
+ }
22
+ export function assertDbFamilyAbsent(primaryPath) {
23
+ const existing = familyEntries(primaryPath);
24
+ if (existing.length > 0) {
25
+ throw new Error("Database family must be absent: " +
26
+ existing
27
+ .map((entry) => join(dirname(primaryPath), entry.name))
28
+ .sort()
29
+ .join(", "));
30
+ }
31
+ }
32
+ export function fingerprintDbFamily(primaryPath) {
33
+ const entries = familyEntries(primaryPath);
34
+ const nonRegular = entries.find((entry) => !entry.isFile());
35
+ if (nonRegular !== undefined) {
36
+ throw new Error("Database family entry must be a regular file: " + nonRegular.name);
37
+ }
38
+ return fingerprintFiles(dirname(primaryPath), entries.map((entry) => entry.name));
39
+ }
40
+ export function prepareColdRepeat(artifactRoot, repeat) {
41
+ if (!Number.isInteger(repeat) || repeat < 1) {
42
+ throw new Error("Repeat must be a positive integer");
43
+ }
44
+ const filename = `repeat-${String(repeat).padStart(3, "0")}.lbug`;
45
+ const graphDbPath = "db/" + filename;
46
+ assertDbFamilyAbsent(join(artifactRoot, "db", filename));
47
+ return { graphDbPath, initialDbFiles: [] };
48
+ }
49
+ export function stageWarmSnapshot(sourcePrimaryPath, artifactRoot) {
50
+ const before = fingerprintDbFamily(sourcePrimaryPath);
51
+ const sourceName = basename(sourcePrimaryPath);
52
+ if (!before.files.some(({ path }) => path === sourceName)) {
53
+ throw new Error("Warm source primary database is missing: " + sourcePrimaryPath);
54
+ }
55
+ const warmDirectory = join(artifactRoot, "inputs", "warm-db");
56
+ fs.mkdirSync(warmDirectory);
57
+ const files = before.files.map(({ path, sha256 }) => {
58
+ const destinationName = "repository.lbug" + path.slice(sourceName.length);
59
+ const destination = join(warmDirectory, destinationName);
60
+ fs.copyFileSync(join(dirname(sourcePrimaryPath), path), destination, fs.constants.COPYFILE_EXCL);
61
+ if (sha256File(destination) !== sha256) {
62
+ throw new Error("Staged warm input hash mismatch: " + destinationName);
63
+ }
64
+ return {
65
+ path: normalizeArtifactPath("inputs/warm-db/" + destinationName),
66
+ sha256,
67
+ };
68
+ });
69
+ const after = fingerprintDbFamily(sourcePrimaryPath);
70
+ if (JSON.stringify(after) !== JSON.stringify(before)) {
71
+ throw new Error("warm source changed during staging");
72
+ }
73
+ return {
74
+ files: files.sort((left, right) => compareArtifactPath(left.path, right.path)),
75
+ };
76
+ }
77
+ export function prepareWarmRepeat(artifactRoot, repeat, snapshot) {
78
+ const prepared = prepareColdRepeat(artifactRoot, repeat);
79
+ const stagedPrefix = "inputs/warm-db/repository.lbug";
80
+ const staged = snapshot.files
81
+ .map((file) => ({
82
+ path: normalizeArtifactPath(file.path),
83
+ sha256: file.sha256,
84
+ }))
85
+ .sort((left, right) => compareArtifactPath(left.path, right.path));
86
+ for (const file of staged) {
87
+ if (file.path !== stagedPrefix &&
88
+ !file.path.startsWith(stagedPrefix + ".")) {
89
+ throw new Error("Invalid staged warm input path: " + file.path);
90
+ }
91
+ if (sha256File(join(artifactRoot, ...file.path.split("/"))) !== file.sha256) {
92
+ throw new Error("staged warm input hash changed: " + file.path);
93
+ }
94
+ }
95
+ const initialDbFiles = staged.map((file) => {
96
+ const destinationPath = prepared.graphDbPath + file.path.slice(stagedPrefix.length);
97
+ const source = join(artifactRoot, ...file.path.split("/"));
98
+ const destination = join(artifactRoot, ...destinationPath.split("/"));
99
+ fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL);
100
+ if (sha256File(destination) !== file.sha256) {
101
+ throw new Error("copied warm input hash mismatch: " + destinationPath);
102
+ }
103
+ return {
104
+ sourcePath: file.path,
105
+ destinationPath,
106
+ sha256: file.sha256,
107
+ };
108
+ });
109
+ return { ...prepared, initialDbFiles };
110
+ }
111
+ function platformPath(path) {
112
+ return process.platform === "win32" ? path.toLowerCase() : path;
113
+ }
114
+ export function canonicalizePath(input) {
115
+ const absolute = resolve(input);
116
+ if (existsSync(absolute)) {
117
+ return platformPath(fs.realpathSync.native(absolute));
118
+ }
119
+ return platformPath(join(fs.realpathSync.native(dirname(absolute)), basename(absolute)));
120
+ }
121
+ export function assertCanonicalPathEqual(left, right) {
122
+ if (canonicalizePath(left) !== canonicalizePath(right)) {
123
+ throw new Error("Canonical paths do not match");
124
+ }
125
+ }
126
+ export function assertCanonicalPathContained(rootPath, candidatePath) {
127
+ const root = canonicalizePath(rootPath);
128
+ const candidate = canonicalizePath(candidatePath);
129
+ const rel = relative(root, candidate);
130
+ if (rel.startsWith("..") || isAbsolute(rel)) {
131
+ throw new Error("Canonical path must be contained in the artifact root");
132
+ }
133
+ }
134
+ export function buildChildEnvironment(inherited, configPath, graphDbPath) {
135
+ const env = { ...inherited };
136
+ delete env.SDL_GRAPH_DB_DIR;
137
+ delete env.SDL_GRAPH_DB_PATH;
138
+ delete env.SDL_DB_PATH;
139
+ delete env.SDL_CONFIG;
140
+ env.SDL_CONFIG = configPath;
141
+ env.SDL_GRAPH_DB_PATH = graphDbPath;
142
+ env.SDL_DB_PATH = graphDbPath;
143
+ return env;
144
+ }
145
+ export function assertChildEnvironment(env, artifactRoot, manifest, repeat, usedDbPaths) {
146
+ if (env.SDL_GRAPH_DB_DIR !== undefined) {
147
+ throw new Error("SDL_GRAPH_DB_DIR must be absent");
148
+ }
149
+ const configPath = env.SDL_CONFIG;
150
+ const graphDbPath = env.SDL_GRAPH_DB_PATH;
151
+ if (configPath === undefined || !fs.statSync(configPath).isFile()) {
152
+ throw new Error("SDL_CONFIG must name the generated config");
153
+ }
154
+ if (graphDbPath === undefined || env.SDL_DB_PATH !== graphDbPath) {
155
+ throw new Error("SDL graph database paths must be present and equal");
156
+ }
157
+ const expectedConfig = join(artifactRoot, ...manifest.inputs.configPath.split("/"));
158
+ const expectedDb = join(artifactRoot, ...repeat.graphDbPath.split("/"));
159
+ assertCanonicalPathContained(artifactRoot, configPath);
160
+ assertCanonicalPathEqual(configPath, expectedConfig);
161
+ assertCanonicalPathContained(artifactRoot, graphDbPath);
162
+ assertCanonicalPathEqual(graphDbPath, expectedDb);
163
+ const canonicalDb = canonicalizePath(graphDbPath);
164
+ if (usedDbPaths.has(canonicalDb)) {
165
+ throw new Error("Graph database path was already used");
166
+ }
167
+ usedDbPaths.add(canonicalDb);
168
+ }
169
+ const BASELINE_METRICS = [
170
+ "indexTimePerFile",
171
+ "indexTimePerSymbol",
172
+ "symbolsPerFile",
173
+ "edgesPerSymbol",
174
+ "graphConnectivity",
175
+ "exportedSymbolRatio",
176
+ "sliceBuildTimeMs",
177
+ "avgSkeletonTimeMs",
178
+ "avgCardTokens",
179
+ "avgSkeletonTokens",
180
+ ];
181
+ function record(value, label) {
182
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
183
+ throw new Error(label + " must be an object");
184
+ }
185
+ return value;
186
+ }
187
+ function requireExactKeys(value, keys, label) {
188
+ const actual = Object.keys(value).sort();
189
+ const expected = [...keys].sort();
190
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
191
+ throw new Error(label + " must contain exactly: " + expected.join(", "));
192
+ }
193
+ }
194
+ export function validateBaselineV1(input, expectedRepoId) {
195
+ const baseline = record(input, "Baseline");
196
+ if (baseline.formatVersion !== 1) {
197
+ throw new Error("Baseline formatVersion must be 1");
198
+ }
199
+ if (baseline.repoId !== expectedRepoId) {
200
+ throw new Error("Baseline target repoId does not match");
201
+ }
202
+ const metrics = record(baseline.metrics, "Baseline metrics");
203
+ requireExactKeys(metrics, BASELINE_METRICS, "Baseline metrics");
204
+ for (const metric of BASELINE_METRICS) {
205
+ if (typeof metrics[metric] !== "number" || !Number.isFinite(metrics[metric])) {
206
+ throw new Error("Baseline metric must be finite: " + metric);
207
+ }
208
+ }
209
+ return input;
210
+ }
211
+ const THRESHOLD_RULES = [
212
+ ["indexing", "indexTimePerFile", "lower-is-better", "maxMs", "allowableIncreasePercent"],
213
+ ["indexing", "indexTimePerSymbol", "lower-is-better", "maxMs", "allowableIncreasePercent"],
214
+ ["quality", "symbolsPerFile", "higher-is-better", "minValue", "allowableDecreasePercent"],
215
+ ["quality", "edgesPerSymbol", "higher-is-better", "minValue", "allowableDecreasePercent"],
216
+ ["quality", "graphConnectivity", "higher-is-better", "minValue", "allowableDecreasePercent"],
217
+ ["quality", "exportedSymbolRatio", "higher-is-better", "minValue", "allowableDecreasePercent"],
218
+ ["performance", "sliceBuildTimeMs", "lower-is-better", "maxMs", "allowableIncreasePercent"],
219
+ ["performance", "avgSkeletonTimeMs", "lower-is-better", "maxMs", "allowableIncreasePercent"],
220
+ ["tokenEfficiency", "avgCardTokens", "lower-is-better", "maxTokens", "allowableIncreasePercent"],
221
+ ["tokenEfficiency", "avgSkeletonTokens", "lower-is-better", "maxTokens", "allowableIncreasePercent"],
222
+ ["coverage", "callEdgeCoverage", "higher-is-better", "minPercent", "allowableDecreasePercent"],
223
+ ["coverage", "importEdgeCoverage", "higher-is-better", "minPercent", "allowableDecreasePercent"],
224
+ ];
225
+ export function validateThresholdConfigV1(input) {
226
+ const config = record(input, "Threshold config");
227
+ if (config.version !== "1.0") {
228
+ throw new Error('Threshold version must be "1.0"');
229
+ }
230
+ const thresholds = record(config.thresholds, "Thresholds");
231
+ if (Object.keys(thresholds).length === 0) {
232
+ throw new Error("Thresholds must not be empty");
233
+ }
234
+ const categories = [...new Set(THRESHOLD_RULES.map(([category]) => category))];
235
+ requireExactKeys(thresholds, categories, "Threshold categories");
236
+ for (const categoryName of categories) {
237
+ const metrics = THRESHOLD_RULES.filter(([category]) => category === categoryName).map(([, metric]) => metric);
238
+ requireExactKeys(record(thresholds[categoryName], categoryName), metrics, categoryName + " thresholds");
239
+ }
240
+ for (const [categoryName, metric, trend, absolute, allowance] of THRESHOLD_RULES) {
241
+ const category = record(thresholds[categoryName], categoryName);
242
+ const rule = record(category[metric], metric);
243
+ requireExactKeys(rule, ["trend", absolute, allowance], metric + " rule");
244
+ if (rule.trend !== trend) {
245
+ throw new Error("Threshold trend mismatch: " + metric);
246
+ }
247
+ for (const key of [absolute, allowance]) {
248
+ if (typeof rule[key] !== "number" || !Number.isFinite(rule[key])) {
249
+ throw new Error("Threshold limit must be finite: " + metric);
250
+ }
251
+ }
252
+ }
253
+ return THRESHOLD_RULES.map(([category, metric]) => category + "/" + metric).sort(compareArtifactPath);
254
+ }
255
+ export function validateThresholdFiles(sourcePath, stagedPath) {
256
+ const source = fs.readFileSync(sourcePath);
257
+ let parsed;
258
+ try {
259
+ parsed = JSON.parse(source.toString("utf8"));
260
+ }
261
+ catch (error) {
262
+ throw new Error("Failed to parse threshold config: " +
263
+ (error instanceof Error ? error.message : String(error)));
264
+ }
265
+ const pairs = validateThresholdConfigV1(parsed);
266
+ if (!source.equals(fs.readFileSync(stagedPath))) {
267
+ throw new Error("Threshold source and staged files must be byte-identical");
268
+ }
269
+ return { sha256: sha256File(sourcePath), pairs };
270
+ }
271
+ function parseJsonFile(filePath, label) {
272
+ try {
273
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
274
+ }
275
+ catch (error) {
276
+ throw new Error("Failed to parse " +
277
+ label +
278
+ ": " +
279
+ (error instanceof Error ? error.message : String(error)));
280
+ }
281
+ }
282
+ export function buildGeneratedRunConfig(baseConfigPath, externalConfigPath, repoId) {
283
+ const base = record(parseJsonFile(baseConfigPath, "base config"), "Base config");
284
+ const external = record(parseJsonFile(externalConfigPath, "external config"), "External config");
285
+ if (!Array.isArray(external.repos)) {
286
+ throw new Error("External config repos must be an array");
287
+ }
288
+ const selected = external.repos.filter((repo) => record(repo, "External repo").repoId === repoId);
289
+ if (selected.length !== 1) {
290
+ throw new Error("External config must contain exactly one selected target");
291
+ }
292
+ const indexing = record(base.indexing, "Base indexing");
293
+ return {
294
+ ...base,
295
+ repos: selected,
296
+ indexing: { ...indexing, enableFileWatching: false },
297
+ };
298
+ }
299
+ export function validateGeneratedRunConfig(input, repoId, targetRoot, runnerRoot) {
300
+ const config = record(input, "Generated config");
301
+ if (!Array.isArray(config.repos) || config.repos.length !== 1) {
302
+ throw new Error("Generated config must contain exactly one repo");
303
+ }
304
+ const repo = record(config.repos[0], "Generated repo");
305
+ if (repo.repoId !== repoId || typeof repo.rootPath !== "string") {
306
+ throw new Error("Generated config target does not match");
307
+ }
308
+ if (record(config.indexing, "Generated indexing").enableFileWatching !== false) {
309
+ throw new Error("Generated config file watching must be disabled");
310
+ }
311
+ assertCanonicalPathEqual(resolve(runnerRoot, repo.rootPath), targetRoot);
312
+ }
313
+ function assertFileHash(filePath, expectedSha256, label) {
314
+ if (sha256File(filePath) !== expectedSha256) {
315
+ throw new Error(label + " hash mismatch");
316
+ }
317
+ }
318
+ export function assertManifestFileHashes(runnerRoot, artifactRoot, manifest, thresholdSourcePath = join(runnerRoot, ...manifest.inputs.thresholdSourcePath.split("/"))) {
319
+ assertFileHash(join(runnerRoot, ...manifest.runner.launcherPath.split("/")), manifest.runner.launcherSha256, "launcher");
320
+ assertFileHash(join(artifactRoot, ...manifest.inputs.configPath.split("/")), manifest.inputs.configSha256, "staged config");
321
+ assertFileHash(join(artifactRoot, ...manifest.inputs.baselinePath.split("/")), manifest.inputs.baselineSha256, "staged baseline");
322
+ for (const filePath of [
323
+ thresholdSourcePath,
324
+ join(artifactRoot, ...manifest.inputs.thresholdPath.split("/")),
325
+ ]) {
326
+ assertFileHash(filePath, manifest.inputs.thresholdSha256, "threshold");
327
+ }
328
+ }
329
+ const execText = (file, args) => execFileSync(file, [...args], { encoding: "utf8" });
330
+ function runGit(rootPath, args, exec) {
331
+ return exec("git", ["-C", rootPath, ...args]);
332
+ }
333
+ export function readGitSnapshot(rootPath, exec = execText) {
334
+ const commit = runGit(rootPath, ["rev-parse", "HEAD"], exec).trim();
335
+ const dirty = runGit(rootPath, ["status", "--porcelain=v1", "--untracked-files=all"], exec).trim().length > 0;
336
+ const files = runGit(rootPath, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], exec)
337
+ .split("\0")
338
+ .filter((path) => path.length > 0)
339
+ .map(normalizeArtifactPath);
340
+ return {
341
+ commit,
342
+ dirty,
343
+ treeSha256: fingerprintFiles(rootPath, files).sha256,
344
+ };
345
+ }
346
+ export function assertGitSnapshot(actual, expected, label) {
347
+ if (actual.commit !== expected.commit) {
348
+ throw new Error(label + " commit mismatch");
349
+ }
350
+ if (actual.dirty !== expected.dirty) {
351
+ throw new Error(label + " dirty state mismatch");
352
+ }
353
+ if (actual.treeSha256 !== expected.treeSha256) {
354
+ throw new Error(label + " source tree mismatch");
355
+ }
356
+ }
357
+ export function assertTargetRef(targetRoot, lock, exec = execText) {
358
+ const snapshot = readGitSnapshot(targetRoot, exec);
359
+ const lockedCommit = runGit(targetRoot, ["rev-parse", "--verify", lock.ref + "^{commit}"], exec).trim();
360
+ if (snapshot.commit !== lockedCommit) {
361
+ throw new Error("Target HEAD does not match the locked ref");
362
+ }
363
+ const trimSlash = (value) => value.trim().replace(/\/+$/u, "");
364
+ const origin = runGit(targetRoot, ["remote", "get-url", "origin"], exec);
365
+ if (trimSlash(origin) !== trimSlash(lock.cloneUrl)) {
366
+ throw new Error("Target origin does not match the lock");
367
+ }
368
+ return snapshot;
369
+ }
370
+ export function assertRunnerSnapshot(runnerRoot, manifest, expectedSource, exec = execText) {
371
+ const actual = readGitSnapshot(runnerRoot, exec);
372
+ assertGitSnapshot(actual, expectedSource, "runner");
373
+ if (actual.commit !== manifest.runner.sdlMcpCommit ||
374
+ actual.dirty !== manifest.runner.sdlMcpSourceDirty) {
375
+ throw new Error("Runner commit or dirty state mismatch");
376
+ }
377
+ if (fingerprintDirectory(join(runnerRoot, "dist")).sha256 !==
378
+ manifest.runner.sdlMcpBuildTreeSha256) {
379
+ throw new Error("Runner dist hash mismatch");
380
+ }
381
+ assertFileHash(join(runnerRoot, ...manifest.runner.launcherPath.split("/")), manifest.runner.launcherSha256, "runner launcher");
382
+ }
383
+ const EXTERNAL_BENCHMARK_OPTIONS = new Map([
384
+ ["--repo-id", "repoId"],
385
+ ["--out-dir", "outDir"],
386
+ ["--lock", "lock"],
387
+ ["--base-config", "baseConfig"],
388
+ ["--external-config", "externalConfig"],
389
+ ["--baseline", "baseline"],
390
+ ["--threshold", "threshold"],
391
+ ["--cache-mode", "cacheMode"],
392
+ ["--repeats", "repeats"],
393
+ ["--warm-db", "warmDb"],
394
+ ["--scip-artifact", "scipArtifact"],
395
+ ]);
396
+ export function parseExternalBenchmarkArgs(argv) {
397
+ const values = new Map();
398
+ for (let index = 0; index < argv.length; index += 2) {
399
+ const flag = argv[index];
400
+ const key = flag === undefined ? undefined : EXTERNAL_BENCHMARK_OPTIONS.get(flag);
401
+ const value = argv[index + 1];
402
+ if (key === undefined) {
403
+ throw new Error("Unknown external benchmark option: " + String(flag));
404
+ }
405
+ if (value === undefined || value.startsWith("--")) {
406
+ throw new Error("Missing value for external benchmark option: " + flag);
407
+ }
408
+ if (values.has(key)) {
409
+ throw new Error("Duplicate external benchmark option: " + flag);
410
+ }
411
+ values.set(key, value);
412
+ }
413
+ const repoId = values.get("repoId");
414
+ const outDir = values.get("outDir");
415
+ if (repoId === undefined || outDir === undefined) {
416
+ throw new Error("--repo-id and --out-dir are required");
417
+ }
418
+ const cacheMode = values.get("cacheMode") ?? "cold";
419
+ if (cacheMode !== "cold" && cacheMode !== "warm") {
420
+ throw new Error("--cache-mode must be cold or warm");
421
+ }
422
+ const repeatsText = values.get("repeats") ?? "1";
423
+ const repeats = Number(repeatsText);
424
+ if (!/^\d+$/u.test(repeatsText) || repeats < 1 || repeats > 20) {
425
+ throw new Error("--repeats must be an integer from 1 through 20");
426
+ }
427
+ const warmDb = values.get("warmDb");
428
+ if ((cacheMode === "warm") !== (warmDb !== undefined)) {
429
+ throw new Error("--warm-db is required for warm mode and forbidden for cold mode");
430
+ }
431
+ return {
432
+ repoId,
433
+ outDir,
434
+ lock: values.get("lock") ?? "scripts/benchmark/matrix-external-repos.lock.json",
435
+ baseConfig: values.get("baseConfig") ?? "config/sdlmcp.config.json",
436
+ externalConfig: values.get("externalConfig") ??
437
+ "benchmarks/real-world/external-repos.config.json",
438
+ baseline: values.get("baseline") ?? ".benchmark/baseline." + repoId + ".json",
439
+ threshold: values.get("threshold") ?? "config/benchmark.config.json",
440
+ cacheMode,
441
+ repeats,
442
+ warmDb,
443
+ scipArtifact: values.get("scipArtifact"),
444
+ };
445
+ }
446
+ export async function runExternalBenchmarkCli(argv, runChild = runBenchmarkChild) {
447
+ return runExternalBenchmarkPrepared(argv, runChild);
448
+ }
449
+ async function runExternalBenchmarkPrepared(argv, runChild) {
450
+ const options = parseExternalBenchmarkArgs(argv);
451
+ const requestedRoot = resolve(options.outDir);
452
+ if (existsSync(requestedRoot)) {
453
+ throw new Error("External benchmark artifact root must be absent");
454
+ }
455
+ const artifactRoot = canonicalizePath(requestedRoot);
456
+ fs.mkdirSync(artifactRoot);
457
+ writeUtf8Output(join(artifactRoot, "preflight.log"), "", "exclusive");
458
+ let state;
459
+ try {
460
+ state = prepareExternalBenchmarkRun(options, artifactRoot);
461
+ }
462
+ catch (error) {
463
+ writeUtf8Output(join(artifactRoot, "preflight-error.json"), JSON.stringify({
464
+ schemaVersion: 1,
465
+ boundary: "post-artifact-pre-manifest",
466
+ message: error instanceof Error ? error.message : String(error),
467
+ }, null, 2) + "\n", "exclusive");
468
+ return 1;
469
+ }
470
+ return executeExternalBenchmarkRepeats(state, runChild);
471
+ }
472
+ function prepareExternalBenchmarkRun(options, artifactRoot) {
473
+ const runnerRoot = canonicalizePath(resolve(dirname(fileURLToPath(import.meta.url)), "../.."));
474
+ const lockPath = canonicalizePath(resolve(runnerRoot, options.lock));
475
+ if (!existsSync(lockPath)) {
476
+ throw new Error("External benchmark lock file does not exist");
477
+ }
478
+ const lock = record(parseJsonFile(lockPath, "external benchmark lock"), "External benchmark lock");
479
+ if (!Array.isArray(lock.repos) || lock.repos.length === 0) {
480
+ throw new Error("External benchmark lock must contain repos");
481
+ }
482
+ const matches = lock.repos
483
+ .map((value) => record(value, "External benchmark lock repo"))
484
+ .filter((value) => value.repoId === options.repoId);
485
+ if (matches.length !== 1) {
486
+ throw new Error("External benchmark lock must contain the target exactly once");
487
+ }
488
+ const selectedLock = matches[0];
489
+ if (typeof selectedLock.ref !== "string" ||
490
+ typeof selectedLock.cloneUrl !== "string") {
491
+ throw new Error("External benchmark lock target is invalid");
492
+ }
493
+ const baseConfigPath = canonicalizePath(resolve(runnerRoot, options.baseConfig));
494
+ const externalConfigPath = canonicalizePath(resolve(runnerRoot, options.externalConfig));
495
+ const generatedConfig = buildGeneratedRunConfig(baseConfigPath, externalConfigPath, options.repoId);
496
+ if (!Array.isArray(generatedConfig.repos)) {
497
+ throw new Error("Generated config repos must be an array");
498
+ }
499
+ const generatedRepo = record(generatedConfig.repos[0], "Generated config repo");
500
+ if (typeof generatedRepo.rootPath !== "string") {
501
+ throw new Error("Generated config rootPath must be a string");
502
+ }
503
+ const targetRoot = canonicalizePath(resolve(runnerRoot, generatedRepo.rootPath));
504
+ validateGeneratedRunConfig(generatedConfig, options.repoId, targetRoot, runnerRoot);
505
+ const targetSnapshot = assertTargetRef(targetRoot, { ref: selectedLock.ref, cloneUrl: selectedLock.cloneUrl });
506
+ const runnerSnapshot = readGitSnapshot(runnerRoot);
507
+ const distFingerprint = fingerprintDirectory(join(runnerRoot, "dist"));
508
+ const launcherPath = join(runnerRoot, "scripts", "external-benchmark-runner.mjs");
509
+ if (!existsSync(launcherPath) || !fs.statSync(launcherPath).isFile()) {
510
+ throw new Error("External benchmark launcher does not exist");
511
+ }
512
+ const packageJson = record(parseJsonFile(join(runnerRoot, "package.json"), "package.json"), "package.json");
513
+ if (typeof packageJson.version !== "string") {
514
+ throw new Error("package.json version must be a string");
515
+ }
516
+ const baselineSourcePath = canonicalizePath(resolve(runnerRoot, options.baseline));
517
+ const baselineText = fs.readFileSync(baselineSourcePath, "utf8");
518
+ const baseline = validateBaselineV1(JSON.parse(baselineText), options.repoId);
519
+ const thresholdSourcePath = canonicalizePath(resolve(runnerRoot, options.threshold));
520
+ const thresholdText = fs.readFileSync(thresholdSourcePath, "utf8");
521
+ const thresholdPairs = validateThresholdConfigV1(JSON.parse(thresholdText));
522
+ for (const directory of ["inputs", "logs", "db", "raw"]) {
523
+ fs.mkdirSync(join(artifactRoot, directory));
524
+ }
525
+ const configPath = join(artifactRoot, "inputs", "sdlmcp.config.json");
526
+ const baselinePath = join(artifactRoot, "inputs", "baseline.json");
527
+ const thresholdPath = join(artifactRoot, "inputs", "threshold.json");
528
+ writeUtf8Output(configPath, JSON.stringify(generatedConfig, null, 2) + "\n", "exclusive");
529
+ writeUtf8Output(baselinePath, baselineText, "exclusive");
530
+ writeUtf8Output(thresholdPath, thresholdText, "exclusive");
531
+ validateGeneratedRunConfig(parseJsonFile(configPath, "staged config"), options.repoId, targetRoot, runnerRoot);
532
+ validateBaselineV1(parseJsonFile(baselinePath, "staged baseline"), options.repoId);
533
+ const stagedThreshold = validateThresholdFiles(thresholdSourcePath, thresholdPath);
534
+ if (JSON.stringify(stagedThreshold.pairs) !== JSON.stringify(thresholdPairs)) {
535
+ throw new Error("Staged threshold contract changed");
536
+ }
537
+ let warmSourcePath = null;
538
+ let warmSourceFingerprint = null;
539
+ let warmSnapshot = null;
540
+ if (options.cacheMode === "warm") {
541
+ warmSourcePath = canonicalizePath(resolve(runnerRoot, options.warmDb));
542
+ warmSourceFingerprint = fingerprintDbFamily(warmSourcePath);
543
+ warmSnapshot = stageWarmSnapshot(warmSourcePath, artifactRoot);
544
+ }
545
+ const repeats = [];
546
+ for (let repeat = 1; repeat <= options.repeats; repeat += 1) {
547
+ const prepared = options.cacheMode === "cold"
548
+ ? prepareColdRepeat(artifactRoot, repeat)
549
+ : prepareWarmRepeat(artifactRoot, repeat, warmSnapshot);
550
+ const number = String(repeat).padStart(3, "0");
551
+ repeats.push({
552
+ repeat,
553
+ graphDbPath: prepared.graphDbPath,
554
+ stdoutPath: `logs/repeat-${number}.stdout.log`,
555
+ stderrPath: `logs/repeat-${number}.stderr.log`,
556
+ initialDbFiles: prepared.initialDbFiles,
557
+ command: [
558
+ "node",
559
+ "dist/cli/index.js",
560
+ "benchmark:ci",
561
+ "--repo-id",
562
+ options.repoId,
563
+ "--baseline-path",
564
+ "inputs/baseline.json",
565
+ "--threshold-path",
566
+ "inputs/threshold.json",
567
+ "--out-exclusive",
568
+ "--out",
569
+ `raw/repeat-${number}.benchmark.json`,
570
+ ],
571
+ });
572
+ }
573
+ let scipArtifactPath = null;
574
+ let scipArtifactSha256 = null;
575
+ if (options.scipArtifact !== undefined) {
576
+ const absoluteScipPath = canonicalizePath(resolve(targetRoot, options.scipArtifact));
577
+ assertCanonicalPathContained(targetRoot, absoluteScipPath);
578
+ if (!fs.statSync(absoluteScipPath).isFile()) {
579
+ throw new Error("SCIP artifact must be a regular file");
580
+ }
581
+ scipArtifactPath = normalizeArtifactPath(relative(targetRoot, absoluteScipPath));
582
+ scipArtifactSha256 = sha256File(absoluteScipPath);
583
+ }
584
+ const manifest = {
585
+ schemaVersion: 1,
586
+ target: {
587
+ repoId: options.repoId,
588
+ sourceRef: selectedLock.ref,
589
+ sourceCommit: targetSnapshot.commit,
590
+ sourceDirty: targetSnapshot.dirty,
591
+ sourceTreeSha256: targetSnapshot.treeSha256,
592
+ scipArtifactPath,
593
+ scipArtifactSha256,
594
+ },
595
+ runner: {
596
+ sdlMcpVersion: packageJson.version,
597
+ sdlMcpCommit: runnerSnapshot.commit,
598
+ sdlMcpSourceDirty: runnerSnapshot.dirty,
599
+ sdlMcpBuildTreeSha256: distFingerprint.sha256,
600
+ launcherPath: "scripts/external-benchmark-runner.mjs",
601
+ launcherSha256: sha256File(launcherPath),
602
+ nodeVersion: process.version,
603
+ platform: process.platform,
604
+ architecture: process.arch,
605
+ cacheMode: options.cacheMode,
606
+ repeats: options.repeats,
607
+ },
608
+ inputs: {
609
+ configPath: "inputs/sdlmcp.config.json",
610
+ configSha256: sha256File(configPath),
611
+ baselinePath: "inputs/baseline.json",
612
+ baselineSha256: sha256File(baselinePath),
613
+ baselineFormatVersion: "1",
614
+ baselineTargetRepoId: baseline.repoId,
615
+ thresholdSourcePath: "config/benchmark.config.json",
616
+ thresholdPath: "inputs/threshold.json",
617
+ thresholdSha256: stagedThreshold.sha256,
618
+ warmSnapshot: warmSnapshot === null ? null : { files: warmSnapshot.files },
619
+ },
620
+ repeats,
621
+ };
622
+ const serializedManifest = serializeRunManifest(manifest);
623
+ writeUtf8Output(join(artifactRoot, "run-manifest.json"), serializedManifest, "exclusive");
624
+ return {
625
+ artifactRoot,
626
+ runnerRoot,
627
+ targetRoot,
628
+ targetSnapshot,
629
+ runnerSnapshot,
630
+ baselineSourcePath,
631
+ thresholdSourcePath,
632
+ thresholdPairs,
633
+ warmSourcePath,
634
+ warmSourceFingerprint,
635
+ manifest,
636
+ manifestSha256: hashSerializedManifest(serializedManifest),
637
+ };
638
+ }
639
+ function assertExternalBenchmarkInputs(state, repeat) {
640
+ assertGitSnapshot(readGitSnapshot(state.targetRoot), state.targetSnapshot, "target");
641
+ assertRunnerSnapshot(state.runnerRoot, state.manifest, state.runnerSnapshot);
642
+ assertManifestFileHashes(state.runnerRoot, state.artifactRoot, state.manifest, state.thresholdSourcePath);
643
+ assertFileHash(state.baselineSourcePath, state.manifest.inputs.baselineSha256, "baseline source");
644
+ const stagedBaselinePath = join(state.artifactRoot, ...state.manifest.inputs.baselinePath.split("/"));
645
+ if (!fs.readFileSync(state.baselineSourcePath).equals(fs.readFileSync(stagedBaselinePath))) {
646
+ throw new Error("Baseline source and staged files must be byte-identical");
647
+ }
648
+ validateBaselineV1(parseJsonFile(state.baselineSourcePath, "baseline"), state.manifest.target.repoId);
649
+ const stagedThresholdPath = join(state.artifactRoot, ...state.manifest.inputs.thresholdPath.split("/"));
650
+ const threshold = validateThresholdFiles(state.thresholdSourcePath, stagedThresholdPath);
651
+ if (JSON.stringify(threshold.pairs) !== JSON.stringify(state.thresholdPairs)) {
652
+ throw new Error("Threshold contract changed");
653
+ }
654
+ validateGeneratedRunConfig(parseJsonFile(join(state.artifactRoot, ...state.manifest.inputs.configPath.split("/")), "staged config"), state.manifest.target.repoId, state.targetRoot, state.runnerRoot);
655
+ if (state.warmSourcePath !== null &&
656
+ state.warmSourceFingerprint !== null &&
657
+ JSON.stringify(fingerprintDbFamily(state.warmSourcePath)) !==
658
+ JSON.stringify(state.warmSourceFingerprint)) {
659
+ throw new Error("Warm source database family changed");
660
+ }
661
+ for (const file of state.manifest.inputs.warmSnapshot?.files ?? []) {
662
+ assertFileHash(join(state.artifactRoot, ...file.path.split("/")), file.sha256, "warm snapshot");
663
+ }
664
+ const graphDbPath = join(state.artifactRoot, ...repeat.graphDbPath.split("/"));
665
+ if (repeat.initialDbFiles.length === 0) {
666
+ assertDbFamilyAbsent(graphDbPath);
667
+ return;
668
+ }
669
+ const actualPaths = collectDbFamilyFiles(graphDbPath).map((filePath) => normalizeArtifactPath(relative(state.artifactRoot, filePath)));
670
+ const expectedPaths = repeat.initialDbFiles
671
+ .map((file) => file.destinationPath)
672
+ .sort(compareArtifactPath);
673
+ if (JSON.stringify(actualPaths) !== JSON.stringify(expectedPaths)) {
674
+ throw new Error("Repeat database family membership changed");
675
+ }
676
+ for (const file of repeat.initialDbFiles) {
677
+ assertFileHash(join(state.artifactRoot, ...file.sourcePath.split("/")), file.sha256, "warm source copy");
678
+ assertFileHash(join(state.artifactRoot, ...file.destinationPath.split("/")), file.sha256, "repeat database");
679
+ }
680
+ }
681
+ class BenchmarkChildFailure extends Error {
682
+ boundary;
683
+ constructor(boundary, cause) {
684
+ super(boundary.replace("-", " ") +
685
+ ": " +
686
+ (cause instanceof Error ? cause.message : String(cause)));
687
+ this.boundary = boundary;
688
+ }
689
+ }
690
+ export function runBenchmarkChild(request) {
691
+ const started = performance.now();
692
+ const stdout = fs.createWriteStream(request.stdoutPath, { flags: "wx" });
693
+ const stderr = fs.createWriteStream(request.stderrPath, { flags: "wx" });
694
+ return new Promise((resolvePromise, rejectPromise) => {
695
+ let settled = false;
696
+ let childClosed = false;
697
+ let stdoutClosed = false;
698
+ let stderrClosed = false;
699
+ let exitCode = null;
700
+ let child;
701
+ let failure;
702
+ const finish = () => {
703
+ if (settled || !childClosed || !stdoutClosed || !stderrClosed)
704
+ return;
705
+ settled = true;
706
+ if (failure !== undefined) {
707
+ rejectPromise(failure);
708
+ return;
709
+ }
710
+ resolvePromise({
711
+ exitCode,
712
+ durationMs: roundDuration(performance.now() - started),
713
+ });
714
+ };
715
+ const fail = (boundary, error) => {
716
+ if (settled || failure !== undefined)
717
+ return;
718
+ failure = new BenchmarkChildFailure(boundary, error);
719
+ child?.kill();
720
+ child?.stdout?.destroy();
721
+ child?.stderr?.destroy();
722
+ stdout.destroy();
723
+ stderr.destroy();
724
+ finish();
725
+ };
726
+ stdout.once("error", (error) => fail("child-stream", error));
727
+ stderr.once("error", (error) => fail("child-stream", error));
728
+ stdout.once("close", () => {
729
+ stdoutClosed = true;
730
+ finish();
731
+ });
732
+ stderr.once("close", () => {
733
+ stderrClosed = true;
734
+ finish();
735
+ });
736
+ try {
737
+ child = spawn(process.execPath, request.command.slice(1), {
738
+ cwd: request.cwd,
739
+ env: request.env,
740
+ stdio: ["ignore", "pipe", "pipe"],
741
+ });
742
+ }
743
+ catch (error) {
744
+ childClosed = true;
745
+ fail("child-spawn", error);
746
+ return;
747
+ }
748
+ child.once("error", (error) => fail("child-spawn", error));
749
+ child.once("close", (code) => {
750
+ exitCode = code;
751
+ childClosed = true;
752
+ finish();
753
+ });
754
+ if (child.stdout === null || child.stderr === null) {
755
+ fail("child-spawn", new Error("Child stdio pipes are unavailable"));
756
+ return;
757
+ }
758
+ child.stdout.once("error", (error) => fail("child-stream", error));
759
+ child.stderr.once("error", (error) => fail("child-stream", error));
760
+ child.stdout.pipe(stdout);
761
+ child.stderr.pipe(stderr);
762
+ });
763
+ }
764
+ async function executeExternalBenchmarkRepeats(state, runChild) {
765
+ const rawRepeats = [];
766
+ let stop;
767
+ let activeRepeat = 1;
768
+ let unexpectedBoundary = "preflight-between-repeats";
769
+ let results;
770
+ const usedDbPaths = new Set();
771
+ try {
772
+ for (const repeat of state.manifest.repeats) {
773
+ activeRepeat = repeat.repeat;
774
+ unexpectedBoundary = "preflight-between-repeats";
775
+ try {
776
+ assertExternalBenchmarkInputs(state, repeat);
777
+ }
778
+ catch (error) {
779
+ const boundary = "preflight-between-repeats";
780
+ writeRepeatDiagnostic(state, repeat, error);
781
+ rawRepeats.push(failedRawRepeat(repeat, boundary));
782
+ stop = { boundary, failedRepeat: repeat.repeat };
783
+ break;
784
+ }
785
+ const request = buildBenchmarkChildRequest(state, repeat, usedDbPaths);
786
+ let childResult;
787
+ try {
788
+ childResult = await runChild(request);
789
+ }
790
+ catch (error) {
791
+ const boundary = error instanceof BenchmarkChildFailure
792
+ ? error.boundary
793
+ : "child-spawn";
794
+ rawRepeats.push(failedRawRepeat(repeat, boundary));
795
+ stop = { boundary, failedRepeat: repeat.repeat };
796
+ break;
797
+ }
798
+ unexpectedBoundary = "raw-result-invalid";
799
+ const raw = readRawBenchmarkRepeat(state, repeat, childResult.exitCode, roundDuration(childResult.durationMs), request.rawResultPath);
800
+ rawRepeats.push(raw);
801
+ if (raw.failureBoundary !== null) {
802
+ stop = {
803
+ boundary: raw.failureBoundary === "unexecuted-after-failure"
804
+ ? "raw-result-invalid"
805
+ : raw.failureBoundary,
806
+ failedRepeat: repeat.repeat,
807
+ };
808
+ break;
809
+ }
810
+ }
811
+ }
812
+ catch (error) {
813
+ const repeat = state.manifest.repeats[activeRepeat - 1];
814
+ const boundary = unexpectedBoundary;
815
+ writeRepeatDiagnostic(state, repeat, error);
816
+ if (!rawRepeats.some((raw) => raw.repeat === activeRepeat)) {
817
+ rawRepeats.push(failedRawRepeat(repeat, boundary));
818
+ }
819
+ stop = { boundary, failedRepeat: activeRepeat };
820
+ }
821
+ finally {
822
+ ensureDeclaredLogs(state);
823
+ results = buildExternalBenchmarkResults(state.manifestSha256, state.manifest.runner.repeats, rawRepeats, stop);
824
+ writeUtf8Output(join(state.artifactRoot, "results.json"), serializeExternalBenchmarkResults(results), "exclusive");
825
+ }
826
+ return results?.passed === true ? 0 : 1;
827
+ }
828
+ function buildBenchmarkChildRequest(state, repeat, usedDbPaths) {
829
+ const configPath = join(state.artifactRoot, ...state.manifest.inputs.configPath.split("/"));
830
+ const graphDbPath = join(state.artifactRoot, ...repeat.graphDbPath.split("/"));
831
+ const env = buildChildEnvironment(process.env, configPath, graphDbPath);
832
+ assertChildEnvironment(env, state.artifactRoot, state.manifest, repeat, usedDbPaths);
833
+ return {
834
+ command: [...repeat.command],
835
+ cwd: state.runnerRoot,
836
+ env,
837
+ stdoutPath: join(state.artifactRoot, ...repeat.stdoutPath.split("/")),
838
+ stderrPath: join(state.artifactRoot, ...repeat.stderrPath.split("/")),
839
+ rawResultPath: join(state.artifactRoot, "raw", `repeat-${String(repeat.repeat).padStart(3, "0")}.benchmark.json`),
840
+ };
841
+ }
842
+ function readRawBenchmarkRepeat(state, repeat, exitCode, durationMs, rawResultPath) {
843
+ if (!existsSync(rawResultPath)) {
844
+ return failedRawRepeat(repeat, "raw-result-missing", exitCode, durationMs);
845
+ }
846
+ let parsed;
847
+ try {
848
+ parsed = JSON.parse(fs.readFileSync(rawResultPath, "utf8"));
849
+ }
850
+ catch {
851
+ return failedRawRepeat(repeat, "raw-result-invalid", exitCode, durationMs);
852
+ }
853
+ let benchmarkResult;
854
+ try {
855
+ benchmarkResult = validateRawBenchmarkResult(parsed, state.manifest.target.repoId, state.thresholdPairs);
856
+ }
857
+ catch (error) {
858
+ return failedRawRepeat(repeat, error instanceof ThresholdEvidenceFailure
859
+ ? "threshold-evidence-invalid"
860
+ : "raw-result-invalid", exitCode, durationMs);
861
+ }
862
+ const evaluations = benchmarkResult.thresholdResult.evaluations;
863
+ const benchmarkResultPath = repeat.command[repeat.command.length - 1] ??
864
+ `raw/repeat-${String(repeat.repeat).padStart(3, "0")}.benchmark.json`;
865
+ const raw = {
866
+ repeat: repeat.repeat,
867
+ exitCode,
868
+ failureBoundary: null,
869
+ durationMs,
870
+ benchmarkResultPath,
871
+ benchmarkResultSha256: sha256File(rawResultPath),
872
+ benchmarkResult,
873
+ };
874
+ if (exitCode === null ||
875
+ (exitCode !== 0 &&
876
+ !evaluations.some((value) => record(value, "Raw threshold evaluation").passed === false))) {
877
+ raw.failureBoundary = "child-exit";
878
+ }
879
+ return raw;
880
+ }
881
+ function failedRawRepeat(repeat, boundary, exitCode = null, durationMs = 0) {
882
+ return {
883
+ repeat: repeat.repeat,
884
+ exitCode,
885
+ failureBoundary: boundary,
886
+ durationMs: roundDuration(durationMs),
887
+ benchmarkResultPath: repeat.command[repeat.command.length - 1] ??
888
+ `raw/repeat-${String(repeat.repeat).padStart(3, "0")}.benchmark.json`,
889
+ benchmarkResultSha256: null,
890
+ benchmarkResult: null,
891
+ };
892
+ }
893
+ function writeRepeatDiagnostic(state, repeat, error) {
894
+ const path = join(state.artifactRoot, ...repeat.stderrPath.split("/"));
895
+ const message = error instanceof Error ? error.message : String(error);
896
+ if (existsSync(path)) {
897
+ fs.appendFileSync(path, message + "\n", "utf8");
898
+ }
899
+ else {
900
+ writeUtf8Output(path, message + "\n", "exclusive");
901
+ }
902
+ }
903
+ function ensureDeclaredLogs(state) {
904
+ for (const repeat of state.manifest.repeats) {
905
+ for (const path of [repeat.stdoutPath, repeat.stderrPath]) {
906
+ const filePath = join(state.artifactRoot, ...path.split("/"));
907
+ if (!existsSync(filePath)) {
908
+ writeUtf8Output(filePath, "", "exclusive");
909
+ }
910
+ }
911
+ }
912
+ }
913
+ function roundDuration(value) {
914
+ return Math.round(value * 1000) / 1000;
915
+ }
916
+ class ThresholdEvidenceFailure extends Error {
917
+ }
918
+ const RAW_METRIC_KEYS = [
919
+ "indexTimePerFile",
920
+ "indexTimePerSymbol",
921
+ "symbolsPerFile",
922
+ "edgesPerSymbol",
923
+ "graphConnectivity",
924
+ "exportedSymbolRatio",
925
+ "sliceBuildTimeMs",
926
+ "avgSkeletonTimeMs",
927
+ "avgCardTokens",
928
+ "avgSkeletonTokens",
929
+ "functionMethodRatio",
930
+ "avgDepsPerSymbol",
931
+ "callEdgeCount",
932
+ "importEdgeCount",
933
+ "totalSymbols",
934
+ "totalFiles",
935
+ "summaryGenerationMs",
936
+ "summaryTokens",
937
+ "healthScore",
938
+ "watcherEventsProcessed",
939
+ "watcherErrors",
940
+ ];
941
+ export function validateRawBenchmarkResult(input, expectedRepoId, expectedPairs) {
942
+ const result = record(input, "Raw benchmark result");
943
+ if (result.repoId !== expectedRepoId) {
944
+ throw new Error("Raw benchmark repoId does not match");
945
+ }
946
+ const metrics = record(result.metrics, "Raw benchmark metrics");
947
+ for (const key of RAW_METRIC_KEYS) {
948
+ const value = metrics[key];
949
+ if (key === "healthScore"
950
+ ? value !== null &&
951
+ (typeof value !== "number" || !Number.isFinite(value))
952
+ : typeof value !== "number" || !Number.isFinite(value)) {
953
+ throw new Error("Raw benchmark metric must be finite: " + key);
954
+ }
955
+ }
956
+ const thresholdResult = record(result.thresholdResult, "Raw threshold result");
957
+ if (typeof thresholdResult.passed !== "boolean" ||
958
+ !Array.isArray(thresholdResult.evaluations) ||
959
+ thresholdResult.evaluations.length === 0) {
960
+ throw new ThresholdEvidenceFailure("Threshold result is incomplete");
961
+ }
962
+ const expected = [...expectedPairs].sort(compareArtifactPath);
963
+ const actual = [];
964
+ let passedCount = 0;
965
+ for (const value of thresholdResult.evaluations) {
966
+ const evaluation = record(value, "Raw threshold evaluation");
967
+ if (typeof evaluation.category !== "string" ||
968
+ typeof evaluation.metricName !== "string") {
969
+ throw new ThresholdEvidenceFailure("Threshold pair is invalid");
970
+ }
971
+ const pair = evaluation.category + "/" + evaluation.metricName;
972
+ if (actual.includes(pair) || !expected.includes(pair)) {
973
+ throw new ThresholdEvidenceFailure("Threshold pair set is invalid");
974
+ }
975
+ actual.push(pair);
976
+ for (const key of ["currentValue", "baselineValue", "delta", "deltaPercent"]) {
977
+ const field = evaluation[key];
978
+ if (field !== undefined &&
979
+ field !== null &&
980
+ (typeof field !== "number" || !Number.isFinite(field))) {
981
+ throw new ThresholdEvidenceFailure("Threshold value is invalid: " + pair + "/" + key);
982
+ }
983
+ }
984
+ if (typeof evaluation.currentValue !== "number" ||
985
+ !Number.isFinite(evaluation.currentValue) ||
986
+ typeof evaluation.passed !== "boolean" ||
987
+ typeof evaluation.message !== "string") {
988
+ throw new ThresholdEvidenceFailure("Threshold evaluation is invalid: " + pair);
989
+ }
990
+ if (evaluation.passed)
991
+ passedCount += 1;
992
+ }
993
+ actual.sort(compareArtifactPath);
994
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
995
+ throw new ThresholdEvidenceFailure("Threshold pair set is not exact");
996
+ }
997
+ const summary = record(thresholdResult.summary, "Threshold summary");
998
+ const total = thresholdResult.evaluations.length;
999
+ if (summary.total !== total ||
1000
+ summary.passed !== passedCount ||
1001
+ summary.failed !== total - passedCount ||
1002
+ thresholdResult.passed !== (passedCount === total)) {
1003
+ throw new ThresholdEvidenceFailure("Threshold summary is inconsistent");
1004
+ }
1005
+ return input;
1006
+ }
1007
+ export function writeDbFamilyFingerprintFile(primaryPath, outputPath) {
1008
+ const fingerprint = fingerprintDbFamily(primaryPath);
1009
+ writeUtf8Output(outputPath, JSON.stringify({
1010
+ schemaVersion: 1,
1011
+ files: fingerprint.files,
1012
+ sha256: fingerprint.sha256,
1013
+ }, null, 2) + "\n", "exclusive");
1014
+ }
1015
+ export function verifyExternalBenchmarkEvidence(options) {
1016
+ const artifactRoot = canonicalizePath(options.root);
1017
+ const runnerRoot = canonicalizePath(resolve(dirname(fileURLToPath(import.meta.url)), "../.."));
1018
+ const manifestPath = containedArtifactPath(artifactRoot, "run-manifest.json");
1019
+ const resultsPath = containedArtifactPath(artifactRoot, "results.json");
1020
+ const manifestText = fs.readFileSync(manifestPath, "utf8");
1021
+ const resultsText = fs.readFileSync(resultsPath, "utf8");
1022
+ const manifest = JSON.parse(manifestText);
1023
+ const results = JSON.parse(resultsText);
1024
+ if (serializeRunManifest(manifest) !== manifestText) {
1025
+ throw new Error("Run manifest bytes are not canonical");
1026
+ }
1027
+ if (serializeExternalBenchmarkResults(results) !== resultsText) {
1028
+ throw new Error("External benchmark results bytes are not canonical");
1029
+ }
1030
+ const manifestSha256 = hashSerializedManifest(manifestText);
1031
+ if (results.runManifestSha256 !== manifestSha256 ||
1032
+ results.passed !== true) {
1033
+ throw new Error("Results do not identify a passing run manifest");
1034
+ }
1035
+ if (manifest.target.repoId !== options.repoId ||
1036
+ manifest.target.sourceRef !== options.sourceRef ||
1037
+ manifest.target.sourceCommit !== options.sourceCommit ||
1038
+ manifest.runner.cacheMode !== options.cacheMode ||
1039
+ manifest.runner.repeats !== options.repeats ||
1040
+ manifest.repeats.length !== options.repeats ||
1041
+ results.repeats.length !== options.repeats) {
1042
+ throw new Error("Verifier options do not match the run manifest");
1043
+ }
1044
+ const runnerSnapshot = readGitSnapshot(runnerRoot);
1045
+ assertRunnerSnapshot(runnerRoot, manifest, runnerSnapshot);
1046
+ const thresholdSourcePath = canonicalizePath(options.thresholdSourcePath ??
1047
+ join(runnerRoot, ...manifest.inputs.thresholdSourcePath.split("/")));
1048
+ assertManifestFileHashes(runnerRoot, artifactRoot, manifest, thresholdSourcePath);
1049
+ const threshold = validateThresholdFiles(thresholdSourcePath, containedArtifactPath(artifactRoot, manifest.inputs.thresholdPath));
1050
+ const configPath = containedArtifactPath(artifactRoot, manifest.inputs.configPath);
1051
+ const config = record(parseJsonFile(configPath, "staged config"), "Staged config");
1052
+ if (!Array.isArray(config.repos) || config.repos.length !== 1) {
1053
+ throw new Error("Staged config must contain one target");
1054
+ }
1055
+ const repo = record(config.repos[0], "Staged config repo");
1056
+ if (typeof repo.rootPath !== "string") {
1057
+ throw new Error("Staged target rootPath must be a string");
1058
+ }
1059
+ const targetRoot = canonicalizePath(resolve(runnerRoot, repo.rootPath));
1060
+ validateGeneratedRunConfig(config, manifest.target.repoId, targetRoot, runnerRoot);
1061
+ const targetSnapshot = readGitSnapshot(targetRoot);
1062
+ assertGitSnapshot(targetSnapshot, {
1063
+ commit: manifest.target.sourceCommit,
1064
+ dirty: manifest.target.sourceDirty,
1065
+ treeSha256: manifest.target.sourceTreeSha256,
1066
+ }, "target");
1067
+ validateBaselineV1(parseJsonFile(containedArtifactPath(artifactRoot, manifest.inputs.baselinePath), "staged baseline"), manifest.target.repoId);
1068
+ if (manifest.target.scipArtifactPath !== null) {
1069
+ const scipPath = canonicalizePath(resolve(targetRoot, manifest.target.scipArtifactPath));
1070
+ assertCanonicalPathContained(targetRoot, scipPath);
1071
+ assertFileHash(scipPath, manifest.target.scipArtifactSha256, "SCIP artifact");
1072
+ }
1073
+ else if (manifest.target.scipArtifactSha256 !== null) {
1074
+ throw new Error("SCIP artifact path and hash must both be null");
1075
+ }
1076
+ if (options.cacheMode === "cold") {
1077
+ if (manifest.inputs.warmSnapshot !== null ||
1078
+ manifest.repeats.some((repeat) => repeat.initialDbFiles.length !== 0)) {
1079
+ throw new Error("Cold evidence must not declare warm inputs");
1080
+ }
1081
+ }
1082
+ else if (manifest.inputs.warmSnapshot === null) {
1083
+ throw new Error("Warm evidence must declare a warm snapshot");
1084
+ }
1085
+ for (const file of manifest.inputs.warmSnapshot?.files ?? []) {
1086
+ assertFileHash(containedArtifactPath(artifactRoot, file.path), file.sha256, "warm snapshot");
1087
+ }
1088
+ const rawRepeats = [];
1089
+ for (const repeat of manifest.repeats) {
1090
+ const result = results.repeats[repeat.repeat - 1];
1091
+ if (result === undefined ||
1092
+ result.repeat !== repeat.repeat ||
1093
+ result.failureBoundary !== null ||
1094
+ result.passed !== true ||
1095
+ result.benchmarkResultSha256 === null) {
1096
+ throw new Error("Result repeat is incomplete: " + repeat.repeat);
1097
+ }
1098
+ for (const path of [repeat.stdoutPath, repeat.stderrPath]) {
1099
+ const logPath = containedArtifactPath(artifactRoot, path);
1100
+ if (!fs.statSync(logPath).isFile()) {
1101
+ throw new Error("Declared log is not a file: " + path);
1102
+ }
1103
+ }
1104
+ const rawPath = containedArtifactPath(artifactRoot, result.benchmarkResultPath);
1105
+ assertFileHash(rawPath, result.benchmarkResultSha256, "raw benchmark result");
1106
+ const benchmarkResult = validateRawBenchmarkResult(parseJsonFile(rawPath, "raw benchmark result"), manifest.target.repoId, threshold.pairs);
1107
+ rawRepeats.push({
1108
+ repeat: repeat.repeat,
1109
+ exitCode: result.exitCode,
1110
+ failureBoundary: null,
1111
+ durationMs: result.durationMs,
1112
+ benchmarkResultPath: result.benchmarkResultPath,
1113
+ benchmarkResultSha256: result.benchmarkResultSha256,
1114
+ benchmarkResult,
1115
+ });
1116
+ const graphDbPath = containedArtifactPath(artifactRoot, repeat.graphDbPath);
1117
+ if (collectDbFamilyFiles(graphDbPath).length === 0) {
1118
+ throw new Error("Repeat database family is missing: " + repeat.repeat);
1119
+ }
1120
+ for (const file of repeat.initialDbFiles) {
1121
+ assertFileHash(containedArtifactPath(artifactRoot, file.sourcePath), file.sha256, "initial database source");
1122
+ assertFileHash(containedArtifactPath(artifactRoot, file.destinationPath), file.sha256, "initial database destination");
1123
+ }
1124
+ }
1125
+ const rebuilt = buildExternalBenchmarkResults(manifestSha256, manifest.runner.repeats, rawRepeats);
1126
+ if (serializeExternalBenchmarkResults(rebuilt) !== resultsText) {
1127
+ throw new Error("Raw benchmark results do not agree with results.json");
1128
+ }
1129
+ const before = fs.readFileSync(canonicalizePath(options.defaultDbBefore));
1130
+ const after = fs.readFileSync(canonicalizePath(options.defaultDbAfter));
1131
+ if (!before.equals(after)) {
1132
+ throw new Error("Default DB family fingerprints differ");
1133
+ }
1134
+ validateDbFamilyFingerprint(JSON.parse(before.toString("utf8")));
1135
+ return 0;
1136
+ }
1137
+ function containedArtifactPath(rootPath, artifactPath) {
1138
+ const normalized = normalizeArtifactPath(artifactPath);
1139
+ const filePath = canonicalizePath(join(rootPath, ...normalized.split("/")));
1140
+ assertCanonicalPathContained(rootPath, filePath);
1141
+ return filePath;
1142
+ }
1143
+ function validateDbFamilyFingerprint(input) {
1144
+ const value = record(input, "DB family fingerprint");
1145
+ requireExactKeys(value, ["schemaVersion", "files", "sha256"], "DB family fingerprint");
1146
+ if (value.schemaVersion !== 1 ||
1147
+ !Array.isArray(value.files) ||
1148
+ typeof value.sha256 !== "string") {
1149
+ throw new Error("DB family fingerprint is invalid");
1150
+ }
1151
+ const files = value.files.map((entry) => {
1152
+ const file = record(entry, "DB family fingerprint file");
1153
+ requireExactKeys(file, ["path", "sha256"], "DB family fingerprint file");
1154
+ if (typeof file.path !== "string" || typeof file.sha256 !== "string") {
1155
+ throw new Error("DB family fingerprint file is invalid");
1156
+ }
1157
+ return {
1158
+ path: normalizeArtifactPath(file.path),
1159
+ sha256: file.sha256,
1160
+ };
1161
+ });
1162
+ const sorted = [...files].sort((left, right) => compareArtifactPath(left.path, right.path));
1163
+ const sha256 = createHash("sha256")
1164
+ .update(JSON.stringify(sorted), "utf8")
1165
+ .digest("hex");
1166
+ if (JSON.stringify(files) !== JSON.stringify(sorted) ||
1167
+ sha256 !== value.sha256) {
1168
+ throw new Error("DB family fingerprint hash is invalid");
1169
+ }
1170
+ }
1171
+ //# sourceMappingURL=external-runner.js.map