prompt-lock 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.
Files changed (80) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +351 -0
  4. package/dist/assertions/builtin.d.ts +5 -0
  5. package/dist/assertions/builtin.d.ts.map +1 -0
  6. package/dist/assertions/builtin.js +172 -0
  7. package/dist/assertions/builtin.js.map +1 -0
  8. package/dist/assertions/custom.d.ts +3 -0
  9. package/dist/assertions/custom.d.ts.map +1 -0
  10. package/dist/assertions/custom.js +26 -0
  11. package/dist/assertions/custom.js.map +1 -0
  12. package/dist/assertions/index.d.ts +3 -0
  13. package/dist/assertions/index.d.ts.map +1 -0
  14. package/dist/assertions/index.js +32 -0
  15. package/dist/assertions/index.js.map +1 -0
  16. package/dist/assertions/json-schema.d.ts +3 -0
  17. package/dist/assertions/json-schema.d.ts.map +1 -0
  18. package/dist/assertions/json-schema.js +35 -0
  19. package/dist/assertions/json-schema.js.map +1 -0
  20. package/dist/cache.d.ts +14 -0
  21. package/dist/cache.d.ts.map +1 -0
  22. package/dist/cache.js +114 -0
  23. package/dist/cache.js.map +1 -0
  24. package/dist/cli.d.ts +3 -0
  25. package/dist/cli.d.ts.map +1 -0
  26. package/dist/cli.js +434 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/config-validation.d.ts +6 -0
  29. package/dist/config-validation.d.ts.map +1 -0
  30. package/dist/config-validation.js +133 -0
  31. package/dist/config-validation.js.map +1 -0
  32. package/dist/github.d.ts +11 -0
  33. package/dist/github.d.ts.map +1 -0
  34. package/dist/github.js +138 -0
  35. package/dist/github.js.map +1 -0
  36. package/dist/index.d.ts +40 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +119 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/providers/anthropic.d.ts +3 -0
  41. package/dist/providers/anthropic.d.ts.map +1 -0
  42. package/dist/providers/anthropic.js +46 -0
  43. package/dist/providers/anthropic.js.map +1 -0
  44. package/dist/providers/custom.d.ts +3 -0
  45. package/dist/providers/custom.d.ts.map +1 -0
  46. package/dist/providers/custom.js +178 -0
  47. package/dist/providers/custom.js.map +1 -0
  48. package/dist/providers/index.d.ts +3 -0
  49. package/dist/providers/index.d.ts.map +1 -0
  50. package/dist/providers/index.js +34 -0
  51. package/dist/providers/index.js.map +1 -0
  52. package/dist/providers/openai.d.ts +3 -0
  53. package/dist/providers/openai.d.ts.map +1 -0
  54. package/dist/providers/openai.js +45 -0
  55. package/dist/providers/openai.js.map +1 -0
  56. package/dist/reporter.d.ts +6 -0
  57. package/dist/reporter.d.ts.map +1 -0
  58. package/dist/reporter.js +251 -0
  59. package/dist/reporter.js.map +1 -0
  60. package/dist/retry.d.ts +8 -0
  61. package/dist/retry.d.ts.map +1 -0
  62. package/dist/retry.js +51 -0
  63. package/dist/retry.js.map +1 -0
  64. package/dist/runner.d.ts +16 -0
  65. package/dist/runner.d.ts.map +1 -0
  66. package/dist/runner.js +203 -0
  67. package/dist/runner.js.map +1 -0
  68. package/dist/snapshot.d.ts +7 -0
  69. package/dist/snapshot.d.ts.map +1 -0
  70. package/dist/snapshot.js +146 -0
  71. package/dist/snapshot.js.map +1 -0
  72. package/dist/types.d.ts +138 -0
  73. package/dist/types.d.ts.map +1 -0
  74. package/dist/types.js +4 -0
  75. package/dist/types.js.map +1 -0
  76. package/dist/utils.d.ts +9 -0
  77. package/dist/utils.d.ts.map +1 -0
  78. package/dist/utils.js +83 -0
  79. package/dist/utils.js.map +1 -0
  80. package/package.json +82 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,65 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2026-04-05
9
+
10
+ ### Added
11
+
12
+ - **Custom HTTP provider** — connect to Ollama, LM Studio, Azure OpenAI, or any REST endpoint with `{ type: 'custom', url: '...' }`
13
+ - **Output caching** (`--cache`) — skip LLM calls for unchanged prompts, with `prompt-lock cache stats` and `cache clear` commands
14
+ - **Retry with exponential backoff** — automatic retry on rate limits (429), server errors (5xx), and network errors (3 retries by default)
15
+ - **GitHub PR comments** (`--github-pr owner/repo#123`) — post markdown results table to pull requests, updates existing comment in place
16
+ - **Dataset testing** — test prompts against multiple inputs via `dataset` config field
17
+ - **Parallel execution** (`--parallel`, `--concurrency`) — run prompts concurrently
18
+ - **Snapshot history** — timestamped snapshots with `prompt-lock history <id>` command
19
+ - **CLI flags** — `--dry-run`, `--verbose`, `--parallel`, `--concurrency`, `--cache`, `--github-pr`
20
+ - **Progress spinner** — visual feedback during LLM calls
21
+ - 3 new assertions: `contains-all`, `no-duplicates`, `max-latency`
22
+ - Config validation module with schema checks for all assertion types and provider configs
23
+ - Dataset results in JSON and HTML reports
24
+
25
+ ### Changed
26
+
27
+ - Template variables now support dashes and dots: `{{user-name}}`, `{{api.version}}`
28
+ - Provider model passing is now type-safe (removed `as any` casts)
29
+ - Snapshots use `{id}/latest.json` format (backward compatible with old `{id}.json`)
30
+
31
+ ### Fixed
32
+
33
+ - Invalid regex in `matches-regex` assertion no longer crashes the run
34
+ - Missing API keys now throw clear actionable error messages
35
+ - HTML report escapes all user-controlled fields (XSS fix)
36
+ - `PromptLock` class defaults `snapshotDir` and `reportDir` properly
37
+
38
+ ### Security
39
+
40
+ - Snapshot and cache file paths sanitize prompt IDs to prevent path traversal
41
+ - Custom provider URLs validated to require `http://` or `https://` protocol
42
+ - Parallel runner uses queue-based dispatch to prevent race conditions
43
+
44
+ ### Packaging
45
+
46
+ - Added `files` field — npm tarball now ships only `dist/`, `README.md`, `CHANGELOG.md`, `LICENSE`
47
+ - Added `exports` field for ESM/CJS resolution
48
+ - Added `declarationMap` for IDE go-to-definition
49
+ - Added `LICENSE` (MIT)
50
+ - `prepublishOnly` now runs both build and tests
51
+ - Added `repository`, `homepage`, `author`, `bugs` to package.json
52
+
53
+ ## [0.1.0] - 2026-04-05
54
+
55
+ ### Added
56
+
57
+ - `PromptLock` class for defining prompts with behavioral assertions and snapshot baselines
58
+ - CLI commands: `init`, `run`, `snapshot`, `diff`
59
+ - 11 built-in assertion types: `contains`, `not-contains`, `starts-with`, `ends-with`, `matches-regex`, `max-length`, `min-length`, `json-valid`, `json-schema`, `no-hallucination-words`, `custom`
60
+ - OpenAI and Anthropic provider adapters with lazy SDK loading
61
+ - Snapshot capture, storage, and diff (file-based in `.promptlock/snapshots/`)
62
+ - Report generation: console (colorized), JSON, and self-contained HTML
63
+ - CI mode with exit code 1 on assertion failure (`--ci` flag)
64
+ - Project scaffolding via `prompt-lock init`
65
+ - GitHub Actions example in README
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shmuli David
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 ADDED
@@ -0,0 +1,351 @@
1
+ # prompt-lock
2
+
3
+ **Version control and behavioral regression testing for LLM prompts.**
4
+
5
+ prompt-lock wraps your prompts with behavioral assertions and snapshot baselines. On every change, it runs the assertion suite and flags regressions — like Jest for LLM behavior.
6
+ The lightweight, code-first alternative to promptfoo. TypeScript-native. Works with any LLM endpoint. Zero cloud dependencies.
7
+
8
+ ## Demo
9
+
10
+ Run the demo without any API keys to see prompt-lock in action:
11
+
12
+ ```bash
13
+ git clone https://github.com/shmulikdav/Promptlock.git
14
+ cd Promptlock
15
+ npm install && npm run build
16
+ node demo.js
17
+ ```
18
+
19
+ This runs 4 simulated prompts (2 passing, 2 failing), saves snapshots, shows diffs, and generates an HTML report — all with mock LLM outputs.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install prompt-lock
25
+ ```
26
+
27
+ You'll also need at least one LLM provider SDK (or use a custom endpoint):
28
+
29
+ ```bash
30
+ # For Anthropic
31
+ npm install @anthropic-ai/sdk
32
+
33
+ # For OpenAI
34
+ npm install openai
35
+
36
+ # For Ollama, LM Studio, Azure, etc. — no extra install needed!
37
+ # Use the custom provider: { type: 'custom', url: 'http://localhost:11434/api/generate' }
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```bash
43
+ # 1. Initialize prompt-lock in your project
44
+ npx prompt-lock init
45
+
46
+ # 2. Edit the example prompt in prompts/example.js
47
+
48
+ # 3. Set your API key
49
+ export ANTHROPIC_API_KEY=your-key-here
50
+
51
+ # 4. Run assertions
52
+ npx prompt-lock run
53
+
54
+ # 5. Save a snapshot baseline
55
+ npx prompt-lock snapshot
56
+ ```
57
+
58
+ ## Defining Prompts
59
+
60
+ Create `.js` files in your `prompts/` directory. You can export a single config or an array of configs:
61
+
62
+ ```javascript
63
+ // prompts/summarizer.js
64
+ module.exports = {
65
+ id: 'article-summarizer',
66
+ version: '1.0.0',
67
+ provider: 'anthropic',
68
+ model: 'claude-sonnet-4-20250514',
69
+
70
+ prompt: `You are a professional summarizer.
71
+ Summarize the following article in 3 bullet points.
72
+ Article: {{article}}`,
73
+
74
+ defaultVars: {
75
+ article: 'The quick brown fox jumped over the lazy dog.'
76
+ },
77
+
78
+ assertions: [
79
+ { type: 'contains', value: '•' },
80
+ { type: 'max-length', chars: 500 },
81
+ { type: 'not-contains', value: 'I cannot' },
82
+ { type: 'max-latency', ms: 10000 },
83
+ ]
84
+ };
85
+ ```
86
+
87
+ ### Using Custom Providers (Ollama, LM Studio, Azure, etc.)
88
+
89
+ ```javascript
90
+ module.exports = {
91
+ id: 'local-test',
92
+ provider: {
93
+ type: 'custom',
94
+ url: 'http://localhost:11434/api/generate',
95
+ // Optional: custom headers for auth
96
+ headers: { 'Authorization': 'Bearer ...' },
97
+ // Optional: custom response path (auto-detects OpenAI, Anthropic, Ollama formats)
98
+ responsePath: 'response',
99
+ },
100
+ model: 'llama3',
101
+ prompt: 'Hello {{name}}',
102
+ defaultVars: { name: 'world' },
103
+ assertions: [
104
+ { type: 'min-length', chars: 5 },
105
+ ],
106
+ };
107
+ ```
108
+
109
+ ### Testing with Datasets
110
+
111
+ Test a prompt against multiple inputs:
112
+
113
+ ```javascript
114
+ module.exports = {
115
+ id: 'classifier',
116
+ provider: 'openai',
117
+ model: 'gpt-4o-mini',
118
+ prompt: 'Classify this ticket: {{ticket}}',
119
+ defaultVars: { ticket: 'My payment failed' },
120
+ dataset: [
121
+ { ticket: 'My payment failed' },
122
+ { ticket: 'How do I reset my password?' },
123
+ { ticket: 'Your product is great!' },
124
+ ],
125
+ assertions: [
126
+ { type: 'json-valid' },
127
+ { type: 'max-latency', ms: 5000 },
128
+ ],
129
+ };
130
+ ```
131
+
132
+ ## Programmatic Usage
133
+
134
+ ```typescript
135
+ import { PromptLock } from 'prompt-lock';
136
+
137
+ const lock = new PromptLock({
138
+ id: 'my-prompt',
139
+ version: '1.0.0',
140
+ provider: 'anthropic',
141
+ model: 'claude-sonnet-4-20250514',
142
+ prompt: 'Translate to French: {{text}}',
143
+ defaultVars: { text: 'Hello world' },
144
+ assertions: [
145
+ { type: 'not-contains', value: 'Hello' },
146
+ { type: 'min-length', chars: 5 },
147
+ ],
148
+ });
149
+
150
+ const results = await lock.run();
151
+ console.log(results[0].passed); // true or false
152
+ ```
153
+
154
+ ## CLI Reference
155
+
156
+ ### `prompt-lock init`
157
+
158
+ Scaffold a `.promptlock/` folder with config and an example prompt.
159
+
160
+ ### `prompt-lock run`
161
+
162
+ Run all assertions against current prompts.
163
+
164
+ ```bash
165
+ prompt-lock run # Run all prompts
166
+ prompt-lock run --id my-prompt # Run a specific prompt
167
+ prompt-lock run --ci # Exit code 1 on failure
168
+ prompt-lock run --report html # Generate HTML report
169
+ prompt-lock run --report json # Generate JSON report
170
+ prompt-lock run --report both # Generate both reports
171
+ prompt-lock run --dry-run # Show what would be tested without calling LLMs
172
+ prompt-lock run --verbose # Show detailed output per prompt
173
+ prompt-lock run --parallel # Run prompts in parallel
174
+ prompt-lock run --concurrency 10 # Max concurrent runs (default: 5)
175
+ prompt-lock run --cache # Cache LLM outputs (skip unchanged prompts)
176
+ prompt-lock run --github-pr owner/repo#123 # Post results as PR comment
177
+ ```
178
+
179
+ ### `prompt-lock snapshot`
180
+
181
+ Capture and save the current output as a baseline. Snapshots are versioned — previous snapshots are kept as history.
182
+
183
+ ```bash
184
+ prompt-lock snapshot # Snapshot all prompts
185
+ prompt-lock snapshot --id my-prompt
186
+ ```
187
+
188
+ ### `prompt-lock diff`
189
+
190
+ Compare current LLM output against the last saved snapshot.
191
+
192
+ ```bash
193
+ prompt-lock diff # Diff all prompts
194
+ prompt-lock diff --id my-prompt
195
+ ```
196
+
197
+ ### `prompt-lock history`
198
+
199
+ View snapshot history for a prompt.
200
+
201
+ ```bash
202
+ prompt-lock history my-prompt
203
+ ```
204
+
205
+ ### `prompt-lock cache`
206
+
207
+ Manage the output cache (used with `--cache` flag).
208
+
209
+ ```bash
210
+ prompt-lock cache stats # Show cache size and entries
211
+ prompt-lock cache clear # Clear all cached outputs
212
+ ```
213
+
214
+ ### Output Caching
215
+
216
+ Use `--cache` to skip LLM calls for prompts that haven't changed. Cached outputs are stored in `.promptlock/cache/` and keyed by prompt text + model name.
217
+
218
+ ```bash
219
+ # First run: calls the LLM, saves to cache
220
+ prompt-lock run --cache
221
+
222
+ # Second run: uses cache, instant results
223
+ prompt-lock run --cache
224
+
225
+ # Clear cache when you want fresh results
226
+ prompt-lock cache clear
227
+ ```
228
+
229
+ ### Retry Logic
230
+
231
+ All LLM provider calls automatically retry on transient errors (rate limits, timeouts, network errors) with exponential backoff. Default: 3 retries. Use `--verbose` to see retry activity.
232
+
233
+ ### GitHub PR Comments
234
+
235
+ Post test results directly to a GitHub pull request:
236
+
237
+ ```bash
238
+ export GITHUB_TOKEN=your-token
239
+ prompt-lock run --ci --github-pr owner/repo#123
240
+ ```
241
+
242
+ This posts a markdown table with pass/fail results and failure details. If a comment already exists, it updates in place.
243
+
244
+ ## Assertion Reference
245
+
246
+ | Assertion | Config | What it checks |
247
+ |-----------|--------|---------------|
248
+ | `contains` | `value: string` | Output contains the string |
249
+ | `not-contains` | `value: string` | Output does NOT contain the string |
250
+ | `contains-all` | `values: string[]` | Output contains ALL listed strings |
251
+ | `starts-with` | `value: string` | Output starts with the string |
252
+ | `ends-with` | `value: string` | Output ends with the string |
253
+ | `matches-regex` | `pattern: string` | Output matches regex pattern |
254
+ | `max-length` | `chars: number` | Output is under N characters |
255
+ | `min-length` | `chars: number` | Output is over N characters |
256
+ | `json-valid` | — | Output is valid JSON |
257
+ | `json-schema` | `schema: object` | Output JSON matches a JSON Schema |
258
+ | `no-hallucination-words` | `words?: string[]` | Output does NOT contain hallucination indicators |
259
+ | `no-duplicates` | `separator?: string` | Output has no duplicate items (split by separator, default `\n`) |
260
+ | `max-latency` | `ms: number` | LLM response time is under N milliseconds |
261
+ | `custom` | `name: string, fn: (output) => boolean` | User-provided function returning boolean |
262
+
263
+ ## Provider Setup
264
+
265
+ ### Anthropic
266
+
267
+ ```bash
268
+ export ANTHROPIC_API_KEY=your-key-here
269
+ ```
270
+
271
+ ### OpenAI
272
+
273
+ ```bash
274
+ export OPENAI_API_KEY=your-key-here
275
+ ```
276
+
277
+ ### Custom Provider (Ollama, LM Studio, Azure, any HTTP endpoint)
278
+
279
+ No API key needed for local models. Just set the URL:
280
+
281
+ ```javascript
282
+ provider: {
283
+ type: 'custom',
284
+ url: 'http://localhost:11434/api/generate', // Ollama
285
+ // url: 'http://localhost:1234/v1/chat/completions', // LM Studio
286
+ // url: 'https://your-resource.openai.azure.com/openai/deployments/gpt-4/chat/completions?api-version=2024-02-01', // Azure
287
+ headers: { 'api-key': process.env.AZURE_API_KEY }, // Optional auth
288
+ responsePath: 'choices[0].message.content', // Optional: path to extract response
289
+ }
290
+ ```
291
+
292
+ Auto-detects response format for OpenAI, Anthropic, and Ollama APIs. Use `responsePath` for custom APIs.
293
+
294
+ ## CI/CD Integration
295
+
296
+ ### GitHub Actions
297
+
298
+ ```yaml
299
+ name: Prompt Regression Tests
300
+ on: [push, pull_request]
301
+ jobs:
302
+ prompt-lock:
303
+ runs-on: ubuntu-latest
304
+ steps:
305
+ - uses: actions/checkout@v4
306
+ - uses: actions/setup-node@v4
307
+ with:
308
+ node-version: '20'
309
+ - run: npm install
310
+ - run: npx prompt-lock run --ci --cache --github-pr ${{ github.repository }}#${{ github.event.pull_request.number }}
311
+ env:
312
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
313
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
314
+ ```
315
+
316
+ - `--ci` ensures exit code 1 when any assertion fails
317
+ - `--cache` skips LLM calls for unchanged prompts (faster, cheaper)
318
+ - `--github-pr` posts results as a PR comment with pass/fail table
319
+ - Automatic retry on rate limits and transient errors (3 retries with exponential backoff)
320
+
321
+ ## Configuration
322
+
323
+ `.promptlock/config.json` (created by `init`):
324
+
325
+ ```json
326
+ {
327
+ "promptsDir": "./prompts",
328
+ "snapshotDir": "./.promptlock/snapshots",
329
+ "reportDir": "./.promptlock/reports",
330
+ "defaultProvider": "anthropic",
331
+ "ci": {
332
+ "failOnRegression": true,
333
+ "reportFormat": ["json", "html"]
334
+ }
335
+ }
336
+ ```
337
+
338
+ ## Template Variables
339
+
340
+ Use `{{variableName}}` in your prompts. Supports alphanumeric, dashes, dots, and underscores:
341
+
342
+ ```
343
+ {{article}} ✅
344
+ {{user-name}} ✅
345
+ {{api.version}} ✅
346
+ {{my_var}} ✅
347
+ ```
348
+
349
+ ## License
350
+
351
+ MIT
@@ -0,0 +1,5 @@
1
+ import { AssertionResult } from '../types';
2
+ type AssertionHandler = (output: string, config: Record<string, unknown>) => AssertionResult;
3
+ export declare const builtinAssertions: Record<string, AssertionHandler>;
4
+ export {};
5
+ //# sourceMappingURL=builtin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtin.d.ts","sourceRoot":"","sources":["../../src/assertions/builtin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAc3C,KAAK,gBAAgB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,eAAe,CAAC;AAE7F,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAoK9D,CAAC"}
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.builtinAssertions = void 0;
4
+ const DEFAULT_HALLUCINATION_WORDS = [
5
+ 'As an AI',
6
+ 'as an AI',
7
+ 'I cannot',
8
+ 'I can\'t',
9
+ 'I don\'t have access',
10
+ 'I\'m not able to',
11
+ 'I am not able to',
12
+ 'As a language model',
13
+ 'as a language model',
14
+ ];
15
+ exports.builtinAssertions = {
16
+ 'contains': (output, config) => {
17
+ const value = config.value;
18
+ return {
19
+ type: 'contains',
20
+ name: `contains "${value}"`,
21
+ passed: output.includes(value),
22
+ expected: `output to contain "${value}"`,
23
+ actual: output.includes(value) ? 'found' : 'not found',
24
+ };
25
+ },
26
+ 'not-contains': (output, config) => {
27
+ const value = config.value;
28
+ return {
29
+ type: 'not-contains',
30
+ name: `not-contains "${value}"`,
31
+ passed: !output.includes(value),
32
+ expected: `output to NOT contain "${value}"`,
33
+ actual: output.includes(value) ? 'found' : 'not found',
34
+ };
35
+ },
36
+ 'starts-with': (output, config) => {
37
+ const value = config.value;
38
+ return {
39
+ type: 'starts-with',
40
+ name: `starts-with "${value}"`,
41
+ passed: output.startsWith(value),
42
+ expected: `output to start with "${value}"`,
43
+ actual: `starts with "${output.slice(0, value.length)}"`,
44
+ };
45
+ },
46
+ 'ends-with': (output, config) => {
47
+ const value = config.value;
48
+ return {
49
+ type: 'ends-with',
50
+ name: `ends-with "${value}"`,
51
+ passed: output.endsWith(value),
52
+ expected: `output to end with "${value}"`,
53
+ actual: `ends with "${output.slice(-value.length)}"`,
54
+ };
55
+ },
56
+ 'matches-regex': (output, config) => {
57
+ const pattern = config.pattern;
58
+ let regex;
59
+ try {
60
+ regex = new RegExp(pattern);
61
+ }
62
+ catch (e) {
63
+ return {
64
+ type: 'matches-regex',
65
+ name: `matches-regex /${pattern}/`,
66
+ passed: false,
67
+ expected: `output to match /${pattern}/`,
68
+ actual: `invalid regex: ${e.message}`,
69
+ message: `Invalid regex pattern: ${e.message}`,
70
+ };
71
+ }
72
+ const matched = regex.test(output);
73
+ return {
74
+ type: 'matches-regex',
75
+ name: `matches-regex /${pattern}/`,
76
+ passed: matched,
77
+ expected: `output to match /${pattern}/`,
78
+ actual: matched ? 'matched' : 'no match',
79
+ };
80
+ },
81
+ 'max-length': (output, config) => {
82
+ const chars = config.chars;
83
+ return {
84
+ type: 'max-length',
85
+ name: `max-length ${chars}`,
86
+ passed: output.length <= chars,
87
+ expected: `<= ${chars} chars`,
88
+ actual: `${output.length} chars`,
89
+ };
90
+ },
91
+ 'min-length': (output, config) => {
92
+ const chars = config.chars;
93
+ return {
94
+ type: 'min-length',
95
+ name: `min-length ${chars}`,
96
+ passed: output.length >= chars,
97
+ expected: `>= ${chars} chars`,
98
+ actual: `${output.length} chars`,
99
+ };
100
+ },
101
+ 'json-valid': (output) => {
102
+ let passed = false;
103
+ let message;
104
+ try {
105
+ JSON.parse(output);
106
+ passed = true;
107
+ }
108
+ catch (e) {
109
+ message = e.message;
110
+ }
111
+ return {
112
+ type: 'json-valid',
113
+ name: 'json-valid',
114
+ passed,
115
+ expected: 'valid JSON',
116
+ actual: passed ? 'valid JSON' : `invalid JSON: ${message}`,
117
+ };
118
+ },
119
+ 'contains-all': (output, config) => {
120
+ const values = config.values;
121
+ const missing = values.filter(v => !output.includes(v));
122
+ return {
123
+ type: 'contains-all',
124
+ name: `contains-all [${values.length} items]`,
125
+ passed: missing.length === 0,
126
+ expected: `output to contain all of: ${values.join(', ')}`,
127
+ actual: missing.length > 0 ? `missing: ${missing.join(', ')}` : 'all found',
128
+ };
129
+ },
130
+ 'no-duplicates': (output, config) => {
131
+ const separator = config.separator ?? '\n';
132
+ const items = output.split(separator).map(s => s.trim()).filter(Boolean);
133
+ const seen = new Set();
134
+ const duplicates = [];
135
+ for (const item of items) {
136
+ if (seen.has(item))
137
+ duplicates.push(item);
138
+ seen.add(item);
139
+ }
140
+ return {
141
+ type: 'no-duplicates',
142
+ name: 'no-duplicates',
143
+ passed: duplicates.length === 0,
144
+ expected: 'no duplicate items',
145
+ actual: duplicates.length > 0 ? `duplicates: ${duplicates.join(', ')}` : 'no duplicates',
146
+ };
147
+ },
148
+ 'max-latency': (output, config) => {
149
+ // This is evaluated by the runner which injects __duration into config
150
+ const ms = config.ms;
151
+ const actual = config.__duration ?? 0;
152
+ return {
153
+ type: 'max-latency',
154
+ name: `max-latency ${ms}ms`,
155
+ passed: actual <= ms,
156
+ expected: `<= ${ms}ms`,
157
+ actual: `${actual}ms`,
158
+ };
159
+ },
160
+ 'no-hallucination-words': (output, config) => {
161
+ const words = config.words ?? DEFAULT_HALLUCINATION_WORDS;
162
+ const found = words.filter(w => output.includes(w));
163
+ return {
164
+ type: 'no-hallucination-words',
165
+ name: 'no-hallucination-words',
166
+ passed: found.length === 0,
167
+ expected: 'no hallucination words',
168
+ actual: found.length > 0 ? `found: ${found.join(', ')}` : 'none found',
169
+ };
170
+ },
171
+ };
172
+ //# sourceMappingURL=builtin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtin.js","sourceRoot":"","sources":["../../src/assertions/builtin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { AssertionResult } from '../types';
2
+ export declare function assertCustom(output: string, name: string, fn: (output: string) => boolean | Promise<boolean>): Promise<AssertionResult>;
3
+ //# sourceMappingURL=custom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom.d.ts","sourceRoot":"","sources":["../../src/assertions/custom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE3C,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACjD,OAAO,CAAC,eAAe,CAAC,CAoB1B"}
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertCustom = assertCustom;
4
+ async function assertCustom(output, name, fn) {
5
+ try {
6
+ const result = await fn(output);
7
+ return {
8
+ type: 'custom',
9
+ name: `custom: ${name}`,
10
+ passed: !!result,
11
+ expected: `custom assertion "${name}" to pass`,
12
+ actual: result ? 'passed' : 'failed',
13
+ };
14
+ }
15
+ catch (e) {
16
+ return {
17
+ type: 'custom',
18
+ name: `custom: ${name}`,
19
+ passed: false,
20
+ expected: `custom assertion "${name}" to pass`,
21
+ actual: `threw error: ${e.message}`,
22
+ message: e.message,
23
+ };
24
+ }
25
+ }
26
+ //# sourceMappingURL=custom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom.js","sourceRoot":"","sources":["../../src/assertions/custom.ts"],"names":[],"mappings":";;AAEA,oCAwBC;AAxBM,KAAK,UAAU,YAAY,CAChC,MAAc,EACd,IAAY,EACZ,EAAkD;IAElD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QAChC,OAAO;YACL,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,WAAW,IAAI,EAAE;YACvB,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,QAAQ,EAAE,qBAAqB,IAAI,WAAW;YAC9C,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ;SACrC,CAAC;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO;YACL,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,WAAW,IAAI,EAAE;YACvB,MAAM,EAAE,KAAK;YACb,QAAQ,EAAE,qBAAqB,IAAI,WAAW;YAC9C,MAAM,EAAE,gBAAiB,CAAW,CAAC,OAAO,EAAE;YAC9C,OAAO,EAAG,CAAW,CAAC,OAAO;SAC9B,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { AssertionConfig, AssertionResult } from '../types';
2
+ export declare function runAssertions(output: string, assertions: AssertionConfig[]): Promise<AssertionResult[]>;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/assertions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAK5D,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,eAAe,EAAE,GAC5B,OAAO,CAAC,eAAe,EAAE,CAAC,CA6B5B"}