@dailephd/my-dev-kit-lab 0.2.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 (250) hide show
  1. package/README.md +272 -0
  2. package/benchmarks/contracts/benchmark-project-profiles.json +1199 -0
  3. package/benchmarks/contracts/todo-behavior.md +70 -0
  4. package/benchmarks/contracts/todo-benchmark-case.json +227 -0
  5. package/benchmarks/projects/README.md +34 -0
  6. package/benchmarks/projects/task-analytics-large-mixed/README.md +1 -0
  7. package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/__init__.py +3 -0
  8. package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/fixtures.py +6 -0
  9. package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/metrics.py +29 -0
  10. package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/models.py +21 -0
  11. package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/parser.py +16 -0
  12. package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/pipeline.py +9 -0
  13. package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/quality.py +8 -0
  14. package/benchmarks/projects/task-analytics-large-mixed/py/task_analytics/reporting.py +11 -0
  15. package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_metrics.py +19 -0
  16. package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_parser.py +15 -0
  17. package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_quality.py +19 -0
  18. package/benchmarks/projects/task-analytics-large-mixed/py/tests/test_reporting.py +15 -0
  19. package/benchmarks/projects/task-analytics-large-mixed/ts/package.json +12 -0
  20. package/benchmarks/projects/task-analytics-large-mixed/ts/src/index.ts +11 -0
  21. package/benchmarks/projects/task-analytics-large-mixed/ts/src/models/analyticsSnapshot.ts +20 -0
  22. package/benchmarks/projects/task-analytics-large-mixed/ts/src/models/project.ts +5 -0
  23. package/benchmarks/projects/task-analytics-large-mixed/ts/src/models/task.ts +10 -0
  24. package/benchmarks/projects/task-analytics-large-mixed/ts/src/reporting/buildProjectLeaderboard.ts +7 -0
  25. package/benchmarks/projects/task-analytics-large-mixed/ts/src/reporting/formatTaskHealthReport.ts +13 -0
  26. package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/buildAnalyticsSnapshot.ts +39 -0
  27. package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/completeTask.ts +10 -0
  28. package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/createTask.ts +21 -0
  29. package/benchmarks/projects/task-analytics-large-mixed/ts/src/services/listTasksByProject.ts +6 -0
  30. package/benchmarks/projects/task-analytics-large-mixed/ts/src/store/projectStore.ts +20 -0
  31. package/benchmarks/projects/task-analytics-large-mixed/ts/src/store/taskStore.ts +44 -0
  32. package/benchmarks/projects/task-analytics-large-mixed/ts/src/validation/projectValidation.ts +12 -0
  33. package/benchmarks/projects/task-analytics-large-mixed/ts/src/validation/taskValidation.ts +18 -0
  34. package/benchmarks/projects/task-analytics-large-mixed/ts/tests/buildAnalyticsSnapshot.test.ts +48 -0
  35. package/benchmarks/projects/task-analytics-large-mixed/ts/tests/completeTask.test.ts +21 -0
  36. package/benchmarks/projects/task-analytics-large-mixed/ts/tests/createTask.test.ts +31 -0
  37. package/benchmarks/projects/task-analytics-large-mixed/ts/tests/listTasksByProject.test.ts +18 -0
  38. package/benchmarks/projects/task-analytics-large-mixed/ts/tests/reporting.test.ts +19 -0
  39. package/benchmarks/projects/task-analytics-large-mixed/ts/tsconfig.json +12 -0
  40. package/benchmarks/projects/task-analytics-large-mixed/ts/vitest.config.ts +5 -0
  41. package/benchmarks/projects/task-workflow-medium-ts/README.md +1 -0
  42. package/benchmarks/projects/task-workflow-medium-ts/package.json +12 -0
  43. package/benchmarks/projects/task-workflow-medium-ts/src/index.ts +9 -0
  44. package/benchmarks/projects/task-workflow-medium-ts/src/models/project.ts +6 -0
  45. package/benchmarks/projects/task-workflow-medium-ts/src/models/task.ts +39 -0
  46. package/benchmarks/projects/task-workflow-medium-ts/src/services/completeTask.ts +15 -0
  47. package/benchmarks/projects/task-workflow-medium-ts/src/services/createTask.ts +26 -0
  48. package/benchmarks/projects/task-workflow-medium-ts/src/services/filterTasks.ts +17 -0
  49. package/benchmarks/projects/task-workflow-medium-ts/src/services/importTasks.ts +33 -0
  50. package/benchmarks/projects/task-workflow-medium-ts/src/services/summarizeTasks.ts +30 -0
  51. package/benchmarks/projects/task-workflow-medium-ts/src/store/taskStore.ts +76 -0
  52. package/benchmarks/projects/task-workflow-medium-ts/src/utils/deterministicId.ts +3 -0
  53. package/benchmarks/projects/task-workflow-medium-ts/src/validation/taskValidation.ts +45 -0
  54. package/benchmarks/projects/task-workflow-medium-ts/tests/completeTask.test.ts +16 -0
  55. package/benchmarks/projects/task-workflow-medium-ts/tests/createTask.test.ts +21 -0
  56. package/benchmarks/projects/task-workflow-medium-ts/tests/filterTasks.test.ts +18 -0
  57. package/benchmarks/projects/task-workflow-medium-ts/tests/importTasks.test.ts +22 -0
  58. package/benchmarks/projects/task-workflow-medium-ts/tests/summarizeTasks.test.ts +29 -0
  59. package/benchmarks/projects/task-workflow-medium-ts/tsconfig.json +12 -0
  60. package/benchmarks/projects/task-workflow-medium-ts/vitest.config.ts +5 -0
  61. package/benchmarks/projects/todo-js/README.md +3 -0
  62. package/benchmarks/projects/todo-js/package.json +11 -0
  63. package/benchmarks/projects/todo-js/src/index.js +2 -0
  64. package/benchmarks/projects/todo-js/src/taskService.js +37 -0
  65. package/benchmarks/projects/todo-js/src/taskStore.js +28 -0
  66. package/benchmarks/projects/todo-js/tests/taskService.test.js +45 -0
  67. package/benchmarks/projects/todo-js/vitest.config.js +5 -0
  68. package/benchmarks/projects/todo-mixed-ts-py/README.md +3 -0
  69. package/benchmarks/projects/todo-mixed-ts-py/package.json +13 -0
  70. package/benchmarks/projects/todo-mixed-ts-py/python/task_service.py +76 -0
  71. package/benchmarks/projects/todo-mixed-ts-py/src/taskCli.ts +38 -0
  72. package/benchmarks/projects/todo-mixed-ts-py/tests/mixedBoundary.test.ts +18 -0
  73. package/benchmarks/projects/todo-mixed-ts-py/tsconfig.json +12 -0
  74. package/benchmarks/projects/todo-mixed-ts-py/vitest.config.ts +5 -0
  75. package/benchmarks/projects/todo-python/README.md +3 -0
  76. package/benchmarks/projects/todo-python/src/__init__.py +4 -0
  77. package/benchmarks/projects/todo-python/src/task_service.py +32 -0
  78. package/benchmarks/projects/todo-python/src/task_store.py +28 -0
  79. package/benchmarks/projects/todo-python/tests/test_task_service.py +52 -0
  80. package/benchmarks/projects/todo-ts/README.md +3 -0
  81. package/benchmarks/projects/todo-ts/package.json +12 -0
  82. package/benchmarks/projects/todo-ts/src/index.ts +2 -0
  83. package/benchmarks/projects/todo-ts/src/taskService.ts +41 -0
  84. package/benchmarks/projects/todo-ts/src/taskStore.ts +34 -0
  85. package/benchmarks/projects/todo-ts/tests/taskService.test.ts +45 -0
  86. package/benchmarks/projects/todo-ts/tsconfig.json +12 -0
  87. package/benchmarks/projects/todo-ts/vitest.config.ts +5 -0
  88. package/dist/scripts/build-gallery.js +3 -0
  89. package/dist/scripts/capture-demo-report.js +3 -0
  90. package/dist/scripts/evaluate-token-savings.js +2 -0
  91. package/dist/scripts/experiments/describeExperiment.js +143 -0
  92. package/dist/scripts/experiments/listExperiments.js +44 -0
  93. package/dist/scripts/experiments/runExperiment.js +199 -0
  94. package/dist/scripts/generate-experiment-plots.js +3 -0
  95. package/dist/scripts/generate-prompt-variants.js +2 -0
  96. package/dist/scripts/render-experiment-report.js +2 -0
  97. package/dist/scripts/run-agent-prompt.js +2 -0
  98. package/dist/scripts/run-controlled-experiment.js +2 -0
  99. package/dist/scripts/run-final-demo.js +3 -0
  100. package/dist/scripts/run-lab-demo.js +5 -0
  101. package/dist/scripts/run-visualization-demos.js +3 -0
  102. package/dist/scripts/security/runCodeql.js +57 -0
  103. package/dist/scripts/security/runDependencyChecks.js +57 -0
  104. package/dist/scripts/security/runFuzzSmoke.js +29 -0
  105. package/dist/scripts/security/runPackageChecks.js +56 -0
  106. package/dist/scripts/security/runSemgrep.js +63 -0
  107. package/dist/scripts/security/validate.js +117 -0
  108. package/dist/scripts/verify-benchmarks.js +202 -0
  109. package/dist/src/agents/adapters/claudeAdapter.js +37 -0
  110. package/dist/src/agents/adapters/codexAdapter.js +110 -0
  111. package/dist/src/agents/adapters/fakeAgentAdapter.js +101 -0
  112. package/dist/src/agents/agentRegistry.js +21 -0
  113. package/dist/src/agents/index.js +7 -0
  114. package/dist/src/agents/parseAgentTokenUsage.js +137 -0
  115. package/dist/src/agents/runAgentPrompt.js +38 -0
  116. package/dist/src/agents/types.js +1 -0
  117. package/dist/src/commands/buildGalleryCommand.js +56 -0
  118. package/dist/src/commands/captureDemoReport.js +116 -0
  119. package/dist/src/commands/evaluateTokenSavings.js +175 -0
  120. package/dist/src/commands/generateExperimentPlotsCommand.js +38 -0
  121. package/dist/src/commands/generatePromptVariants.js +67 -0
  122. package/dist/src/commands/renderExperimentReportCommand.js +131 -0
  123. package/dist/src/commands/runAgentPromptCommand.js +132 -0
  124. package/dist/src/commands/runControlledExperimentCommand.js +174 -0
  125. package/dist/src/commands/runFinalDemoCommand.js +123 -0
  126. package/dist/src/commands/runLabDemo.js +62 -0
  127. package/dist/src/commands/runVisualizationDemosCommand.js +67 -0
  128. package/dist/src/core/commandLine.js +59 -0
  129. package/dist/src/core/countTokens.js +8 -0
  130. package/dist/src/core/fileGlobs.js +100 -0
  131. package/dist/src/core/localProjectTarget.js +75 -0
  132. package/dist/src/core/pathSafety.js +19 -0
  133. package/dist/src/core/pythonCommand.js +30 -0
  134. package/dist/src/core/resolveCommand.js +110 -0
  135. package/dist/src/core/runMeasuredCommand.js +143 -0
  136. package/dist/src/evaluation/benchmarkMetadata.js +207 -0
  137. package/dist/src/evaluation/buildExperimentMatrix.js +75 -0
  138. package/dist/src/evaluation/classifyAgentRunOutcome.js +40 -0
  139. package/dist/src/evaluation/compareExperimentRuns.js +79 -0
  140. package/dist/src/evaluation/compareTokenSavings.js +47 -0
  141. package/dist/src/evaluation/controlledExperimentTypes.js +1 -0
  142. package/dist/src/evaluation/index.js +18 -0
  143. package/dist/src/evaluation/parseAgentAnswer.js +230 -0
  144. package/dist/src/evaluation/projectComplexity.js +126 -0
  145. package/dist/src/evaluation/projectFileTree.js +83 -0
  146. package/dist/src/evaluation/readEvaluationCases.js +59 -0
  147. package/dist/src/evaluation/renderTokenSavingsReportInput.js +55 -0
  148. package/dist/src/evaluation/runControlledExperiment.js +158 -0
  149. package/dist/src/evaluation/runMyDevKitRetrieval.js +197 -0
  150. package/dist/src/evaluation/runRawFullFileBaseline.js +31 -0
  151. package/dist/src/evaluation/scoreCorrectness.js +127 -0
  152. package/dist/src/evaluation/types.js +1 -0
  153. package/dist/src/evaluation/writeExperimentArtifacts.js +104 -0
  154. package/dist/src/evaluation/writeTokenSavingsArtifacts.js +57 -0
  155. package/dist/src/experiments/config.js +24 -0
  156. package/dist/src/experiments/defaultRegistry.js +7 -0
  157. package/dist/src/experiments/errors.js +18 -0
  158. package/dist/src/experiments/index.js +9 -0
  159. package/dist/src/experiments/outputPaths.js +25 -0
  160. package/dist/src/experiments/plugins/contextStrategyComparison/config.js +37 -0
  161. package/dist/src/experiments/plugins/contextStrategyComparison/index.js +3 -0
  162. package/dist/src/experiments/plugins/contextStrategyComparison/plugin.js +83 -0
  163. package/dist/src/experiments/plugins/contextStrategyComparison/resultMapping.js +260 -0
  164. package/dist/src/experiments/plugins/index.js +1 -0
  165. package/dist/src/experiments/registry.js +43 -0
  166. package/dist/src/experiments/results.js +48 -0
  167. package/dist/src/experiments/runner.js +181 -0
  168. package/dist/src/experiments/target.js +8 -0
  169. package/dist/src/experiments/types.js +1 -0
  170. package/dist/src/gallery/index.js +2 -0
  171. package/dist/src/gallery/types.js +1 -0
  172. package/dist/src/gallery/writeGalleryManifest.js +214 -0
  173. package/dist/src/index.js +12 -0
  174. package/dist/src/plots/buildExperimentPlotData.js +137 -0
  175. package/dist/src/plots/index.js +4 -0
  176. package/dist/src/plots/renderSvgChart.js +82 -0
  177. package/dist/src/plots/types.js +1 -0
  178. package/dist/src/plots/writePlotArtifacts.js +46 -0
  179. package/dist/src/prompts/buildPromptContext.js +68 -0
  180. package/dist/src/prompts/generateMyDevKitPrompt.js +106 -0
  181. package/dist/src/prompts/generatePromptVariants.js +36 -0
  182. package/dist/src/prompts/generateRawFullFilePrompt.js +97 -0
  183. package/dist/src/prompts/index.js +7 -0
  184. package/dist/src/prompts/measurePromptComplexity.js +41 -0
  185. package/dist/src/prompts/types.js +1 -0
  186. package/dist/src/prompts/writePromptArtifacts.js +43 -0
  187. package/dist/src/report/buildExperimentReportInput.js +339 -0
  188. package/dist/src/report/experimentReportTypes.js +1 -0
  189. package/dist/src/report/experiments/buildPluginExperimentReport.js +153 -0
  190. package/dist/src/report/experiments/experimentReportModel.js +1 -0
  191. package/dist/src/report/experiments/index.js +4 -0
  192. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +133 -0
  193. package/dist/src/report/experiments/writePluginExperimentReports.js +30 -0
  194. package/dist/src/report/index.js +8 -0
  195. package/dist/src/report/renderExperimentHtmlReport.js +354 -0
  196. package/dist/src/report/renderHtmlReport.js +103 -0
  197. package/dist/src/report/types.js +10 -0
  198. package/dist/src/report/writeExperimentReportArtifacts.js +38 -0
  199. package/dist/src/report/writeReportArtifacts.js +39 -0
  200. package/dist/src/screenshot/captureReportScreenshot.js +75 -0
  201. package/dist/src/screenshot/index.js +2 -0
  202. package/dist/src/screenshot/types.js +1 -0
  203. package/dist/src/securityValidation/artifacts.js +15 -0
  204. package/dist/src/securityValidation/cliAdversarial/adversarialCliConfig.js +38 -0
  205. package/dist/src/securityValidation/cliAdversarial/dataVolumeChecks.js +194 -0
  206. package/dist/src/securityValidation/cliAdversarial/jsonStdoutChecks.js +359 -0
  207. package/dist/src/securityValidation/cliAdversarial/malformedArtifactChecks.js +284 -0
  208. package/dist/src/securityValidation/cliAdversarial/malformedArtifactFixtures.js +79 -0
  209. package/dist/src/securityValidation/cliAdversarial/pathBoundaryChecks.js +431 -0
  210. package/dist/src/securityValidation/cliAdversarial/pathCases.js +144 -0
  211. package/dist/src/securityValidation/cliAdversarial/readOnlyBoundaryChecks.js +294 -0
  212. package/dist/src/securityValidation/cliAdversarial/runAdversarialCheck.js +149 -0
  213. package/dist/src/securityValidation/cliAdversarial/subprocessSafetyChecks.js +214 -0
  214. package/dist/src/securityValidation/cliAdversarial/tempWorkspace.js +160 -0
  215. package/dist/src/securityValidation/commandRunner.js +136 -0
  216. package/dist/src/securityValidation/config.js +39 -0
  217. package/dist/src/securityValidation/dependencies/parseNpmAudit.js +115 -0
  218. package/dist/src/securityValidation/dependencies/parseNpmLs.js +71 -0
  219. package/dist/src/securityValidation/dependencies/parseNpmOutdated.js +41 -0
  220. package/dist/src/securityValidation/dependencies/runDependencyChecks.js +239 -0
  221. package/dist/src/securityValidation/dependencies/runOsvScanner.js +43 -0
  222. package/dist/src/securityValidation/fuzz/fuzzHarness.js +61 -0
  223. package/dist/src/securityValidation/fuzz/fuzzTargets.js +204 -0
  224. package/dist/src/securityValidation/fuzz/randomInput.js +0 -0
  225. package/dist/src/securityValidation/index.js +34 -0
  226. package/dist/src/securityValidation/packageChecks/forbiddenPackageContents.js +67 -0
  227. package/dist/src/securityValidation/packageChecks/parseNpmPackDryRun.js +56 -0
  228. package/dist/src/securityValidation/packageChecks/runPackageChecks.js +88 -0
  229. package/dist/src/securityValidation/report/renderSecurityReport.js +248 -0
  230. package/dist/src/securityValidation/report/securityReportTypes.js +1 -0
  231. package/dist/src/securityValidation/staticScans/codeql.js +66 -0
  232. package/dist/src/securityValidation/staticScans/semgrep.js +180 -0
  233. package/dist/src/securityValidation/testMatrix.js +535 -0
  234. package/dist/src/securityValidation/types.js +34 -0
  235. package/dist/src/securityValidation/validate/resolveTarget.js +32 -0
  236. package/dist/src/securityValidation/validate/runSecurityValidation.js +169 -0
  237. package/dist/src/securityValidation/validate/verdict.js +73 -0
  238. package/dist/src/visualizationDemos/buildMyDevKitVisualizationCommands.js +59 -0
  239. package/dist/src/visualizationDemos/index.js +4 -0
  240. package/dist/src/visualizationDemos/runVisualizationDemos.js +82 -0
  241. package/dist/src/visualizationDemos/types.js +1 -0
  242. package/dist/src/visualizationDemos/writeVisualizationDemoArtifacts.js +25 -0
  243. package/docs/METRICS.md +286 -0
  244. package/examples/demo-report-input.json +78 -0
  245. package/examples/lab-demo-cases.json +35 -0
  246. package/examples/real-agent-campaign-cases.json +118 -0
  247. package/examples/token-savings-cases.json +122 -0
  248. package/package.json +91 -0
  249. package/tests/fixtures/fake-adversarial-cli.js +152 -0
  250. package/tests/fixtures/fake-my-dev-kit-cli.js +83 -0
@@ -0,0 +1,181 @@
1
+ import path from "node:path";
2
+ import { createDefaultExperimentPluginRegistry } from "./defaultRegistry.js";
3
+ import { buildDefaultExperimentOutputRoot, isPathInside } from "./outputPaths.js";
4
+ import { resolveExperimentTarget } from "./target.js";
5
+ export async function runExperiment(options) {
6
+ const toolRoot = path.resolve(options.toolRoot ?? process.cwd());
7
+ const registry = options.registry ?? createDefaultExperimentPluginRegistry();
8
+ const plugin = registry.get(options.pluginId);
9
+ const runId = options.runId ?? defaultRunId(options.pluginId);
10
+ const startedAt = options.startedAt ?? new Date();
11
+ const target = resolveExperimentTarget(options.targetPath, toolRoot);
12
+ const validation = plugin.validateConfig(options.config ?? {});
13
+ if (!validation.valid || validation.config === undefined) {
14
+ throw new Error(`Invalid experiment config for ${plugin.metadata.id}: ${validation.errors.join("; ")}`);
15
+ }
16
+ const outputRoot = resolveExperimentOutputRoot({
17
+ toolRoot,
18
+ pluginId: plugin.metadata.id,
19
+ runId,
20
+ target,
21
+ optionsOutputRoot: options.outputRoot,
22
+ inputConfig: options.config,
23
+ });
24
+ assertOutputRootAllowed({ outputRoot, target });
25
+ const context = buildExperimentContext({
26
+ runId,
27
+ startedAt,
28
+ pluginId: plugin.metadata.id,
29
+ pluginSchemaVersion: plugin.metadata.schemaVersion,
30
+ toolRoot,
31
+ target,
32
+ outputRoot,
33
+ config: withOutputRoot(validation.config, outputRoot),
34
+ inputs: options.inputs,
35
+ logger: options.logger,
36
+ });
37
+ try {
38
+ await plugin.prepare?.(context);
39
+ const result = await plugin.run(context);
40
+ const summary = result.summary ?? plugin.summarize?.(result, context);
41
+ return normalizeResult({
42
+ result,
43
+ summary,
44
+ validation,
45
+ outputRoot,
46
+ pluginSchemaVersion: plugin.metadata.schemaVersion,
47
+ });
48
+ }
49
+ catch (error) {
50
+ return buildFailedRun({
51
+ error,
52
+ runId,
53
+ startedAt,
54
+ pluginId: plugin.metadata.id,
55
+ pluginSchemaVersion: plugin.metadata.schemaVersion,
56
+ target,
57
+ outputRoot,
58
+ validationWarnings: validation.warnings,
59
+ });
60
+ }
61
+ finally {
62
+ await plugin.cleanup?.(context);
63
+ }
64
+ }
65
+ function buildExperimentContext(args) {
66
+ return {
67
+ runId: args.runId,
68
+ startedAt: args.startedAt,
69
+ pluginId: args.pluginId,
70
+ pluginSchemaVersion: args.pluginSchemaVersion,
71
+ toolRoot: args.toolRoot,
72
+ targetRoot: args.target.targetRoot,
73
+ isSelfTarget: args.target.isSelf,
74
+ target: args.target,
75
+ config: args.config,
76
+ outputRoot: args.outputRoot,
77
+ environment: {
78
+ platform: process.platform,
79
+ nodeVersion: process.version,
80
+ },
81
+ inputs: args.inputs,
82
+ logger: args.logger,
83
+ };
84
+ }
85
+ function resolveExperimentOutputRoot(args) {
86
+ const explicitOutDir = readStringField(args.inputConfig, "outDir");
87
+ const outputRoot = args.optionsOutputRoot ??
88
+ explicitOutDir ??
89
+ buildDefaultExperimentOutputRoot({
90
+ toolRoot: args.toolRoot,
91
+ pluginId: args.pluginId,
92
+ runId: args.runId,
93
+ target: args.target,
94
+ });
95
+ return path.isAbsolute(outputRoot) ? path.resolve(outputRoot) : path.resolve(args.toolRoot, outputRoot);
96
+ }
97
+ function assertOutputRootAllowed(args) {
98
+ if (!args.target.isSelf && isPathInside(args.target.targetRoot, args.outputRoot)) {
99
+ throw new Error(`Experiment output root must not be inside the external target project: ${args.outputRoot}`);
100
+ }
101
+ }
102
+ function withOutputRoot(config, outputRoot) {
103
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
104
+ return config;
105
+ }
106
+ return {
107
+ ...config,
108
+ outDir: outputRoot,
109
+ };
110
+ }
111
+ function normalizeResult(args) {
112
+ const validationWarnings = args.validation.warnings.map((message) => ({
113
+ code: "config-validation-warning",
114
+ message,
115
+ }));
116
+ const warnings = [...args.result.warnings, ...validationWarnings];
117
+ return {
118
+ ...args.result,
119
+ summary: args.summary ?? args.result.summary,
120
+ warnings,
121
+ metadata: {
122
+ ...args.result.metadata,
123
+ outputRoot: args.outputRoot,
124
+ pluginSchemaVersion: args.pluginSchemaVersion,
125
+ },
126
+ };
127
+ }
128
+ function buildFailedRun(args) {
129
+ const completedAt = new Date().toISOString();
130
+ const failure = {
131
+ code: "experiment-run-failed",
132
+ message: args.error instanceof Error ? args.error.message : String(args.error),
133
+ recoverable: false,
134
+ };
135
+ const warnings = args.validationWarnings.map((message) => ({
136
+ code: "config-validation-warning",
137
+ message,
138
+ }));
139
+ const artifacts = [];
140
+ const summary = {
141
+ status: "failed",
142
+ totalCases: 0,
143
+ completedCases: 0,
144
+ partialCases: 0,
145
+ failedCases: 0,
146
+ skippedCases: 0,
147
+ metrics: [],
148
+ warnings,
149
+ failures: [failure],
150
+ };
151
+ return {
152
+ runId: args.runId,
153
+ pluginId: args.pluginId,
154
+ startedAt: args.startedAt.toISOString(),
155
+ completedAt,
156
+ status: "failed",
157
+ target: args.target,
158
+ variants: [],
159
+ cases: [],
160
+ metrics: [],
161
+ artifacts,
162
+ warnings,
163
+ failures: [failure],
164
+ summary,
165
+ metadata: {
166
+ outputRoot: args.outputRoot,
167
+ pluginSchemaVersion: args.pluginSchemaVersion,
168
+ },
169
+ };
170
+ }
171
+ function readStringField(value, key) {
172
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
173
+ return undefined;
174
+ }
175
+ const field = value[key];
176
+ return typeof field === "string" && field.trim() ? field : undefined;
177
+ }
178
+ function defaultRunId(pluginId) {
179
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
180
+ return `${pluginId}-${timestamp}`;
181
+ }
@@ -0,0 +1,8 @@
1
+ import { resolveLocalProjectTarget } from "../core/localProjectTarget.js";
2
+ export function resolveExperimentTarget(targetPathArg, toolRoot) {
3
+ const target = resolveLocalProjectTarget(targetPathArg, toolRoot);
4
+ return {
5
+ ...target,
6
+ kind: target.isSelf ? "self" : "external-local",
7
+ };
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export * from "./writeGalleryManifest.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,214 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ function toRelativePath(outDir, targetPath) {
5
+ if (!targetPath) {
6
+ return undefined;
7
+ }
8
+ return path.relative(outDir, targetPath).replace(/\\/g, "/");
9
+ }
10
+ function inferItemStatus(summary, screenshot, warnings) {
11
+ if (summary.completedCaseCount === 0) {
12
+ return "skipped";
13
+ }
14
+ if (screenshot.status === "failed" || warnings.length > 0 || summary.skippedCaseCount > 0) {
15
+ return "warning";
16
+ }
17
+ return "pass";
18
+ }
19
+ export async function writeGalleryManifest(options) {
20
+ const outDir = path.resolve(options.outDir);
21
+ const manifestPath = path.join(outDir, "gallery-manifest.json");
22
+ const requiredArtifactEntries = [
23
+ ["summaryPath", options.artifactPaths.summaryPath],
24
+ ["runsPath", options.artifactPaths.runsPath],
25
+ ["htmlPath", options.artifactPaths.htmlPath]
26
+ ];
27
+ for (const [label, artifactPath] of requiredArtifactEntries) {
28
+ if (!artifactPath) {
29
+ throw new Error(`Missing required artifact path: ${label}`);
30
+ }
31
+ if (!existsSync(artifactPath)) {
32
+ throw new Error(`Required artifact does not exist: ${artifactPath}`);
33
+ }
34
+ }
35
+ const warnings = [...options.warnings];
36
+ if (options.screenshot.warning) {
37
+ warnings.push(options.screenshot.warning);
38
+ }
39
+ if (options.screenshot.status === "failed" && options.screenshot.error) {
40
+ warnings.push(`PNG screenshot capture failed: ${options.screenshot.error}`);
41
+ }
42
+ const itemStatus = inferItemStatus(options.summary, options.screenshot, warnings);
43
+ const manifest = {
44
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
45
+ projectName: "my-dev-kit-lab",
46
+ title: "my-dev-kit-lab demo gallery",
47
+ description: "Deterministic Milestone 1 MVP artifacts combining benchmark validation, token-savings evaluation, reports, and optional screenshots.",
48
+ outputDirectory: ".",
49
+ items: [
50
+ {
51
+ id: "token-savings-demo",
52
+ title: "Token savings demo",
53
+ description: "Static context comparison between raw full-file reading and external my-dev-kit retrieval.",
54
+ kind: "token-savings-report",
55
+ status: itemStatus,
56
+ htmlPath: toRelativePath(outDir, options.artifactPaths.htmlPath) ?? "token-savings-report.html",
57
+ screenshotPath: options.screenshot.status === "captured" && existsSync(options.artifactPaths.pngPath)
58
+ ? toRelativePath(outDir, options.artifactPaths.pngPath)
59
+ : undefined,
60
+ summaryPath: toRelativePath(outDir, options.artifactPaths.summaryPath),
61
+ runsPath: toRelativePath(outDir, options.artifactPaths.runsPath),
62
+ metrics: [
63
+ { id: "case-count", label: "Case count", value: options.summary.caseCount },
64
+ { id: "completed-case-count", label: "Completed case count", value: options.summary.completedCaseCount },
65
+ { id: "skipped-case-count", label: "Skipped case count", value: options.summary.skippedCaseCount },
66
+ { id: "average-raw-tokens", label: "Average raw tokens", value: options.summary.averageRawTokens.toFixed(2) },
67
+ { id: "average-my-dev-kit-tokens", label: "Average my-dev-kit tokens", value: options.summary.averageMyDevKitTokens.toFixed(2) },
68
+ { id: "average-tokens-saved", label: "Average tokens saved", value: options.summary.averageTokensSaved.toFixed(2) },
69
+ { id: "average-percent-saved", label: "Average percent saved", value: options.summary.averagePercentSaved.toFixed(2), unit: "%" },
70
+ { id: "token-count-method", label: "Token count method", value: options.summary.tokenCountMethod }
71
+ ],
72
+ warnings
73
+ }
74
+ ],
75
+ warnings
76
+ };
77
+ await mkdir(outDir, { recursive: true });
78
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
79
+ return { manifest, manifestPath };
80
+ }
81
+ export async function writeExperimentGalleryManifest(options) {
82
+ const outDir = path.resolve(options.outDir);
83
+ await mkdir(outDir, { recursive: true });
84
+ const warnings = [...(options.warnings ?? [])];
85
+ const items = [];
86
+ if (options.experimentDir) {
87
+ items.push({
88
+ id: "controlled-experiment",
89
+ title: "Controlled experiment artifacts",
90
+ description: "Raw controlled experiment JSON artifacts and per-run records.",
91
+ kind: "controlled-experiment",
92
+ status: existsSync(path.join(options.experimentDir, "experiment-summary.json")) ? "pass" : "warning",
93
+ htmlPath: "",
94
+ summaryPath: toRelativePath(outDir, path.join(options.experimentDir, "experiment-summary.json")),
95
+ runsPath: toRelativePath(outDir, path.join(options.experimentDir, "experiment-runs.json")),
96
+ artifactPaths: [path.join(options.experimentDir, "experiment-comparisons.json"), path.join(options.experimentDir, "experiment-config.json")]
97
+ .filter(existsSync)
98
+ .map((artifactPath) => toRelativePath(outDir, artifactPath) ?? artifactPath),
99
+ tags: ["experiment"],
100
+ metrics: [],
101
+ warnings: []
102
+ });
103
+ }
104
+ if (options.reportDir) {
105
+ const pngPath = path.join(options.reportDir, "experiment-report.png");
106
+ items.push({
107
+ id: "experiment-report",
108
+ title: "Experiment report",
109
+ description: "Final controlled experiment HTML report.",
110
+ kind: "experiment-report",
111
+ status: existsSync(path.join(options.reportDir, "experiment-report.html")) ? "pass" : "warning",
112
+ htmlPath: toRelativePath(outDir, path.join(options.reportDir, "experiment-report.html")) ?? "experiment-report.html",
113
+ screenshotPath: existsSync(pngPath) ? toRelativePath(outDir, pngPath) : undefined,
114
+ summaryPath: toRelativePath(outDir, path.join(options.reportDir, "experiment-report.json")),
115
+ artifactPaths: [path.join(options.reportDir, "experiment-report-artifacts.json")]
116
+ .filter(existsSync)
117
+ .map((artifactPath) => toRelativePath(outDir, artifactPath) ?? artifactPath),
118
+ tags: ["report"],
119
+ metrics: [],
120
+ warnings: []
121
+ });
122
+ }
123
+ if (options.plotsDir) {
124
+ const chartPaths = await listSvgCharts(path.join(options.plotsDir, "charts"));
125
+ items.push({
126
+ id: "experiment-plots",
127
+ title: "Experiment plots",
128
+ description: "Static SVG charts and plot-ready data from controlled experiment comparisons.",
129
+ kind: "experiment-plots",
130
+ status: chartPaths.length > 0 ? "pass" : "warning",
131
+ htmlPath: "",
132
+ summaryPath: toRelativePath(outDir, path.join(options.plotsDir, "plots-summary.json")),
133
+ runsPath: toRelativePath(outDir, path.join(options.plotsDir, "plot-data.json")),
134
+ artifactPaths: chartPaths.map((artifactPath) => toRelativePath(outDir, artifactPath) ?? artifactPath),
135
+ tags: ["plots", "charts"],
136
+ metrics: [{ id: "chart-count", label: "Chart count", value: chartPaths.length }],
137
+ warnings: chartPaths.length === 0 ? ["No chart SVG files found."] : []
138
+ });
139
+ }
140
+ if (options.visualizationsDir) {
141
+ const artifacts = await listFiles(path.join(options.visualizationsDir, "artifacts"));
142
+ items.push({
143
+ id: "visualization-demo",
144
+ title: "my-dev-kit visualization demos",
145
+ description: "Bounded my-dev-kit visualization command smoke artifacts.",
146
+ kind: "visualization-demo",
147
+ status: existsSync(path.join(options.visualizationsDir, "visualization-demo-summary.json")) ? "pass" : "warning",
148
+ htmlPath: "",
149
+ summaryPath: toRelativePath(outDir, path.join(options.visualizationsDir, "visualization-demo-summary.json")),
150
+ runsPath: toRelativePath(outDir, path.join(options.visualizationsDir, "visualization-demo-runs.json")),
151
+ artifactPaths: artifacts.map((artifactPath) => toRelativePath(outDir, artifactPath) ?? artifactPath),
152
+ tags: ["visualization"],
153
+ metrics: [{ id: "artifact-count", label: "Artifact count", value: artifacts.length }],
154
+ warnings: []
155
+ });
156
+ }
157
+ items.push({
158
+ id: "final-demo",
159
+ title: "Final demo artifact index",
160
+ description: "Combined artifact set for the final my-dev-kit-lab batch demo.",
161
+ kind: "final-demo",
162
+ status: warnings.length > 0 ? "warning" : "pass",
163
+ htmlPath: "gallery-index.html",
164
+ tags: ["final-demo"],
165
+ metrics: [{ id: "item-count", label: "Gallery item count", value: items.length }],
166
+ warnings
167
+ });
168
+ const manifest = {
169
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
170
+ projectName: "my-dev-kit-lab",
171
+ title: options.title ?? "my-dev-kit-lab final demo gallery",
172
+ description: options.description ?? "Controlled experiment report, plots, visualization demos, and final artifact index.",
173
+ outputDirectory: ".",
174
+ items,
175
+ warnings
176
+ };
177
+ const manifestPath = path.join(outDir, "gallery-manifest.json");
178
+ const indexPath = path.join(outDir, "gallery-index.html");
179
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
180
+ await writeFile(indexPath, renderGalleryIndex(manifest), "utf8");
181
+ return { manifest, manifestPath, indexPath };
182
+ }
183
+ async function listSvgCharts(chartsDir) {
184
+ return (await listFiles(chartsDir)).filter((filePath) => filePath.endsWith(".svg"));
185
+ }
186
+ async function listFiles(dir) {
187
+ try {
188
+ const entries = await readdir(dir, { withFileTypes: true });
189
+ const nested = await Promise.all(entries.map((entry) => {
190
+ const entryPath = path.join(dir, entry.name);
191
+ return entry.isDirectory() ? listFiles(entryPath) : Promise.resolve([entryPath]);
192
+ }));
193
+ return nested.flat().sort();
194
+ }
195
+ catch {
196
+ return [];
197
+ }
198
+ }
199
+ function renderGalleryIndex(manifest) {
200
+ return `<!DOCTYPE html>
201
+ <html lang="en">
202
+ <head><meta charset="utf-8" /><title>${escapeHtml(manifest.title)}</title><style>body{font-family:Arial,Helvetica,sans-serif;margin:32px;color:#17212b}section{border:1px solid #d9e1ea;border-radius:8px;padding:16px;margin:12px 0}code{background:#f2f4f7;padding:2px 4px}</style></head>
203
+ <body><h1>${escapeHtml(manifest.title)}</h1><p>${escapeHtml(manifest.description)}</p>${manifest.items
204
+ .map((item) => `<section><h2>${escapeHtml(item.title)}</h2><p>${escapeHtml(item.description)}</p><p>Status: <strong>${escapeHtml(item.status)}</strong></p><p>Kind: ${escapeHtml(item.kind)}</p>${item.htmlPath ? `<p>HTML: <code>${escapeHtml(item.htmlPath)}</code></p>` : ""}${item.summaryPath ? `<p>Summary: <code>${escapeHtml(item.summaryPath)}</code></p>` : ""}${item.screenshotPath ? `<p>Screenshot: <code>${escapeHtml(item.screenshotPath)}</code></p>` : ""}${item.artifactPaths?.length ? `<ul>${item.artifactPaths.map((artifact) => `<li><code>${escapeHtml(artifact)}</code></li>`).join("")}</ul>` : ""}</section>`)
205
+ .join("")}</body></html>`;
206
+ }
207
+ function escapeHtml(value) {
208
+ return String(value ?? "")
209
+ .replaceAll("&", "&amp;")
210
+ .replaceAll("<", "&lt;")
211
+ .replaceAll(">", "&gt;")
212
+ .replaceAll('"', "&quot;")
213
+ .replaceAll("'", "&#39;");
214
+ }
@@ -0,0 +1,12 @@
1
+ export const projectName = "my-dev-kit-lab";
2
+ export function describeLab() {
3
+ return `${projectName} hosts deterministic benchmark projects and validation workflows for my-dev-kit.`;
4
+ }
5
+ export * from "./report/index.js";
6
+ export * from "./screenshot/index.js";
7
+ export * from "./evaluation/index.js";
8
+ export * from "./prompts/index.js";
9
+ export * from "./agents/index.js";
10
+ export * from "./gallery/index.js";
11
+ export * from "./plots/index.js";
12
+ export * from "./visualizationDemos/index.js";
@@ -0,0 +1,137 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function buildExperimentPlotData(options) {
4
+ const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
5
+ const experimentDir = path.resolve(repoRoot, options.experimentDir);
6
+ const runsPayload = JSON.parse(await readFile(path.join(experimentDir, "experiment-runs.json"), "utf8"));
7
+ const comparisonsPayload = JSON.parse(await readFile(path.join(experimentDir, "experiment-comparisons.json"), "utf8"));
8
+ return buildExperimentPlotDataFromRecords({
9
+ experimentDir,
10
+ runs: runsPayload.runs ?? [],
11
+ comparisons: comparisonsPayload.comparisons ?? [],
12
+ generatedAt: options.generatedAt
13
+ });
14
+ }
15
+ export function buildExperimentPlotDataFromRecords(args) {
16
+ const skippedPoints = [];
17
+ const warnings = [];
18
+ const runById = new Map(args.runs.map((run) => [run.runId, run]));
19
+ const plots = [
20
+ scatter("token-savings-vs-prompt-length", "Token savings vs prompt length", "Prompt estimated tokens", "Token savings percent"),
21
+ scatter("time-reduction-vs-prompt-length", "Execution time reduction vs prompt length", "Prompt estimated tokens", "Duration reduction percent"),
22
+ scatter("token-savings-vs-project-complexity", "Token savings vs project complexity", "Project complexity score", "Token savings percent"),
23
+ scatter("time-reduction-vs-project-complexity", "Execution time reduction vs project complexity", "Project complexity score", "Duration reduction percent"),
24
+ { id: "correctness-by-strategy", title: "Correctness score by strategy", xLabel: "Prompt strategy", yLabel: "Correctness score", kind: "bar", points: [], warnings: [] },
25
+ { id: "run-outcomes-by-agent", title: "Run outcome counts by agent", xLabel: "Agent", yLabel: "Run count", kind: "bar", points: [], warnings: [] }
26
+ ];
27
+ const byId = new Map(plots.map((plot) => [plot.id, plot]));
28
+ for (const comparison of args.comparisons) {
29
+ const rawRun = comparison.rawRunId ? runById.get(comparison.rawRunId) : undefined;
30
+ const guidedRun = comparison.myDevKitRunId ? runById.get(comparison.myDevKitRunId) : undefined;
31
+ const promptTokens = guidedRun?.promptMetrics.promptEstimatedTokens ?? rawRun?.promptMetrics.promptEstimatedTokens;
32
+ const projectComplexityScore = guidedRun?.projectComplexityScore ?? rawRun?.projectComplexityScore;
33
+ const label = [comparison.caseId, comparison.benchmarkProject, comparison.complexityLevel].join(" / ");
34
+ addComparisonPoint({
35
+ plot: byId.get("token-savings-vs-prompt-length"),
36
+ skippedPoints,
37
+ comparison,
38
+ x: promptTokens,
39
+ y: comparison.tokenSavingsPercent,
40
+ label,
41
+ reason: comparison.tokenComparisonAvailable ? undefined : "Token comparison unavailable."
42
+ });
43
+ addComparisonPoint({
44
+ plot: byId.get("time-reduction-vs-prompt-length"),
45
+ skippedPoints,
46
+ comparison,
47
+ x: promptTokens,
48
+ y: comparison.durationReductionPercent,
49
+ label,
50
+ reason: typeof comparison.durationReductionPercent === "number" ? undefined : "Duration comparison unavailable."
51
+ });
52
+ addComparisonPoint({
53
+ plot: byId.get("token-savings-vs-project-complexity"),
54
+ skippedPoints,
55
+ comparison,
56
+ x: projectComplexityScore,
57
+ y: comparison.tokenSavingsPercent,
58
+ label,
59
+ reason: comparison.tokenComparisonAvailable ? undefined : "Token comparison unavailable."
60
+ });
61
+ addComparisonPoint({
62
+ plot: byId.get("time-reduction-vs-project-complexity"),
63
+ skippedPoints,
64
+ comparison,
65
+ x: projectComplexityScore,
66
+ y: comparison.durationReductionPercent,
67
+ label,
68
+ reason: typeof comparison.durationReductionPercent === "number" ? undefined : "Duration comparison unavailable."
69
+ });
70
+ }
71
+ for (const run of args.runs) {
72
+ byId.get("correctness-by-strategy").points.push({
73
+ x: strategyIndex(run.promptStrategy),
74
+ y: run.correctness.correctnessScore,
75
+ group: run.agentId,
76
+ label: [run.caseId, run.benchmarkProject, run.promptComplexityLevel, run.promptStrategy].join(" / "),
77
+ metadata: { strategy: run.promptStrategy, status: run.status }
78
+ });
79
+ }
80
+ for (const [key, count] of countOutcomeGroups(args.runs)) {
81
+ const [agentId, status] = key.split("|");
82
+ byId.get("run-outcomes-by-agent").points.push({
83
+ x: agentIndex(agentId),
84
+ y: count,
85
+ group: status,
86
+ label: `${agentId} ${status}`,
87
+ metadata: { agentId, status }
88
+ });
89
+ }
90
+ for (const plot of plots) {
91
+ if (plot.points.length === 0) {
92
+ plot.warnings.push("No comparable data available.");
93
+ warnings.push(`${plot.title}: no comparable data available.`);
94
+ }
95
+ }
96
+ return {
97
+ generatedAt: args.generatedAt ?? new Date().toISOString(),
98
+ sourceExperimentDir: args.experimentDir,
99
+ plots,
100
+ skippedPoints,
101
+ warnings
102
+ };
103
+ }
104
+ function scatter(id, title, xLabel, yLabel) {
105
+ return { id, title, xLabel, yLabel, kind: "scatter", points: [], warnings: [] };
106
+ }
107
+ function addComparisonPoint(args) {
108
+ if (args.reason || typeof args.x !== "number" || typeof args.y !== "number" || !Number.isFinite(args.x) || !Number.isFinite(args.y)) {
109
+ args.skippedPoints.push({ plotId: args.plot.id, label: args.label, reason: args.reason ?? "Point value unavailable." });
110
+ return;
111
+ }
112
+ args.plot.points.push({
113
+ x: args.x,
114
+ y: args.y,
115
+ group: args.comparison.agentId,
116
+ label: args.label,
117
+ metadata: {
118
+ caseId: args.comparison.caseId,
119
+ benchmarkProject: args.comparison.benchmarkProject,
120
+ complexityLevel: args.comparison.complexityLevel
121
+ }
122
+ });
123
+ }
124
+ function countOutcomeGroups(runs) {
125
+ const counts = new Map();
126
+ for (const run of runs) {
127
+ const key = `${run.agentId}|${run.status}`;
128
+ counts.set(key, (counts.get(key) ?? 0) + 1);
129
+ }
130
+ return counts;
131
+ }
132
+ function strategyIndex(strategy) {
133
+ return strategy === "raw-full-file" ? 1 : 2;
134
+ }
135
+ function agentIndex(agentId) {
136
+ return agentId === "fake-agent" ? 1 : agentId === "codex" ? 2 : agentId === "claude" ? 3 : 4;
137
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./types.js";
2
+ export * from "./buildExperimentPlotData.js";
3
+ export * from "./renderSvgChart.js";
4
+ export * from "./writePlotArtifacts.js";
@@ -0,0 +1,82 @@
1
+ const COLORS = ["#2563eb", "#16a34a", "#dc2626", "#9333ea", "#ea580c", "#0891b2"];
2
+ export function renderSvgChart(plot) {
3
+ const width = 760;
4
+ const height = 420;
5
+ const margin = { left: 72, right: 28, top: 58, bottom: 72 };
6
+ const innerWidth = width - margin.left - margin.right;
7
+ const innerHeight = height - margin.top - margin.bottom;
8
+ const groups = [...new Set(plot.points.map((point) => point.group))].sort();
9
+ const colorFor = (group) => COLORS[Math.max(0, groups.indexOf(group)) % COLORS.length];
10
+ if (plot.points.length === 0) {
11
+ return wrapSvg(width, height, `${title(plot.title)}${text(width / 2, height / 2, "No comparable data available", "middle", "empty")}`);
12
+ }
13
+ const xs = plot.points.map((point) => point.x);
14
+ const ys = plot.points.map((point) => point.y);
15
+ const xDomain = domain(xs);
16
+ const yDomain = domain(ys);
17
+ const xScale = (value) => margin.left + ((value - xDomain[0]) / (xDomain[1] - xDomain[0])) * innerWidth;
18
+ const yScale = (value) => margin.top + innerHeight - ((value - yDomain[0]) / (yDomain[1] - yDomain[0])) * innerHeight;
19
+ const body = plot.kind === "bar"
20
+ ? renderBars(plot, margin, innerWidth, innerHeight, colorFor)
21
+ : plot.points
22
+ .map((point) => `<circle cx="${round(xScale(point.x))}" cy="${round(yScale(point.y))}" r="5" fill="${colorFor(point.group)}"><title>${escapeXml(point.label)} (${point.x}, ${point.y})</title></circle>`)
23
+ .join("");
24
+ return wrapSvg(width, height, [
25
+ title(plot.title),
26
+ `<line x1="${margin.left}" y1="${margin.top + innerHeight}" x2="${margin.left + innerWidth}" y2="${margin.top + innerHeight}" stroke="#344054" />`,
27
+ `<line x1="${margin.left}" y1="${margin.top}" x2="${margin.left}" y2="${margin.top + innerHeight}" stroke="#344054" />`,
28
+ text(width / 2, height - 20, plot.xLabel, "middle", "axis-label"),
29
+ `<text x="20" y="${height / 2}" class="axis-label" transform="rotate(-90 20 ${height / 2})" text-anchor="middle">${escapeXml(plot.yLabel)}</text>`,
30
+ text(margin.left, margin.top + innerHeight + 20, String(round(xDomain[0])), "middle", "tick"),
31
+ text(margin.left + innerWidth, margin.top + innerHeight + 20, String(round(xDomain[1])), "middle", "tick"),
32
+ text(margin.left - 10, margin.top + innerHeight, String(round(yDomain[0])), "end", "tick"),
33
+ text(margin.left - 10, margin.top + 4, String(round(yDomain[1])), "end", "tick"),
34
+ body,
35
+ renderLegend(groups, colorFor, width - 180, 70)
36
+ ].join(""));
37
+ }
38
+ function renderBars(plot, margin, innerWidth, innerHeight, colorFor) {
39
+ const maxY = Math.max(1, ...plot.points.map((point) => point.y));
40
+ const barWidth = Math.max(12, innerWidth / Math.max(1, plot.points.length) - 6);
41
+ return plot.points
42
+ .map((point, index) => {
43
+ const height = (point.y / maxY) * innerHeight;
44
+ const x = margin.left + index * (barWidth + 6);
45
+ const y = margin.top + innerHeight - height;
46
+ return `<rect x="${round(x)}" y="${round(y)}" width="${round(barWidth)}" height="${round(height)}" fill="${colorFor(point.group)}"><title>${escapeXml(point.label)}: ${point.y}</title></rect>`;
47
+ })
48
+ .join("");
49
+ }
50
+ function renderLegend(groups, colorFor, x, y) {
51
+ if (groups.length === 0)
52
+ return "";
53
+ return groups
54
+ .map((group, index) => {
55
+ const rowY = y + index * 20;
56
+ return `<rect x="${x}" y="${rowY - 10}" width="10" height="10" fill="${colorFor(group)}" /><text x="${x + 16}" y="${rowY}" class="legend">${escapeXml(group)}</text>`;
57
+ })
58
+ .join("");
59
+ }
60
+ function wrapSvg(width, height, body) {
61
+ return `<svg xmlns="http://www.w3.org/2000/svg" role="img" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><style>text{font-family:Arial,Helvetica,sans-serif;fill:#17212b}.title{font-size:20px;font-weight:700}.axis-label{font-size:13px}.tick,.legend{font-size:12px}.empty{font-size:18px;fill:#667085}</style>${body}</svg>\n`;
62
+ }
63
+ function title(value) {
64
+ return text(28, 34, value, "start", "title");
65
+ }
66
+ function text(x, y, value, anchor, className) {
67
+ return `<text x="${x}" y="${y}" text-anchor="${anchor}" class="${className}">${escapeXml(value)}</text>`;
68
+ }
69
+ function domain(values) {
70
+ const min = Math.min(...values);
71
+ const max = Math.max(...values);
72
+ if (min === max)
73
+ return [min - 1, max + 1];
74
+ const padding = (max - min) * 0.1;
75
+ return [min - padding, max + padding];
76
+ }
77
+ function round(value) {
78
+ return Math.round(value * 100) / 100;
79
+ }
80
+ function escapeXml(value) {
81
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
82
+ }
@@ -0,0 +1 @@
1
+ export {};