specproof 0.2.1 → 0.3.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/README.md +35 -27
- package/components/CoverageProof.tsx +2 -2
- package/next.config.js +3 -0
- package/package.json +26 -10
- package/scripts/cli.ts +113 -0
- package/scripts/generate-proof.ts +100 -17
- package/.github/workflows/ci.yml +0 -69
- package/.github/workflows/release.yml +0 -49
- package/CHANGELOG.md +0 -52
- package/CLAUDE.md +0 -82
- package/app/proof-contract.test.ts +0 -95
- package/app/proof.generated.json +0 -288
- package/bun.lock +0 -991
- package/eslint.config.mjs +0 -43
- package/example/api/openapi.json +0 -88
- package/example/tests/auth.test.ts +0 -25
- package/example/tests/client.ts +0 -53
- package/example/tests/tasks.test.ts +0 -83
- package/lib/api-test-coverage.test.ts +0 -423
- package/marketing/index.html +0 -1083
- package/marketing/package.json +0 -10
- package/marketing/server.ts +0 -27
- package/public/banner.png +0 -0
- package/public/icon.png +0 -0
- package/public/logo.png +0 -0
- package/vitest.config.ts +0 -19
package/README.md
CHANGED
|
@@ -1,53 +1,61 @@
|
|
|
1
|
-
<img src="public/
|
|
1
|
+
<img src="https://raw.githubusercontent.com/Durable-Quality/specproof/main/public/icon.png" alt="SpecProof" width="120" />
|
|
2
2
|
|
|
3
3
|
# SpecProof
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Audit your API test coverage against your OpenAPI spec. SpecProof cross-examines every operation and response status in the spec against your test suite's assertions and renders the verdicts as a browsable report. Click any verdict to read the test that proves it, or see exactly what's untested.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **Sources of truth** (read from the target repo, `SPECPROOF_REPO`): the OpenAPI spec (auto-discovered `openapi*.json` / `swagger*.json`, or set `SPECPROOF_SPEC`) + the repo's `*.test.ts` files, parsed from `describe("METHOD /path")` blocks and `.status).toBe(NNN)` assertions. `{param}`, `[param]`, and `:param` path segments are treated as equivalent when matching.
|
|
10
|
-
- **Generator**: `scripts/generate-proof.ts` compiles them into `app/proof.generated.json` — checked in, deterministic, regenerated automatically before `dev`/`build`.
|
|
11
|
-
- **Consumer**: the app renders the checked-in artifact; no target checkout is needed to build or deploy.
|
|
12
|
-
- **Drift guard**: `app/proof-contract.test.ts` fails CI when the artifact is stale relative to the spec/tests, when the proof doesn't cover exactly the spec's operations, or when a quoted snippet no longer points at a real `it()` block. It self-skips when no target repo is configured.
|
|
13
|
-
|
|
14
|
-
## Setup
|
|
7
|
+
## Quick start
|
|
15
8
|
|
|
16
9
|
Requires [Bun](https://bun.sh).
|
|
17
10
|
|
|
18
11
|
```bash
|
|
19
|
-
bun install
|
|
20
|
-
|
|
12
|
+
bun add -d specproof # or npm install -D specproof
|
|
13
|
+
bunx specproof dev # audit the current repo → http://localhost:3001
|
|
21
14
|
```
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
The OpenAPI spec is auto-discovered (`openapi*.json` / `swagger*.json`); tests are your `*.test.ts` / `*.test.tsx` / `*.test.js` files.
|
|
24
17
|
|
|
25
|
-
##
|
|
18
|
+
## CLI
|
|
26
19
|
|
|
27
20
|
```bash
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
bun run generate:proof # regenerate app/proof.generated.json only
|
|
32
|
-
bun run test:unit # contract tests (self-skip without a configured target repo)
|
|
33
|
-
bun run lint # ESLint + tsc
|
|
21
|
+
specproof generate [--out proof.json] [--check] # compile the coverage proof
|
|
22
|
+
specproof dev # generate + serve the report
|
|
23
|
+
specproof build && specproof start # production build + serve
|
|
34
24
|
```
|
|
35
25
|
|
|
36
|
-
|
|
26
|
+
| Option | Env var | Meaning |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `--repo <path>` | `SPECPROOF_REPO` | Repo to audit (default: current directory) |
|
|
29
|
+
| `--spec <path>` | `SPECPROOF_SPEC` | Spec file, relative to the repo root, when auto-discovery doesn't apply |
|
|
30
|
+
| `--out <path>` | `SPECPROOF_OUT` | Where `generate` writes the proof |
|
|
31
|
+
| `--port <port>` | | Port for `dev` / `start` (default: 3001) |
|
|
37
32
|
|
|
38
|
-
|
|
33
|
+
## Drift guard in CI
|
|
34
|
+
|
|
35
|
+
Commit the proof next to your code, then have CI fail whenever the spec or tests change without regenerating it:
|
|
39
36
|
|
|
40
37
|
```bash
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
specproof generate --out specproof.json # regenerate + commit
|
|
39
|
+
specproof generate --out specproof.json --check # CI: exits 1 when stale
|
|
43
40
|
```
|
|
44
41
|
|
|
45
42
|
## Conventions the parser relies on
|
|
46
43
|
|
|
47
|
-
- Test suites titled `describe("METHOD /path")` (`GET|POST|PUT|DELETE|PATCH`)
|
|
44
|
+
- Test suites titled `describe("METHOD /path")` (`GET|POST|PUT|DELETE|PATCH`). The path is matched against the spec's paths; `{param}`, `[param]`, and `:param` segments are equivalent.
|
|
48
45
|
- Status assertions written as `.status).toBe(NNN)` or `.status).toEqual(NNN)`.
|
|
49
46
|
- Prettier-consistent formatting: `it()`/`test()` blocks are extracted by indentation, not a full parser.
|
|
50
47
|
|
|
51
|
-
##
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bun install
|
|
52
|
+
bun run dev # audits the bundled example/ fixture on port 3001
|
|
53
|
+
bun run test:unit # analyzer unit tests + proof drift guards
|
|
54
|
+
bun run lint # ESLint + tsc
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Point a checkout at a real repo with `SPECPROOF_REPO=/path/to/repo bun run dev`. See [CLAUDE.md](CLAUDE.md) for architecture notes.
|
|
58
|
+
|
|
59
|
+
## License
|
|
52
60
|
|
|
53
|
-
|
|
61
|
+
MIT
|
|
@@ -370,8 +370,8 @@ export function CoverageProof({
|
|
|
370
370
|
>
|
|
371
371
|
<p className="text-sm font-semibold tracking-[0.14em]">NO API DEFINITION PROVIDED</p>
|
|
372
372
|
<div className="mt-4 flex flex-col gap-1 text-xs text-muted-foreground">
|
|
373
|
-
<code className="text-foreground/70">
|
|
374
|
-
<code className="text-foreground/70">
|
|
373
|
+
<code className="text-foreground/70">specproof dev --repo /path/to/repo</code>
|
|
374
|
+
<code className="text-foreground/70">specproof generate --spec path/to/openapi.json</code>
|
|
375
375
|
</div>
|
|
376
376
|
</section>
|
|
377
377
|
) : (
|
package/next.config.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** @type {import('next').NextConfig} */
|
|
2
2
|
const nextConfig = {
|
|
3
3
|
reactStrictMode: false,
|
|
4
|
+
// The app is built from source even when installed under node_modules
|
|
5
|
+
// (`specproof dev`/`build`); without this, Next's loaders skip its files.
|
|
6
|
+
transpilePackages: ['specproof'],
|
|
4
7
|
};
|
|
5
8
|
|
|
6
9
|
export default nextConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specproof",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Audit view of a repo's API test coverage: every OpenAPI operation and response status, cross-examined against the repo's test assertions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:Durable-Quality/specproof",
|
|
@@ -13,6 +13,22 @@
|
|
|
13
13
|
"audit"
|
|
14
14
|
],
|
|
15
15
|
"type": "module",
|
|
16
|
+
"bin": {
|
|
17
|
+
"specproof": "scripts/cli.ts"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"app",
|
|
21
|
+
"components",
|
|
22
|
+
"lib",
|
|
23
|
+
"scripts",
|
|
24
|
+
"!app/proof-contract.test.ts",
|
|
25
|
+
"!app/proof.generated.json",
|
|
26
|
+
"!lib/api-test-coverage.test.ts",
|
|
27
|
+
"next.config.js",
|
|
28
|
+
"postcss.config.js",
|
|
29
|
+
"tailwind.config.ts",
|
|
30
|
+
"tsconfig.json"
|
|
31
|
+
],
|
|
16
32
|
"scripts": {
|
|
17
33
|
"build": "bun run generate:proof && bunx next build",
|
|
18
34
|
"dev": "bun run generate:proof && bunx next dev --port 3001",
|
|
@@ -28,29 +44,29 @@
|
|
|
28
44
|
"dependencies": {
|
|
29
45
|
"@radix-ui/react-accordion": "^1.2.16",
|
|
30
46
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
47
|
+
"@types/node": "24.1.0",
|
|
48
|
+
"@types/react": "^19.2.0",
|
|
49
|
+
"@types/react-dom": "^19.2.0",
|
|
50
|
+
"autoprefixer": "^10.4.22",
|
|
31
51
|
"class-variance-authority": "^0.7.1",
|
|
32
52
|
"clsx": "^2.1.1",
|
|
33
53
|
"lucide-react": "^0.562.0",
|
|
34
54
|
"next": "^16.0.7",
|
|
55
|
+
"postcss": "^8.5.6",
|
|
56
|
+
"postcss-import": "^16.1.1",
|
|
35
57
|
"react": "19.2.3",
|
|
36
58
|
"react-dom": "19.2.3",
|
|
37
59
|
"tailwind-merge": "^3.4.0",
|
|
38
|
-
"tailwindcss
|
|
60
|
+
"tailwindcss": "^3.4.18",
|
|
61
|
+
"tailwindcss-animate": "^1.0.7",
|
|
62
|
+
"typescript": "^5.9.3"
|
|
39
63
|
},
|
|
40
64
|
"devDependencies": {
|
|
41
65
|
"@eslint/js": "^9.39.1",
|
|
42
|
-
"@types/node": "24.1.0",
|
|
43
|
-
"@types/react": "^19.2.0",
|
|
44
|
-
"@types/react-dom": "^19.2.0",
|
|
45
|
-
"autoprefixer": "^10.4.22",
|
|
46
66
|
"eslint": "^9.39.1",
|
|
47
67
|
"eslint-plugin-react": "^7.37.5",
|
|
48
68
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
49
69
|
"globals": "^16.5.0",
|
|
50
|
-
"postcss": "^8.5.6",
|
|
51
|
-
"postcss-import": "^16.1.1",
|
|
52
|
-
"tailwindcss": "^3.4.18",
|
|
53
|
-
"typescript": "^5.9.3",
|
|
54
70
|
"typescript-eslint": "^8.48.1",
|
|
55
71
|
"vitest": "^4.1.8"
|
|
56
72
|
}
|
package/scripts/cli.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// SpecProof CLI — the entrypoint when the package is installed into a target
|
|
3
|
+
// repo (`bunx specproof <command>`). Audits the repo it is run from (override
|
|
4
|
+
// with --repo / SPECPROOF_REPO) while Next.js dev/build/start run against the
|
|
5
|
+
// SpecProof package directory itself, wherever it is installed.
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
import { spawnSync } from 'child_process';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const appRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
|
|
14
|
+
const USAGE = `specproof — audit a repo's API test coverage against its OpenAPI spec
|
|
15
|
+
|
|
16
|
+
Usage: specproof <command> [options]
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
generate Compile the coverage proof from the target repo's spec + tests
|
|
20
|
+
dev generate, then serve the audit view with next dev
|
|
21
|
+
build generate, then production-build the audit view
|
|
22
|
+
start Serve the production build
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--repo <path> Repo to audit (default: current directory; env SPECPROOF_REPO)
|
|
26
|
+
--spec <path> OpenAPI spec, relative to the repo root when the auto-discovery
|
|
27
|
+
of openapi*.json / swagger*.json doesn't apply (env SPECPROOF_SPEC)
|
|
28
|
+
--out <path> generate only: where to write the proof (default: the app's
|
|
29
|
+
bundled artifact; env SPECPROOF_OUT)
|
|
30
|
+
--check generate only: verify the proof at --out is up to date instead
|
|
31
|
+
of writing — exits 1 on drift (the CI guard)
|
|
32
|
+
--port <port> dev/start only: port to serve on (default: 3001)
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
function fail(message: string): never {
|
|
36
|
+
console.error(`specproof: ${message}\n\n${USAGE}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
41
|
+
|
|
42
|
+
let port = '3001';
|
|
43
|
+
const generateArgs: string[] = [];
|
|
44
|
+
for (let i = 0; i < rest.length; i++) {
|
|
45
|
+
const arg = rest[i];
|
|
46
|
+
const value = () => {
|
|
47
|
+
const v = rest[++i];
|
|
48
|
+
if (!v) fail(`${arg} requires a value`);
|
|
49
|
+
return v;
|
|
50
|
+
};
|
|
51
|
+
// --repo/--spec become env vars so the analyzer (which reads them at import
|
|
52
|
+
// time) picks them up when the generator is dynamically imported below.
|
|
53
|
+
if (arg === '--repo') process.env.SPECPROOF_REPO = value();
|
|
54
|
+
else if (arg === '--spec') process.env.SPECPROOF_SPEC = value();
|
|
55
|
+
else if (arg === '--port') port = value();
|
|
56
|
+
else if (arg === '--out' || arg === '--check') {
|
|
57
|
+
if (command !== 'generate') fail(`${arg} only applies to the generate command`);
|
|
58
|
+
generateArgs.push(arg);
|
|
59
|
+
if (arg === '--out') generateArgs.push(value());
|
|
60
|
+
} else fail(`unknown option: ${arg}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function generate(argv: string[]): number {
|
|
64
|
+
// Dynamic import so the env vars set above are read, not the values at CLI
|
|
65
|
+
// startup. require() keeps this synchronous under Bun and Node alike.
|
|
66
|
+
const require = createRequire(import.meta.url);
|
|
67
|
+
const { parseGenerateArgs, runGenerate } = require('./generate-proof') as
|
|
68
|
+
typeof import('./generate-proof');
|
|
69
|
+
return runGenerate(parseGenerateArgs(argv));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function nextCli(...args: string[]): number {
|
|
73
|
+
const require = createRequire(import.meta.url);
|
|
74
|
+
// Resolve Next's bin relative to this package so the CLI works no matter
|
|
75
|
+
// where the consumer's package manager hoisted the dependency tree.
|
|
76
|
+
const nextBin = require.resolve('next/dist/bin/next', { paths: [appRoot] });
|
|
77
|
+
const result = spawnSync(process.execPath, [nextBin, ...args], {
|
|
78
|
+
stdio: 'inherit',
|
|
79
|
+
env: process.env,
|
|
80
|
+
});
|
|
81
|
+
return result.status ?? 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
switch (command) {
|
|
85
|
+
case 'generate':
|
|
86
|
+
process.exit(generate(generateArgs));
|
|
87
|
+
break;
|
|
88
|
+
case 'dev':
|
|
89
|
+
case 'build': {
|
|
90
|
+
// dev/build always refresh the bundled artifact — it is what the app
|
|
91
|
+
// renders. A consumer's committed copy (--out) is generate's concern.
|
|
92
|
+
const generated = generate([]);
|
|
93
|
+
if (generated !== 0) process.exit(generated);
|
|
94
|
+
process.exit(
|
|
95
|
+
command === 'dev'
|
|
96
|
+
? nextCli('dev', appRoot, '--port', port)
|
|
97
|
+
: nextCli('build', appRoot)
|
|
98
|
+
);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case 'start':
|
|
102
|
+
process.exit(nextCli('start', appRoot, '--port', port));
|
|
103
|
+
break;
|
|
104
|
+
case 'help':
|
|
105
|
+
case '--help':
|
|
106
|
+
case '-h':
|
|
107
|
+
case undefined:
|
|
108
|
+
console.log(USAGE);
|
|
109
|
+
process.exit(command ? 0 : 1);
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
fail(`unknown command: ${command}`);
|
|
113
|
+
}
|
|
@@ -4,16 +4,24 @@
|
|
|
4
4
|
// SPECPROOF_SPEC).
|
|
5
5
|
//
|
|
6
6
|
// Run via `bun run generate:proof` (also runs automatically before
|
|
7
|
-
// `bun run dev` / `bun run build`)
|
|
8
|
-
// app/proof.generated.json;
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// `bun run dev` / `bun run build`), or `specproof generate` when installed.
|
|
8
|
+
// The output defaults to the checked-in artifact at app/proof.generated.json;
|
|
9
|
+
// override with --out / SPECPROOF_OUT to write the proof into the audited
|
|
10
|
+
// repo instead (e.g. to commit it there). --check verifies the output file is
|
|
11
|
+
// up to date without writing — the CI drift guard. When no target spec is
|
|
12
|
+
// found, the existing artifact is left untouched so the app still builds and
|
|
13
|
+
// renders.
|
|
11
14
|
|
|
12
15
|
import fs from 'fs';
|
|
13
16
|
import path from 'path';
|
|
14
17
|
import { fileURLToPath } from 'url';
|
|
15
18
|
|
|
16
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
buildCoverageReport,
|
|
21
|
+
resolveSpecPath,
|
|
22
|
+
TARGET_REPO_ROOT,
|
|
23
|
+
type CoverageReport,
|
|
24
|
+
} from '../lib/api-test-coverage';
|
|
17
25
|
|
|
18
26
|
const appRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
27
|
|
|
@@ -26,24 +34,99 @@ export function buildProof(): Record<string, unknown> {
|
|
|
26
34
|
return buildCoverageReport() as unknown as Record<string, unknown>;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
export interface GenerateOptions {
|
|
38
|
+
/** Where to write (or, with check, what to verify). Resolved against cwd.
|
|
39
|
+
* Defaults to SPECPROOF_OUT, then the app's checked-in artifact. */
|
|
40
|
+
out?: string;
|
|
41
|
+
/** Verify the file at `out` matches a fresh proof instead of writing. */
|
|
42
|
+
check?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Parses --out <path> / --check from a generate argv slice. Throws on
|
|
46
|
+
* anything unrecognized so callers surface their own usage text. */
|
|
47
|
+
export function parseGenerateArgs(argv: string[]): GenerateOptions {
|
|
48
|
+
const options: GenerateOptions = {};
|
|
49
|
+
for (let i = 0; i < argv.length; i++) {
|
|
50
|
+
const arg = argv[i];
|
|
51
|
+
if (arg === '--check') {
|
|
52
|
+
options.check = true;
|
|
53
|
+
} else if (arg === '--out') {
|
|
54
|
+
const value = argv[++i];
|
|
55
|
+
if (!value) throw new Error('--out requires a path');
|
|
56
|
+
options.out = value;
|
|
57
|
+
} else {
|
|
58
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return options;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function runGenerate(options: GenerateOptions = {}): number {
|
|
65
|
+
const outPath = path.resolve(
|
|
66
|
+
options.out ?? process.env.SPECPROOF_OUT ?? GENERATED_PROOF_PATH
|
|
67
|
+
);
|
|
68
|
+
const relative = path.relative(process.cwd(), outPath);
|
|
69
|
+
const outLabel = relative && !relative.startsWith('..') ? relative : outPath;
|
|
32
70
|
|
|
33
|
-
if (isMain) {
|
|
34
71
|
if (!resolveSpecPath()) {
|
|
35
|
-
|
|
36
|
-
`
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
|
|
72
|
+
const hint =
|
|
73
|
+
`no OpenAPI spec found under ${TARGET_REPO_ROOT} — ` +
|
|
74
|
+
'set SPECPROOF_REPO to the repo to audit (or SPECPROOF_SPEC to the spec file)';
|
|
75
|
+
if (options.check) {
|
|
76
|
+
// A drift check with nothing to check against is a failure, not a skip:
|
|
77
|
+
// CI asked us to verify the proof and we can't.
|
|
78
|
+
console.error(`generate-proof: ${hint}`);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
if (!fs.existsSync(outPath)) {
|
|
82
|
+
// The artifact is not published with the package, so a fresh install has
|
|
83
|
+
// no proof at all. Write an empty one: the app needs the file to build,
|
|
84
|
+
// and an empty report renders the "no API definition" state.
|
|
85
|
+
const empty: CoverageReport = {
|
|
86
|
+
tags: [],
|
|
87
|
+
operationCount: 0,
|
|
88
|
+
coveredCount: 0,
|
|
89
|
+
totalCount: 0,
|
|
90
|
+
untestedOperations: 0,
|
|
91
|
+
};
|
|
92
|
+
fs.writeFileSync(outPath, JSON.stringify(empty, null, 2) + '\n');
|
|
93
|
+
console.warn(`generate-proof: ${hint}; wrote an empty proof to ${outLabel}`);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
console.warn(`generate-proof: ${hint}; keeping the existing proof`);
|
|
97
|
+
return 0;
|
|
40
98
|
}
|
|
99
|
+
|
|
41
100
|
const proof = buildProof();
|
|
42
101
|
const operationCount = (proof.operationCount as number) ?? 0;
|
|
43
102
|
if (operationCount === 0) {
|
|
44
103
|
console.error('generate-proof: no operations found in the OpenAPI spec — refusing to write an empty proof');
|
|
45
|
-
|
|
104
|
+
return 1;
|
|
46
105
|
}
|
|
47
|
-
|
|
48
|
-
|
|
106
|
+
const serialized = JSON.stringify(proof, null, 2) + '\n';
|
|
107
|
+
|
|
108
|
+
if (options.check) {
|
|
109
|
+
const existing = fs.existsSync(outPath) ? fs.readFileSync(outPath, 'utf8') : null;
|
|
110
|
+
if (existing !== serialized) {
|
|
111
|
+
console.error(
|
|
112
|
+
`generate-proof: ${outLabel} is stale relative to the spec/tests — ` +
|
|
113
|
+
'regenerate it (specproof generate) and commit the result'
|
|
114
|
+
);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
console.log(`generate-proof: ${outLabel} is up to date (${operationCount} operations)`);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fs.writeFileSync(outPath, serialized);
|
|
122
|
+
console.log(`generate-proof: wrote ${outLabel} (${operationCount} operations)`);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isMain =
|
|
127
|
+
process.argv[1] &&
|
|
128
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
129
|
+
|
|
130
|
+
if (isMain) {
|
|
131
|
+
process.exit(runGenerate(parseGenerateArgs(process.argv.slice(2))));
|
|
49
132
|
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
name: 🤖 CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches: main
|
|
6
|
-
merge_group:
|
|
7
|
-
push:
|
|
8
|
-
branches: main
|
|
9
|
-
workflow_dispatch:
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
build:
|
|
13
|
-
name: Build
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
steps:
|
|
16
|
-
- name: 📥 Checkout code
|
|
17
|
-
uses: actions/checkout@v5
|
|
18
|
-
|
|
19
|
-
- name: 🍞 Setup Bun
|
|
20
|
-
uses: oven-sh/setup-bun@v2
|
|
21
|
-
with:
|
|
22
|
-
bun-version: latest
|
|
23
|
-
|
|
24
|
-
- name: 🧰 Install dependencies
|
|
25
|
-
run: bun install
|
|
26
|
-
|
|
27
|
-
- name: 🏗️ Build
|
|
28
|
-
run: bun run build
|
|
29
|
-
|
|
30
|
-
lint:
|
|
31
|
-
name: Lint
|
|
32
|
-
runs-on: ubuntu-latest
|
|
33
|
-
steps:
|
|
34
|
-
- name: 📥 Checkout code
|
|
35
|
-
uses: actions/checkout@v5
|
|
36
|
-
|
|
37
|
-
- name: 🍞 Setup Bun
|
|
38
|
-
uses: oven-sh/setup-bun@v2
|
|
39
|
-
with:
|
|
40
|
-
bun-version: latest
|
|
41
|
-
|
|
42
|
-
- name: 🧰 Install dependencies
|
|
43
|
-
run: bun install
|
|
44
|
-
|
|
45
|
-
- name: 🧹 ESLint
|
|
46
|
-
run: bun run lint:es
|
|
47
|
-
|
|
48
|
-
- name: 🔎 Typecheck
|
|
49
|
-
run: bun run lint:ts
|
|
50
|
-
|
|
51
|
-
test:
|
|
52
|
-
name: Test
|
|
53
|
-
runs-on: ubuntu-latest
|
|
54
|
-
env:
|
|
55
|
-
NODE_ENV: test
|
|
56
|
-
steps:
|
|
57
|
-
- name: 📥 Checkout code
|
|
58
|
-
uses: actions/checkout@v5
|
|
59
|
-
|
|
60
|
-
- name: 🍞 Setup Bun
|
|
61
|
-
uses: oven-sh/setup-bun@v2
|
|
62
|
-
with:
|
|
63
|
-
bun-version: latest
|
|
64
|
-
|
|
65
|
-
- name: 🧰 Install dependencies
|
|
66
|
-
run: bun install
|
|
67
|
-
|
|
68
|
-
- name: 🧾 Run unit & contract tests
|
|
69
|
-
run: bun run test:unit
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
name: 🚢 Release
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: main
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
contents: read
|
|
9
|
-
id-token: write # npm provenance
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
publish:
|
|
13
|
-
name: 🚀 Publish to npm
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
|
|
16
|
-
env:
|
|
17
|
-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
18
|
-
|
|
19
|
-
steps:
|
|
20
|
-
- name: 📥 Checkout code
|
|
21
|
-
uses: actions/checkout@v5
|
|
22
|
-
|
|
23
|
-
- name: 🟢 Setup Node
|
|
24
|
-
uses: actions/setup-node@v4
|
|
25
|
-
with:
|
|
26
|
-
node-version: 22
|
|
27
|
-
registry-url: https://registry.npmjs.org
|
|
28
|
-
|
|
29
|
-
- name: 🔎 Skip if this version is already on npm
|
|
30
|
-
id: check
|
|
31
|
-
run: |
|
|
32
|
-
NAME=$(node -p "require('./package.json').name")
|
|
33
|
-
VERSION=$(node -p "require('./package.json').version")
|
|
34
|
-
if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
|
|
35
|
-
echo "::notice::$NAME@$VERSION is already published — nothing to do"
|
|
36
|
-
echo "published=true" >> "$GITHUB_OUTPUT"
|
|
37
|
-
else
|
|
38
|
-
echo "published=false" >> "$GITHUB_OUTPUT"
|
|
39
|
-
fi
|
|
40
|
-
|
|
41
|
-
- name: ⚠️ Warn when NPM_TOKEN is missing
|
|
42
|
-
if: steps.check.outputs.published == 'false' && env.NPM_TOKEN == ''
|
|
43
|
-
run: echo "::warning::NPM_TOKEN secret is not set — skipping npm publish. Add it under Settings → Secrets and variables → Actions."
|
|
44
|
-
|
|
45
|
-
- name: 🚀 Publish to npm
|
|
46
|
-
if: steps.check.outputs.published == 'false' && env.NPM_TOKEN != ''
|
|
47
|
-
run: npm publish --provenance --access public
|
|
48
|
-
env:
|
|
49
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/CHANGELOG.md
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
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.0.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [0.2.1] - 2026-07-14
|
|
9
|
-
|
|
10
|
-
### Changed
|
|
11
|
-
- Renamed the CI workflow file from `test-unit.yml` to `ci.yml` and refreshed the emoji in both workflow titles.
|
|
12
|
-
|
|
13
|
-
## [0.2.0] - 2026-07-14
|
|
14
|
-
|
|
15
|
-
### Added
|
|
16
|
-
- Bundled `example/` demo target: the fictional TaskFlow API (7 operations) plus fixture test files, audited automatically when no `SPECPROOF_REPO` is set — the app now renders a meaningful proof out of the box, demonstrating every verdict state.
|
|
17
|
-
- Unit test suite for the coverage analyzer (`lib/api-test-coverage.test.ts`, 30 tests): path-param normalization, snippet extraction, test-file parsing, spec discovery, evidence merging, and report assembly with independently computed expectations.
|
|
18
|
-
- CI workflow with separate `Build`, `Lint`, and `Test` jobs, running on pull requests and in the merge queue.
|
|
19
|
-
- Merge queue ruleset on `main`: PRs required, all CI checks must pass, no direct pushes.
|
|
20
|
-
- Marketing showcase page (`marketing/`) and brand assets (`public/`), with a banner in the README.
|
|
21
|
-
|
|
22
|
-
### Changed
|
|
23
|
-
- Releases now publish only after a PR merges to `main` through the merge queue; the manual release trigger was removed.
|
|
24
|
-
- `buildCoverageReport()` accepts an optional target repo root, and the analyzer's parsing internals are exported for testing.
|
|
25
|
-
- Larger SpecProof masthead in the coverage view.
|
|
26
|
-
|
|
27
|
-
## [0.1.0] - 2026-07-13
|
|
28
|
-
|
|
29
|
-
### Added
|
|
30
|
-
- Initial public release under new name SpecProof (npm package `specproof`).
|
|
31
|
-
- GitHub Actions release workflow: automatically publishes to npm when package version is updated.
|
|
32
|
-
- Comprehensive project documentation in CLAUDE.md.
|
|
33
|
-
- Generalized spec/test discovery: auto-discovers OpenAPI specs (`openapi*.json`, `swagger*.json`) and test files in any target repo; no longer OmniLens-specific.
|
|
34
|
-
- Support for explicit spec path via `SPECPROOF_SPEC` environment variable.
|
|
35
|
-
|
|
36
|
-
### Changed
|
|
37
|
-
- **Breaking**: Renamed environment variable `OMNILENS_REPO` → `SPECPROOF_REPO` for specifying the target repo to audit.
|
|
38
|
-
- **Breaking**: Renamed environment variable `OMNILENS_SPEC` → `SPECPROOF_SPEC` for specifying the OpenAPI spec path.
|
|
39
|
-
- Renamed npm package from `test-ledger` to `specproof`.
|
|
40
|
-
- Renamed generator script `scripts/generate-ledger.ts` → `scripts/generate-proof.ts` and npm command `generate:ledger` → `generate:proof`.
|
|
41
|
-
- Renamed generated artifact `app/ledger.generated.json` → `app/proof.generated.json`.
|
|
42
|
-
- Renamed contract test file `app/ledger-contract.test.ts` → `app/proof-contract.test.ts`.
|
|
43
|
-
- Renamed stylesheet `app/ledger.css` → `app/proof.css`.
|
|
44
|
-
- Renamed React component `CoverageLedger` → `CoverageProof`.
|
|
45
|
-
- Updated CSS class prefix `lg-` → `sp-` throughout the codebase.
|
|
46
|
-
- Generalized code comments and documentation from OmniLens-specific to target-agnostic language.
|
|
47
|
-
- GitHub Actions workflow `test-unit.yml` renamed to "Proof Contract Tests" with updated environment variable references.
|
|
48
|
-
- Removed `"private": true` from package.json to allow npm publishing.
|
|
49
|
-
- Updated package.json metadata: added `repository`, `homepage`, `description`, and `keywords`.
|
|
50
|
-
|
|
51
|
-
### Removed
|
|
52
|
-
- OmniLens-specific hardcoded paths and configuration.
|