@workos/quickstudy 0.0.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +270 -0
  3. package/examples/harbor-notes/README.md +40 -0
  4. package/examples/harbor-notes/evals/create-note/EVAL.ts +14 -0
  5. package/examples/harbor-notes/evals/create-note/PROMPT.md +9 -0
  6. package/examples/harbor-notes/evals/create-note/local/README.txt +1 -0
  7. package/examples/harbor-notes/experiments/scripted.ts +6 -0
  8. package/examples/harbor-notes/package.json +6 -0
  9. package/examples/harbor-notes/quickstudy.identity.json +1 -0
  10. package/examples/harbor-notes/runtime.ts +48 -0
  11. package/examples/harbor-notes/semantic-example.ts +21 -0
  12. package/images/agent-runtime/Dockerfile +58 -0
  13. package/images/egress-proxy/Dockerfile +28 -0
  14. package/images/mcp-proxy/Dockerfile +30 -0
  15. package/package.json +53 -0
  16. package/src/adapters/claude.ts +107 -0
  17. package/src/adapters/codex.ts +107 -0
  18. package/src/adapters/echo.ts +57 -0
  19. package/src/adapters/parse.ts +117 -0
  20. package/src/adapters/types.ts +152 -0
  21. package/src/build-info.generated.ts +12 -0
  22. package/src/cli.ts +787 -0
  23. package/src/completeness.ts +104 -0
  24. package/src/diagnose/excerpt.ts +106 -0
  25. package/src/diagnose/prompt.ts +175 -0
  26. package/src/diagnose/render.ts +55 -0
  27. package/src/diagnose/run.ts +290 -0
  28. package/src/diagnose/select.ts +110 -0
  29. package/src/diagnose/types.ts +88 -0
  30. package/src/evals/discovery.ts +173 -0
  31. package/src/evals/prompt.ts +190 -0
  32. package/src/evals/result.ts +10 -0
  33. package/src/evals/types.ts +115 -0
  34. package/src/execution-policy.ts +71 -0
  35. package/src/experiments/discovery.ts +76 -0
  36. package/src/experiments/groups.ts +119 -0
  37. package/src/experiments/types.ts +116 -0
  38. package/src/export-types.ts +127 -0
  39. package/src/export.ts +381 -0
  40. package/src/hash.ts +74 -0
  41. package/src/identity-diff.ts +30 -0
  42. package/src/ids.ts +30 -0
  43. package/src/index.ts +58 -0
  44. package/src/isolation/docker.ts +639 -0
  45. package/src/isolation/image-contexts.generated.ts +927 -0
  46. package/src/isolation/images.ts +138 -0
  47. package/src/isolation/mcp-proxy/server.ts +260 -0
  48. package/src/isolation/mcp.ts +144 -0
  49. package/src/isolation/proxy/allowlist.ts +148 -0
  50. package/src/isolation/proxy/server.ts +382 -0
  51. package/src/llm.ts +132 -0
  52. package/src/manifest.ts +228 -0
  53. package/src/model-identity.ts +12 -0
  54. package/src/plan.ts +55 -0
  55. package/src/probe.ts +426 -0
  56. package/src/report/pass-at-k.ts +76 -0
  57. package/src/report/report.ts +731 -0
  58. package/src/runner/context.ts +96 -0
  59. package/src/runner/deadline.ts +37 -0
  60. package/src/runner/execute.ts +992 -0
  61. package/src/runner/run-lock.ts +32 -0
  62. package/src/runner/scheduler.ts +62 -0
  63. package/src/runner/score-worker.ts +107 -0
  64. package/src/runner/scorer-worker.ts +61 -0
  65. package/src/runtime/types.ts +89 -0
  66. package/src/secrets.ts +151 -0
  67. package/src/semantic.ts +185 -0
  68. package/src/serve.ts +52 -0
  69. package/src/source-identity.ts +76 -0
  70. package/src/store/artifacts.ts +146 -0
  71. package/src/store/db.ts +318 -0
  72. package/src/store/schema.ts +39 -0
  73. package/src/surface-usage.ts +297 -0
  74. package/src/ui-bundle.generated.ts +12 -0
  75. package/ui/dist/index.html +32 -0
package/src/hash.ts ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Content-hashing primitives for the v3 run manifest. Extracted from the
3
+ * legacy experiment-identity module (deleted with the v2 stack) as a
4
+ * byte-for-byte behavior match, so digests recorded by v2 runs stay
5
+ * comparable with v3 ones.
6
+ */
7
+
8
+ import { createHash } from "node:crypto";
9
+ import { lstatSync, readdirSync, readFileSync, readlinkSync } from "node:fs";
10
+ import { basename, join, relative } from "node:path";
11
+
12
+ /** A content digest plus the file accounting that produced it. */
13
+ export interface ContentIdentity {
14
+ sha256: string;
15
+ files?: number;
16
+ excluded?: number;
17
+ }
18
+
19
+ /**
20
+ * Basenames that look like credentials never enter a content digest.
21
+ * (The legacy scenario loader's sidecar filename guard shared this exact
22
+ * regex before its deletion; it stays exported for tests and any future
23
+ * consumer with the same "invisible to the digest ⇒ carries no behavior"
24
+ * invariant.)
25
+ */
26
+ export const SECRET_FILE = /(?:^|[._-])(?:env|credentials?|tokens?|secrets?|private[._-]?keys?|mcp[._-]?auth)(?:$|[._-])|\.(?:pem|key|p12|pfx)$/i;
27
+ const SKIP_DIR = new Set([".git", "node_modules", "vendor", ".venv", "results", "dist"]);
28
+
29
+ /** SHA-256 hex digest of a string or byte buffer. */
30
+ export function hashString(value: string | Uint8Array): string {
31
+ return createHash("sha256").update(value).digest("hex");
32
+ }
33
+
34
+ /**
35
+ * Hash a directory exactly by relative path, type, executable bit and bytes.
36
+ * Secret-looking files are rejected before read and represented only by an
37
+ * exclusion count; their names and contents never enter the digest.
38
+ */
39
+ export function hashTree(root: string): ContentIdentity {
40
+ const hash = createHash("sha256");
41
+ let files = 0;
42
+ let excluded = 0;
43
+ const walk = (dir: string): void => {
44
+ for (const name of readdirSync(dir).sort()) {
45
+ const path = join(dir, name);
46
+ const rel = relative(root, path).split("\\").join("/");
47
+ const stat = lstatSync(path);
48
+ if (stat.isDirectory()) {
49
+ if (SKIP_DIR.has(name) || SECRET_FILE.test(name)) {
50
+ excluded += 1;
51
+ continue;
52
+ }
53
+ walk(path);
54
+ continue;
55
+ }
56
+ if (SECRET_FILE.test(basename(path))) {
57
+ excluded += 1;
58
+ continue;
59
+ }
60
+ if (stat.isSymbolicLink()) {
61
+ hash.update(`L\0${rel}\0${readlinkSync(path)}\0`);
62
+ files += 1;
63
+ continue;
64
+ }
65
+ if (!stat.isFile()) continue;
66
+ hash.update(`F\0${rel}\0${stat.mode & 0o111 ? "x" : "-"}\0`);
67
+ hash.update(readFileSync(path));
68
+ hash.update("\0");
69
+ files += 1;
70
+ }
71
+ };
72
+ walk(root);
73
+ return { sha256: hash.digest("hex"), files, excluded };
74
+ }
@@ -0,0 +1,30 @@
1
+ /** Browser-safe canonicalization and structural identity diffing. */
2
+
3
+ export function canonicalJson(value: unknown): string {
4
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
5
+ if (value !== null && typeof value === "object") {
6
+ const entries = Object.entries(value as Record<string, unknown>)
7
+ .filter(([, item]) => item !== undefined)
8
+ .sort(([a], [b]) => a.localeCompare(b));
9
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
10
+ }
11
+ return JSON.stringify(value);
12
+ }
13
+
14
+ export interface IdentityMismatch {
15
+ path: string;
16
+ a: unknown;
17
+ b: unknown;
18
+ }
19
+
20
+ export function diffIdentity(a: unknown, b: unknown, path = "identity"): IdentityMismatch[] {
21
+ if (canonicalJson(a) === canonicalJson(b)) return [];
22
+ if (a !== null && b !== null && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
23
+ const left = a as Record<string, unknown>;
24
+ const right = b as Record<string, unknown>;
25
+ return [...new Set([...Object.keys(left), ...Object.keys(right)])]
26
+ .sort()
27
+ .flatMap((key) => diffIdentity(left[key], right[key], `${path}.${key}`));
28
+ }
29
+ return [{ path, a: a ?? null, b: b ?? null }];
30
+ }
package/src/ids.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Minimal ULID generator (https://github.com/ulid/spec).
3
+ *
4
+ * The data model keys runs and attempts by ulid, but this phase's runtime
5
+ * dependencies are limited to `zod` and `yaml` — so we hand-roll the 26
6
+ * characters here rather than pull a package: 48-bit millisecond timestamp +
7
+ * 80 bits of randomness, Crockford base32. Lexicographic order is creation
8
+ * order, which keeps `results/<run>/<attempt>/` listings chronological.
9
+ */
10
+
11
+ const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
12
+
13
+ /**
14
+ * `random` overrides the 16 entropy bytes — only fixture seeders pass it, so
15
+ * committed dev data regenerates byte-identically. Production callers omit it.
16
+ */
17
+ export function ulid(now: number = Date.now(), random?: Uint8Array): string {
18
+ const chars: string[] = Array.from({ length: 26 }, () => "0");
19
+ let time = now;
20
+ for (let i = 9; i >= 0; i -= 1) {
21
+ chars[i] = ENCODING[time % 32] as string;
22
+ time = Math.floor(time / 32);
23
+ }
24
+ // 256 is an exact multiple of 32, so a modulo here is bias-free.
25
+ const bytes = random ?? crypto.getRandomValues(new Uint8Array(16));
26
+ for (let i = 0; i < 16; i += 1) {
27
+ chars[10 + i] = ENCODING[(bytes[i] as number) % 32] as string;
28
+ }
29
+ return chars.join("");
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The package's public entry point (`exports["."]` in package.json).
3
+ *
4
+ * The v3 authoring API: what an EVAL.ts scorer and an experiment module
5
+ * import from "quickstudy".
6
+ */
7
+
8
+ export type {
9
+ CheckResult,
10
+ EvalContext,
11
+ EvalExecResult,
12
+ EvalMetadata,
13
+ EvalResult,
14
+ EvalScorer,
15
+ EvalSuite,
16
+ EvidenceReference,
17
+ LoadedEval,
18
+ } from "./evals/types.ts";
19
+ export { EVAL_SUITES } from "./evals/types.ts";
20
+ export { defineExperiment } from "./experiments/types.ts";
21
+ export type { AgentSpec, Experiment, LoadedExperiment, Runtime } from "./experiments/types.ts";
22
+ export type {
23
+ ProvisionedEnvironment,
24
+ RuntimeCapabilities,
25
+ RuntimeProvisionContext,
26
+ ScorerCapabilities,
27
+ ScorerCapabilityContext,
28
+ WebPolicy,
29
+ } from "./runtime/types.ts";
30
+ export type { McpServerAuthConfig, McpServerConfig } from "./isolation/mcp.ts";
31
+ export { compareGroups } from "./experiments/groups.ts";
32
+ export type { ComparisonGroup } from "./experiments/groups.ts";
33
+
34
+ // Probe scaffolding: app boot, readiness, Playwright invocation, and probe
35
+ // utilities consumed by scorer libraries. Exported so consumer scorers import
36
+ // the harness by package name, never by relative path into src/.
37
+ export {
38
+ buildStartScript,
39
+ deepJsonEqual,
40
+ deliverSpecSource,
41
+ evaluateAssertions,
42
+ fetchViaExec,
43
+ readAppLogTail,
44
+ runPlaywrightSpec,
45
+ startAppInBackground,
46
+ stopBackgroundApp,
47
+ truncateOutput,
48
+ waitForAppReady,
49
+ } from "./probe.ts";
50
+ export type { JsonAssertion, ProbeExecFn } from "./probe.ts";
51
+
52
+ export type { SurfaceUsageConfig, SurfaceUsage } from "./surface-usage.ts";
53
+
54
+ export { defineSemanticScorer, SemanticScoringError } from "./semantic.ts";
55
+ export type { SemanticRubric, SemanticJudge, SemanticJudgeRequest, SemanticJudgeResponse } from "./semantic.ts";
56
+ export type { AgentOutput, JudgeRecord } from "./evals/types.ts";
57
+
58
+ export { extractSurfaceUsage, surfaceUsageConfigFromRuntime, observationConfig } from "./surface-usage.ts";