autor3search-typescript 0.1.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/dist/benchproto/types.d.ts +32 -0
  4. package/dist/benchproto/types.js +28 -0
  5. package/dist/benchproto/types.js.map +1 -0
  6. package/dist/benchrun/child.d.ts +1 -0
  7. package/dist/benchrun/child.js +222 -0
  8. package/dist/benchrun/child.js.map +1 -0
  9. package/dist/benchrun/invoke.d.ts +61 -0
  10. package/dist/benchrun/invoke.js +152 -0
  11. package/dist/benchrun/invoke.js.map +1 -0
  12. package/dist/cli/cmd-baseline.d.ts +15 -0
  13. package/dist/cli/cmd-baseline.js +292 -0
  14. package/dist/cli/cmd-baseline.js.map +1 -0
  15. package/dist/cli/cmd-doctor.d.ts +11 -0
  16. package/dist/cli/cmd-doctor.js +26 -0
  17. package/dist/cli/cmd-doctor.js.map +1 -0
  18. package/dist/cli/cmd-eval.d.ts +30 -0
  19. package/dist/cli/cmd-eval.js +173 -0
  20. package/dist/cli/cmd-eval.js.map +1 -0
  21. package/dist/cli/cmd-init.d.ts +16 -0
  22. package/dist/cli/cmd-init.js +343 -0
  23. package/dist/cli/cmd-init.js.map +1 -0
  24. package/dist/cli/cmd-profile.d.ts +11 -0
  25. package/dist/cli/cmd-profile.js +140 -0
  26. package/dist/cli/cmd-profile.js.map +1 -0
  27. package/dist/cli/cmd-report.d.ts +15 -0
  28. package/dist/cli/cmd-report.js +70 -0
  29. package/dist/cli/cmd-report.js.map +1 -0
  30. package/dist/cli/cmd-status.d.ts +11 -0
  31. package/dist/cli/cmd-status.js +143 -0
  32. package/dist/cli/cmd-status.js.map +1 -0
  33. package/dist/cli/cmd-stop.d.ts +15 -0
  34. package/dist/cli/cmd-stop.js +143 -0
  35. package/dist/cli/cmd-stop.js.map +1 -0
  36. package/dist/cli/main.d.ts +31 -0
  37. package/dist/cli/main.js +133 -0
  38. package/dist/cli/main.js.map +1 -0
  39. package/dist/cli/runctx.d.ts +29 -0
  40. package/dist/cli/runctx.js +49 -0
  41. package/dist/cli/runctx.js.map +1 -0
  42. package/dist/cli/speedup.d.ts +14 -0
  43. package/dist/cli/speedup.js +25 -0
  44. package/dist/cli/speedup.js.map +1 -0
  45. package/dist/config/load.d.ts +6 -0
  46. package/dist/config/load.js +172 -0
  47. package/dist/config/load.js.map +1 -0
  48. package/dist/config/schema.d.ts +42 -0
  49. package/dist/config/schema.js +25 -0
  50. package/dist/config/schema.js.map +1 -0
  51. package/dist/discover/benchmarks.d.ts +26 -0
  52. package/dist/discover/benchmarks.js +88 -0
  53. package/dist/discover/benchmarks.js.map +1 -0
  54. package/dist/discover/files.d.ts +23 -0
  55. package/dist/discover/files.js +82 -0
  56. package/dist/discover/files.js.map +1 -0
  57. package/dist/doctor/doctor.d.ts +56 -0
  58. package/dist/doctor/doctor.js +255 -0
  59. package/dist/doctor/doctor.js.map +1 -0
  60. package/dist/freeze/freeze.d.ts +23 -0
  61. package/dist/freeze/freeze.js +107 -0
  62. package/dist/freeze/freeze.js.map +1 -0
  63. package/dist/freeze/manifest.d.ts +6 -0
  64. package/dist/freeze/manifest.js +9 -0
  65. package/dist/freeze/manifest.js.map +1 -0
  66. package/dist/freeze/safepath.d.ts +8 -0
  67. package/dist/freeze/safepath.js +22 -0
  68. package/dist/freeze/safepath.js.map +1 -0
  69. package/dist/gitx/git.d.ts +87 -0
  70. package/dist/gitx/git.js +235 -0
  71. package/dist/gitx/git.js.map +1 -0
  72. package/dist/measure/interleave.d.ts +32 -0
  73. package/dist/measure/interleave.js +42 -0
  74. package/dist/measure/interleave.js.map +1 -0
  75. package/dist/pipeline/eval.d.ts +67 -0
  76. package/dist/pipeline/eval.js +495 -0
  77. package/dist/pipeline/eval.js.map +1 -0
  78. package/dist/pm/detect.d.ts +17 -0
  79. package/dist/pm/detect.js +131 -0
  80. package/dist/pm/detect.js.map +1 -0
  81. package/dist/profile/profile.d.ts +53 -0
  82. package/dist/profile/profile.js +105 -0
  83. package/dist/profile/profile.js.map +1 -0
  84. package/dist/results/results.d.ts +65 -0
  85. package/dist/results/results.js +214 -0
  86. package/dist/results/results.js.map +1 -0
  87. package/dist/runner/exec.d.ts +69 -0
  88. package/dist/runner/exec.js +185 -0
  89. package/dist/runner/exec.js.map +1 -0
  90. package/dist/scope/scope.d.ts +20 -0
  91. package/dist/scope/scope.js +70 -0
  92. package/dist/scope/scope.js.map +1 -0
  93. package/dist/state/baseline.d.ts +35 -0
  94. package/dist/state/baseline.js +38 -0
  95. package/dist/state/baseline.js.map +1 -0
  96. package/dist/state/home.d.ts +9 -0
  97. package/dist/state/home.js +72 -0
  98. package/dist/state/home.js.map +1 -0
  99. package/dist/state/lock.d.ts +11 -0
  100. package/dist/state/lock.js +89 -0
  101. package/dist/state/lock.js.map +1 -0
  102. package/dist/state/runnaming.d.ts +27 -0
  103. package/dist/state/runnaming.js +47 -0
  104. package/dist/state/runnaming.js.map +1 -0
  105. package/dist/state/stop.d.ts +28 -0
  106. package/dist/state/stop.js +59 -0
  107. package/dist/state/stop.js.map +1 -0
  108. package/dist/stats/delta.d.ts +29 -0
  109. package/dist/stats/delta.js +41 -0
  110. package/dist/stats/delta.js.map +1 -0
  111. package/dist/stats/geomean.d.ts +4 -0
  112. package/dist/stats/geomean.js +21 -0
  113. package/dist/stats/geomean.js.map +1 -0
  114. package/dist/stats/mannwhitney.d.ts +27 -0
  115. package/dist/stats/mannwhitney.js +158 -0
  116. package/dist/stats/mannwhitney.js.map +1 -0
  117. package/dist/verdict/verdict.d.ts +46 -0
  118. package/dist/verdict/verdict.js +102 -0
  119. package/dist/verdict/verdict.js.map +1 -0
  120. package/dist/version.d.ts +2 -0
  121. package/dist/version.js +3 -0
  122. package/dist/version.js.map +1 -0
  123. package/package.json +35 -0
  124. package/templates/program.md +131 -0
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared naming constants and the `-tag`-inference helper, previously
3
+ * copied (with an identical comment explaining why) across `cmd-baseline.ts`,
4
+ * `cmd-eval.ts`, `cmd-stop.ts`, `cmd-status.ts` and `pipeline/eval.ts`.
5
+ *
6
+ * The duplication was a deliberate, documented choice during earlier tasks
7
+ * (a scheduling constraint: `cmd-baseline.ts` was already-shipped code a
8
+ * later task was not to modify) rather than an oversight. That constraint
9
+ * no longer applies -- per Ruling 44 ("eliminate duplication when you can,
10
+ * guard it only when you cannot"), these five copies are consolidated here.
11
+ * A future change to the branch-naming scheme or the worktree/frozen layout
12
+ * now cannot silently diverge between call sites by being made in one copy
13
+ * and forgotten in the others.
14
+ */
15
+ /** Prefix `baseline` uses for the run branch it creates: `<prefix><tag>`. */
16
+ export declare const BRANCH_PREFIX = "autor3search-typescript/";
17
+ /** Directory name, under the per-run state dir, of the pinned baseline worktree. */
18
+ export declare const WORKTREE_DIRNAME = "baseline-worktree";
19
+ /** Directory name, under the per-run state dir, of the frozen test/bench snapshot. */
20
+ export declare const FROZEN_DIRNAME = "frozen";
21
+ /**
22
+ * `-tag`'s default: the tag encoded in the current run branch's name.
23
+ * Every command that accepts `-tag` falls back to this when it is omitted,
24
+ * so a human or agent already checked out on a run branch never has to
25
+ * repeat the tag on every invocation.
26
+ */
27
+ export declare function inferTagFromBranch(repoRoot: string): Promise<string>;
@@ -0,0 +1,47 @@
1
+ import { currentBranch } from '../gitx/git.js';
2
+ /**
3
+ * Shared naming constants and the `-tag`-inference helper, previously
4
+ * copied (with an identical comment explaining why) across `cmd-baseline.ts`,
5
+ * `cmd-eval.ts`, `cmd-stop.ts`, `cmd-status.ts` and `pipeline/eval.ts`.
6
+ *
7
+ * The duplication was a deliberate, documented choice during earlier tasks
8
+ * (a scheduling constraint: `cmd-baseline.ts` was already-shipped code a
9
+ * later task was not to modify) rather than an oversight. That constraint
10
+ * no longer applies -- per Ruling 44 ("eliminate duplication when you can,
11
+ * guard it only when you cannot"), these five copies are consolidated here.
12
+ * A future change to the branch-naming scheme or the worktree/frozen layout
13
+ * now cannot silently diverge between call sites by being made in one copy
14
+ * and forgotten in the others.
15
+ */
16
+ /** Prefix `baseline` uses for the run branch it creates: `<prefix><tag>`. */
17
+ export const BRANCH_PREFIX = 'autor3search-typescript/';
18
+ /** Directory name, under the per-run state dir, of the pinned baseline worktree. */
19
+ export const WORKTREE_DIRNAME = 'baseline-worktree';
20
+ /** Directory name, under the per-run state dir, of the frozen test/bench snapshot. */
21
+ export const FROZEN_DIRNAME = 'frozen';
22
+ /**
23
+ * `-tag`'s default: the tag encoded in the current run branch's name.
24
+ * Every command that accepts `-tag` falls back to this when it is omitted,
25
+ * so a human or agent already checked out on a run branch never has to
26
+ * repeat the tag on every invocation.
27
+ */
28
+ export async function inferTagFromBranch(repoRoot) {
29
+ let branch;
30
+ try {
31
+ branch = await currentBranch(repoRoot);
32
+ }
33
+ catch (e) {
34
+ const message = e instanceof Error ? e.message : String(e);
35
+ throw new Error(`-tag was not given, and the current branch could not be determined: ${message}`);
36
+ }
37
+ if (!branch.startsWith(BRANCH_PREFIX)) {
38
+ throw new Error(`-tag was not given, and the current branch (${JSON.stringify(branch)}) is not a run branch ` +
39
+ `(expected "${BRANCH_PREFIX}<tag>"). Pass -tag <tag> explicitly.`);
40
+ }
41
+ const tag = branch.slice(BRANCH_PREFIX.length);
42
+ if (tag === '') {
43
+ throw new Error(`-tag was not given, and the current branch (${JSON.stringify(branch)}) names no tag`);
44
+ }
45
+ return tag;
46
+ }
47
+ //# sourceMappingURL=runnaming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runnaming.js","sourceRoot":"","sources":["../../src/state/runnaming.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAE9C;;;;;;;;;;;;;GAaG;AAEH,6EAA6E;AAC7E,MAAM,CAAC,MAAM,aAAa,GAAG,0BAA0B,CAAA;AAEvD,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,mBAAmB,CAAA;AAEnD,sFAAsF;AACtF,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAA;AAEtC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,QAAgB;IACvD,IAAI,MAAc,CAAA;IAClB,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAA;IACxC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QAC1D,MAAM,IAAI,KAAK,CAAC,uEAAuE,OAAO,EAAE,CAAC,CAAA;IACnG,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CACb,+CAA+C,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,wBAAwB;YAC3F,cAAc,aAAa,sCAAsC,CACpE,CAAA;IACH,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAA;IAC9C,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,+CAA+C,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAA;IACxG,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}
@@ -0,0 +1,28 @@
1
+ export interface StopRequest {
2
+ requestedAt: string;
3
+ /** True when the user also wants the in-flight eval abandoned. */
4
+ force: boolean;
5
+ }
6
+ export declare function stopPath(dir: string): string;
7
+ /**
8
+ * A human runs `stop` in another terminal while an eval may be mid-read of
9
+ * this exact file. Write via a temp file plus rename (atomic within a
10
+ * directory) so a reader never observes a half-written stop.json.
11
+ */
12
+ export declare function requestStop(dir: string, force: boolean): Promise<void>;
13
+ /**
14
+ * Returns null when no stop has been requested (ENOENT) or when stop.json
15
+ * can't be parsed. The parse case is genuinely expected here -- a human's
16
+ * `stop` process could be killed mid-write despite the rename above, or a
17
+ * reader could race a writer that has not finished flushing yet -- so it must
18
+ * never crash the eval.
19
+ *
20
+ * Any other read failure (EACCES, a different uid, a transient I/O error,
21
+ * ...) is a different fact entirely: the request may well exist and simply
22
+ * be unreadable. Silently reporting that as "no request" would mean a user
23
+ * runs `stop`, it succeeds, and the eval side never observes it -- no error,
24
+ * no log, nothing. So only ENOENT (plus a genuine parse failure) collapses
25
+ * to null; everything else propagates.
26
+ */
27
+ export declare function readStop(dir: string): Promise<StopRequest | null>;
28
+ export declare function clearStop(dir: string): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ export function stopPath(dir) {
5
+ return path.join(dir, 'stop.json');
6
+ }
7
+ /**
8
+ * A human runs `stop` in another terminal while an eval may be mid-read of
9
+ * this exact file. Write via a temp file plus rename (atomic within a
10
+ * directory) so a reader never observes a half-written stop.json.
11
+ */
12
+ export async function requestStop(dir, force) {
13
+ await mkdir(dir, { recursive: true });
14
+ const rec = { requestedAt: new Date().toISOString(), force };
15
+ const tmpPath = path.join(dir, `.stop.json.${process.pid}.${randomUUID()}.tmp`);
16
+ await writeFile(tmpPath, JSON.stringify(rec));
17
+ try {
18
+ await rename(tmpPath, stopPath(dir));
19
+ }
20
+ catch (err) {
21
+ await rm(tmpPath, { force: true });
22
+ throw err;
23
+ }
24
+ }
25
+ /**
26
+ * Returns null when no stop has been requested (ENOENT) or when stop.json
27
+ * can't be parsed. The parse case is genuinely expected here -- a human's
28
+ * `stop` process could be killed mid-write despite the rename above, or a
29
+ * reader could race a writer that has not finished flushing yet -- so it must
30
+ * never crash the eval.
31
+ *
32
+ * Any other read failure (EACCES, a different uid, a transient I/O error,
33
+ * ...) is a different fact entirely: the request may well exist and simply
34
+ * be unreadable. Silently reporting that as "no request" would mean a user
35
+ * runs `stop`, it succeeds, and the eval side never observes it -- no error,
36
+ * no log, nothing. So only ENOENT (plus a genuine parse failure) collapses
37
+ * to null; everything else propagates.
38
+ */
39
+ export async function readStop(dir) {
40
+ let text;
41
+ try {
42
+ text = await readFile(stopPath(dir), 'utf8');
43
+ }
44
+ catch (err) {
45
+ if (err.code === 'ENOENT')
46
+ return null;
47
+ throw err;
48
+ }
49
+ try {
50
+ return JSON.parse(text);
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ export async function clearStop(dir) {
57
+ await rm(stopPath(dir), { force: true });
58
+ }
59
+ //# sourceMappingURL=stop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stop.js","sourceRoot":"","sources":["../../src/state/stop.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACzE,OAAO,IAAI,MAAM,WAAW,CAAA;AAQ5B,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAA;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAW,EAAE,KAAc;IAC3D,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACrC,MAAM,GAAG,GAAgB,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,CAAA;IACzE,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,OAAO,CAAC,GAAG,IAAI,UAAU,EAAE,MAAM,CAAC,CAAA;IAC/E,MAAM,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAA;IAC7C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAA;IACtC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAClC,MAAM,GAAG,CAAA;IACX,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAW;IACxC,IAAI,IAAY,CAAA;IAChB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAA;IAC9C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAA;QACjE,MAAM,GAAG,CAAA;IACX,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAgB,CAAA;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW;IACzC,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AAC1C,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The significance threshold. Deliberately a constant and not a config key:
3
+ * it is the one number the optimizing agent must not be able to argue with.
4
+ */
5
+ export declare const ALPHA = 0.05;
6
+ /** One benchmark compared across the two sides. */
7
+ export interface Delta {
8
+ name: string;
9
+ /** Median nanoseconds per operation on the baseline. */
10
+ baseNs: number;
11
+ /** Median nanoseconds per operation on the candidate. */
12
+ candNs: number;
13
+ /** Percent change; negative is faster. */
14
+ pctChange: number;
15
+ /** Two-sided p-value. */
16
+ p: number;
17
+ /** p < ALPHA, uncorrected. The honest statistic, not the KEEP threshold. */
18
+ significant: boolean;
19
+ /** Whether the exact test was used. */
20
+ exact: boolean;
21
+ }
22
+ export declare function compare(name: string, base: readonly number[], cand: readonly number[]): Delta;
23
+ /**
24
+ * Compares every benchmark, requiring both sides to declare exactly the same
25
+ * set. A mismatch is an error rather than an intersection: silently comparing
26
+ * only the overlap would let a candidate drop a benchmark it regressed and
27
+ * still be scored.
28
+ */
29
+ export declare function compareAll(base: ReadonlyMap<string, number[]>, cand: ReadonlyMap<string, number[]>): Delta[];
@@ -0,0 +1,41 @@
1
+ import { median } from './geomean.js';
2
+ import { mannWhitneyU } from './mannwhitney.js';
3
+ /**
4
+ * The significance threshold. Deliberately a constant and not a config key:
5
+ * it is the one number the optimizing agent must not be able to argue with.
6
+ */
7
+ export const ALPHA = 0.05;
8
+ export function compare(name, base, cand) {
9
+ const baseNs = median(base);
10
+ const candNs = median(cand);
11
+ const { p, exact } = mannWhitneyU(base, cand);
12
+ return {
13
+ name,
14
+ baseNs,
15
+ candNs,
16
+ pctChange: ((candNs - baseNs) / baseNs) * 100,
17
+ p,
18
+ significant: p < ALPHA,
19
+ exact,
20
+ };
21
+ }
22
+ /**
23
+ * Compares every benchmark, requiring both sides to declare exactly the same
24
+ * set. A mismatch is an error rather than an intersection: silently comparing
25
+ * only the overlap would let a candidate drop a benchmark it regressed and
26
+ * still be scored.
27
+ */
28
+ export function compareAll(base, cand) {
29
+ const missing = [...base.keys()].filter((k) => !cand.has(k)).sort();
30
+ if (missing.length > 0) {
31
+ throw new Error(`candidate is missing benchmarks present in the baseline: ${missing.join(', ')}`);
32
+ }
33
+ const extra = [...cand.keys()].filter((k) => !base.has(k)).sort();
34
+ if (extra.length > 0) {
35
+ throw new Error(`candidate declares unexpected benchmarks absent from the baseline: ${extra.join(', ')}`);
36
+ }
37
+ return [...base.keys()]
38
+ .sort()
39
+ .map((name) => compare(name, base.get(name), cand.get(name)));
40
+ }
41
+ //# sourceMappingURL=delta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delta.js","sourceRoot":"","sources":["../../src/stats/delta.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE/C;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,IAAI,CAAA;AAmBzB,MAAM,UAAU,OAAO,CACrB,IAAY,EACZ,IAAuB,EACvB,IAAuB;IAEvB,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAC3B,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAC7C,OAAO;QACL,IAAI;QACJ,MAAM;QACN,MAAM;QACN,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,GAAG;QAC7C,CAAC;QACD,WAAW,EAAE,CAAC,GAAG,KAAK;QACtB,KAAK;KACN,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,IAAmC,EACnC,IAAmC;IAEnC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IACnE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,4DAA4D,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACjF,CAAA;IACH,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IACjE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CACb,sEAAsE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACzF,CAAA;IACH,CAAC;IACD,OAAO,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;SACpB,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAE,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,CAAC,CAAA;AACnE,CAAC"}
@@ -0,0 +1,4 @@
1
+ /** Geometric mean. Used to combine per-benchmark ratios into one score. */
2
+ export declare function geoMean(values: readonly number[]): number;
3
+ /** Median. Does not modify the caller's array. */
4
+ export declare function median(values: readonly number[]): number;
@@ -0,0 +1,21 @@
1
+ /** Geometric mean. Used to combine per-benchmark ratios into one score. */
2
+ export function geoMean(values) {
3
+ if (values.length === 0)
4
+ throw new Error('geoMean: empty value set');
5
+ let sumLog = 0;
6
+ for (const v of values) {
7
+ if (!(v > 0))
8
+ throw new Error(`geoMean: values must be positive, got ${v}`);
9
+ sumLog += Math.log(v);
10
+ }
11
+ return Math.exp(sumLog / values.length);
12
+ }
13
+ /** Median. Does not modify the caller's array. */
14
+ export function median(values) {
15
+ if (values.length === 0)
16
+ throw new Error('median: empty value set');
17
+ const xs = values.slice().sort((a, b) => a - b);
18
+ const mid = xs.length >> 1;
19
+ return xs.length % 2 === 1 ? xs[mid] : (xs[mid - 1] + xs[mid]) / 2;
20
+ }
21
+ //# sourceMappingURL=geomean.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geomean.js","sourceRoot":"","sources":["../../src/stats/geomean.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,MAAM,UAAU,OAAO,CAAC,MAAyB;IAC/C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAA;IACpE,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,EAAE,CAAC,CAAA;QAC3E,MAAM,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;AACzC,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,MAAM,CAAC,MAAyB;IAC9C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAA;IACnE,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC/C,MAAM,GAAG,GAAG,EAAE,CAAC,MAAM,IAAI,CAAC,CAAA;IAC1B,OAAO,EAAE,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAE,GAAG,EAAE,CAAC,GAAG,CAAE,CAAC,GAAG,CAAC,CAAA;AACvE,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Exact and approximate Mann-Whitney U test.
3
+ *
4
+ * Benchmark rounds give us small samples (default 10 per side), where the
5
+ * normal approximation is poor. For small n with no ties we enumerate the
6
+ * exact null distribution instead; that is also what makes the p-value floor
7
+ * in `minAchievableP` a real, reportable property rather than an estimate.
8
+ */
9
+ export interface MannWhitneyResult {
10
+ /** The U statistic for the smaller of the two rank sums. */
11
+ u: number;
12
+ /** Two-sided p-value, in [0, 1]. */
13
+ p: number;
14
+ /** True when the exact null distribution was used. */
15
+ exact: boolean;
16
+ }
17
+ /**
18
+ * The smallest two-sided p-value the exact test can ever produce at these
19
+ * sample sizes: 2 / C(n1+n2, n1). No degree of separation between the samples
20
+ * can go below it. For equal sizes this is the 2/C(2n,n) floor that makes a
21
+ * Bonferroni-corrected KEEP unreachable once there are enough benchmarks.
22
+ */
23
+ export declare function minAchievableP(n1: number, n2: number): number;
24
+ /**
25
+ * Two-sided Mann-Whitney U test. Neither input is modified.
26
+ */
27
+ export declare function mannWhitneyU(a: readonly number[], b: readonly number[]): MannWhitneyResult;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Exact and approximate Mann-Whitney U test.
3
+ *
4
+ * Benchmark rounds give us small samples (default 10 per side), where the
5
+ * normal approximation is poor. For small n with no ties we enumerate the
6
+ * exact null distribution instead; that is also what makes the p-value floor
7
+ * in `minAchievableP` a real, reportable property rather than an estimate.
8
+ */
9
+ /**
10
+ * Largest n1*n2 for which we build the exact distribution.
11
+ *
12
+ * The DP table's total footprint grows much faster than n1*n2 itself: at
13
+ * n1=n2=50 (2_500) it holds 1,628,226 Float64 entries, about 12 MB; at
14
+ * n1=n2=100 (10_000) — a value `count` can reach with no upper bound in
15
+ * config, and one the README will tell users to raise on a noisy machine —
16
+ * it holds 25,512,701 entries, about 195 MB, once per benchmark per
17
+ * experiment. That is a plausible way to OOM a memory-constrained CI runner,
18
+ * not a theoretical edge case. Above this bound we fall back to the normal
19
+ * approximation, which is essentially exact once each sample exceeds ~50
20
+ * observations.
21
+ */
22
+ const MAX_EXACT_CELLS = 2_500;
23
+ /**
24
+ * Counts of the U statistic under the null hypothesis.
25
+ *
26
+ * table[u] is the number of arrangements of n1 and n2 observations giving
27
+ * statistic u, via c(i, j, u) = c(i-1, j, u-j) + c(i, j-1, u).
28
+ */
29
+ function uCounts(n1, n2) {
30
+ const table = [];
31
+ for (let i = 0; i <= n1; i++) {
32
+ const row = [];
33
+ for (let j = 0; j <= n2; j++)
34
+ row.push(new Float64Array(i * j + 1));
35
+ table.push(row);
36
+ }
37
+ table[0][0][0] = 1;
38
+ for (let i = 0; i <= n1; i++) {
39
+ for (let j = 0; j <= n2; j++) {
40
+ if (i === 0 && j === 0)
41
+ continue;
42
+ const cur = table[i][j];
43
+ for (let u = 0; u <= i * j; u++) {
44
+ let v = 0;
45
+ if (i > 0 && u - j >= 0 && u - j <= (i - 1) * j)
46
+ v += table[i - 1][j][u - j];
47
+ if (j > 0 && u <= i * (j - 1))
48
+ v += table[i][j - 1][u];
49
+ cur[u] = v;
50
+ }
51
+ }
52
+ }
53
+ return table[n1][n2];
54
+ }
55
+ /** Binomial coefficient, exact for the sizes we use. */
56
+ function binomial(n, k) {
57
+ let r = 1;
58
+ for (let i = 1; i <= k; i++)
59
+ r = (r * (n - k + i)) / i;
60
+ return Math.round(r);
61
+ }
62
+ /**
63
+ * The smallest two-sided p-value the exact test can ever produce at these
64
+ * sample sizes: 2 / C(n1+n2, n1). No degree of separation between the samples
65
+ * can go below it. For equal sizes this is the 2/C(2n,n) floor that makes a
66
+ * Bonferroni-corrected KEEP unreachable once there are enough benchmarks.
67
+ */
68
+ export function minAchievableP(n1, n2) {
69
+ return Math.min(1, 2 / binomial(n1 + n2, n1));
70
+ }
71
+ /** Tie-averaged ranks of `values`, in the original element order. */
72
+ function rank(values) {
73
+ const idx = values.map((v, i) => ({ v, i }));
74
+ idx.sort((a, b) => a.v - b.v);
75
+ const ranks = new Array(values.length);
76
+ let i = 0;
77
+ while (i < idx.length) {
78
+ let j = i;
79
+ while (j + 1 < idx.length && idx[j + 1].v === idx[i].v)
80
+ j++;
81
+ // Ranks are 1-based; tied entries share their average rank.
82
+ const avg = (i + j + 2) / 2;
83
+ for (let k = i; k <= j; k++)
84
+ ranks[idx[k].i] = avg;
85
+ i = j + 1;
86
+ }
87
+ return ranks;
88
+ }
89
+ function hasTies(values) {
90
+ return new Set(values).size !== values.length;
91
+ }
92
+ /** Normal CDF via the complementary error function. */
93
+ function normalCdf(z) {
94
+ // Abramowitz & Stegun 7.1.26 applied to erf.
95
+ const sign = z < 0 ? -1 : 1;
96
+ const x = Math.abs(z) / Math.SQRT2;
97
+ const t = 1 / (1 + 0.3275911 * x);
98
+ const y = 1 -
99
+ ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t +
100
+ 0.254829592) *
101
+ t *
102
+ Math.exp(-x * x);
103
+ return 0.5 * (1 + sign * y);
104
+ }
105
+ /**
106
+ * Two-sided Mann-Whitney U test. Neither input is modified.
107
+ */
108
+ export function mannWhitneyU(a, b) {
109
+ if (a.length === 0 || b.length === 0) {
110
+ throw new Error('mannWhitneyU: each sample needs at least one observation');
111
+ }
112
+ // These slices are not what stops caller mutation today: rank() sorts a
113
+ // derived array of {v, i} pairs rather than `combined`, and `combined`
114
+ // (built with concat() below) is already a fresh array regardless of
115
+ // whether xa/xb are copies. The "does not mutate its inputs" guarantee
116
+ // currently comes entirely from rank()'s design. We keep these copies
117
+ // anyway — they are cheap, and they keep that guarantee true even if
118
+ // rank() is later changed to sort its argument in place.
119
+ const xa = a.slice();
120
+ const xb = b.slice();
121
+ const n1 = xa.length;
122
+ const n2 = xb.length;
123
+ const combined = xa.concat(xb);
124
+ const ranks = rank(combined);
125
+ let rankSumA = 0;
126
+ for (let i = 0; i < n1; i++)
127
+ rankSumA += ranks[i];
128
+ const uA = rankSumA - (n1 * (n1 + 1)) / 2;
129
+ const uB = n1 * n2 - uA;
130
+ const u = Math.min(uA, uB);
131
+ const ties = hasTies(combined);
132
+ if (!ties && n1 * n2 <= MAX_EXACT_CELLS) {
133
+ const counts = uCounts(n1, n2);
134
+ let atOrBelow = 0;
135
+ for (let k = 0; k <= u; k++)
136
+ atOrBelow += counts[k];
137
+ const total = binomial(n1 + n2, n1);
138
+ const p = Math.min(1, (2 * atOrBelow) / total);
139
+ return { u, p, exact: true };
140
+ }
141
+ // Normal approximation with a tie correction on the variance.
142
+ const mean = (n1 * n2) / 2;
143
+ const n = n1 + n2;
144
+ const counts = new Map();
145
+ for (const v of combined)
146
+ counts.set(v, (counts.get(v) ?? 0) + 1);
147
+ let tieTerm = 0;
148
+ for (const c of counts.values())
149
+ tieTerm += c * c * c - c;
150
+ const variance = ((n1 * n2) / 12) * (n + 1 - tieTerm / (n * (n - 1)));
151
+ if (variance <= 0)
152
+ return { u, p: 1, exact: false };
153
+ // Continuity correction.
154
+ const z = (Math.abs(u - mean) - 0.5) / Math.sqrt(variance);
155
+ const p = Math.min(1, 2 * (1 - normalCdf(z)));
156
+ return { u, p, exact: false };
157
+ }
158
+ //# sourceMappingURL=mannwhitney.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mannwhitney.js","sourceRoot":"","sources":["../../src/stats/mannwhitney.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { type Delta } from '../stats/delta.js';
2
+ export type Status = 'keep' | 'discard' | 'fail' | 'crash';
3
+ export declare const EXIT_CODES: Record<Status, number>;
4
+ export type DiscardReason =
5
+ /** Nothing measurably moved. */
6
+ 'no_significant_improvement'
7
+ /** It really did speed things up, by less than min_effect_pct. */
8
+ | 'improvement_below_min_effect'
9
+ /** At least one benchmark got significantly and materially worse. */
10
+ | 'significant_regression';
11
+ export interface Verdict {
12
+ status: Status;
13
+ /** geomean of candidate/baseline time ratios. Below 1 is faster. */
14
+ score: number;
15
+ reason?: DiscardReason;
16
+ /** Names of benchmarks that triggered the regression guard. */
17
+ regressions: string[];
18
+ correctedAlpha: number;
19
+ /** Never changes the decision; explains when it cannot be read at face value. */
20
+ warnings: string[];
21
+ }
22
+ export interface VerdictInput {
23
+ deltas: Delta[];
24
+ minEffectPct: number;
25
+ maxRegressPct: number;
26
+ /** Measured rounds per side, for the sample-size warnings. */
27
+ rounds: number;
28
+ }
29
+ export declare function decide(input: VerdictInput): Verdict;
30
+ /**
31
+ * The smallest per-side round count whose exact p-floor clears ALPHA/k, or
32
+ * `undefined` if no count up to `MAX_REQUIRED_COUNT` does.
33
+ *
34
+ * The brief's version returned the cap itself in that case, which reads as
35
+ * "raising count to 64 fixes it" when 64 in fact still cannot clear the
36
+ * bar — misleading advice for a caller that acts on it and then watches
37
+ * every experiment keep discarding anyway. Reporting "unreachable"
38
+ * explicitly instead lets the caller be told to reduce benchmark count
39
+ * instead of chasing a round count that was never going to help.
40
+ *
41
+ * Exported for testing: the case this guards against needs k on the order
42
+ * of 1e36 (floor(64, 64) is about 8.35e-38) to reach the cap for real,
43
+ * which is unreachable through `decide`'s own Delta[] input without
44
+ * constructing an array of that size.
45
+ */
46
+ export declare function requiredCount(k: number): number | undefined;
@@ -0,0 +1,102 @@
1
+ import { geoMean } from '../stats/geomean.js';
2
+ import { ALPHA } from '../stats/delta.js';
3
+ import { minAchievableP } from '../stats/mannwhitney.js';
4
+ export const EXIT_CODES = {
5
+ keep: 0,
6
+ discard: 1,
7
+ fail: 2,
8
+ crash: 3,
9
+ };
10
+ /** At 95% confidence the median's interval needs at least this many samples. */
11
+ const MIN_ROUNDS_FOR_CI = 6;
12
+ export function decide(input) {
13
+ const { deltas, minEffectPct, maxRegressPct, rounds } = input;
14
+ if (deltas.length === 0) {
15
+ throw new Error('cannot decide with no benchmarks compared');
16
+ }
17
+ // decide() must be total: its Delta[] input comes from the statistics
18
+ // layer, which this module does not control. Left unchecked, a zero or
19
+ // negative baseNs/candNs (division, or a 0/0 ratio) or an out-of-range p
20
+ // would either throw geoMean's generic "must be positive" error with no
21
+ // benchmark attached, or -- for a positive/zero ratio, which geoMean's
22
+ // check happily lets through -- silently produce an infinite score. Reject
23
+ // it explicitly here instead, naming the benchmark, so a corrupt upstream
24
+ // Delta is diagnosable at the boundary rather than three frames deep or
25
+ // not diagnosed at all.
26
+ for (const delta of deltas) {
27
+ if (!(delta.baseNs > 0) || !Number.isFinite(delta.baseNs)) {
28
+ throw new Error(`invalid delta for "${delta.name}": baseNs must be a positive finite number, got ${delta.baseNs}`);
29
+ }
30
+ if (!(delta.candNs > 0) || !Number.isFinite(delta.candNs)) {
31
+ throw new Error(`invalid delta for "${delta.name}": candNs must be a positive finite number, got ${delta.candNs}`);
32
+ }
33
+ if (!(delta.p >= 0 && delta.p <= 1)) {
34
+ throw new Error(`invalid delta for "${delta.name}": p must be a number in [0, 1], got ${delta.p}`);
35
+ }
36
+ }
37
+ const score = geoMean(deltas.map((d) => d.candNs / d.baseNs));
38
+ const k = deltas.length;
39
+ const correctedAlpha = ALPHA / k;
40
+ const warnings = [];
41
+ if (rounds < MIN_ROUNDS_FOR_CI) {
42
+ warnings.push(`only ${rounds} rounds per side: a 95% confidence interval for the median needs at least ` +
43
+ `${MIN_ROUNDS_FOR_CI} observations, so below that it is unbounded`);
44
+ }
45
+ const floor = minAchievableP(rounds, rounds);
46
+ if (floor > correctedAlpha) {
47
+ const suggestion = requiredCount(k);
48
+ const advice = suggestion === undefined
49
+ ? `No per-side round count up to ${MAX_REQUIRED_COUNT} would clear it either — reduce the ` +
50
+ `number of benchmarks compared instead.`
51
+ : `Raise count to at least ${suggestion}.`;
52
+ warnings.push(`no KEEP was reachable: with ${rounds} rounds per side the smallest attainable two-sided ` +
53
+ `p-value is ${floor.toExponential(2)}, above the Bonferroni-corrected threshold of ` +
54
+ `${correctedAlpha.toExponential(2)} for ${k} benchmarks — every experiment would discard ` +
55
+ `regardless of what changed. ${advice}`);
56
+ }
57
+ // Harm first, and deliberately at the UNCORRECTED alpha. The Bonferroni
58
+ // correction only ever makes significance harder to declare; applying it
59
+ // here would make real regressions easier to miss. Conservative about
60
+ // accepting a win, liberal about catching damage.
61
+ const regressions = deltas
62
+ .filter((d) => d.significant && d.pctChange > maxRegressPct)
63
+ .map((d) => d.name);
64
+ if (regressions.length > 0) {
65
+ return { status: 'discard', score, reason: 'significant_regression', regressions, correctedAlpha, warnings };
66
+ }
67
+ const anyCorrectedSignificantWin = deltas.some((d) => d.p < correctedAlpha && d.pctChange < 0);
68
+ if (!anyCorrectedSignificantWin) {
69
+ return { status: 'discard', score, reason: 'no_significant_improvement', regressions, correctedAlpha, warnings };
70
+ }
71
+ const threshold = 1 - minEffectPct / 100;
72
+ if (!(score < threshold)) {
73
+ return { status: 'discard', score, reason: 'improvement_below_min_effect', regressions, correctedAlpha, warnings };
74
+ }
75
+ return { status: 'keep', score, regressions, correctedAlpha, warnings };
76
+ }
77
+ /** Largest per-side round count `requiredCount` will search up to. */
78
+ const MAX_REQUIRED_COUNT = 64;
79
+ /**
80
+ * The smallest per-side round count whose exact p-floor clears ALPHA/k, or
81
+ * `undefined` if no count up to `MAX_REQUIRED_COUNT` does.
82
+ *
83
+ * The brief's version returned the cap itself in that case, which reads as
84
+ * "raising count to 64 fixes it" when 64 in fact still cannot clear the
85
+ * bar — misleading advice for a caller that acts on it and then watches
86
+ * every experiment keep discarding anyway. Reporting "unreachable"
87
+ * explicitly instead lets the caller be told to reduce benchmark count
88
+ * instead of chasing a round count that was never going to help.
89
+ *
90
+ * Exported for testing: the case this guards against needs k on the order
91
+ * of 1e36 (floor(64, 64) is about 8.35e-38) to reach the cap for real,
92
+ * which is unreachable through `decide`'s own Delta[] input without
93
+ * constructing an array of that size.
94
+ */
95
+ export function requiredCount(k) {
96
+ for (let n = 4; n <= MAX_REQUIRED_COUNT; n++) {
97
+ if (minAchievableP(n, n) < ALPHA / k)
98
+ return n;
99
+ }
100
+ return undefined;
101
+ }
102
+ //# sourceMappingURL=verdict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verdict.js","sourceRoot":"","sources":["../../src/verdict/verdict.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ /** The published package version. Kept in step with package.json by hand. */
2
+ export declare const VERSION = "0.1.0";
@@ -0,0 +1,3 @@
1
+ /** The published package version. Kept in step with package.json by hand. */
2
+ export const VERSION = '0.1.0';
3
+ //# sourceMappingURL=version.js.map