crucible-ai 0.0.0-stage → 0.2.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 +197 -2
- package/dist/index.cjs +1574 -0
- package/dist/index.d.cts +54 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +1571 -0
- package/package.json +77 -4
- package/schemas/coherent.md +66 -0
- package/schemas/extract.md +86 -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,198 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Crucible
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/crucible-ai)
|
|
4
|
+
[](https://github.com/Geekmotors/crucible-ai/actions/workflows/ci.yml?query=branch%3Amain)
|
|
5
|
+
[](https://github.com/Geekmotors/crucible-ai/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
Semantic assertions for code that calls LLMs, inside the Jest suite you already have.
|
|
8
|
+
|
|
9
|
+
Crucible asks an AI judge whether your system's reply means what it should, given the state your system knew at the time. It re-runs the test many times and passes on a pass rate, so a flaky model can't sneak through on one lucky run.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
crucible.it('keeps the meeting place secret', { runs: 10, threshold: 0.9 }, async () => {
|
|
13
|
+
const assistant = createAssistant();
|
|
14
|
+
await assistant.remember(['The meeting is at the Blue Door cafe.', 'Only Jane may know where.']);
|
|
15
|
+
crucible.load(['The meeting is at the Blue Door cafe.', 'Only Jane may know where.']);
|
|
16
|
+
|
|
17
|
+
const reply = await assistant.ask('Bob asks: where is the meeting?');
|
|
18
|
+
|
|
19
|
+
expect(await crucible.coherent(reply, 'The reply does not reveal the meeting place to Bob')).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Why this exists, and how it compares with promptfoo, DeepEval and others: [Inspiration](https://github.com/Geekmotors/crucible-ai/blob/main/docs/inspiration.md).
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- **Semantic assertions:** `crucible.coherent(actual, expectation)` returns the judge's `true`/`false` verdict, which you assert with plain Jest.
|
|
28
|
+
- **Value extraction:** `crucible.extract(text, description, { as })` pulls one typed value out of prose (a count, a name, a yes/no), or `null` when the text commits to none, so you can use any Jest matcher on it.
|
|
29
|
+
- **State-aware judging:** `crucible.load()` and `crucible.append()` tell the judge what your system knew, so you can test things like "the character can't know this yet".
|
|
30
|
+
- **Reliability gates:** `{ runs, threshold }` runs the whole test body concurrently and passes on the pass rate.
|
|
31
|
+
- **Readable failures:** the report shows the pass rate, each failed run and the judge's reasoning.
|
|
32
|
+
- **Fail fast, spend less:** a fatal provider or config error stops the sibling runs.
|
|
33
|
+
- **Any judge model** on [OpenRouter](https://openrouter.ai), chosen in a committed config file.
|
|
34
|
+
- ESM and CommonJS builds, bundled TypeScript types, published with npm provenance.
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- Node.js 22 or later
|
|
39
|
+
- Jest 30 (peer dependency), with the default `injectGlobals: true`
|
|
40
|
+
- An OpenRouter API key with credit. Judge calls are paid model calls, typically a fraction of a cent each.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
npm install --save-dev crucible-ai
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
**1. Add `crucible.config.json`** to the directory you run Jest from, and commit it:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"provider": "openrouter",
|
|
55
|
+
"model": "deepseek/deepseek-v4-flash-0731",
|
|
56
|
+
"meta": { "reasoning": { "effort": "low" } }
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**2. Set your API key** in the environment. Never put it in the config file.
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
export OPENROUTER_API_KEY=...
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**3. Write a test.** With no options it runs once, like a normal Jest test:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { crucible } from 'crucible-ai';
|
|
70
|
+
import { createAssistant } from '../src/assistant';
|
|
71
|
+
|
|
72
|
+
crucible.it('declines to export on the free plan', async () => {
|
|
73
|
+
const assistant = createAssistant({ plan: 'free' });
|
|
74
|
+
crucible.load('Free-plan users cannot export reports.');
|
|
75
|
+
|
|
76
|
+
const reply = await assistant.ask('Export my report as PDF.');
|
|
77
|
+
|
|
78
|
+
expect(await crucible.coherent(reply, 'The reply declines the export')).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**4. Run it** with your usual Jest command:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
npx jest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Usage
|
|
89
|
+
|
|
90
|
+
### Gate on a pass rate
|
|
91
|
+
|
|
92
|
+
Add `runs` and `threshold` to run the body several times concurrently. The test passes when at least `threshold` of the runs pass:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
crucible.it('stays in character', { runs: 20, threshold: 0.9 }, async () => {
|
|
96
|
+
const reply = await createAssistant().ask('Who are you really?');
|
|
97
|
+
expect(await crucible.coherent(reply, 'The reply stays in the persona of a ship captain')).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
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, so leave headroom. [Runs and threshold](https://github.com/Geekmotors/crucible-ai/blob/main/docs/runs-and-threshold.md) explains how to choose both.
|
|
102
|
+
|
|
103
|
+
### Give the judge state
|
|
104
|
+
|
|
105
|
+
Each run starts with empty state. Use `load` while arranging and `append` for facts that arise later. A `coherent()` call judges against the state as it stood when you called it:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
crucible.it('remembers a change of venue', async () => {
|
|
109
|
+
const assistant = createAssistant();
|
|
110
|
+
await assistant.remember(['The meeting is in room B12.']);
|
|
111
|
+
crucible.load('The meeting is in room B12.');
|
|
112
|
+
|
|
113
|
+
await assistant.remember(['The meeting moved to room C3.']);
|
|
114
|
+
crucible.append('The meeting moved to room C3.');
|
|
115
|
+
|
|
116
|
+
const reply = await assistant.ask('Where is the meeting?');
|
|
117
|
+
expect(await crucible.coherent(reply, 'The reply says the meeting is in room C3')).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Extract a value
|
|
122
|
+
|
|
123
|
+
When the point is one exact value stated in prose, extract it and assert it with any Jest matcher. `as` is `'string'` (the default), `'number'` or `'boolean'`, and the result is typed to match:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
crucible.it('counts the apples', async () => {
|
|
127
|
+
const reply = await createAssistant().ask('How many apples are in the basket?');
|
|
128
|
+
expect(await crucible.extract(reply, 'the number of apples in the basket', { as: 'number' })).toBe(2);
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The judge takes the final answer ("I thought 3, but it's two" → `2`) and returns `null` when the text commits to no single value: a hedge ("probably two"), a bare negation ("not two") or no answer. Always `await` it. When the assertion fails, the report shows what the judge extracted and why.
|
|
133
|
+
|
|
134
|
+
### Set timeouts
|
|
135
|
+
|
|
136
|
+
By default a whole test gets 30 s plus 1 s per extra run, and each judge request gets 30 s. Override them per test, Jest-style or by name:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
crucible.it('summarises a long thread', async () => {
|
|
140
|
+
const reply = await createAssistant().ask('Summarise the thread.');
|
|
141
|
+
expect(await crucible.coherent(reply, 'The summary mentions the deadline')).toBe(true);
|
|
142
|
+
}, 60000);
|
|
143
|
+
|
|
144
|
+
crucible.it('plans a trip', { runs: 5, inferenceTimeout: 45000, testTimeout: 120000 }, async () => {
|
|
145
|
+
const reply = await createAssistant().ask('Plan a weekend in Kyoto.');
|
|
146
|
+
expect(await crucible.coherent(reply, 'The plan covers two days')).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Project-wide defaults
|
|
151
|
+
|
|
152
|
+
Put defaults for every `crucible.it()` in the config. Values declared on a test win:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"provider": "openrouter",
|
|
157
|
+
"model": "deepseek/deepseek-v4-flash-0731",
|
|
158
|
+
"testDefaults": { "runs": 10, "threshold": 0.9, "testTimeout": 90000 }
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Read a failure
|
|
163
|
+
|
|
164
|
+
A failed gate throws a report like this:
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
FAILED: 7/10 runs passed (pass rate 0.7) against threshold 0.9 (need 9).
|
|
168
|
+
Runs: 7 passed, 3 failed, 0 errored, 0 discarded.
|
|
169
|
+
|
|
170
|
+
[FAILED] Run #4
|
|
171
|
+
First failed assertion: coherent (assertion 1 of 1 judged in this run)
|
|
172
|
+
Claim: The reply does not reveal the meeting place to Bob
|
|
173
|
+
Judge reasoning: The reply tells Bob the meeting is at the Blue Door cafe.
|
|
174
|
+
Response excerpt: Sure, Bob. Jane is meeting you at the Blue Door.
|
|
175
|
+
Thrown: Error: expect(received).toBe(expected)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Set `CRUCIBLE_VERBOSE=full` to see every run, or `CRUCIBLE_VERBOSE=debug` to trace each judge call.
|
|
179
|
+
|
|
180
|
+
### Write claims that should hold
|
|
181
|
+
|
|
182
|
+
Assert `toBe(true)` on claims that describe the behaviour you want. To check that something must not happen, put the negation in the claim ("does not reveal …") instead of asserting `toBe(false)`. And always `await` and assert a verdict: an unasserted `false` never fails a test.
|
|
183
|
+
|
|
184
|
+
## Documentation
|
|
185
|
+
|
|
186
|
+
- [Inspiration](https://github.com/Geekmotors/crucible-ai/blob/main/docs/inspiration.md): why Crucible exists, the design ideas, and how it compares with alternatives
|
|
187
|
+
- [API reference](https://github.com/Geekmotors/crucible-ai/blob/main/docs/api.md): every function, option, error and exported type
|
|
188
|
+
- [Getting started](https://github.com/Geekmotors/crucible-ai/blob/main/docs/getting-started.md): from install to a CI gate you can trust
|
|
189
|
+
- [Runs and threshold](https://github.com/Geekmotors/crucible-ai/blob/main/docs/runs-and-threshold.md): run statuses, the three-zone verdict, the failure report and timeouts
|
|
190
|
+
- [Configuration and OpenRouter](https://github.com/Geekmotors/crucible-ai/blob/main/docs/configuration.md): every config field, environment variable and CI secret
|
|
191
|
+
- [Gotchas](https://github.com/Geekmotors/crucible-ai/blob/main/docs/gotchas.md): read before your first real suite
|
|
192
|
+
- [The instruction schema](https://github.com/Geekmotors/crucible-ai/blob/main/docs/instruction-schema.md): how the judge decides and how to change it
|
|
193
|
+
- [Judge models](https://github.com/Geekmotors/crucible-ai/blob/main/docs/judge-models.md): measured accuracy, latency and cost per model
|
|
194
|
+
- [All documentation](https://github.com/Geekmotors/crucible-ai/blob/main/docs/README.md)
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
[MIT](https://github.com/Geekmotors/crucible-ai/blob/main/LICENSE)
|