@glubean/runner 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/engine-bridge.d.ts +4 -0
  2. package/dist/engine-bridge.d.ts.map +1 -1
  3. package/dist/engine-bridge.js +10 -1
  4. package/dist/engine-bridge.js.map +1 -1
  5. package/dist/executor.d.ts +2 -2
  6. package/dist/executor.d.ts.map +1 -1
  7. package/dist/executor.js +9 -226
  8. package/dist/executor.js.map +1 -1
  9. package/dist/harness.js +3 -79
  10. package/dist/harness.js.map +1 -1
  11. package/dist/index.d.ts +16 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +17 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/load/continuation-pool.d.ts +82 -0
  16. package/dist/load/continuation-pool.d.ts.map +1 -0
  17. package/dist/load/continuation-pool.js +154 -0
  18. package/dist/load/continuation-pool.js.map +1 -0
  19. package/dist/load/execute-iteration.d.ts +126 -0
  20. package/dist/load/execute-iteration.d.ts.map +1 -0
  21. package/dist/load/execute-iteration.js +367 -0
  22. package/dist/load/execute-iteration.js.map +1 -0
  23. package/dist/load/histogram.d.ts +63 -0
  24. package/dist/load/histogram.d.ts.map +1 -0
  25. package/dist/load/histogram.js +149 -0
  26. package/dist/load/histogram.js.map +1 -0
  27. package/dist/load/orchestrator.d.ts +55 -0
  28. package/dist/load/orchestrator.d.ts.map +1 -0
  29. package/dist/load/orchestrator.js +571 -0
  30. package/dist/load/orchestrator.js.map +1 -0
  31. package/dist/load/reducer.d.ts +109 -0
  32. package/dist/load/reducer.d.ts.map +1 -0
  33. package/dist/load/reducer.js +718 -0
  34. package/dist/load/reducer.js.map +1 -0
  35. package/dist/load/route-key.d.ts +38 -0
  36. package/dist/load/route-key.d.ts.map +1 -0
  37. package/dist/load/route-key.js +107 -0
  38. package/dist/load/route-key.js.map +1 -0
  39. package/dist/load/samples.d.ts +83 -0
  40. package/dist/load/samples.d.ts.map +1 -0
  41. package/dist/load/samples.js +269 -0
  42. package/dist/load/samples.js.map +1 -0
  43. package/dist/load/sink.d.ts +127 -0
  44. package/dist/load/sink.d.ts.map +1 -0
  45. package/dist/load/sink.js +351 -0
  46. package/dist/load/sink.js.map +1 -0
  47. package/dist/load/subprocess.d.ts +83 -0
  48. package/dist/load/subprocess.d.ts.map +1 -0
  49. package/dist/load/subprocess.js +229 -0
  50. package/dist/load/subprocess.js.map +1 -0
  51. package/dist/load/threshold.d.ts +44 -0
  52. package/dist/load/threshold.d.ts.map +1 -0
  53. package/dist/load/threshold.js +197 -0
  54. package/dist/load/threshold.js.map +1 -0
  55. package/dist/load/timeline.d.ts +36 -0
  56. package/dist/load/timeline.d.ts.map +1 -0
  57. package/dist/load/timeline.js +158 -0
  58. package/dist/load/timeline.js.map +1 -0
  59. package/dist/load-harness.d.ts +2 -0
  60. package/dist/load-harness.d.ts.map +1 -0
  61. package/dist/load-harness.js +105 -0
  62. package/dist/load-harness.js.map +1 -0
  63. package/dist/runner-resolve.d.ts +53 -0
  64. package/dist/runner-resolve.d.ts.map +1 -0
  65. package/dist/runner-resolve.js +264 -0
  66. package/dist/runner-resolve.js.map +1 -0
  67. package/dist/workflow/event-timeline.d.ts +3 -0
  68. package/dist/workflow/event-timeline.d.ts.map +1 -0
  69. package/dist/workflow/event-timeline.js +72 -0
  70. package/dist/workflow/event-timeline.js.map +1 -0
  71. package/package.json +4 -4
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Subprocess load execution — spawn the runner's `load-harness` in a child
3
+ * process so the harness (which calls `runLoad`) and the user `.load.ts` file
4
+ * CO-RESOLVE the same `@glubean/sdk`, mirroring how `glubean run` spawns the
5
+ * project-local test harness.
6
+ *
7
+ * Why a subprocess at all: running `runLoad` in-process against a user file that
8
+ * resolves a DIFFERENT `@glubean/sdk` instance (a globally-installed CLI vs. a
9
+ * project with its own, non-deduped sdk) split-brains the SDK runtime carrier —
10
+ * the scenario reads one AsyncLocalStorage carrier while `runLoad` installs
11
+ * another. Spawning the project-local runner via tsx makes both halves resolve
12
+ * the project's sdk, eliminating the hazard.
13
+ *
14
+ * This module owns the PARENT side (`runLoadFileInSubprocess`) plus the small
15
+ * pure helpers the child `load-harness` shares (`collectLoadPlans`,
16
+ * `withProcessEnvFallback`) and the wire-message contract between them
17
+ * (`WIRE_PREFIX` / `LoadHarnessMessage`) — kept here as a single source of truth
18
+ * so producer and consumer can't drift.
19
+ */
20
+ import { spawn } from "node:child_process";
21
+ import { existsSync } from "node:fs";
22
+ import { fileURLToPath } from "node:url";
23
+ import { dirname, resolve } from "node:path";
24
+ import { resolveRunnerRoot, resolveTsxPath, prepareZeroProject } from "../runner-resolve.js";
25
+ // ── Shared wire contract (child stdout → parent) ─────────────────────────────
26
+ /**
27
+ * Marker prefixed to every harness protocol line on stdout. The user's load file
28
+ * + plugins share stdout, whose `console.log` may itself print JSON; gating on
29
+ * this token stops a stray `{"type":"error",...}` log from spoofing an outcome or
30
+ * a failure. It leads with the ASCII Unit Separator (0x1F) — a control byte that
31
+ * ordinary text logs never emit — so collision is negligible. Lines without it
32
+ * are forwarded to the parent's stdout as ordinary user output.
33
+ *
34
+ * (stdout, not a dedicated fd: tsx re-spawns an inner node process and forwards
35
+ * only fd 0/1/2, so an extra protocol fd wouldn't reach the harness.)
36
+ */
37
+ export const WIRE_PREFIX = "\x1fglubean-load-wire\x1f";
38
+ // ── Shared pure helpers (used by load-harness in the child) ──────────────────
39
+ /** A LoadPlan-shaped export: the runnable marker the orchestrator consumes. */
40
+ function isLoadPlan(value) {
41
+ return (typeof value === "object" &&
42
+ value !== null &&
43
+ value.__glubean_type === "load-runner");
44
+ }
45
+ /**
46
+ * Collect every LoadPlan exported by a module namespace (flattening `.each()`
47
+ * arrays of plans).
48
+ */
49
+ export function collectLoadPlans(ns) {
50
+ const plans = [];
51
+ for (const value of Object.values(ns)) {
52
+ if (isLoadPlan(value)) {
53
+ plans.push(value);
54
+ }
55
+ else if (Array.isArray(value)) {
56
+ for (const v of value)
57
+ if (isLoadPlan(v))
58
+ plans.push(v);
59
+ }
60
+ }
61
+ return plans;
62
+ }
63
+ /**
64
+ * Wrap an env map so a missing key falls back to `process.env` — the same
65
+ * shell/CI env semantics the test harness gives `ctx.vars` / `ctx.secrets`, so
66
+ * `BASE_URL=... glubean load ...` resolves even without a `.env` entry.
67
+ *
68
+ * Applied INSIDE the child (which inherits the parent's `process.env`): the
69
+ * parent ships the raw resolved `{ vars, secrets }` over stdin — a Proxy can't
70
+ * survive JSON serialization, so the fallback must be re-established where
71
+ * `process.env` is actually present. Spreading the proxy (for `ctx.vars.all()`)
72
+ * still yields only the explicit map's own keys, not the whole environment.
73
+ */
74
+ export function withProcessEnvFallback(map) {
75
+ // Parity with the test harness's env proxy: an empty/nullish map value is
76
+ // treated as MISSING and falls back to process.env (so `BASE_URL=` in .env plus
77
+ // `BASE_URL=... glubean load` resolves), and process.env empties read as unset.
78
+ return new Proxy(map, {
79
+ get(target, prop) {
80
+ if (typeof prop === "string") {
81
+ const value = target[prop];
82
+ if (value !== undefined && value !== null && value !== "")
83
+ return value;
84
+ return process.env[prop] || undefined;
85
+ }
86
+ return Reflect.get(target, prop);
87
+ },
88
+ has(target, prop) {
89
+ return Reflect.has(target, prop) || (typeof prop === "string" && process.env[prop] !== undefined);
90
+ },
91
+ });
92
+ }
93
+ const __dirname = dirname(fileURLToPath(import.meta.url));
94
+ // This module builds to dist/load/subprocess.js; load-harness.js and the runner
95
+ // package root sit two and three levels up respectively.
96
+ const BUNDLED_DIST_DIR = resolve(__dirname, "..");
97
+ const BUNDLED_PKG_ROOT = resolve(__dirname, "..", "..");
98
+ /** Parse one prefixed protocol line into a wire message, or null if malformed. */
99
+ function parseHarnessLine(line) {
100
+ const trimmed = line.trim();
101
+ if (!trimmed)
102
+ return null;
103
+ let parsed;
104
+ try {
105
+ parsed = JSON.parse(trimmed);
106
+ }
107
+ catch {
108
+ return null; // partial / malformed protocol line — ignore
109
+ }
110
+ if (typeof parsed !== "object" || parsed === null)
111
+ return null;
112
+ const msg = parsed;
113
+ if (msg.type === "artifact" && typeof msg.runnerId === "string" && msg.artifact && typeof msg.artifact === "object") {
114
+ return { type: "artifact", runnerId: msg.runnerId, artifact: msg.artifact };
115
+ }
116
+ if (msg.type === "error" && typeof msg.message === "string") {
117
+ return { type: "error", message: msg.message };
118
+ }
119
+ if (msg.type === "done")
120
+ return { type: "done" };
121
+ return null;
122
+ }
123
+ /**
124
+ * Run one `.load.ts` file's plans in a child process and collect their
125
+ * artifacts. Spawns `load-harness.js` (project-local when the project ships a
126
+ * built runner with it, else the CLI's bundled copy) through tsx so the harness
127
+ * and the user file resolve the same `@glubean/sdk`. The raw `{ vars, secrets }`
128
+ * are written to the child's stdin; the child applies the process.env fallback.
129
+ *
130
+ * Mirrors the in-process contract: a file's import failure or a plan's run
131
+ * failure becomes an `errors[]` entry (never throws), so other files/plans still
132
+ * produce results.
133
+ */
134
+ export async function runLoadFileInSubprocess(file, opts) {
135
+ const { vars, secrets, cwd } = opts;
136
+ // Resolve the runner the same way `glubean run` does (dual-package hazard fix).
137
+ const resolved = resolveRunnerRoot(cwd, BUNDLED_DIST_DIR, BUNDLED_PKG_ROOT);
138
+ let distDir = resolved.distDir;
139
+ let pkgRoot = resolved.pkgRoot;
140
+ let harnessPath = resolve(distDir, "load-harness.js");
141
+ if (!existsSync(harnessPath)) {
142
+ // The resolved runner predates load support (has harness.js but no
143
+ // load-harness.js). Fall back to the bundled harness so load still runs.
144
+ // (Re-introduces the in-process split-brain only when the project ALSO ships
145
+ // its own non-deduped sdk against a too-old runner — no worse than before,
146
+ // and the common up-to-date case is fully correct. Upgrading the project's
147
+ // @glubean/runner restores shared module identity.)
148
+ distDir = BUNDLED_DIST_DIR;
149
+ pkgRoot = BUNDLED_PKG_ROOT;
150
+ harnessPath = resolve(BUNDLED_DIST_DIR, "load-harness.js");
151
+ }
152
+ const zp = prepareZeroProject(cwd, distDir, pkgRoot);
153
+ try {
154
+ const env = { ...process.env, ...zp.env };
155
+ const child = spawn("node", [resolveTsxPath(), ...zp.tsxArgs, harnessPath, `--file=${file}`], {
156
+ cwd,
157
+ env,
158
+ stdio: ["pipe", "pipe", "pipe"],
159
+ });
160
+ child.stdin.write(JSON.stringify({ vars, secrets }));
161
+ child.stdin.end();
162
+ const outcomes = [];
163
+ const errors = [];
164
+ let stdoutBuffer = "";
165
+ let sawDone = false;
166
+ const stderrChunks = [];
167
+ const consumeLine = (line) => {
168
+ if (!line.startsWith(WIRE_PREFIX)) {
169
+ // Ordinary user/plugin output — forward it so nothing is swallowed.
170
+ if (line.length > 0)
171
+ process.stdout.write(line + "\n");
172
+ return;
173
+ }
174
+ const msg = parseHarnessLine(line.slice(WIRE_PREFIX.length));
175
+ if (!msg)
176
+ return;
177
+ if (msg.type === "artifact")
178
+ outcomes.push({ runnerId: msg.runnerId, artifact: msg.artifact });
179
+ else if (msg.type === "error")
180
+ errors.push({ message: msg.message });
181
+ else
182
+ sawDone = true; // terminal sentinel: the harness finished cleanly
183
+ };
184
+ child.stdout.on("data", (chunk) => {
185
+ stdoutBuffer += chunk.toString();
186
+ const lines = stdoutBuffer.split("\n");
187
+ stdoutBuffer = lines.pop() ?? "";
188
+ for (const line of lines)
189
+ consumeLine(line);
190
+ });
191
+ // Forward child stderr LIVE so user/plugin diagnostics (console.error) stay
192
+ // visible as they did in-process, and keep a copy for the crash message.
193
+ child.stderr.on("data", (chunk) => {
194
+ stderrChunks.push(chunk);
195
+ process.stderr.write(chunk);
196
+ });
197
+ await new Promise((resolveClose) => {
198
+ child.on("error", (err) => {
199
+ errors.push({
200
+ message: err.code === "ENOENT"
201
+ ? "NODE_NOT_FOUND: Node.js is not installed. Glubean requires Node.js 20+ to run load plans."
202
+ : `failed to start load subprocess for ${file}: ${err.message}`,
203
+ });
204
+ resolveClose();
205
+ });
206
+ child.on("close", (code, signal) => {
207
+ if (stdoutBuffer)
208
+ consumeLine(stdoutBuffer);
209
+ // No `done` sentinel ⇒ the child died before finishing (crash / OOM /
210
+ // SIGKILL), even if it already emitted some artifacts. Surface a crash
211
+ // error so a PARTIAL run is never mistaken for a complete one. (A clean
212
+ // import-failure path still emits `done`, so it never lands here.)
213
+ if (!sawDone) {
214
+ const stderr = Buffer.concat(stderrChunks).toString().trim();
215
+ const how = signal ? `signal ${signal}` : `exit code ${code}`;
216
+ errors.push({
217
+ message: `load subprocess for ${file} did not complete (${how})${stderr ? `:\n${stderr}` : ""}`,
218
+ });
219
+ }
220
+ resolveClose();
221
+ });
222
+ });
223
+ return { outcomes, errors };
224
+ }
225
+ finally {
226
+ zp.cleanup();
227
+ }
228
+ }
229
+ //# sourceMappingURL=subprocess.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subprocess.js","sourceRoot":"","sources":["../../src/load/subprocess.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE7F,gFAAgF;AAEhF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,2BAA2B,CAAC;AAcvD,gFAAgF;AAEhF,+EAA+E;AAC/E,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAAsC,CAAC,cAAc,KAAK,aAAa,CACzE,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAA2B;IAC1D,MAAM,KAAK,GAAe,EAAE,CAAC;IAC7B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;QACtC,IAAI,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAChC,KAAK,MAAM,CAAC,IAAI,KAAK;gBAAE,IAAI,UAAU,CAAC,CAAC,CAAC;oBAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAA2B;IAChE,0EAA0E;IAC1E,gFAAgF;IAChF,gFAAgF;IAChF,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE;QACpB,GAAG,CAAC,MAAM,EAAE,IAAI;YACd,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;gBAC3B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE;oBAAE,OAAO,KAAK,CAAC;gBACxE,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;YACxC,CAAC;YACD,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACnC,CAAC;QACD,GAAG,CAAC,MAAM,EAAE,IAAI;YACd,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC,CAAC;QACpG,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AA+BD,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,gFAAgF;AAChF,yDAAyD;AACzD,MAAM,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;AAClD,MAAM,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAExD,kFAAkF;AAClF,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,6CAA6C;IAC5D,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/D,MAAM,GAAG,GAAG,MAAuF,CAAC;IACpG,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACpH,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAwB,EAAE,CAAC;IAC9F,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC;IACjD,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACjD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,IAAY,EACZ,IAAwB;IAExB,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAEpC,gFAAgF;IAChF,MAAM,QAAQ,GAAG,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,EAAE,gBAAgB,CAAC,CAAC;IAC5E,IAAI,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;IAC/B,IAAI,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;IAC/B,IAAI,WAAW,GAAG,OAAO,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;IACtD,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7B,mEAAmE;QACnE,yEAAyE;QACzE,6EAA6E;QAC7E,2EAA2E;QAC3E,2EAA2E;QAC3E,oDAAoD;QACpD,OAAO,GAAG,gBAAgB,CAAC;QAC3B,OAAO,GAAG,gBAAgB,CAAC;QAC3B,WAAW,GAAG,OAAO,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,CAAC;IAC7D,CAAC;IAED,MAAM,EAAE,GAAG,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IACrD,IAAI,CAAC;QACH,MAAM,GAAG,GAA2B,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,EAA4B,CAAC;QAC5F,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,cAAc,EAAE,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,UAAU,IAAI,EAAE,CAAC,EAAE;YAC5F,GAAG;YACH,GAAG;YACH,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;SAChC,CAAC,CAAC;QAEH,KAAK,CAAC,KAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;QACtD,KAAK,CAAC,KAAM,CAAC,GAAG,EAAE,CAAC;QAEnB,MAAM,QAAQ,GAA4B,EAAE,CAAC;QAC7C,MAAM,MAAM,GAA0B,EAAE,CAAC;QACzC,IAAI,YAAY,GAAG,EAAE,CAAC;QACtB,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAQ,EAAE;YACzC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;gBAClC,oEAAoE;gBACpE,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;oBAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;gBACvD,OAAO;YACT,CAAC;YACD,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;YAC7D,IAAI,CAAC,GAAG;gBAAE,OAAO;YACjB,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU;gBAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;iBAC1F,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO;gBAAE,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;;gBAChE,OAAO,GAAG,IAAI,CAAC,CAAC,kDAAkD;QACzE,CAAC,CAAC;QAEF,KAAK,CAAC,MAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACzC,YAAY,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YACjC,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACvC,YAAY,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;YACjC,KAAK,MAAM,IAAI,IAAI,KAAK;gBAAE,WAAW,CAAC,IAAI,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QACH,4EAA4E;QAC5E,yEAAyE;QACzE,KAAK,CAAC,MAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACzC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,MAAM,IAAI,OAAO,CAAO,CAAC,YAAY,EAAE,EAAE;YACvC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;gBACxB,MAAM,CAAC,IAAI,CAAC;oBACV,OAAO,EACJ,GAA6B,CAAC,IAAI,KAAK,QAAQ;wBAC9C,CAAC,CAAC,2FAA2F;wBAC7F,CAAC,CAAC,uCAAuC,IAAI,KAAK,GAAG,CAAC,OAAO,EAAE;iBACpE,CAAC,CAAC;gBACH,YAAY,EAAE,CAAC;YACjB,CAAC,CAAC,CAAC;YACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;gBACjC,IAAI,YAAY;oBAAE,WAAW,CAAC,YAAY,CAAC,CAAC;gBAC5C,sEAAsE;gBACtE,uEAAuE;gBACvE,wEAAwE;gBACxE,mEAAmE;gBACnE,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;oBAC7D,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,UAAU,MAAM,EAAE,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC;oBAC9D,MAAM,CAAC,IAAI,CAAC;wBACV,OAAO,EAAE,uBAAuB,IAAI,sBAAsB,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;qBAChG,CAAC,CAAC;gBACL,CAAC;gBACD,YAAY,EAAE,CAAC;YACjB,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAC9B,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,OAAO,EAAE,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Structured threshold parsing + evaluation (M4-a).
3
+ *
4
+ * A `loadRunner({ thresholds })` config carries human expressions per scope/metric
5
+ * (`errorRate: "<1%"`, `p95: "<800ms"`, `throughputPerSec: ">100/s"`). This module
6
+ * parses those expressions, evaluates them against a finalized `LoadArtifact`, and
7
+ * produces the `ThresholdEvaluation[]` + a refined pass verdict (a crash-free run
8
+ * only passes if every configured-and-evaluable threshold holds).
9
+ *
10
+ * Evaluation runs OUTSIDE the reducer (which never sees the plan's thresholds) as a
11
+ * post-finalize step in the orchestrator. Scopes whose data isn't present yet
12
+ * (primary / endToEnd / continuation phase splits — M5/M6) and metrics N/A to a
13
+ * scope (e.g. throughput on a step) are skipped rather than failed.
14
+ */
15
+ import type { LoadArtifact, LoadThresholds, ThresholdEvaluation } from "@glubean/sdk/load";
16
+ type Op = "<" | "<=" | ">" | ">=";
17
+ /** Metrics a threshold can target (the keys of `LoadThresholdScope`). */
18
+ declare const THRESHOLD_METRICS: readonly ["errorRate", "p50", "p90", "p95", "p99", "throughputPerSec", "backlog", "backpressureMs"];
19
+ type ThresholdMetric = (typeof THRESHOLD_METRICS)[number];
20
+ /** A parsed threshold: comparison operator + the value in the metric's base unit. */
21
+ export interface ParsedThreshold {
22
+ op: Op;
23
+ /** Value in the metric's base unit: errorRate=fraction, latency/backpressure=ms,
24
+ * throughput=/s, backlog=count. */
25
+ value: number;
26
+ }
27
+ /**
28
+ * Parse one threshold expression for a given metric, normalizing its value to the
29
+ * metric's base unit (`%`→fraction, `s`→ms, etc.). Throws on a malformed
30
+ * expression or a unit that doesn't fit the metric.
31
+ */
32
+ export declare function parseThresholdExpression(expr: string, metric: ThresholdMetric): ParsedThreshold;
33
+ /**
34
+ * Evaluate every configured threshold against the artifact, returning the
35
+ * `ThresholdEvaluation[]` and the refined run pass (crash-free AND every evaluable
36
+ * threshold holds). Thresholds whose scope data is absent or whose metric is N/A
37
+ * to the scope are skipped (not failed).
38
+ */
39
+ export declare function evaluateThresholds(artifact: LoadArtifact, thresholds: LoadThresholds): {
40
+ thresholds: ThresholdEvaluation[];
41
+ pass: boolean;
42
+ };
43
+ export {};
44
+ //# sourceMappingURL=threshold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threshold.d.ts","sourceRoot":"","sources":["../../src/load/threshold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EACV,YAAY,EAEZ,cAAc,EACd,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAE3B,KAAK,EAAE,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC;AAElC,yEAAyE;AACzE,QAAA,MAAM,iBAAiB,qGASb,CAAC;AACX,KAAK,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,EAAE,CAAC;IACP;wCACoC;IACpC,KAAK,EAAE,MAAM,CAAC;CACf;AAID;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,eAAe,CAqC/F;AA+FD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,cAAc,GACzB;IAAE,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAgFtD"}
@@ -0,0 +1,197 @@
1
+ /** Metrics a threshold can target (the keys of `LoadThresholdScope`). */
2
+ const THRESHOLD_METRICS = [
3
+ "errorRate",
4
+ "p50",
5
+ "p90",
6
+ "p95",
7
+ "p99",
8
+ "throughputPerSec",
9
+ "backlog",
10
+ "backpressureMs",
11
+ ];
12
+ const EXPR_RE = /^(<=|>=|<|>)\s*(-?[0-9.]+)\s*(%|ms|s|\/s)?$/;
13
+ /**
14
+ * Parse one threshold expression for a given metric, normalizing its value to the
15
+ * metric's base unit (`%`→fraction, `s`→ms, etc.). Throws on a malformed
16
+ * expression or a unit that doesn't fit the metric.
17
+ */
18
+ export function parseThresholdExpression(expr, metric) {
19
+ const m = EXPR_RE.exec(expr.trim());
20
+ if (!m) {
21
+ throw new Error(`invalid threshold expression ${JSON.stringify(expr)} — expected e.g. "<1%", "<800ms", ">100/s"`);
22
+ }
23
+ const op = m[1];
24
+ const num = Number(m[2]);
25
+ const unit = m[3];
26
+ if (!Number.isFinite(num))
27
+ throw new Error(`invalid threshold number in ${JSON.stringify(expr)}`);
28
+ const badUnit = () => {
29
+ throw new Error(`threshold unit "${unit}" is not valid for metric "${metric}" (in ${JSON.stringify(expr)})`);
30
+ };
31
+ let value;
32
+ switch (metric) {
33
+ case "errorRate":
34
+ // A bare number is already a fraction (0..1); "%" scales a percentage down.
35
+ value = unit === "%" ? num / 100 : unit === undefined ? num : badUnit();
36
+ break;
37
+ case "p50":
38
+ case "p90":
39
+ case "p95":
40
+ case "p99":
41
+ case "backpressureMs":
42
+ value = unit === "s" ? num * 1000 : unit === "ms" || unit === undefined ? num : badUnit();
43
+ break;
44
+ case "throughputPerSec":
45
+ value = unit === "/s" || unit === undefined ? num : badUnit();
46
+ break;
47
+ case "backlog":
48
+ value = unit === undefined ? num : badUnit();
49
+ break;
50
+ }
51
+ return { op, value };
52
+ }
53
+ function compare(actual, op, value) {
54
+ switch (op) {
55
+ case "<":
56
+ return actual < value;
57
+ case "<=":
58
+ return actual <= value;
59
+ case ">":
60
+ return actual > value;
61
+ case ">=":
62
+ return actual >= value;
63
+ }
64
+ }
65
+ /**
66
+ * Combine an endpoint's / step's per-phase rows (M5 splits a route or step hit in
67
+ * both phases into two rows) into ONE scope for thresholding:
68
+ * - throughput is ADDITIVE → summed (60/s primary + 60/s continuation = 120/s);
69
+ * - errorRate is weighted by each row's denominator (Σ rate·weight / Σ weight), so
70
+ * it stays request-based for endpoints and invocation-based for steps;
71
+ * - percentiles can't be merged from summaries, so each is the MAX across rows —
72
+ * exact for the usual `<` upper-bound threshold (every row under X ⟺ max under X
73
+ * ⟹ the merged distribution is under X), conservative otherwise.
74
+ * A single row (the no-split common case) combines to its own values unchanged.
75
+ */
76
+ function combineRows(rows) {
77
+ let errorWeighted = 0;
78
+ let errorWeight = 0;
79
+ let throughput = 0;
80
+ let hasThroughput = false;
81
+ const latency = { p50: 0, p90: 0, p95: 0, p99: 0 };
82
+ for (const r of rows) {
83
+ errorWeighted += r.errorRate * r.errorWeight;
84
+ errorWeight += r.errorWeight;
85
+ if (r.throughputPerSec !== undefined) {
86
+ throughput += r.throughputPerSec;
87
+ hasThroughput = true;
88
+ }
89
+ latency.p50 = Math.max(latency.p50, r.latency.p50);
90
+ latency.p90 = Math.max(latency.p90, r.latency.p90);
91
+ latency.p95 = Math.max(latency.p95, r.latency.p95);
92
+ latency.p99 = Math.max(latency.p99, r.latency.p99);
93
+ }
94
+ return {
95
+ errorRate: errorWeight > 0 ? errorWeighted / errorWeight : 0,
96
+ ...(hasThroughput ? { throughputPerSec: throughput } : {}),
97
+ latency,
98
+ };
99
+ }
100
+ /** Pull the actual value for `metric` from a scope's data, or undefined if N/A. */
101
+ function actualFor(metric, data) {
102
+ switch (metric) {
103
+ case "errorRate":
104
+ return data.errorRate;
105
+ case "p50":
106
+ return data.latency?.p50;
107
+ case "p90":
108
+ return data.latency?.p90;
109
+ case "p95":
110
+ return data.latency?.p95;
111
+ case "p99":
112
+ return data.latency?.p99;
113
+ case "throughputPerSec":
114
+ return data.throughputPerSec;
115
+ case "backlog":
116
+ return data.backlog;
117
+ case "backpressureMs":
118
+ return data.backpressureMs;
119
+ }
120
+ }
121
+ /**
122
+ * Evaluate every configured threshold against the artifact, returning the
123
+ * `ThresholdEvaluation[]` and the refined run pass (crash-free AND every evaluable
124
+ * threshold holds). Thresholds whose scope data is absent or whose metric is N/A
125
+ * to the scope are skipped (not failed).
126
+ */
127
+ export function evaluateThresholds(artifact, thresholds) {
128
+ const out = [];
129
+ const evalScope = (scope, target, data, cfg) => {
130
+ if (!data || !cfg)
131
+ return;
132
+ for (const metric of THRESHOLD_METRICS) {
133
+ const expr = cfg[metric];
134
+ if (expr === undefined)
135
+ continue;
136
+ const actual = actualFor(metric, data);
137
+ if (actual === undefined)
138
+ continue; // metric not applicable to this scope
139
+ const { op, value } = parseThresholdExpression(expr, metric);
140
+ out.push({
141
+ scope,
142
+ ...(target !== undefined ? { target } : {}),
143
+ metric,
144
+ expression: expr,
145
+ actual,
146
+ pass: compare(actual, op, value),
147
+ source: "glubean",
148
+ });
149
+ }
150
+ };
151
+ const s = artifact.summary;
152
+ evalScope("transaction", undefined, { errorRate: s.errorRate, latency: s.latency, throughputPerSec: s.throughputPerSec }, thresholds.transaction);
153
+ evalScope("primary", undefined, s.primary, thresholds.primary);
154
+ evalScope("endToEnd", undefined, s.endToEnd, thresholds.endToEnd);
155
+ // The continuation summary needs numeric projection: `backpressureMs` is a
156
+ // percentile distribution (compare against p95), and the threshold-relevant
157
+ // backlog is the PEAK (`maxBacklog`), not the drained-to-0 live count.
158
+ const continuationData = s.continuation
159
+ ? { backlog: s.continuation.maxBacklog, backpressureMs: s.continuation.backpressureMs?.p95 ?? 0 }
160
+ : undefined;
161
+ evalScope("continuation", undefined, continuationData, thresholds.continuation);
162
+ // Endpoints / steps can split into per-phase rows (M5: a route or step hit in
163
+ // both the primary and continuation phase appears twice). Combine the matching
164
+ // rows into one scope so additive metrics (throughput) sum and a slow phase still
165
+ // shows up (latency = max) — picking one row (e.g. `find`) would let a slow
166
+ // continuation hide behind a fast primary. No matching row → skipped, not failed.
167
+ if (thresholds.endpoints) {
168
+ for (const [routeKey, cfg] of Object.entries(thresholds.endpoints)) {
169
+ const eps = artifact.endpoints.filter((e) => e.routeKey === routeKey);
170
+ // Endpoint error rate is over REQUESTS.
171
+ const rows = eps.map((e) => ({
172
+ errorRate: e.errorRate,
173
+ errorWeight: e.requestCount,
174
+ throughputPerSec: e.throughputPerSec,
175
+ latency: e.latency,
176
+ }));
177
+ evalScope("endpoint", routeKey, rows.length > 0 ? combineRows(rows) : undefined, cfg);
178
+ }
179
+ }
180
+ if (thresholds.steps) {
181
+ for (const [stepId, cfg] of Object.entries(thresholds.steps)) {
182
+ const sts = artifact.steps.filter((x) => x.stepId === stepId);
183
+ // Step error rate is over EXECUTED invocations (skipped ones aren't failures);
184
+ // steps have no throughput metric.
185
+ const rows = sts.map((s) => ({
186
+ errorRate: s.errorRate,
187
+ errorWeight: Math.max(0, s.invocationCount - s.skippedCount),
188
+ latency: s.latency,
189
+ }));
190
+ evalScope("step", stepId, rows.length > 0 ? combineRows(rows) : undefined, cfg);
191
+ }
192
+ }
193
+ // A crash already fails the run; otherwise every evaluable threshold must hold.
194
+ const pass = s.pass && out.every((e) => e.pass);
195
+ return { thresholds: out, pass };
196
+ }
197
+ //# sourceMappingURL=threshold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threshold.js","sourceRoot":"","sources":["../../src/load/threshold.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { LoadTimeline as LoadTimelineArtifact } from "@glubean/sdk/load";
2
+ export declare class LoadTimeline {
3
+ private readonly baseWindowMs;
4
+ private readonly maxWindows;
5
+ private windowMs;
6
+ private windows;
7
+ /** @param baseWindowMs initial window width (ms). @param maxWindows coarsening cap. */
8
+ constructor(baseWindowMs?: number, maxWindows?: number);
9
+ /** The window index covering `offsetMs` (ms from run start), coarsening first if the
10
+ * index would exceed the cap (so the series stays ≤ maxWindows). */
11
+ private indexFor;
12
+ /** The LAST window covered by a run of length `runEndMs` (the run spans [0, runEndMs)), or
13
+ * -1 for a non-positive length. A run ending exactly on a boundary (runEndMs = k·windowMs)
14
+ * covers windows 0..k-1, NOT a window that starts at the run end (codex). Coarsens so the
15
+ * index fits the cap. */
16
+ private runEndIndex;
17
+ /** The window covering `offsetMs`, creating it if absent. */
18
+ private windowFor;
19
+ /** Double the window width and merge each adjacent pair (2k, 2k+1 → k). All folds are
20
+ * commutative (sum / max / histogram merge), so Map iteration order is irrelevant. */
21
+ private coarsen;
22
+ /** Record one request observation at `offsetMs`. `inFlight` is the live iteration count
23
+ * at that moment — sampled so a window busy with requests (e.g. a poll) shows concurrency. */
24
+ recordRequest(offsetMs: number, durationMs: number, ok: boolean, inFlight: number): void;
25
+ /** Record one started iteration at `offsetMs`. `inFlight` is the live count just after the
26
+ * start (its local peak), sampled so even a same-window start+end shows its concurrency. */
27
+ recordIterationStart(offsetMs: number, inFlight: number): void;
28
+ /** Record one completed iteration (iteration:end) at `offsetMs`. */
29
+ recordIterationEnd(offsetMs: number): void;
30
+ /** Emit the dense series: every window from 0..last, idle windows zero-filled. `runEndMs`
31
+ * (the run's `load:end` offset) extends the series to the actual run end, so a trailing
32
+ * idle / sustained-in-flight period after the last recorded event (an abort, drain timeout,
33
+ * or hung iteration) is still present instead of being truncated (codex). */
34
+ finalize(runEndMs?: number): LoadTimelineArtifact;
35
+ }
36
+ //# sourceMappingURL=timeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../../src/load/timeline.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,IAAI,oBAAoB,EAAmC,MAAM,mBAAmB,CAAC;AAoB/G,qBAAa,YAAY;IAQrB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAR7B,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,OAAO,CAA6B;IAE5C,uFAAuF;gBAEpE,YAAY,SAAM,EAClB,UAAU,SAAM;IAKnC;yEACqE;IACrE,OAAO,CAAC,QAAQ;IAShB;;;8BAG0B;IAC1B,OAAO,CAAC,WAAW;IAMnB,6DAA6D;IAC7D,OAAO,CAAC,SAAS;IAUjB;2FACuF;IACvF,OAAO,CAAC,OAAO;IAoBf;mGAC+F;IAC/F,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAQxF;iGAC6F;IAC7F,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAM9D,oEAAoE;IACpE,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAI1C;;;kFAG8E;IAC9E,QAAQ,CAAC,QAAQ,SAAI,GAAG,oBAAoB;CA2C7C"}