stepproof 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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/dist/adapters/anthropic.d.ts +8 -0
  4. package/dist/adapters/anthropic.d.ts.map +1 -0
  5. package/dist/adapters/anthropic.js +26 -0
  6. package/dist/adapters/anthropic.js.map +1 -0
  7. package/dist/adapters/base.d.ts +4 -0
  8. package/dist/adapters/base.d.ts.map +1 -0
  9. package/dist/adapters/base.js +2 -0
  10. package/dist/adapters/base.js.map +1 -0
  11. package/dist/adapters/index.d.ts +4 -0
  12. package/dist/adapters/index.d.ts.map +1 -0
  13. package/dist/adapters/index.js +13 -0
  14. package/dist/adapters/index.js.map +1 -0
  15. package/dist/adapters/openai.d.ts +8 -0
  16. package/dist/adapters/openai.d.ts.map +1 -0
  17. package/dist/adapters/openai.js +25 -0
  18. package/dist/adapters/openai.js.map +1 -0
  19. package/dist/assertions/engine.d.ts +6 -0
  20. package/dist/assertions/engine.d.ts.map +1 -0
  21. package/dist/assertions/engine.js +124 -0
  22. package/dist/assertions/engine.js.map +1 -0
  23. package/dist/cli.d.ts +3 -0
  24. package/dist/cli.d.ts.map +1 -0
  25. package/dist/cli.js +126 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/commands/init.d.ts +2 -0
  28. package/dist/commands/init.d.ts.map +1 -0
  29. package/dist/commands/init.js +39 -0
  30. package/dist/commands/init.js.map +1 -0
  31. package/dist/core/scenario-parser.d.ts +4 -0
  32. package/dist/core/scenario-parser.d.ts.map +1 -0
  33. package/dist/core/scenario-parser.js +92 -0
  34. package/dist/core/scenario-parser.js.map +1 -0
  35. package/dist/core/scenario-runner.d.ts +11 -0
  36. package/dist/core/scenario-runner.d.ts.map +1 -0
  37. package/dist/core/scenario-runner.js +85 -0
  38. package/dist/core/scenario-runner.js.map +1 -0
  39. package/dist/core/types.d.ts +71 -0
  40. package/dist/core/types.d.ts.map +1 -0
  41. package/dist/core/types.js +2 -0
  42. package/dist/core/types.js.map +1 -0
  43. package/dist/reporters/json-reporter.d.ts +4 -0
  44. package/dist/reporters/json-reporter.d.ts.map +1 -0
  45. package/dist/reporters/json-reporter.js +9 -0
  46. package/dist/reporters/json-reporter.js.map +1 -0
  47. package/dist/reporters/junit-reporter.d.ts +3 -0
  48. package/dist/reporters/junit-reporter.d.ts.map +1 -0
  49. package/dist/reporters/junit-reporter.js +34 -0
  50. package/dist/reporters/junit-reporter.js.map +1 -0
  51. package/dist/reporters/sarif-reporter.d.ts +3 -0
  52. package/dist/reporters/sarif-reporter.d.ts.map +1 -0
  53. package/dist/reporters/sarif-reporter.js +47 -0
  54. package/dist/reporters/sarif-reporter.js.map +1 -0
  55. package/dist/reporters/terminal-reporter.d.ts +4 -0
  56. package/dist/reporters/terminal-reporter.d.ts.map +1 -0
  57. package/dist/reporters/terminal-reporter.js +73 -0
  58. package/dist/reporters/terminal-reporter.js.map +1 -0
  59. package/package.json +62 -0
  60. package/schemas/scenario.schema.json +119 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-reporter.d.ts","sourceRoot":"","sources":["../../src/reporters/terminal-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAe,MAAM,kBAAkB,CAAC;AAKpE,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAwB7E;AAsDD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAKpF"}
@@ -0,0 +1,73 @@
1
+ import chalk from 'chalk';
2
+ import { createRequire } from 'node:module';
3
+ const require = createRequire(import.meta.url);
4
+ const { version } = require('../../package.json');
5
+ export function printReport(report, reportPath) {
6
+ const { scenarioName, iterations, steps, allPassed, durationMs } = report;
7
+ console.log('');
8
+ console.log(chalk.bold('stepproof') + chalk.dim(` v${version}`));
9
+ console.log(chalk.dim('─'.repeat(50)));
10
+ console.log(`${chalk.bold('Scenario:')} ${scenarioName}`);
11
+ console.log(`${chalk.bold('Iterations:')} ${iterations}`);
12
+ console.log(`${chalk.bold('Duration:')} ${formatDuration(durationMs)}`);
13
+ console.log('');
14
+ for (const step of steps) {
15
+ printStepSummary(step, iterations);
16
+ }
17
+ console.log(chalk.dim('─'.repeat(50)));
18
+ printSummaryLine(allPassed, steps);
19
+ if (reportPath) {
20
+ console.log('');
21
+ console.log(chalk.dim(`Report written to: ${reportPath}`));
22
+ }
23
+ console.log('');
24
+ }
25
+ function printStepSummary(step, iterations) {
26
+ const { stepId, passes, totalRuns, passRate, minPassRate, belowThreshold, failures } = step;
27
+ const statusIcon = belowThreshold ? chalk.red('✗') : chalk.green('✓');
28
+ const rateColor = belowThreshold ? chalk.red : chalk.green;
29
+ const pct = (passRate * 100).toFixed(1);
30
+ const threshold = (minPassRate * 100).toFixed(0);
31
+ console.log(` ${statusIcon} ${chalk.bold(stepId)}`);
32
+ console.log(` ${renderBar(passes, totalRuns)} ${passes}/${totalRuns} iterations`);
33
+ console.log(` Pass rate: ${rateColor(`${pct}%`)} ${chalk.dim(`(threshold: ${threshold}%)`)}`);
34
+ if (belowThreshold) {
35
+ console.log(` ${chalk.red(`✗ BELOW THRESHOLD — ${failures} failure${failures === 1 ? '' : 's'}`)}`);
36
+ }
37
+ console.log('');
38
+ }
39
+ function renderBar(passes, total) {
40
+ const barWidth = 20;
41
+ const filled = Math.round((passes / total) * barWidth);
42
+ const empty = barWidth - filled;
43
+ const bar = chalk.green('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
44
+ return `[${bar}]`;
45
+ }
46
+ function printSummaryLine(allPassed, steps) {
47
+ const failing = steps.filter((s) => s.belowThreshold);
48
+ if (allPassed) {
49
+ console.log(chalk.bold.green('PASSED') + chalk.dim(' — all steps above threshold'));
50
+ }
51
+ else {
52
+ const stepList = failing.map((s) => chalk.red(s.stepId)).join(', ');
53
+ console.log(chalk.bold.red('FAILED') +
54
+ chalk.dim(` — ${failing.length} step${failing.length === 1 ? '' : 's'} below threshold: `) +
55
+ stepList);
56
+ }
57
+ }
58
+ function formatDuration(ms) {
59
+ if (ms < 1000)
60
+ return `${ms}ms`;
61
+ if (ms < 60_000)
62
+ return `${(ms / 1000).toFixed(1)}s`;
63
+ const mins = Math.floor(ms / 60_000);
64
+ const secs = ((ms % 60_000) / 1000).toFixed(0);
65
+ return `${mins}m ${secs}s`;
66
+ }
67
+ export function printProgress(stepId, iteration, total) {
68
+ process.stdout.write(`\r ${chalk.dim('Running')} ${stepId} — iteration ${iteration}/${total}...`);
69
+ if (iteration === total) {
70
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
71
+ }
72
+ }
73
+ //# sourceMappingURL=terminal-reporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-reporter.js","sourceRoot":"","sources":["../../src/reporters/terminal-reporter.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "stepproof",
3
+ "version": "0.2.0",
4
+ "description": "Regression testing for multi-step AI workflows. Not observability — a CI gate.",
5
+ "main": "dist/cli.js",
6
+ "bin": {
7
+ "stepproof": "dist/cli.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx src/cli.ts",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "schemas",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "keywords": [
24
+ "ai",
25
+ "testing",
26
+ "regression",
27
+ "agents",
28
+ "llm",
29
+ "cli",
30
+ "openai",
31
+ "anthropic",
32
+ "ci"
33
+ ],
34
+ "author": "Bilko",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/StanislavBG/stepproof.git"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "dependencies": {
47
+ "@anthropic-ai/sdk": "^0.37.0",
48
+ "@preflight/license": "^1.0.0",
49
+ "ajv": "^8.17.1",
50
+ "chalk": "^5.3.0",
51
+ "commander": "^12.1.0",
52
+ "js-yaml": "^4.1.0",
53
+ "openai": "^4.77.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/js-yaml": "^4.0.9",
57
+ "@types/node": "^20.17.0",
58
+ "tsx": "^4.19.0",
59
+ "typescript": "^5.7.0",
60
+ "vitest": "^2.1.0"
61
+ }
62
+ }
@@ -0,0 +1,119 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://stepproof.dev/schemas/scenario.schema.json",
4
+ "title": "StepProof Scenario",
5
+ "description": "Schema for a stepproof scenario YAML file",
6
+ "type": "object",
7
+ "required": ["name", "steps"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "name": {
11
+ "type": "string",
12
+ "description": "Human-readable name for this scenario"
13
+ },
14
+ "iterations": {
15
+ "type": "integer",
16
+ "minimum": 1,
17
+ "maximum": 1000,
18
+ "default": 10,
19
+ "description": "Number of times to run the full scenario"
20
+ },
21
+ "variables": {
22
+ "type": "object",
23
+ "description": "Global variables available in prompt templates as {{variable_name}}",
24
+ "additionalProperties": { "type": "string" }
25
+ },
26
+ "steps": {
27
+ "type": "array",
28
+ "minItems": 1,
29
+ "description": "Ordered list of steps to execute per iteration",
30
+ "items": {
31
+ "$ref": "#/definitions/Step"
32
+ }
33
+ }
34
+ },
35
+ "definitions": {
36
+ "Step": {
37
+ "type": "object",
38
+ "required": ["id", "provider", "model", "prompt"],
39
+ "additionalProperties": false,
40
+ "properties": {
41
+ "id": {
42
+ "type": "string",
43
+ "pattern": "^[a-z][a-z0-9_]*$",
44
+ "description": "Unique step identifier (snake_case). Referenced as {{step_id.output}} in later steps."
45
+ },
46
+ "provider": {
47
+ "type": "string",
48
+ "enum": ["openai", "anthropic"],
49
+ "description": "LLM provider to use for this step"
50
+ },
51
+ "model": {
52
+ "type": "string",
53
+ "description": "Model ID (e.g., gpt-4o, claude-sonnet-4-6)"
54
+ },
55
+ "prompt": {
56
+ "type": "string",
57
+ "description": "Prompt template. Use {{variable}} for substitution, {{step_id.output}} for prior step outputs."
58
+ },
59
+ "system": {
60
+ "type": "string",
61
+ "description": "Optional system prompt"
62
+ },
63
+ "min_pass_rate": {
64
+ "type": "number",
65
+ "minimum": 0,
66
+ "maximum": 1,
67
+ "default": 0.8,
68
+ "description": "Minimum fraction of iterations that must pass assertions (0.0–1.0). CI exits 1 if below this."
69
+ },
70
+ "assertions": {
71
+ "type": "array",
72
+ "default": [],
73
+ "description": "List of assertions to run on the step output",
74
+ "items": {
75
+ "$ref": "#/definitions/Assertion"
76
+ }
77
+ }
78
+ }
79
+ },
80
+ "Assertion": {
81
+ "type": "object",
82
+ "required": ["type"],
83
+ "properties": {
84
+ "type": {
85
+ "type": "string",
86
+ "enum": ["contains", "not_contains", "regex", "json_schema", "llm_judge"],
87
+ "description": "Assertion type"
88
+ },
89
+ "value": {
90
+ "type": "string",
91
+ "description": "For contains/not_contains: the substring to check. For regex: the pattern."
92
+ },
93
+ "schema": {
94
+ "type": "string",
95
+ "description": "For json_schema: path to JSON schema file (relative to scenario file)"
96
+ },
97
+ "prompt": {
98
+ "type": "string",
99
+ "description": "For llm_judge: the evaluation prompt. The step output is appended automatically."
100
+ },
101
+ "pass_on": {
102
+ "type": "string",
103
+ "default": "yes",
104
+ "description": "For llm_judge: the expected response prefix (case-insensitive). Default: \"yes\""
105
+ },
106
+ "provider": {
107
+ "type": "string",
108
+ "enum": ["openai", "anthropic"],
109
+ "default": "anthropic",
110
+ "description": "For llm_judge: which provider to use. Default: anthropic"
111
+ },
112
+ "model": {
113
+ "type": "string",
114
+ "description": "For llm_judge: override model. Defaults to cheapest available (haiku/gpt-4o-mini)"
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }