specproof 0.1.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.
@@ -0,0 +1,50 @@
1
+ name: ๐Ÿš€ Release
2
+
3
+ on:
4
+ push:
5
+ branches: main
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write # npm provenance
11
+
12
+ jobs:
13
+ publish:
14
+ name: ๐Ÿš€ Publish to npm
15
+ runs-on: ubuntu-latest
16
+
17
+ env:
18
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
19
+
20
+ steps:
21
+ - name: ๐Ÿ“ฅ Checkout code
22
+ uses: actions/checkout@v5
23
+
24
+ - name: ๐ŸŸข Setup Node
25
+ uses: actions/setup-node@v4
26
+ with:
27
+ node-version: 22
28
+ registry-url: https://registry.npmjs.org
29
+
30
+ - name: ๐Ÿ”Ž Skip if this version is already on npm
31
+ id: check
32
+ run: |
33
+ NAME=$(node -p "require('./package.json').name")
34
+ VERSION=$(node -p "require('./package.json').version")
35
+ if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
36
+ echo "::notice::$NAME@$VERSION is already published โ€” nothing to do"
37
+ echo "published=true" >> "$GITHUB_OUTPUT"
38
+ else
39
+ echo "published=false" >> "$GITHUB_OUTPUT"
40
+ fi
41
+
42
+ - name: โš ๏ธ Warn when NPM_TOKEN is missing
43
+ if: steps.check.outputs.published == 'false' && env.NPM_TOKEN == ''
44
+ run: echo "::warning::NPM_TOKEN secret is not set โ€” skipping npm publish. Add it under Settings โ†’ Secrets and variables โ†’ Actions."
45
+
46
+ - name: ๐Ÿš€ Publish to npm
47
+ if: steps.check.outputs.published == 'false' && env.NPM_TOKEN != ''
48
+ run: npm publish --provenance --access public
49
+ env:
50
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,36 @@
1
+ name: ๐Ÿงพ Proof Contract Tests
2
+
3
+ on:
4
+ push:
5
+ branches: main
6
+ pull_request:
7
+ branches: main
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ test-unit:
12
+ name: ๐Ÿงพ Run proof contract tests
13
+ runs-on: ubuntu-latest
14
+
15
+ env:
16
+ NODE_ENV: test
17
+
18
+ steps:
19
+ - name: ๐Ÿ“ฅ Checkout code
20
+ uses: actions/checkout@v5
21
+
22
+ - name: ๐Ÿž Setup Bun
23
+ uses: oven-sh/setup-bun@v2
24
+ with:
25
+ bun-version: latest
26
+
27
+ - name: ๐Ÿงฐ Install dependencies
28
+ run: bun install
29
+
30
+ # The contract tests self-skip when no audited repo is configured.
31
+ # To guard a specific target in CI, check it out here and set
32
+ # SPECPROOF_REPO (and optionally SPECPROOF_SPEC) on the test step.
33
+ - name: ๐Ÿงพ Run contract tests
34
+ run: bun run test:unit
35
+ env:
36
+ NODE_ENV: test
package/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
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.1.0] - 2026-07-13
9
+
10
+ ### Added
11
+ - Initial public release under new name SpecProof (npm package `specproof`).
12
+ - GitHub Actions release workflow: automatically publishes to npm when package version is updated.
13
+ - Comprehensive project documentation in CLAUDE.md.
14
+ - Generalized spec/test discovery: auto-discovers OpenAPI specs (`openapi*.json`, `swagger*.json`) and test files in any target repo; no longer OmniLens-specific.
15
+ - Support for explicit spec path via `SPECPROOF_SPEC` environment variable.
16
+
17
+ ### Changed
18
+ - **Breaking**: Renamed environment variable `OMNILENS_REPO` โ†’ `SPECPROOF_REPO` for specifying the target repo to audit.
19
+ - **Breaking**: Renamed environment variable `OMNILENS_SPEC` โ†’ `SPECPROOF_SPEC` for specifying the OpenAPI spec path.
20
+ - Renamed npm package from `test-ledger` to `specproof`.
21
+ - Renamed generator script `scripts/generate-ledger.ts` โ†’ `scripts/generate-proof.ts` and npm command `generate:ledger` โ†’ `generate:proof`.
22
+ - Renamed generated artifact `app/ledger.generated.json` โ†’ `app/proof.generated.json`.
23
+ - Renamed contract test file `app/ledger-contract.test.ts` โ†’ `app/proof-contract.test.ts`.
24
+ - Renamed stylesheet `app/ledger.css` โ†’ `app/proof.css`.
25
+ - Renamed React component `CoverageLedger` โ†’ `CoverageProof`.
26
+ - Updated CSS class prefix `lg-` โ†’ `sp-` throughout the codebase.
27
+ - Generalized code comments and documentation from OmniLens-specific to target-agnostic language.
28
+ - GitHub Actions workflow `test-unit.yml` renamed to "Proof Contract Tests" with updated environment variable references.
29
+ - Removed `"private": true` from package.json to allow npm publishing.
30
+ - Updated package.json metadata: added `repository`, `homepage`, `description`, and `keywords`.
31
+
32
+ ### Removed
33
+ - OmniLens-specific hardcoded paths and configuration.
package/CLAUDE.md ADDED
@@ -0,0 +1,72 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ SpecProof: a standalone Next.js app that renders an audit view of any repo's API test coverage: every OpenAPI operation and response status, cross-examined against the target repo's test assertions. Clicking a stamped verdict shows the actual test snippet that proves (or fails to prove) coverage.
8
+
9
+ This repo does not contain an API or its tests โ€” it audits a target repo from the outside, the way OpenAPI tooling works when installed into a repo. Nothing repo-specific is baked in.
10
+
11
+ ## Architecture: generate โ†’ checked-in artifact โ†’ contract-test
12
+
13
+ 1. **Sources of truth**, read from the target repo (`SPECPROOF_REPO` env var; defaults to the current working directory):
14
+ - The OpenAPI spec โ€” auto-discovered as the shallowest `openapi*.json` / `swagger*.json` in the tree, or set explicitly with `SPECPROOF_SPEC` (relative to the repo root).
15
+ - The repo's `*.test.ts` / `*.test.tsx` / `*.test.js` files โ€” parsed via regex for `describe("METHOD /path")` blocks and `.status).toBe(NNN)` / `.status).toEqual(NNN)` assertions.
16
+ 2. **Analyzer** (`lib/api-test-coverage.ts`): joins the two sources into a `CoverageReport` (tags โ†’ operations โ†’ statuses, each status with assertion counts and extracted `it()` snippets). Operations are joined on the describe title's method + path; `{param}`, `[param]`, and `:param` segments are treated as equivalent. When several test files describe the same operation, the one with the most `it()` blocks wins.
17
+ 3. **Generator** (`scripts/generate-proof.ts`): calls the analyzer, writes the result to `app/proof.generated.json`. This is checked in, deterministic, and regenerated automatically before `dev`/`build`. When no target spec is found it leaves the existing artifact untouched (exit 0), so the app always builds.
18
+ 4. **Consumer** (`app/page.tsx` โ†’ `components/CoverageProof.tsx`): renders the checked-in artifact only. No target checkout is needed to build or deploy the app itself. An empty proof renders an empty-state hint.
19
+ 5. **Drift guard** (`app/proof-contract.test.ts`): fails when the artifact is stale relative to the spec/tests, when the proof's audited operations don't exactly match the spec's operations, or when a quoted snippet no longer points at a real `it()`/`test()` line. This suite self-skips (via `describe.skipIf`) when no target spec is resolvable.
20
+
21
+ Because of this split, most day-to-day work happens in exactly one of two places:
22
+ - Changing what's audited/how coverage is computed โ†’ `lib/api-test-coverage.ts` (+ regenerate the proof).
23
+ - Changing how the proof is displayed โ†’ `components/CoverageProof.tsx` / `app/proof.css`, reading the existing `app/proof.generated.json` as fixture data โ€” no target checkout needed.
24
+
25
+ ## Commands
26
+
27
+ Requires [Bun](https://bun.sh).
28
+
29
+ ```bash
30
+ bun install
31
+ bun run dev # generate:proof + next dev (port 3001)
32
+ bun run build # generate:proof + next build
33
+ bun run start # serve the production build (port 3001)
34
+ bun run generate:proof # regenerate app/proof.generated.json only
35
+ bun run test:unit # contract tests (vitest run; self-skips without a configured target)
36
+ bun run test:unit:watch # vitest watch mode
37
+ bun run lint # eslint + tsc --noEmit
38
+ bun run lint:es # eslint only
39
+ bun run lint:ts # tsc --noEmit only
40
+ ```
41
+
42
+ To run a single test file: `bunx vitest run app/proof-contract.test.ts` (there is currently only the one test file).
43
+
44
+ The generator/contract tests need a target repo:
45
+
46
+ ```bash
47
+ SPECPROOF_REPO=/path/to/target-repo bun run generate:proof
48
+ # optionally, if the spec has a nonstandard name/location:
49
+ SPECPROOF_REPO=/path/to/target-repo SPECPROOF_SPEC=docs/api-spec.json bun run generate:proof
50
+ ```
51
+
52
+ ## Updating the proof
53
+
54
+ When a target repo's test or OpenAPI spec changes, the checked-in `app/proof.generated.json` goes stale and the contract tests fail. Fix:
55
+
56
+ ```bash
57
+ SPECPROOF_REPO=/path/to/target-repo bun run generate:proof
58
+ git add app/proof.generated.json && git commit
59
+ ```
60
+
61
+ ## Releasing
62
+
63
+ Merges to `main` trigger `.github/workflows/release.yml`, which publishes to npm when `package.json`'s version isn't already on the registry (needs the `NPM_TOKEN` repo secret). To release: bump `version` in `package.json` and merge.
64
+
65
+ ## Notes on the parsing approach
66
+
67
+ `lib/api-test-coverage.ts` parses test files with regexes rather than a real TS/AST parser, relying on prettier-consistent formatting conventions in the audited repo:
68
+ - `describe("METHOD /path")` titles are matched for the HTTP method prefix (`GET|POST|PUT|DELETE|PATCH`) followed by the operation path.
69
+ - `it()`/`test()` blocks are extracted by matching the opening call, then scanning for the next `});` at the same indentation level โ€” not a brace-matching parser, so it depends on consistent formatting in the audited repo.
70
+ - Snippet source is dedented before being stored/rendered.
71
+
72
+ If this parsing logic needs updating, check it against real test files in a target repo โ€” the contract test's "every quoted snippet points at a real it()/test() block" check is what catches regressions here.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # SpecProof
2
+
3
+ A standalone Next.js app that renders an audit view of any repo's API test coverage: every OpenAPI operation and response status, cross-examined against the repo's test assertions. Click a stamped verdict to read the test itself.
4
+
5
+ Point it at a repo the same way OpenAPI tooling works when installed into one โ€” no repo-specific configuration is baked in:
6
+
7
+ - **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.
8
+ - **Generator**: `scripts/generate-proof.ts` compiles them into `app/proof.generated.json` โ€” checked in, deterministic, regenerated automatically before `dev`/`build`.
9
+ - **Consumer**: the app renders the checked-in artifact; no target checkout is needed to build or deploy.
10
+ - **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.
11
+
12
+ ## Setup
13
+
14
+ Requires [Bun](https://bun.sh).
15
+
16
+ ```bash
17
+ bun install
18
+ SPECPROOF_REPO=/path/to/your-repo bun run dev # compiles the proof, then serves on http://localhost:3001
19
+ ```
20
+
21
+ `SPECPROOF_REPO` defaults to the current working directory, so running the generator from inside the target repo needs no configuration at all. If the spec isn't named `openapi*.json` / `swagger*.json`, point `SPECPROOF_SPEC` at it (relative to the repo root).
22
+
23
+ ## Commands
24
+
25
+ ```bash
26
+ bun run dev # generate:proof + next dev (port 3001)
27
+ bun run build # generate:proof + next build
28
+ bun run start # serve the production build (port 3001)
29
+ bun run generate:proof # regenerate app/proof.generated.json only
30
+ bun run test:unit # contract tests (self-skip without a configured target repo)
31
+ bun run lint # ESLint + tsc
32
+ ```
33
+
34
+ ## Updating the proof
35
+
36
+ When the target repo's tests or OpenAPI spec change, the checked-in artifact goes stale and the contract tests fail. The fix:
37
+
38
+ ```bash
39
+ SPECPROOF_REPO=/path/to/your-repo bun run generate:proof
40
+ git add app/proof.generated.json && git commit
41
+ ```
42
+
43
+ ## Conventions the parser relies on
44
+
45
+ - Test suites titled `describe("METHOD /path")` (`GET|POST|PUT|DELETE|PATCH`) โ€” the path is matched against the spec's paths.
46
+ - Status assertions written as `.status).toBe(NNN)` or `.status).toEqual(NNN)`.
47
+ - Prettier-consistent formatting: `it()`/`test()` blocks are extracted by indentation, not a full parser.
48
+
49
+ ## Releasing
50
+
51
+ Merges to `main` run the [Release workflow](.github/workflows/release.yml), which publishes the package to npm whenever `package.json`'s version isn't on the registry yet (requires the `NPM_TOKEN` repository secret).
@@ -0,0 +1,31 @@
1
+ @import 'tailwindcss/base';
2
+ @import 'tailwindcss/components';
3
+ @import 'tailwindcss/utilities';
4
+
5
+ /* Same dark theme tokens as apps/web โ€” the proof renders on the shadcn
6
+ variable vocabulary (background/foreground/muted/border/โ€ฆ). */
7
+ :root {
8
+ --background: 0 0% 0.4%;
9
+ --foreground: 0 0% 98%;
10
+ --primary: 0 0% 98%;
11
+ --primary-foreground: 0 0% 9%;
12
+ --secondary: 0 0% 14.9%;
13
+ --secondary-foreground: 0 0% 98%;
14
+ --muted: 0 0% 25%;
15
+ --muted-foreground: 0 0% 75%;
16
+ --accent: 0 0% 25%;
17
+ --accent-foreground: 0 0% 98%;
18
+ --border: 0 0% 20%;
19
+ --input: 0 0% 14.9%;
20
+ --ring: 0 0% 83.1%;
21
+ --radius: 0.5rem;
22
+ }
23
+
24
+ * {
25
+ border-color: hsl(var(--border));
26
+ }
27
+
28
+ body {
29
+ background-color: hsl(var(--background));
30
+ color: hsl(var(--foreground));
31
+ }
package/app/layout.tsx ADDED
@@ -0,0 +1,29 @@
1
+ import "./globals.css";
2
+ import { ReactNode } from "react";
3
+ import type { Metadata } from "next";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "SpecProof",
7
+ description:
8
+ "Every documented API operation and status code, cross-examined against the repo's test suites."
9
+ };
10
+
11
+ export default function RootLayout({ children }: { children: ReactNode }) {
12
+ return (
13
+ <html lang="en" className="dark">
14
+ <head>
15
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
16
+ <link
17
+ rel="preconnect"
18
+ href="https://fonts.gstatic.com"
19
+ crossOrigin="anonymous"
20
+ />
21
+ <link
22
+ href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@300;400;500;600;700&display=swap"
23
+ rel="stylesheet"
24
+ />
25
+ </head>
26
+ <body className="min-h-screen bg-background antialiased">{children}</body>
27
+ </html>
28
+ );
29
+ }
package/app/page.tsx ADDED
@@ -0,0 +1,20 @@
1
+ import type { CoverageReport } from '@/lib/api-test-coverage';
2
+ import { CoverageProof } from '@/components/CoverageProof';
3
+
4
+ import proof from './proof.generated.json';
5
+
6
+ import './proof.css';
7
+
8
+ /**
9
+ * SpecProof
10
+ *
11
+ * Audit view of a repo's API test coverage: the OpenAPI spec's operations and
12
+ * response statuses joined against the repo's test assertions. Renders the
13
+ * checked-in proof.generated.json artifact โ€” regenerated automatically
14
+ * before dev/build and kept in lockstep with the sources by the
15
+ * proof-contract test.
16
+ */
17
+ export default function SpecProofPage() {
18
+ const report = proof as unknown as CoverageReport;
19
+ return <CoverageProof report={report} compiledAt={new Date().toISOString()} />;
20
+ }
@@ -0,0 +1,95 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import {
6
+ resolveSpecPath,
7
+ TARGET_REPO_ROOT,
8
+ type CoverageReport,
9
+ } from "@/lib/api-test-coverage";
10
+ import { buildProof, GENERATED_PROOF_PATH } from "@/scripts/generate-proof";
11
+
12
+ /**
13
+ * Contract tests that keep the rendered proof in lockstep with the target
14
+ * repo it audits. Three drift modes are covered:
15
+ *
16
+ * 1. The OpenAPI spec gains/loses an operation without the proof following
17
+ * (spec <-> proof, both directions).
18
+ * 2. A quoted test snippet no longer points at a real it()/test() block in
19
+ * its test file โ€” the evidence the UI shows must be re-readable from the
20
+ * source it cites.
21
+ * 3. The spec or a test file changed without regenerating
22
+ * proof.generated.json.
23
+ */
24
+
25
+ function loadCheckedInProof(): CoverageReport {
26
+ return JSON.parse(fs.readFileSync(GENERATED_PROOF_PATH, "utf8"));
27
+ }
28
+
29
+ // The drift guards compare the artifact against the audited target repo
30
+ // (SPECPROOF_REPO, or the current directory). Without an OpenAPI spec to audit
31
+ // there is nothing to compare against, so the suite self-skips.
32
+ const specPath = resolveSpecPath();
33
+ if (!specPath) {
34
+ console.warn(
35
+ `proof-contract: skipping โ€” no OpenAPI spec found under ${TARGET_REPO_ROOT} (set SPECPROOF_REPO to the repo to audit, or SPECPROOF_SPEC to the spec file)`,
36
+ );
37
+ }
38
+
39
+ describe.skipIf(!specPath)("SpecProof contract", () => {
40
+ it("audits every operation in the OpenAPI spec, and nothing else", () => {
41
+ const spec = JSON.parse(fs.readFileSync(specPath!, "utf8")) as {
42
+ paths: Record<string, Record<string, unknown>>;
43
+ };
44
+
45
+ const documented = Object.entries(spec.paths)
46
+ .flatMap(([opPath, methods]) =>
47
+ Object.keys(methods).map((method) => `${method} ${opPath}`),
48
+ )
49
+ .sort();
50
+
51
+ const audited = loadCheckedInProof()
52
+ .tags.flatMap((tag) => tag.operations)
53
+ .map((op) => `${op.method} ${op.specPath}`)
54
+ .sort();
55
+
56
+ expect(documented.length).toBeGreaterThan(0);
57
+ expect(audited).toEqual(documented);
58
+ });
59
+
60
+ it("every quoted snippet points at a real it()/test() block in its test file", () => {
61
+ const sources = new Map<string, string[]>();
62
+ const readLines = (testFile: string) => {
63
+ const cached = sources.get(testFile);
64
+ if (cached) return cached;
65
+ const lines = fs
66
+ .readFileSync(path.join(TARGET_REPO_ROOT, testFile), "utf8")
67
+ .split("\n");
68
+ sources.set(testFile, lines);
69
+ return lines;
70
+ };
71
+
72
+ for (const tag of loadCheckedInProof().tags) {
73
+ for (const op of tag.operations) {
74
+ if (!op.testFile) continue;
75
+ for (const status of op.statuses) {
76
+ for (const snippet of status.snippets) {
77
+ const line = readLines(op.testFile)[snippet.startLine - 1] ?? "";
78
+ expect(
79
+ /(?:it|test)\(/.test(line),
80
+ `${op.method} ${op.specPath} ${status.code}: snippet "${snippet.title}" cites ${op.testFile}:${snippet.startLine}, but that line is not an it()/test() block`,
81
+ ).toBe(true);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ });
87
+
88
+ it("proof.generated.json is up to date with the spec and tests", () => {
89
+ const checkedIn = loadCheckedInProof();
90
+ // If this fails, the OpenAPI spec or a test file changed without
91
+ // regenerating the proof: run `bun run generate:proof` and commit the
92
+ // result.
93
+ expect(checkedIn).toEqual(buildProof());
94
+ });
95
+ });
package/app/proof.css ADDED
@@ -0,0 +1,217 @@
1
+ /* ============================================================================
2
+ SpecProof โ€” functional coverage report
3
+ Single-typeface Geist Mono tabulation: dotted index leaders, punch-card
4
+ status marks, stamped verdicts. All rules scoped under .proof-root.
5
+ ============================================================================ */
6
+
7
+ .proof-root {
8
+ --sp-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
9
+
10
+ /* method colors โ€” same accent vocabulary as the marketing/POC surfaces */
11
+ --sp-get: #00e5a0;
12
+ --sp-post: #4d9fff;
13
+ --sp-put: #f59e0b;
14
+ --sp-delete: #ef4444;
15
+ --sp-patch: #c084fc;
16
+
17
+ /* verdicts */
18
+ --sp-ok: #00e5a0;
19
+ --sp-gap: #ff5c5c;
20
+ --sp-undoc: #f5a623;
21
+
22
+ --sp-ink: #e6e6e2;
23
+ --sp-faint: rgba(255, 255, 255, 0.38);
24
+ --sp-hair: rgba(255, 255, 255, 0.09);
25
+ --sp-hair-strong: rgba(255, 255, 255, 0.22);
26
+
27
+ font-family: var(--sp-mono);
28
+ color: var(--sp-ink);
29
+ }
30
+
31
+ .proof-page {
32
+ position: relative;
33
+ isolation: isolate;
34
+ }
35
+
36
+ /* faint top glow behind content */
37
+ .proof-page::before {
38
+ content: "";
39
+ position: absolute;
40
+ inset: 0;
41
+ z-index: -1;
42
+ background: radial-gradient(120% 40% at 50% -5%, rgba(0, 229, 160, 0.055), transparent 60%);
43
+ pointer-events: none;
44
+ }
45
+
46
+ /* ---- entrance choreography ------------------------------------------------ */
47
+ @keyframes sp-rise {
48
+ from {
49
+ opacity: 0;
50
+ transform: translateY(14px);
51
+ }
52
+ to {
53
+ opacity: 1;
54
+ transform: translateY(0);
55
+ }
56
+ }
57
+
58
+ .sp-rise {
59
+ opacity: 0;
60
+ animation: sp-rise 0.65s cubic-bezier(0.22, 1, 0.36, 1) forwards;
61
+ animation-delay: calc(var(--sp-stagger, 0) * 70ms);
62
+ }
63
+
64
+ @media (prefers-reduced-motion: reduce) {
65
+ .sp-rise {
66
+ animation: none;
67
+ opacity: 1;
68
+ }
69
+ }
70
+
71
+ /* ---- proof rules ----------------------------------------------------------- */
72
+ .sp-rule-double {
73
+ border-top: 3px double var(--sp-hair-strong);
74
+ }
75
+
76
+ .sp-leader {
77
+ flex: 1 1 2rem;
78
+ min-width: 2rem;
79
+ border-bottom: 1px dotted rgba(255, 255, 255, 0.18);
80
+ transform: translateY(-0.3em);
81
+ }
82
+
83
+ /* ---- method chips ----------------------------------------------------------- */
84
+ .sp-method {
85
+ --mc: var(--sp-faint);
86
+ color: var(--mc);
87
+ border-left: 3px solid var(--mc);
88
+ padding-left: 0.6rem;
89
+ font-size: 0.72rem;
90
+ font-weight: 600;
91
+ letter-spacing: 0.14em;
92
+ line-height: 1.9;
93
+ transition: border-left-width 150ms ease;
94
+ }
95
+ .sp-method[data-method="get"] { --mc: var(--sp-get); }
96
+ .sp-method[data-method="post"] { --mc: var(--sp-post); }
97
+ .sp-method[data-method="put"] { --mc: var(--sp-put); }
98
+ .sp-method[data-method="delete"] { --mc: var(--sp-delete); }
99
+ .sp-method[data-method="patch"] { --mc: var(--sp-patch); }
100
+
101
+ .sp-oprow:hover .sp-method,
102
+ .sp-oprow[data-state="open"] .sp-method {
103
+ border-left-width: 6px;
104
+ }
105
+
106
+ /* ---- punch-card status marks (trigger row) ---------------------------------- */
107
+ .sp-marks {
108
+ display: inline-flex;
109
+ gap: 3px;
110
+ align-items: center;
111
+ }
112
+ .sp-mark {
113
+ width: 7px;
114
+ height: 11px;
115
+ border: 1px solid var(--sp-faint);
116
+ background: transparent;
117
+ }
118
+ .sp-mark[data-state="ok"] {
119
+ background: var(--sp-ok);
120
+ border-color: var(--sp-ok);
121
+ }
122
+ .sp-mark[data-state="gap"] {
123
+ border-color: var(--sp-gap);
124
+ border-style: dashed;
125
+ }
126
+ .sp-mark[data-state="undoc"] {
127
+ border-color: var(--sp-undoc);
128
+ background: repeating-linear-gradient(
129
+ 45deg,
130
+ var(--sp-undoc) 0 1px,
131
+ transparent 1px 3px
132
+ );
133
+ }
134
+
135
+ /* ---- status-code ink by class ------------------------------------------------ */
136
+ .sp-code[data-class="2"] { color: var(--sp-ok); }
137
+ .sp-code[data-class="3"] { color: var(--sp-post); }
138
+ .sp-code[data-class="4"] { color: var(--sp-undoc); }
139
+ .sp-code[data-class="5"] { color: var(--sp-gap); }
140
+
141
+ /* ---- verdict stamps ----------------------------------------------------------- */
142
+ .sp-stamp {
143
+ font-size: 0.62rem;
144
+ font-weight: 600;
145
+ letter-spacing: 0.16em;
146
+ padding: 0.18rem 0.5rem;
147
+ white-space: nowrap;
148
+ }
149
+ .sp-stamp[data-verdict="ok"] {
150
+ color: var(--sp-ok);
151
+ border: 1px solid color-mix(in srgb, var(--sp-ok) 45%, transparent);
152
+ background: color-mix(in srgb, var(--sp-ok) 8%, transparent);
153
+ }
154
+ .sp-stamp[data-verdict="gap"] {
155
+ color: var(--sp-gap);
156
+ border: 1px dashed color-mix(in srgb, var(--sp-gap) 55%, transparent);
157
+ }
158
+ .sp-stamp[data-verdict="undoc"] {
159
+ color: var(--sp-undoc);
160
+ border: 1px solid color-mix(in srgb, var(--sp-undoc) 45%, transparent);
161
+ background: repeating-linear-gradient(
162
+ 45deg,
163
+ color-mix(in srgb, var(--sp-undoc) 10%, transparent) 0 4px,
164
+ transparent 4px 8px
165
+ );
166
+ }
167
+
168
+ /* clickable stamps (verified / undocumented open the test-code panel) */
169
+ button.sp-stamp {
170
+ cursor: pointer;
171
+ transition: transform 120ms ease, box-shadow 120ms ease;
172
+ }
173
+ button.sp-stamp:hover {
174
+ transform: translateY(-1px);
175
+ box-shadow: 0 3px 14px -4px color-mix(in srgb, currentColor 55%, transparent);
176
+ }
177
+ button.sp-stamp:focus-visible {
178
+ outline: 1px solid currentColor;
179
+ outline-offset: 2px;
180
+ }
181
+
182
+ /* ---- test-code panel ----------------------------------------------------------- */
183
+ .sp-codeblock {
184
+ border: 1px solid var(--sp-hair);
185
+ background: rgba(255, 255, 255, 0.02);
186
+ font-size: 0.72rem;
187
+ line-height: 1.7;
188
+ overflow-x: auto;
189
+ }
190
+ .sp-codeline {
191
+ display: grid;
192
+ grid-template-columns: 3.2rem 1fr;
193
+ padding-right: 1rem;
194
+ white-space: pre;
195
+ }
196
+ .sp-codeline[data-hit] {
197
+ background: color-mix(in srgb, var(--sp-ok) 8%, transparent);
198
+ box-shadow: inset 2px 0 0 var(--sp-ok);
199
+ }
200
+ .sp-lineno {
201
+ padding-right: 0.9rem;
202
+ text-align: right;
203
+ color: rgba(255, 255, 255, 0.22);
204
+ user-select: none;
205
+ }
206
+ .sp-codeline[data-hit] .sp-lineno {
207
+ color: var(--sp-ok);
208
+ }
209
+
210
+ /* ---- row hover bleed ----------------------------------------------------------- */
211
+ .sp-oprow {
212
+ transition: background-color 180ms ease;
213
+ }
214
+ .sp-oprow:hover,
215
+ .sp-oprow[data-state="open"] {
216
+ background: rgba(255, 255, 255, 0.025);
217
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "tags": [],
3
+ "operationCount": 0,
4
+ "coveredCount": 0,
5
+ "totalCount": 0,
6
+ "untestedOperations": 0
7
+ }