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 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
- # Temporary Holding Version
1
+ # Crucible
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/crucible-ai)](https://www.npmjs.com/package/crucible-ai)
4
+ [![ci](https://github.com/Geekmotors/crucible-ai/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Geekmotors/crucible-ai/actions/workflows/ci.yml?query=branch%3Amain)
5
+ [![license](https://img.shields.io/npm/l/crucible-ai)](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)