crucible-ai 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +161 -2
- package/dist/index.cjs +1403 -0
- package/dist/index.d.cts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +1400 -0
- package/package.json +77 -4
- package/schemas/coherent.md +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jack
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,162 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Crucible
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/jackula83/crucible-ai/actions/workflows/ci.yml?query=branch%3Amain)
|
|
4
|
+
|
|
5
|
+
A semantic assertion library for stateful systems.
|
|
6
|
+
|
|
7
|
+
Recently when I was working on one of my AI side projects (which I am calling Proscenium), I had trouble testing its inference capabilities.
|
|
8
|
+
|
|
9
|
+
Most of what we live with today, i.e. Jest, xUnit, JUnit are deterministic testing. However inference is non-deterministic, and we need a way to test inference.
|
|
10
|
+
|
|
11
|
+
Proscenium is a storytelling app, where it keeps a world log of events and use a memory management technique to prevent omniscience, which is the #1 killer to long form storytelling.
|
|
12
|
+
|
|
13
|
+
Using Proscenium and Typescript as an example, the aim is:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
crucible.it("Jane looks for Bob", { runs: 20, threshold: 0.95 }, async () => {
|
|
17
|
+
// arrange
|
|
18
|
+
const story = await sut.createNewStory("The Long Weekend");
|
|
19
|
+
await sut.memory("Bob is at the casino");
|
|
20
|
+
await sut.memory("Jane is at home");
|
|
21
|
+
crucible.load(story.getLogs());
|
|
22
|
+
|
|
23
|
+
// act
|
|
24
|
+
const response = await sut.input("Jane looks for Bob");
|
|
25
|
+
|
|
26
|
+
// assert
|
|
27
|
+
expect(
|
|
28
|
+
await crucible.coherent(
|
|
29
|
+
response,
|
|
30
|
+
"Jane won't find Bob, because they aren't in the same place"
|
|
31
|
+
)
|
|
32
|
+
).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`crucible.it()` re-runs the whole test body (arrange, act, assert) 20 times in parallel and goes green when the pass rate is at least 0.95 — pass^k-style reliability as a CI gate, not single-shot luck. Assertions stay native (`toBe(true)`); when a run fails, Crucible prints the judge's reasoning. Omit `runs` and it's a plain single-shot test.
|
|
37
|
+
|
|
38
|
+
A threshold is a sampled estimate: a system that is truly 95% reliable fails a `{ runs: 20, threshold: 0.95 }` gate about 26% of the time. Before picking your numbers, read [docs/runs-and-threshold.md](https://github.com/jackula83/crucible-ai/blob/main/docs/runs-and-threshold.md) — the `crucible.it()` reference, the three-zone verdict, and headroom guidance with worked examples.
|
|
39
|
+
|
|
40
|
+
## Get started
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npm install --save-dev crucible-ai
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Crucible needs Node.js 22+, Jest 30, a committed `crucible.config.json` naming the judge model, and an `OPENROUTER_API_KEY` in the environment. [Getting started](https://github.com/jackula83/crucible-ai/blob/main/docs/getting-started.md) walks you from install to a CI gate you can trust; the [documentation index](https://github.com/jackula83/crucible-ai/blob/main/docs/README.md) lists every page — configuration and OpenRouter reference, API reference, gotchas, and how the instruction schema drives the judge.
|
|
47
|
+
|
|
48
|
+
The same shape ports to each ecosystem's idiomatic form (roadmap — v1 is TypeScript/Jest):
|
|
49
|
+
|
|
50
|
+
<details>
|
|
51
|
+
<summary><b>C# (xUnit)</b></summary>
|
|
52
|
+
|
|
53
|
+
```csharp
|
|
54
|
+
[CrucibleFact(Runs = 20, Threshold = 0.95)]
|
|
55
|
+
public async Task JaneLooksForBob()
|
|
56
|
+
{
|
|
57
|
+
// arrange
|
|
58
|
+
var story = await sut.CreateNewStory("The Long Weekend");
|
|
59
|
+
await sut.Memory("Bob is at the casino");
|
|
60
|
+
await sut.Memory("Jane is at home");
|
|
61
|
+
Crucible.Load(story.GetLogs());
|
|
62
|
+
|
|
63
|
+
// act
|
|
64
|
+
var response = await sut.Input("Jane looks for Bob");
|
|
65
|
+
|
|
66
|
+
// assert
|
|
67
|
+
Assert.True(await Crucible.Coherent(
|
|
68
|
+
response,
|
|
69
|
+
"Jane won't find Bob, because they aren't in the same place"));
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
</details>
|
|
74
|
+
|
|
75
|
+
<details>
|
|
76
|
+
<summary><b>Java (JUnit 5)</b></summary>
|
|
77
|
+
|
|
78
|
+
```java
|
|
79
|
+
@CrucibleTest(runs = 20, threshold = 0.95)
|
|
80
|
+
void janeLooksForBob() throws Exception {
|
|
81
|
+
// arrange
|
|
82
|
+
var story = sut.createNewStory("The Long Weekend");
|
|
83
|
+
sut.memory("Bob is at the casino");
|
|
84
|
+
sut.memory("Jane is at home");
|
|
85
|
+
Crucible.load(story.getLogs());
|
|
86
|
+
|
|
87
|
+
// act
|
|
88
|
+
var response = sut.input("Jane looks for Bob");
|
|
89
|
+
|
|
90
|
+
// assert
|
|
91
|
+
assertTrue(Crucible.coherent(
|
|
92
|
+
response,
|
|
93
|
+
"Jane won't find Bob, because they aren't in the same place"));
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
</details>
|
|
98
|
+
|
|
99
|
+
<details>
|
|
100
|
+
<summary><b>Python (pytest)</b></summary>
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
@crucible.it(runs=20, threshold=0.95)
|
|
104
|
+
async def test_jane_looks_for_bob():
|
|
105
|
+
# arrange
|
|
106
|
+
story = await sut.create_new_story("The Long Weekend")
|
|
107
|
+
await sut.memory("Bob is at the casino")
|
|
108
|
+
await sut.memory("Jane is at home")
|
|
109
|
+
crucible.load(story.get_logs())
|
|
110
|
+
|
|
111
|
+
# act
|
|
112
|
+
response = await sut.input("Jane looks for Bob")
|
|
113
|
+
|
|
114
|
+
# assert
|
|
115
|
+
assert await crucible.coherent(
|
|
116
|
+
response, "Jane won't find Bob, because they aren't in the same place"
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
</details>
|
|
121
|
+
|
|
122
|
+
## Judge validation
|
|
123
|
+
|
|
124
|
+
The `coherent` instruction schema — the language-agnostic core that judge
|
|
125
|
+
models execute — was validated against real judge models over OpenRouter
|
|
126
|
+
(4-case conformance matrix × 5 repetitions per model; schema as system
|
|
127
|
+
prompt; details in [docs/judge-models.md](https://github.com/jackula83/crucible-ai/blob/main/docs/judge-models.md)):
|
|
128
|
+
|
|
129
|
+
| Model | JSON compliance | Verdict accuracy | Median latency | Cost / call | Speed | Cost | Reliability |
|
|
130
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
131
|
+
| `deepseek/deepseek-v4-flash-0731` | 20/20 | 20/20 | 4.8 s | ~$0.00004 | 3/5 | 5/5 | 5/5 |
|
|
132
|
+
| `google/gemini-3.8-flash` | 20/20 | 20/20 | 3.2 s | ~$0.0014 | 4/5 | 4/5 | 5/5 |
|
|
133
|
+
| `anthropic/claude-sonnet-5` | 20/20 | 20/20 | 2.4 s | ~$0.0025 | 5/5 | 3/5 | 5/5 |
|
|
134
|
+
|
|
135
|
+
Every model held the strict-JSON output contract for all 60 calls and
|
|
136
|
+
returned the correct verdict on every case, including the knowledge-boundary
|
|
137
|
+
(anti-omniscience) leak. Recommended default judge:
|
|
138
|
+
`deepseek/deepseek-v4-flash-0731` — equal accuracy at a fraction of the
|
|
139
|
+
cost, which dominates once multi-run reliability gates multiply call volume.
|
|
140
|
+
Reliability was graded by comparing each model's verdicts and reasoning
|
|
141
|
+
against a frontier model's independent interpretation of the same cases.
|
|
142
|
+
|
|
143
|
+
For trust and transparency, the verbatim inputs and unedited model replies
|
|
144
|
+
from these runs are published in
|
|
145
|
+
[docs/judge-validation-runs.md](https://github.com/jackula83/crucible-ai/blob/main/docs/judge-validation-runs.md) — see for
|
|
146
|
+
yourself that the concept works.
|
|
147
|
+
|
|
148
|
+
## Alternatives
|
|
149
|
+
|
|
150
|
+
**[promptfoo](https://www.promptfoo.dev)** is the closest neighbour and the difference is philosophical. Promptfoo is config-first: the test suite is a YAML file, the CLI is the primary surface, and the unit of work is an eval run across a matrix of prompts and providers. Crucible is code-first: the test is a function in an existing test suite, the system under test is the developer's own engine instantiated in-process, and the unit of work is a single assertion inside a red-green loop.
|
|
151
|
+
|
|
152
|
+
The practical consequence is state. Promptfoo's model is prompt in, output out, assert on the output. State can be passed through template variables and a custom rubric prompt, and people do, but it is a workaround rather than a concept the framework has. Crucible treats state as a first-class input because that is the only way to express whether a character *should* know something.
|
|
153
|
+
|
|
154
|
+
Worth noting that promptfoo was acquired by OpenAI in March 2026 and is being integrated into their Frontier platform. It remains open source and the team committed to continuity. Its roadmap is enterprise security and red teaming, which is a real need and not this one.
|
|
155
|
+
|
|
156
|
+
**[DeepEval](https://deepeval.com)** has the strongest metric library going, and G-Eval is a genuinely good primitive. It shipped a TypeScript SDK in July 2026, so the old "Python-only" complaint is out of date. Parity is another matter: LLM-as-a-judge metrics and fully local evaluation still live in the Python package, and the team is refreshingly direct that TypeScript follows Python rather than standing alongside it. That is a reasonable call for them and a bad fit for anyone whose production system is not Python.
|
|
157
|
+
|
|
158
|
+
**[vitest-evals](https://github.com/getsentry/vitest-evals)** (Sentry) and **[evalite](https://github.com/mattpocock/evalite)** are the closest ergonomic neighbours in TypeScript: evals that live in or next to the unit-test workflow, local-first and free. Both score a stateless input/output pair against a dataset or scorer; neither has a concept of state as an assertion input, nor of a reliability threshold across repeated runs.
|
|
159
|
+
|
|
160
|
+
**[semantic-expect](https://github.com/agorischek/semantic-expect)** deserves a nod as prior art: LLM-judged matchers for Jest/Vitest with an N-run, pass-count configuration — the same probabilistic-assertion instinct. It is early, stateless, and tied to OpenAI.
|
|
161
|
+
|
|
162
|
+
**[Braintrust](https://www.braintrust.dev)** and **[LangSmith](https://docs.langchain.com/langsmith)** are platform plays — datasets, experiments, traces, and dashboards, with test-runner integrations attached. Good fits for teams that want a hosted eval loop; Crucible is a library for teams that want assertions in a test suite.
|