@smthrs/evals 0.0.0-stage → 1.0.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/LICENSE +21 -0
  3. package/README.md +62 -2
  4. package/dist/cjs/Baseline.d.ts +118 -0
  5. package/dist/cjs/Baseline.d.ts.map +1 -0
  6. package/dist/cjs/Baseline.js +158 -0
  7. package/dist/cjs/Baseline.js.map +7 -0
  8. package/dist/cjs/CaseExecutor.d.ts +85 -0
  9. package/dist/cjs/CaseExecutor.d.ts.map +1 -0
  10. package/dist/cjs/CaseExecutor.js +59 -0
  11. package/dist/cjs/CaseExecutor.js.map +7 -0
  12. package/dist/cjs/EvalError.d.ts +64 -0
  13. package/dist/cjs/EvalError.d.ts.map +1 -0
  14. package/dist/cjs/EvalError.js +54 -0
  15. package/dist/cjs/EvalError.js.map +7 -0
  16. package/dist/cjs/Gate.d.ts +55 -0
  17. package/dist/cjs/Gate.d.ts.map +1 -0
  18. package/dist/cjs/Gate.js +88 -0
  19. package/dist/cjs/Gate.js.map +7 -0
  20. package/dist/cjs/Regression.d.ts +116 -0
  21. package/dist/cjs/Regression.d.ts.map +1 -0
  22. package/dist/cjs/Regression.js +191 -0
  23. package/dist/cjs/Regression.js.map +7 -0
  24. package/dist/cjs/Report.d.ts +114 -0
  25. package/dist/cjs/Report.d.ts.map +1 -0
  26. package/dist/cjs/Report.js +161 -0
  27. package/dist/cjs/Report.js.map +7 -0
  28. package/dist/cjs/Runner.d.ts +229 -0
  29. package/dist/cjs/Runner.d.ts.map +1 -0
  30. package/dist/cjs/Runner.js +430 -0
  31. package/dist/cjs/Runner.js.map +7 -0
  32. package/dist/cjs/Suite.d.ts +139 -0
  33. package/dist/cjs/Suite.d.ts.map +1 -0
  34. package/dist/cjs/Suite.js +221 -0
  35. package/dist/cjs/Suite.js.map +7 -0
  36. package/dist/cjs/Trials.d.ts +93 -0
  37. package/dist/cjs/Trials.d.ts.map +1 -0
  38. package/dist/cjs/Trials.js +82 -0
  39. package/dist/cjs/Trials.js.map +7 -0
  40. package/dist/cjs/index.d.ts +76 -0
  41. package/dist/cjs/index.d.ts.map +1 -0
  42. package/dist/cjs/index.js +51 -0
  43. package/dist/cjs/index.js.map +7 -0
  44. package/dist/cjs/internal/CanonicalJson.d.ts +44 -0
  45. package/dist/cjs/internal/CanonicalJson.d.ts.map +1 -0
  46. package/dist/cjs/internal/CanonicalJson.js +114 -0
  47. package/dist/cjs/internal/CanonicalJson.js.map +7 -0
  48. package/dist/cjs/internal/controlCharacters.d.ts +44 -0
  49. package/dist/cjs/internal/controlCharacters.d.ts.map +1 -0
  50. package/dist/cjs/internal/controlCharacters.js +42 -0
  51. package/dist/cjs/internal/controlCharacters.js.map +7 -0
  52. package/dist/cjs/internal/scorerLabel.d.ts +33 -0
  53. package/dist/cjs/internal/scorerLabel.d.ts.map +1 -0
  54. package/dist/cjs/internal/scorerLabel.js +28 -0
  55. package/dist/cjs/internal/scorerLabel.js.map +7 -0
  56. package/dist/cjs/internal/tupleKey.d.ts +20 -0
  57. package/dist/cjs/internal/tupleKey.d.ts.map +1 -0
  58. package/dist/cjs/internal/tupleKey.js +25 -0
  59. package/dist/cjs/internal/tupleKey.js.map +7 -0
  60. package/dist/cjs/package.json +1 -0
  61. package/dist/esm/Baseline.d.ts +118 -0
  62. package/dist/esm/Baseline.d.ts.map +1 -0
  63. package/dist/esm/Baseline.js +203 -0
  64. package/dist/esm/Baseline.js.map +1 -0
  65. package/dist/esm/CaseExecutor.d.ts +85 -0
  66. package/dist/esm/CaseExecutor.d.ts.map +1 -0
  67. package/dist/esm/CaseExecutor.js +60 -0
  68. package/dist/esm/CaseExecutor.js.map +1 -0
  69. package/dist/esm/EvalError.d.ts +64 -0
  70. package/dist/esm/EvalError.d.ts.map +1 -0
  71. package/dist/esm/EvalError.js +65 -0
  72. package/dist/esm/EvalError.js.map +1 -0
  73. package/dist/esm/Gate.d.ts +55 -0
  74. package/dist/esm/Gate.d.ts.map +1 -0
  75. package/dist/esm/Gate.js +90 -0
  76. package/dist/esm/Gate.js.map +1 -0
  77. package/dist/esm/Regression.d.ts +116 -0
  78. package/dist/esm/Regression.d.ts.map +1 -0
  79. package/dist/esm/Regression.js +195 -0
  80. package/dist/esm/Regression.js.map +1 -0
  81. package/dist/esm/Report.d.ts +114 -0
  82. package/dist/esm/Report.d.ts.map +1 -0
  83. package/dist/esm/Report.js +188 -0
  84. package/dist/esm/Report.js.map +1 -0
  85. package/dist/esm/Runner.d.ts +229 -0
  86. package/dist/esm/Runner.d.ts.map +1 -0
  87. package/dist/esm/Runner.js +430 -0
  88. package/dist/esm/Runner.js.map +1 -0
  89. package/dist/esm/Suite.d.ts +139 -0
  90. package/dist/esm/Suite.d.ts.map +1 -0
  91. package/dist/esm/Suite.js +255 -0
  92. package/dist/esm/Suite.js.map +1 -0
  93. package/dist/esm/Trials.d.ts +93 -0
  94. package/dist/esm/Trials.d.ts.map +1 -0
  95. package/dist/esm/Trials.js +116 -0
  96. package/dist/esm/Trials.js.map +1 -0
  97. package/dist/esm/index.d.ts +76 -0
  98. package/dist/esm/index.d.ts.map +1 -0
  99. package/dist/esm/index.js +76 -0
  100. package/dist/esm/index.js.map +1 -0
  101. package/dist/esm/internal/CanonicalJson.d.ts +44 -0
  102. package/dist/esm/internal/CanonicalJson.d.ts.map +1 -0
  103. package/dist/esm/internal/CanonicalJson.js +202 -0
  104. package/dist/esm/internal/CanonicalJson.js.map +1 -0
  105. package/dist/esm/internal/controlCharacters.d.ts +44 -0
  106. package/dist/esm/internal/controlCharacters.d.ts.map +1 -0
  107. package/dist/esm/internal/controlCharacters.js +65 -0
  108. package/dist/esm/internal/controlCharacters.js.map +1 -0
  109. package/dist/esm/internal/scorerLabel.d.ts +33 -0
  110. package/dist/esm/internal/scorerLabel.d.ts.map +1 -0
  111. package/dist/esm/internal/scorerLabel.js +33 -0
  112. package/dist/esm/internal/scorerLabel.js.map +1 -0
  113. package/dist/esm/internal/tupleKey.d.ts +20 -0
  114. package/dist/esm/internal/tupleKey.d.ts.map +1 -0
  115. package/dist/esm/internal/tupleKey.js +20 -0
  116. package/dist/esm/internal/tupleKey.js.map +1 -0
  117. package/docs/README.md +130 -0
  118. package/docs/api.md +626 -0
  119. package/docs/concepts/determinism.md +64 -0
  120. package/docs/concepts/fixed-suites.md +71 -0
  121. package/docs/concepts/step-keys.md +60 -0
  122. package/docs/guides/author-a-suite.md +115 -0
  123. package/docs/guides/gate-a-run-in-ci.md +129 -0
  124. package/docs/guides/maintain-a-baseline.md +91 -0
  125. package/docs/guides/run-a-suite.md +150 -0
  126. package/docs/installation.md +46 -0
  127. package/docs/quickstart.md +238 -0
  128. package/docs/troubleshooting.md +148 -0
  129. package/package.json +198 -3
  130. package/src/Baseline.ts +259 -0
  131. package/src/CaseExecutor.ts +104 -0
  132. package/src/EvalError.ts +74 -0
  133. package/src/Gate.ts +128 -0
  134. package/src/Regression.ts +314 -0
  135. package/src/Report.ts +268 -0
  136. package/src/Runner.ts +759 -0
  137. package/src/Suite.ts +358 -0
  138. package/src/Trials.ts +154 -0
  139. package/src/index.ts +76 -0
  140. package/src/internal/CanonicalJson.ts +234 -0
  141. package/src/internal/controlCharacters.ts +66 -0
  142. package/src/internal/scorerLabel.ts +35 -0
  143. package/src/internal/tupleKey.ts +20 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scorerLabel.js","sourceRoot":"","sources":["../../../src/internal/scorerLabel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,QAA+E,EACvE,EAAE;IACV,MAAM,IAAI,GAAG,QAAQ,CAAC,UAAU,CAAA;IAChC,OAAO,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAA;AAC/G,CAAC,CAAA","sourcesContent":["/**\n * The one scorer label rule every rendering in this package shares.\n *\n * A scorer has two identities: `scorer` is the key, a digest of the scorer's\n * own declaration and the only thing a baseline matches on, and `scorerName`\n * is the readable name carried beside it. A digest is unreadable in a report\n * cell, a CI log line, or a diagnostic, so each of them prints the name with\n * the first eight key characters instead. Without a name there is nothing to\n * abbreviate the key down to, so the label is the key itself: a truncated\n * digest and no name would leave a reader nothing to match a baseline record\n * on. One rule is what keeps a report from naming one scorer two ways, an\n * inconclusive reason built by the runner and a Missing row built by the\n * reporter agree.\n *\n * @since 0.1.0\n */\n\n/**\n * Renders `name (first 8 of the key)`, or the bare key when the scorer carries\n * no name.\n *\n * Every flow carries a name, so a scorer a run holds always has one. A stored\n * observation may not: a baseline recorded before a scorer was named, or one\n * written by another tool, carries the key alone, and it labels as the key\n * rather than as a blank followed by a digest.\n *\n * @since 0.1.0\n * @private\n */\nexport const scorerLabel = (\n identity: { readonly scorer: string; readonly scorerName?: string | undefined }\n): string => {\n const name = identity.scorerName\n return name === undefined || name.length === 0 ? identity.scorer : `${name} (${identity.scorer.slice(0, 8)})`\n}\n"]}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The one injective tuple encoder every key in this package is built with.
3
+ *
4
+ * Joining caller-supplied strings on a delimiter is not injective: under a NUL
5
+ * separator the tuples `["a", "b" + NUL + "c"]` and `["a" + NUL + "b", "c"]`
6
+ * produce one key, so two distinct score jobs could share an identity and two
7
+ * distinct baseline records could sort as one. Encoding the tuple as a JSON
8
+ * array keeps the components separated by the encoding itself, whatever they
9
+ * contain.
10
+ *
11
+ * @since 0.1.0
12
+ */
13
+ /**
14
+ * Encodes a tuple of strings as one key.
15
+ *
16
+ * @since 0.1.0
17
+ * @private
18
+ */
19
+ export declare const tupleKey: (...parts: ReadonlyArray<string>) => string;
20
+ //# sourceMappingURL=tupleKey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tupleKey.d.ts","sourceRoot":"","sources":["../../../src/internal/tupleKey.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GAAI,GAAG,OAAO,aAAa,CAAC,MAAM,CAAC,KAAG,MAA+B,CAAA"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The one injective tuple encoder every key in this package is built with.
3
+ *
4
+ * Joining caller-supplied strings on a delimiter is not injective: under a NUL
5
+ * separator the tuples `["a", "b" + NUL + "c"]` and `["a" + NUL + "b", "c"]`
6
+ * produce one key, so two distinct score jobs could share an identity and two
7
+ * distinct baseline records could sort as one. Encoding the tuple as a JSON
8
+ * array keeps the components separated by the encoding itself, whatever they
9
+ * contain.
10
+ *
11
+ * @since 0.1.0
12
+ */
13
+ /**
14
+ * Encodes a tuple of strings as one key.
15
+ *
16
+ * @since 0.1.0
17
+ * @private
18
+ */
19
+ export const tupleKey = (...parts) => JSON.stringify(parts);
20
+ //# sourceMappingURL=tupleKey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tupleKey.js","sourceRoot":"","sources":["../../../src/internal/tupleKey.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,GAAG,KAA4B,EAAU,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA","sourcesContent":["/**\n * The one injective tuple encoder every key in this package is built with.\n *\n * Joining caller-supplied strings on a delimiter is not injective: under a NUL\n * separator the tuples `[\"a\", \"b\" + NUL + \"c\"]` and `[\"a\" + NUL + \"b\", \"c\"]`\n * produce one key, so two distinct score jobs could share an identity and two\n * distinct baseline records could sort as one. Encoding the tuple as a JSON\n * array keeps the components separated by the encoding itself, whatever they\n * contain.\n *\n * @since 0.1.0\n */\n\n/**\n * Encodes a tuple of strings as one key.\n *\n * @since 0.1.0\n * @private\n */\nexport const tupleKey = (...parts: ReadonlyArray<string>): string => JSON.stringify(parts)\n"]}
package/docs/README.md ADDED
@@ -0,0 +1,130 @@
1
+ ---
2
+ title: "@smthrs/evals"
3
+ description: "Fixed-suite evaluation for flows: score a target against committed cases, compare the run with a baseline, and gate CI on the result."
4
+ ---
5
+
6
+ `@smthrs/evals` tells you whether a flow still behaves. A flow is a declaration
7
+ of agent work built with [@smthrs/core](/api/core), and the thing a suite grades
8
+ is one execution of it. You declare a fixed set of cases and the scorers that
9
+ grade them, run the suite, and commit what it scored as a baseline file. Every
10
+ later run is compared with that file and graded into a CI exit code.
11
+
12
+ ## Why you would reach for it
13
+
14
+ A unit test asserts equality. A flow that calls a model produces an answer that
15
+ is rarely equal to anything, only better or worse than the last one, so the
16
+ usual assertion has nothing to hold. Scoring the answer gives you a number, and
17
+ a number on its own decides nothing until there is another number to compare it
18
+ with.
19
+
20
+ This package supplies that half of the loop:
21
+
22
+ - **Fixed suites.** `Suite.make` validates a declaration once and freezes it, so
23
+ the input half of the measurement cannot drift between the run that recorded a
24
+ baseline and the run being judged.
25
+ - **A committed baseline.** `Baseline.write` emits byte-stable JSON, so the
26
+ record of what the target used to score is a file you commit and diff.
27
+ - **A comparison that names the fault.** A score that dropped at a changed step
28
+ key is a regression in the target. A score that moved at an unchanged step key
29
+ is nondeterminism in the target or the scorer. Both are red, and they send you
30
+ to different places.
31
+ - **A grade CI can act on.** `Gate.ciGrade` returns 0 for a pass, 1 for a real
32
+ red, and 5 for a harness that could not decide, so a broken scorer never reads
33
+ as a failing target.
34
+
35
+ Every stage is an `Effect` value, and nothing in a run reads a clock or
36
+ generates an identifier: `runId` and `at` come from the caller, so two runs over
37
+ the same inputs produce byte-identical observations.
38
+
39
+ The package is at 1.0.0-rc.0 and is not on the npm registry yet, so you get it
40
+ by working in a clone of the
41
+ [Smithers repository](https://github.com/smithersai/smithers).
42
+ [Installation](./installation.md) has the steps.
43
+
44
+ ## A working example
45
+
46
+ This script scores one case, compares the run with a committed
47
+ `baseline.json`, prints the Markdown report, and exits with the gate's grade.
48
+
49
+ ```ts
50
+ import { Baseline, CaseExecutor, Gate, Regression, Report, Runner, Suite } from "@smthrs/evals"
51
+ import { Binding, Scorer } from "@smthrs/scorers"
52
+ import { Effect, Layer } from "effect"
53
+ import { readFile } from "node:fs/promises"
54
+
55
+ const greet = ({ name: "greet" })
56
+
57
+ const polite = Scorer.make({
58
+ id: "example/polite",
59
+ version: "1",
60
+ name: "polite",
61
+ score: ({ output }) => Effect.succeed({ score: String(output).startsWith("Hello") ? 1 : 0 })
62
+ })
63
+
64
+ const executor = CaseExecutor.make((suiteCase) =>
65
+ Effect.succeed({
66
+ output: `Hello, ${(suiteCase.input as { readonly name: string }).name}`,
67
+ stepKey: suiteCase.name,
68
+ latencyMs: 0,
69
+ target: greet
70
+ })
71
+ )
72
+
73
+ const program = Effect.gen(function*() {
74
+ const suite = yield* Suite.make({
75
+ name: "greetings",
76
+ cases: [{ name: "ada", input: { name: "Ada" } }],
77
+ bindings: [Binding.make({ scorer: polite, appliesTo: greet })],
78
+ concurrency: 1
79
+ })
80
+ const run = yield* Runner.run(suite, { runId: "ci-1", at: "2026-01-01T00:00:00.000Z" })
81
+ const committed = yield* Baseline.load(
82
+ yield* Effect.promise(() => readFile("baseline.json", "utf8"))
83
+ )
84
+ const comparison = yield* Regression.compare(committed, run)
85
+ yield* Effect.sync(() => process.stdout.write(Report.markdown(comparison)))
86
+ return Gate.ciGrade(yield* Gate.check(comparison, { mean: 1 })).exitCode
87
+ }).pipe(Effect.provide(Layer.succeed(CaseExecutor.CaseExecutor)(executor)))
88
+
89
+ process.exitCode = await Effect.runPromise(program)
90
+ ```
91
+
92
+ `Runner.run` needs only the `CaseExecutor` service: scoring runs in process by
93
+ default. Write `baseline.json` once with `Baseline.fromRun` and `Baseline.write`
94
+ before the comparison has anything to read;
95
+ [Quickstart](./quickstart.md) walks both halves of the loop end to end.
96
+
97
+ ## How this relates to @smthrs/agent
98
+
99
+ What a suite grades is a flow execution, and a `CaseExecutor` is one callback
100
+ from a case to that execution. Anything can sit behind the callback: a plain
101
+ function, a flow, or a whole agent run. That keeps `@smthrs/evals` independent
102
+ of what it measures, and it is why the examples on this site grade a two-line
103
+ greeting.
104
+
105
+ The parent package, [@smthrs/agent](/api/agent), is what usually sits behind the
106
+ callback in practice. An `AgentAction` or `AgentSession` runs the model-backed
107
+ work, the executor reduces that run to an output and a step key, and the suite
108
+ grades it like any other execution. The evaluation suite that ships with
109
+ Smithers does exactly this: it runs the agent loop against a scripted model with
110
+ no network access, so a score moves only when the agent's behavior moves.
111
+
112
+ `@smthrs/agent` is in turn one package of the smithers command-line tool,
113
+ [@smthrs/cli](/api/cli), which is where the whole system is driven from.
114
+
115
+ ## Where to go next
116
+
117
+ - [Installation](./installation.md): what the package needs to run, and its
118
+ entry points.
119
+ - [Quickstart](./quickstart.md): one complete loop, from suite to gate.
120
+ - How-to guides: [author a suite](./guides/author-a-suite.md),
121
+ [run a suite](./guides/run-a-suite.md),
122
+ [maintain a baseline](./guides/maintain-a-baseline.md),
123
+ [gate a run in CI](./guides/gate-a-run-in-ci.md).
124
+ - Concepts: [fixed suites](./concepts/fixed-suites.md),
125
+ [step keys and comparison](./concepts/step-keys.md),
126
+ [determinism and canonical artifacts](./concepts/determinism.md).
127
+ - The [API reference](./api.md) documents every export, the failure codes, and
128
+ the batch protocol.
129
+ - [Troubleshooting](./troubleshooting.md) maps every failure code to its cause
130
+ and its fix.