specproof 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +0 -1
- package/.github/workflows/test-unit.yml +45 -12
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +17 -7
- package/README.md +2 -0
- package/app/proof.generated.json +286 -5
- package/components/CoverageProof.tsx +1 -1
- package/eslint.config.mjs +1 -1
- package/example/api/openapi.json +88 -0
- package/example/tests/auth.test.ts +25 -0
- package/example/tests/client.ts +53 -0
- package/example/tests/tasks.test.ts +83 -0
- package/lib/api-test-coverage.test.ts +423 -0
- package/lib/api-test-coverage.ts +10 -10
- package/marketing/index.html +1083 -0
- package/marketing/package.json +10 -0
- package/marketing/server.ts +27 -0
- package/package.json +1 -1
- package/public/banner.png +0 -0
- package/public/icon.png +0 -0
- package/public/logo.png +0 -0
- package/tsconfig.json +3 -1
- package/vitest.config.ts +2 -1
|
@@ -0,0 +1,1083 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>SpecProof — the receipt for your API coverage</title>
|
|
7
|
+
<meta name="description" content="SpecProof audits any repo's OpenAPI spec against its own test suite and shows the exact test that proves — or fails to prove — each documented response status." />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
|
11
|
+
<script>document.documentElement.classList.add('js');</script>
|
|
12
|
+
<style>
|
|
13
|
+
/* ======================================================================
|
|
14
|
+
Same vocabulary as the app (app/proof.css + globals.css):
|
|
15
|
+
Geist Mono, hairlines at 9%/22% white, dotted leaders, punch-card
|
|
16
|
+
marks, outlined verdict stamps, 70ms-staggered rise.
|
|
17
|
+
====================================================================== */
|
|
18
|
+
:root {
|
|
19
|
+
--bg: #010101; /* hsl(0 0% 0.4%) */
|
|
20
|
+
--ink: #e6e6e2;
|
|
21
|
+
--muted: rgba(230, 230, 226, 0.72);
|
|
22
|
+
--faint: rgba(255, 255, 255, 0.38);
|
|
23
|
+
--hair: rgba(255, 255, 255, 0.09);
|
|
24
|
+
--hair-strong: rgba(255, 255, 255, 0.22);
|
|
25
|
+
|
|
26
|
+
--get: #00e5a0;
|
|
27
|
+
--post: #4d9fff;
|
|
28
|
+
--put: #f59e0b;
|
|
29
|
+
--delete: #ef4444;
|
|
30
|
+
|
|
31
|
+
--ok: #00e5a0;
|
|
32
|
+
--gap: #ff5c5c;
|
|
33
|
+
--undoc: #f5a623;
|
|
34
|
+
|
|
35
|
+
--mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
* { box-sizing: border-box; }
|
|
39
|
+
|
|
40
|
+
html { scroll-behavior: smooth; }
|
|
41
|
+
|
|
42
|
+
body {
|
|
43
|
+
margin: 0;
|
|
44
|
+
background: var(--bg);
|
|
45
|
+
color: var(--ink);
|
|
46
|
+
font-family: var(--mono);
|
|
47
|
+
font-size: 16px;
|
|
48
|
+
-webkit-font-smoothing: antialiased;
|
|
49
|
+
position: relative;
|
|
50
|
+
isolation: isolate;
|
|
51
|
+
overflow-x: hidden;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* faint top glow behind content — identical to .proof-page::before */
|
|
55
|
+
body::before {
|
|
56
|
+
content: "";
|
|
57
|
+
position: fixed;
|
|
58
|
+
inset: 0;
|
|
59
|
+
z-index: -1;
|
|
60
|
+
background: radial-gradient(120% 40% at 50% -5%, rgba(0, 229, 160, 0.055), transparent 60%);
|
|
61
|
+
pointer-events: none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
a { color: inherit; }
|
|
65
|
+
button { font-family: inherit; }
|
|
66
|
+
|
|
67
|
+
.wrap {
|
|
68
|
+
max-width: 896px; /* app: max-w-4xl */
|
|
69
|
+
margin: 0 auto;
|
|
70
|
+
padding: 0 1.5rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* micro-label: the app's tracking-[0.14em]/[0.18em] uppercase captions */
|
|
74
|
+
.micro {
|
|
75
|
+
font-size: 0.65rem;
|
|
76
|
+
letter-spacing: 0.16em;
|
|
77
|
+
color: var(--faint);
|
|
78
|
+
text-transform: uppercase;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ---- entrance choreography (identical curve to .sp-rise) ------------- */
|
|
82
|
+
@keyframes rise {
|
|
83
|
+
from { opacity: 0; transform: translateY(14px); }
|
|
84
|
+
to { opacity: 1; transform: translateY(0); }
|
|
85
|
+
}
|
|
86
|
+
/* Plays immediately (like the app's .sp-rise). With JS present, below-fold
|
|
87
|
+
elements are paused until the IntersectionObserver marks them .in — but a
|
|
88
|
+
missing/failed script can never leave the page blank. */
|
|
89
|
+
.rise {
|
|
90
|
+
opacity: 0;
|
|
91
|
+
animation: rise 0.65s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
|
92
|
+
animation-delay: calc(var(--st, 0) * 70ms);
|
|
93
|
+
}
|
|
94
|
+
html.js .rise:not(.in) { animation-play-state: paused; }
|
|
95
|
+
@media (prefers-reduced-motion: reduce) {
|
|
96
|
+
.rise { animation: none; opacity: 1; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ---- shared proof primitives (verbatim from app/proof.css) ----------- */
|
|
100
|
+
.sp-rule-double { border-top: 3px double var(--hair-strong); }
|
|
101
|
+
|
|
102
|
+
.sp-leader {
|
|
103
|
+
flex: 1 1 2rem;
|
|
104
|
+
min-width: 2rem;
|
|
105
|
+
border-bottom: 1px dotted rgba(255, 255, 255, 0.18);
|
|
106
|
+
transform: translateY(-0.3em);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.sp-method {
|
|
110
|
+
--mc: var(--faint);
|
|
111
|
+
color: var(--mc);
|
|
112
|
+
border-left: 3px solid var(--mc);
|
|
113
|
+
padding-left: 0.6rem;
|
|
114
|
+
font-size: 0.72rem;
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
letter-spacing: 0.14em;
|
|
117
|
+
line-height: 1.9;
|
|
118
|
+
text-transform: uppercase;
|
|
119
|
+
transition: border-left-width 150ms ease;
|
|
120
|
+
}
|
|
121
|
+
.sp-method[data-method="get"] { --mc: var(--get); }
|
|
122
|
+
.sp-method[data-method="post"] { --mc: var(--post); }
|
|
123
|
+
.sp-method[data-method="put"] { --mc: var(--put); }
|
|
124
|
+
.sp-method[data-method="delete"] { --mc: var(--delete); }
|
|
125
|
+
|
|
126
|
+
.sp-marks { display: inline-flex; gap: 3px; align-items: center; }
|
|
127
|
+
.sp-mark {
|
|
128
|
+
width: 7px;
|
|
129
|
+
height: 11px;
|
|
130
|
+
border: 1px solid var(--faint);
|
|
131
|
+
background: transparent;
|
|
132
|
+
}
|
|
133
|
+
.sp-mark[data-state="ok"] { background: var(--ok); border-color: var(--ok); }
|
|
134
|
+
.sp-mark[data-state="gap"] { border-color: var(--gap); border-style: dashed; }
|
|
135
|
+
.sp-mark[data-state="undoc"] {
|
|
136
|
+
border-color: var(--undoc);
|
|
137
|
+
background: repeating-linear-gradient(45deg, var(--undoc) 0 1px, transparent 1px 3px);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.sp-code[data-class="2"] { color: var(--ok); }
|
|
141
|
+
.sp-code[data-class="3"] { color: var(--post); }
|
|
142
|
+
.sp-code[data-class="4"] { color: var(--undoc); }
|
|
143
|
+
.sp-code[data-class="5"] { color: var(--gap); }
|
|
144
|
+
|
|
145
|
+
.sp-stamp {
|
|
146
|
+
font-size: 0.62rem;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
letter-spacing: 0.16em;
|
|
149
|
+
padding: 0.18rem 0.5rem;
|
|
150
|
+
white-space: nowrap;
|
|
151
|
+
background: transparent;
|
|
152
|
+
}
|
|
153
|
+
.sp-stamp[data-verdict="ok"] {
|
|
154
|
+
color: var(--ok);
|
|
155
|
+
border: 1px solid color-mix(in srgb, var(--ok) 45%, transparent);
|
|
156
|
+
background: color-mix(in srgb, var(--ok) 8%, transparent);
|
|
157
|
+
}
|
|
158
|
+
.sp-stamp[data-verdict="gap"] {
|
|
159
|
+
color: var(--gap);
|
|
160
|
+
border: 1px dashed color-mix(in srgb, var(--gap) 55%, transparent);
|
|
161
|
+
}
|
|
162
|
+
.sp-stamp[data-verdict="undoc"] {
|
|
163
|
+
color: var(--undoc);
|
|
164
|
+
border: 1px solid color-mix(in srgb, var(--undoc) 45%, transparent);
|
|
165
|
+
background: repeating-linear-gradient(
|
|
166
|
+
45deg,
|
|
167
|
+
color-mix(in srgb, var(--undoc) 10%, transparent) 0 4px,
|
|
168
|
+
transparent 4px 8px
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
button.sp-stamp {
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
transition: transform 120ms ease, box-shadow 120ms ease;
|
|
174
|
+
}
|
|
175
|
+
button.sp-stamp:hover {
|
|
176
|
+
transform: translateY(-1px);
|
|
177
|
+
box-shadow: 0 3px 14px -4px color-mix(in srgb, currentColor 55%, transparent);
|
|
178
|
+
}
|
|
179
|
+
button.sp-stamp:focus-visible { outline: 1px solid currentColor; outline-offset: 2px; }
|
|
180
|
+
|
|
181
|
+
.sp-codeblock {
|
|
182
|
+
margin: 0;
|
|
183
|
+
border: 1px solid var(--hair);
|
|
184
|
+
background: rgba(255, 255, 255, 0.02);
|
|
185
|
+
font-size: 0.72rem;
|
|
186
|
+
line-height: 1.7;
|
|
187
|
+
overflow-x: auto;
|
|
188
|
+
padding: 0.5rem 0;
|
|
189
|
+
white-space: normal; /* markup newlines between codelines must not render */
|
|
190
|
+
}
|
|
191
|
+
.sp-codeline {
|
|
192
|
+
display: grid;
|
|
193
|
+
grid-template-columns: 3.2rem 1fr;
|
|
194
|
+
padding-right: 1rem;
|
|
195
|
+
white-space: pre;
|
|
196
|
+
}
|
|
197
|
+
.sp-codeline code { font-family: inherit; }
|
|
198
|
+
.sp-codeline[data-hit] {
|
|
199
|
+
background: color-mix(in srgb, var(--ok) 8%, transparent);
|
|
200
|
+
box-shadow: inset 2px 0 0 var(--ok);
|
|
201
|
+
}
|
|
202
|
+
.sp-lineno {
|
|
203
|
+
padding-right: 0.9rem;
|
|
204
|
+
text-align: right;
|
|
205
|
+
color: rgba(255, 255, 255, 0.22);
|
|
206
|
+
user-select: none;
|
|
207
|
+
}
|
|
208
|
+
.sp-codeline[data-hit] .sp-lineno { color: var(--ok); }
|
|
209
|
+
|
|
210
|
+
/* ================================================================ nav */
|
|
211
|
+
.nav {
|
|
212
|
+
position: sticky;
|
|
213
|
+
top: 0;
|
|
214
|
+
z-index: 40;
|
|
215
|
+
backdrop-filter: blur(10px);
|
|
216
|
+
background: color-mix(in srgb, var(--bg) 78%, transparent);
|
|
217
|
+
border-bottom: 1px solid var(--hair);
|
|
218
|
+
}
|
|
219
|
+
.nav .wrap {
|
|
220
|
+
display: flex;
|
|
221
|
+
align-items: center;
|
|
222
|
+
justify-content: space-between;
|
|
223
|
+
gap: 1.5rem;
|
|
224
|
+
height: 3.5rem;
|
|
225
|
+
}
|
|
226
|
+
.nav .brand {
|
|
227
|
+
font-weight: 600;
|
|
228
|
+
letter-spacing: -0.02em;
|
|
229
|
+
font-size: 0.95rem;
|
|
230
|
+
text-decoration: none;
|
|
231
|
+
}
|
|
232
|
+
.nav .links {
|
|
233
|
+
display: flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
gap: 1.6rem;
|
|
236
|
+
}
|
|
237
|
+
.nav .links a {
|
|
238
|
+
font-size: 0.65rem;
|
|
239
|
+
letter-spacing: 0.16em;
|
|
240
|
+
color: var(--faint);
|
|
241
|
+
text-decoration: none;
|
|
242
|
+
transition: color 150ms ease;
|
|
243
|
+
}
|
|
244
|
+
.nav .links a:hover { color: var(--ink); }
|
|
245
|
+
.nav .links a.gh {
|
|
246
|
+
color: var(--ink);
|
|
247
|
+
border: 1px solid var(--hair-strong);
|
|
248
|
+
padding: 0.34rem 0.7rem;
|
|
249
|
+
transition: border-color 150ms ease, color 150ms ease;
|
|
250
|
+
}
|
|
251
|
+
.nav .links a.gh:hover { border-color: var(--ok); color: var(--ok); }
|
|
252
|
+
@media (max-width: 560px) {
|
|
253
|
+
.nav .links a:not(.gh) { display: none; }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* =============================================================== hero */
|
|
257
|
+
.hero { padding: 5.5rem 0 4rem; }
|
|
258
|
+
|
|
259
|
+
.hero .kicker {
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
gap: 0.8rem;
|
|
263
|
+
margin-bottom: 1.6rem;
|
|
264
|
+
}
|
|
265
|
+
.hero .kicker .sp-marks { transform: translateY(1px); }
|
|
266
|
+
|
|
267
|
+
h1.headline {
|
|
268
|
+
font-size: clamp(1.85rem, 5vw, 2.9rem);
|
|
269
|
+
line-height: 1.16;
|
|
270
|
+
font-weight: 600;
|
|
271
|
+
letter-spacing: -0.02em;
|
|
272
|
+
margin: 0 0 1.4rem;
|
|
273
|
+
max-width: 21em;
|
|
274
|
+
}
|
|
275
|
+
h1.headline .muted { color: var(--faint); font-weight: 500; }
|
|
276
|
+
.stamp-word {
|
|
277
|
+
display: inline-block;
|
|
278
|
+
color: var(--ok);
|
|
279
|
+
border: 1px solid color-mix(in srgb, var(--ok) 45%, transparent);
|
|
280
|
+
background: color-mix(in srgb, var(--ok) 8%, transparent);
|
|
281
|
+
padding: 0.02em 0.22em;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.sub {
|
|
285
|
+
max-width: 42em;
|
|
286
|
+
font-size: 0.9rem;
|
|
287
|
+
line-height: 1.75;
|
|
288
|
+
color: var(--muted);
|
|
289
|
+
margin: 0 0 2.4rem;
|
|
290
|
+
font-weight: 400;
|
|
291
|
+
}
|
|
292
|
+
.sub em { font-style: normal; color: var(--ink); }
|
|
293
|
+
|
|
294
|
+
.cta-row {
|
|
295
|
+
display: flex;
|
|
296
|
+
flex-wrap: wrap;
|
|
297
|
+
align-items: stretch;
|
|
298
|
+
gap: 0.9rem;
|
|
299
|
+
margin-bottom: 3.2rem;
|
|
300
|
+
}
|
|
301
|
+
.copy-chip {
|
|
302
|
+
display: flex;
|
|
303
|
+
align-items: center;
|
|
304
|
+
gap: 0.8rem;
|
|
305
|
+
border: 1px solid var(--hair-strong);
|
|
306
|
+
padding: 0.65rem 1rem;
|
|
307
|
+
font-size: 0.76rem;
|
|
308
|
+
background: rgba(255, 255, 255, 0.02);
|
|
309
|
+
}
|
|
310
|
+
.copy-chip .prompt { color: var(--faint); user-select: none; }
|
|
311
|
+
.copy-chip button {
|
|
312
|
+
all: unset;
|
|
313
|
+
cursor: pointer;
|
|
314
|
+
font-family: inherit;
|
|
315
|
+
font-size: 0.62rem;
|
|
316
|
+
font-weight: 600;
|
|
317
|
+
letter-spacing: 0.16em;
|
|
318
|
+
color: var(--faint);
|
|
319
|
+
border-left: 1px solid var(--hair-strong);
|
|
320
|
+
padding-left: 0.8rem;
|
|
321
|
+
transition: color 150ms ease;
|
|
322
|
+
}
|
|
323
|
+
.copy-chip button:hover, .copy-chip button.copied { color: var(--ok); }
|
|
324
|
+
|
|
325
|
+
.btn-ghost {
|
|
326
|
+
display: inline-flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
gap: 0.5rem;
|
|
329
|
+
border: 1px solid var(--hair-strong);
|
|
330
|
+
padding: 0.65rem 1.1rem;
|
|
331
|
+
font-size: 0.76rem;
|
|
332
|
+
text-decoration: none;
|
|
333
|
+
transition: border-color 150ms ease;
|
|
334
|
+
}
|
|
335
|
+
.btn-ghost:hover { border-color: var(--ink); }
|
|
336
|
+
|
|
337
|
+
/* facts strip — mirrors the app's summary <dl> under its double rule */
|
|
338
|
+
.facts {
|
|
339
|
+
display: flex;
|
|
340
|
+
flex-wrap: wrap;
|
|
341
|
+
align-items: baseline;
|
|
342
|
+
gap: 0.75rem 2.5rem;
|
|
343
|
+
margin: 0;
|
|
344
|
+
padding: 0.85rem 0;
|
|
345
|
+
border-bottom: 1px solid var(--hair);
|
|
346
|
+
}
|
|
347
|
+
.facts .fact { display: flex; align-items: baseline; gap: 0.65rem; }
|
|
348
|
+
.facts dd {
|
|
349
|
+
margin: 0;
|
|
350
|
+
font-size: 1.25rem;
|
|
351
|
+
font-variant-numeric: tabular-nums;
|
|
352
|
+
}
|
|
353
|
+
.facts dt {
|
|
354
|
+
font-size: 0.65rem;
|
|
355
|
+
letter-spacing: 0.18em;
|
|
356
|
+
color: var(--faint);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* =========================================================== sections */
|
|
360
|
+
section { padding: 4.2rem 0; }
|
|
361
|
+
section + section { border-top: 1px solid var(--hair); }
|
|
362
|
+
|
|
363
|
+
/* numbered section head — same anatomy as the app's tag headers */
|
|
364
|
+
.section-head {
|
|
365
|
+
display: flex;
|
|
366
|
+
align-items: baseline;
|
|
367
|
+
gap: 1rem;
|
|
368
|
+
border-bottom: 1px solid var(--hair-strong);
|
|
369
|
+
padding-bottom: 0.55rem;
|
|
370
|
+
margin-bottom: 2.2rem;
|
|
371
|
+
}
|
|
372
|
+
.section-head .no {
|
|
373
|
+
font-size: 0.65rem;
|
|
374
|
+
letter-spacing: 0.2em;
|
|
375
|
+
color: var(--faint);
|
|
376
|
+
}
|
|
377
|
+
.section-head h2 {
|
|
378
|
+
margin: 0;
|
|
379
|
+
font-size: 0.9rem;
|
|
380
|
+
font-weight: 600;
|
|
381
|
+
letter-spacing: 0.08em;
|
|
382
|
+
text-transform: uppercase;
|
|
383
|
+
}
|
|
384
|
+
.section-head .anno {
|
|
385
|
+
font-size: 0.72rem;
|
|
386
|
+
color: var(--faint);
|
|
387
|
+
}
|
|
388
|
+
@media (max-width: 560px) { .section-head .anno { display: none; } }
|
|
389
|
+
|
|
390
|
+
/* ============================================================ exhibit */
|
|
391
|
+
.frame {
|
|
392
|
+
border: 1px solid var(--hair-strong);
|
|
393
|
+
background: rgba(255, 255, 255, 0.012);
|
|
394
|
+
}
|
|
395
|
+
.frame-bar {
|
|
396
|
+
display: flex;
|
|
397
|
+
align-items: center;
|
|
398
|
+
justify-content: space-between;
|
|
399
|
+
gap: 1rem;
|
|
400
|
+
padding: 0.6rem 1.1rem;
|
|
401
|
+
border-bottom: 1px solid var(--hair);
|
|
402
|
+
}
|
|
403
|
+
.frame-bar .hint { color: var(--ok); }
|
|
404
|
+
.frame-body { padding: 1.6rem 1.4rem 1.8rem; }
|
|
405
|
+
@media (max-width: 560px) { .frame-body { padding: 1.2rem 0.85rem 1.4rem; } }
|
|
406
|
+
|
|
407
|
+
/* masthead replica */
|
|
408
|
+
.ex-mast {
|
|
409
|
+
display: flex;
|
|
410
|
+
flex-wrap: wrap;
|
|
411
|
+
align-items: flex-end;
|
|
412
|
+
justify-content: space-between;
|
|
413
|
+
gap: 1.2rem;
|
|
414
|
+
margin-bottom: 1.6rem;
|
|
415
|
+
}
|
|
416
|
+
.ex-mast .name {
|
|
417
|
+
font-size: 1.9rem;
|
|
418
|
+
font-weight: 600;
|
|
419
|
+
letter-spacing: -0.02em;
|
|
420
|
+
line-height: 1;
|
|
421
|
+
}
|
|
422
|
+
.ex-mast .pct {
|
|
423
|
+
font-size: 1.9rem;
|
|
424
|
+
font-weight: 600;
|
|
425
|
+
font-variant-numeric: tabular-nums;
|
|
426
|
+
line-height: 1;
|
|
427
|
+
text-align: right;
|
|
428
|
+
}
|
|
429
|
+
.ex-mast .pct small { font-size: 1.05rem; font-weight: 400; color: var(--faint); }
|
|
430
|
+
.ex-mast .pct .micro { display: block; margin-top: 0.5rem; }
|
|
431
|
+
|
|
432
|
+
.ex-summary {
|
|
433
|
+
display: flex;
|
|
434
|
+
flex-wrap: wrap;
|
|
435
|
+
align-items: baseline;
|
|
436
|
+
gap: 0.6rem 2rem;
|
|
437
|
+
border-bottom: 1px solid var(--hair);
|
|
438
|
+
padding: 0.7rem 0;
|
|
439
|
+
margin-bottom: 1rem;
|
|
440
|
+
}
|
|
441
|
+
.ex-summary .cell { display: flex; align-items: baseline; gap: 0.55rem; }
|
|
442
|
+
.ex-summary .v { font-size: 1rem; font-variant-numeric: tabular-nums; }
|
|
443
|
+
.ex-summary .compiled { margin-left: auto; }
|
|
444
|
+
@media (max-width: 680px) { .ex-summary .compiled { margin-left: 0; } }
|
|
445
|
+
|
|
446
|
+
.ex-legend {
|
|
447
|
+
display: flex;
|
|
448
|
+
flex-wrap: wrap;
|
|
449
|
+
gap: 0.5rem 1.5rem;
|
|
450
|
+
margin-bottom: 2rem;
|
|
451
|
+
}
|
|
452
|
+
.ex-legend span { display: inline-flex; align-items: center; gap: 0.5rem; }
|
|
453
|
+
|
|
454
|
+
/* tag header replica */
|
|
455
|
+
.ex-tag-head {
|
|
456
|
+
display: flex;
|
|
457
|
+
align-items: baseline;
|
|
458
|
+
gap: 1rem;
|
|
459
|
+
border-bottom: 1px solid var(--hair-strong);
|
|
460
|
+
padding-bottom: 0.5rem;
|
|
461
|
+
}
|
|
462
|
+
.ex-tag-head h3 {
|
|
463
|
+
margin: 0;
|
|
464
|
+
font-size: 0.82rem;
|
|
465
|
+
font-weight: 600;
|
|
466
|
+
letter-spacing: 0.08em;
|
|
467
|
+
text-transform: uppercase;
|
|
468
|
+
}
|
|
469
|
+
.ex-tag-head .desc { font-size: 0.72rem; color: var(--faint); }
|
|
470
|
+
.ex-tag-head .tally { font-size: 0.82rem; font-variant-numeric: tabular-nums; color: var(--faint); }
|
|
471
|
+
.ex-tag-head .tally b { color: var(--ink); font-weight: 500; }
|
|
472
|
+
@media (max-width: 560px) { .ex-tag-head .desc { display: none; } }
|
|
473
|
+
|
|
474
|
+
/* operation rows */
|
|
475
|
+
.oprow { border-bottom: 1px solid var(--hair); transition: background-color 180ms ease; }
|
|
476
|
+
.oprow:hover, .oprow[data-open] { background: rgba(255, 255, 255, 0.025); }
|
|
477
|
+
.oprow:hover .sp-method, .oprow[data-open] .sp-method { border-left-width: 6px; }
|
|
478
|
+
|
|
479
|
+
.optrigger {
|
|
480
|
+
all: unset;
|
|
481
|
+
display: flex;
|
|
482
|
+
align-items: baseline;
|
|
483
|
+
gap: 0.8rem;
|
|
484
|
+
width: 100%;
|
|
485
|
+
padding: 0.85rem 0.75rem;
|
|
486
|
+
cursor: pointer;
|
|
487
|
+
}
|
|
488
|
+
.optrigger:focus-visible { outline: 1px solid var(--hair-strong); outline-offset: -1px; }
|
|
489
|
+
.optrigger .sp-method { width: 4rem; flex-shrink: 0; }
|
|
490
|
+
.optrigger .path { font-size: 0.82rem; letter-spacing: -0.01em; }
|
|
491
|
+
.optrigger .path em { font-style: normal; color: var(--faint); }
|
|
492
|
+
.optrigger .tally {
|
|
493
|
+
width: 3rem;
|
|
494
|
+
flex-shrink: 0;
|
|
495
|
+
text-align: right;
|
|
496
|
+
font-size: 0.75rem;
|
|
497
|
+
font-variant-numeric: tabular-nums;
|
|
498
|
+
color: var(--faint);
|
|
499
|
+
}
|
|
500
|
+
.optrigger .chev {
|
|
501
|
+
flex-shrink: 0;
|
|
502
|
+
color: var(--faint);
|
|
503
|
+
font-size: 0.7rem;
|
|
504
|
+
transition: transform 200ms ease;
|
|
505
|
+
}
|
|
506
|
+
.oprow[data-open] .chev { transform: rotate(180deg); }
|
|
507
|
+
@media (max-width: 560px) {
|
|
508
|
+
.optrigger .sp-leader { display: none; }
|
|
509
|
+
.optrigger { flex-wrap: wrap; }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.opbody { display: none; padding: 0 0.75rem 1.3rem; }
|
|
513
|
+
.oprow[data-open] .opbody { display: block; }
|
|
514
|
+
.opbody .inner { padding-left: calc(0.6rem + 3px); }
|
|
515
|
+
.opbody .op-summary { margin: 0 0 0.8rem; font-size: 0.75rem; color: var(--faint); }
|
|
516
|
+
|
|
517
|
+
.statusrow {
|
|
518
|
+
display: flex;
|
|
519
|
+
align-items: baseline;
|
|
520
|
+
gap: 0.8rem;
|
|
521
|
+
padding: 0.42rem 0;
|
|
522
|
+
}
|
|
523
|
+
.statusrow .sp-code { width: 2rem; flex-shrink: 0; font-size: 0.82rem; font-weight: 600; }
|
|
524
|
+
.statusrow .desc { font-size: 0.75rem; color: var(--faint); }
|
|
525
|
+
.statusrow .count { flex-shrink: 0; font-size: 0.68rem; color: var(--faint); }
|
|
526
|
+
@media (max-width: 560px) {
|
|
527
|
+
.statusrow { flex-wrap: wrap; }
|
|
528
|
+
.statusrow .sp-leader { display: none; }
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.evidence { display: none; margin: 0.5rem 0 0.9rem; }
|
|
532
|
+
.evidence[data-open] { display: block; }
|
|
533
|
+
.evidence figure { margin: 0; display: flex; flex-direction: column; gap: 0.5rem; }
|
|
534
|
+
.evidence figcaption { display: flex; align-items: baseline; gap: 0.8rem; }
|
|
535
|
+
.evidence figcaption .t { font-size: 0.72rem; font-weight: 500; }
|
|
536
|
+
.evidence figcaption .ln { flex-shrink: 0; font-size: 0.65rem; color: var(--faint); }
|
|
537
|
+
|
|
538
|
+
.opfoot {
|
|
539
|
+
display: flex;
|
|
540
|
+
align-items: center;
|
|
541
|
+
gap: 0.5rem;
|
|
542
|
+
margin-top: 0.9rem;
|
|
543
|
+
padding-top: 0.7rem;
|
|
544
|
+
border-top: 1px dashed var(--hair-strong);
|
|
545
|
+
font-size: 0.68rem;
|
|
546
|
+
color: var(--faint);
|
|
547
|
+
}
|
|
548
|
+
.opfoot .file { color: var(--muted); }
|
|
549
|
+
|
|
550
|
+
/* ============================================================= method */
|
|
551
|
+
.steps { display: flex; flex-direction: column; gap: 2.6rem; }
|
|
552
|
+
.step {
|
|
553
|
+
--sc: var(--faint);
|
|
554
|
+
display: grid;
|
|
555
|
+
grid-template-columns: 9.5rem 1fr;
|
|
556
|
+
gap: 1.5rem;
|
|
557
|
+
}
|
|
558
|
+
@media (max-width: 640px) { .step { grid-template-columns: 1fr; gap: 0.7rem; } }
|
|
559
|
+
.step .label {
|
|
560
|
+
color: var(--sc);
|
|
561
|
+
border-left: 3px solid var(--sc);
|
|
562
|
+
padding-left: 0.6rem;
|
|
563
|
+
font-size: 0.72rem;
|
|
564
|
+
font-weight: 600;
|
|
565
|
+
letter-spacing: 0.14em;
|
|
566
|
+
line-height: 1.9;
|
|
567
|
+
align-self: start;
|
|
568
|
+
}
|
|
569
|
+
.step[data-accent="get"] { --sc: var(--get); }
|
|
570
|
+
.step[data-accent="post"] { --sc: var(--post); }
|
|
571
|
+
.step[data-accent="put"] { --sc: var(--put); }
|
|
572
|
+
.step h3 {
|
|
573
|
+
margin: 0 0 0.55rem;
|
|
574
|
+
font-size: 0.88rem;
|
|
575
|
+
font-weight: 600;
|
|
576
|
+
letter-spacing: -0.01em;
|
|
577
|
+
}
|
|
578
|
+
.step p {
|
|
579
|
+
margin: 0;
|
|
580
|
+
font-size: 0.8rem;
|
|
581
|
+
line-height: 1.75;
|
|
582
|
+
color: var(--muted);
|
|
583
|
+
max-width: 40em;
|
|
584
|
+
}
|
|
585
|
+
.step p + p { margin-top: 0.6rem; }
|
|
586
|
+
.step code {
|
|
587
|
+
font-family: inherit;
|
|
588
|
+
font-size: 0.92em;
|
|
589
|
+
color: var(--ink);
|
|
590
|
+
background: var(--hair);
|
|
591
|
+
padding: 0.06em 0.35em;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/* ========================================================== quickstart */
|
|
595
|
+
.code-shell {
|
|
596
|
+
border: 1px solid var(--hair-strong);
|
|
597
|
+
background: rgba(255, 255, 255, 0.015);
|
|
598
|
+
}
|
|
599
|
+
.code-shell .bar {
|
|
600
|
+
display: flex;
|
|
601
|
+
align-items: center;
|
|
602
|
+
justify-content: space-between;
|
|
603
|
+
padding: 0.55rem 1rem;
|
|
604
|
+
border-bottom: 1px solid var(--hair);
|
|
605
|
+
}
|
|
606
|
+
.code-shell .bar button {
|
|
607
|
+
all: unset;
|
|
608
|
+
cursor: pointer;
|
|
609
|
+
font-family: inherit;
|
|
610
|
+
font-size: 0.62rem;
|
|
611
|
+
font-weight: 600;
|
|
612
|
+
letter-spacing: 0.16em;
|
|
613
|
+
color: var(--faint);
|
|
614
|
+
transition: color 150ms ease;
|
|
615
|
+
}
|
|
616
|
+
.code-shell .bar button:hover, .code-shell .bar button.copied { color: var(--ok); }
|
|
617
|
+
.code-shell pre {
|
|
618
|
+
margin: 0;
|
|
619
|
+
padding: 1.1rem 1.2rem;
|
|
620
|
+
font-size: 0.8rem;
|
|
621
|
+
line-height: 2;
|
|
622
|
+
overflow-x: auto;
|
|
623
|
+
}
|
|
624
|
+
.code-shell .c1 { color: var(--faint); user-select: none; }
|
|
625
|
+
.code-shell .c2 { color: var(--ok); }
|
|
626
|
+
.code-shell .cm { color: var(--faint); }
|
|
627
|
+
|
|
628
|
+
.notes {
|
|
629
|
+
margin-top: 1.4rem;
|
|
630
|
+
display: flex;
|
|
631
|
+
flex-direction: column;
|
|
632
|
+
gap: 0.55rem;
|
|
633
|
+
}
|
|
634
|
+
.notes p {
|
|
635
|
+
margin: 0;
|
|
636
|
+
font-size: 0.75rem;
|
|
637
|
+
line-height: 1.75;
|
|
638
|
+
color: var(--faint);
|
|
639
|
+
display: flex;
|
|
640
|
+
gap: 0.7rem;
|
|
641
|
+
}
|
|
642
|
+
.notes p::before { content: "·"; color: var(--hair-strong); }
|
|
643
|
+
.notes code {
|
|
644
|
+
font-family: inherit;
|
|
645
|
+
color: var(--muted);
|
|
646
|
+
background: var(--hair);
|
|
647
|
+
padding: 0.06em 0.35em;
|
|
648
|
+
}
|
|
649
|
+
.notes a { color: var(--muted); }
|
|
650
|
+
.notes a:hover { color: var(--ok); }
|
|
651
|
+
|
|
652
|
+
/* ============================================================= footer */
|
|
653
|
+
footer { padding: 3.4rem 0 3rem; border-top: 1px solid var(--hair); }
|
|
654
|
+
.foot-stamp {
|
|
655
|
+
display: inline-block;
|
|
656
|
+
border: 2px solid var(--faint);
|
|
657
|
+
color: var(--faint);
|
|
658
|
+
font-size: 0.7rem;
|
|
659
|
+
font-weight: 600;
|
|
660
|
+
letter-spacing: 0.2em;
|
|
661
|
+
padding: 0.5rem 0.8rem;
|
|
662
|
+
margin-bottom: 1.6rem;
|
|
663
|
+
}
|
|
664
|
+
.foot-links {
|
|
665
|
+
display: flex;
|
|
666
|
+
flex-wrap: wrap;
|
|
667
|
+
justify-content: space-between;
|
|
668
|
+
gap: 1rem;
|
|
669
|
+
font-size: 0.7rem;
|
|
670
|
+
color: var(--faint);
|
|
671
|
+
}
|
|
672
|
+
.foot-links a { text-decoration: none; transition: color 150ms ease; }
|
|
673
|
+
.foot-links a:hover { color: var(--ok); }
|
|
674
|
+
</style>
|
|
675
|
+
</head>
|
|
676
|
+
<body>
|
|
677
|
+
|
|
678
|
+
<nav class="nav">
|
|
679
|
+
<div class="wrap">
|
|
680
|
+
<a class="brand" href="#">SpecProof</a>
|
|
681
|
+
<div class="links">
|
|
682
|
+
<a href="#exhibit">EXHIBIT</a>
|
|
683
|
+
<a href="#method">METHOD</a>
|
|
684
|
+
<a href="#quickstart">QUICKSTART</a>
|
|
685
|
+
<a class="gh" href="https://github.com/Durable-Quality/specproof">GITHUB ↗</a>
|
|
686
|
+
</div>
|
|
687
|
+
</div>
|
|
688
|
+
</nav>
|
|
689
|
+
|
|
690
|
+
<header class="hero">
|
|
691
|
+
<div class="wrap">
|
|
692
|
+
<div class="kicker rise in" style="--st:0">
|
|
693
|
+
<span class="sp-marks" aria-hidden>
|
|
694
|
+
<span class="sp-mark" data-state="ok"></span>
|
|
695
|
+
<span class="sp-mark" data-state="ok"></span>
|
|
696
|
+
<span class="sp-mark" data-state="gap"></span>
|
|
697
|
+
<span class="sp-mark" data-state="undoc"></span>
|
|
698
|
+
</span>
|
|
699
|
+
<span class="micro">API test-coverage audit · OpenAPI × test suite</span>
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
<h1 class="headline rise in" style="--st:1">
|
|
703
|
+
Coverage percentages are a claim.<br>
|
|
704
|
+
<span class="muted">SpecProof is the</span> <span class="stamp-word">receipt.</span>
|
|
705
|
+
</h1>
|
|
706
|
+
|
|
707
|
+
<p class="sub rise in" style="--st:2">
|
|
708
|
+
SpecProof cross-examines any repo's OpenAPI spec against its own test suite and stamps a
|
|
709
|
+
verdict on every documented response status — <em>verified</em>, <em>no test</em>, or
|
|
710
|
+
<em>undocumented</em>. Click a verdict, read the exact test that proves it.
|
|
711
|
+
</p>
|
|
712
|
+
|
|
713
|
+
<div class="cta-row rise in" style="--st:3">
|
|
714
|
+
<div class="copy-chip">
|
|
715
|
+
<span class="prompt">$</span>
|
|
716
|
+
<span>git clone github:Durable-Quality/specproof</span>
|
|
717
|
+
<button data-copy="git clone https://github.com/Durable-Quality/specproof.git">COPY</button>
|
|
718
|
+
</div>
|
|
719
|
+
<a class="btn-ghost" href="#exhibit">Inspect the exhibit ↓</a>
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
<dl class="facts sp-rule-double rise in" style="--st:4">
|
|
723
|
+
<div class="fact">
|
|
724
|
+
<dd>0</dd>
|
|
725
|
+
<dt>REPO-SPECIFIC CONFIG</dt>
|
|
726
|
+
</div>
|
|
727
|
+
<div class="fact">
|
|
728
|
+
<dd>1</dd>
|
|
729
|
+
<dt>CHECKED-IN ARTIFACT</dt>
|
|
730
|
+
</div>
|
|
731
|
+
<div class="fact">
|
|
732
|
+
<dd>CI</dd>
|
|
733
|
+
<dt>FAILS WHEN THE PROOF GOES STALE</dt>
|
|
734
|
+
</div>
|
|
735
|
+
</dl>
|
|
736
|
+
</div>
|
|
737
|
+
</header>
|
|
738
|
+
|
|
739
|
+
<section id="exhibit">
|
|
740
|
+
<div class="wrap">
|
|
741
|
+
<div class="section-head rise">
|
|
742
|
+
<span class="no">01</span>
|
|
743
|
+
<h2>Exhibit</h2>
|
|
744
|
+
<span class="anno">the audit view, verbatim — expand an operation, click a verdict</span>
|
|
745
|
+
<span class="sp-leader" aria-hidden></span>
|
|
746
|
+
<span class="micro">LIVE</span>
|
|
747
|
+
</div>
|
|
748
|
+
|
|
749
|
+
<div class="frame rise">
|
|
750
|
+
<div class="frame-bar micro">
|
|
751
|
+
<span>RENDERED FROM app/proof.generated.json</span>
|
|
752
|
+
<span class="hint">INTERACTIVE</span>
|
|
753
|
+
</div>
|
|
754
|
+
<div class="frame-body">
|
|
755
|
+
|
|
756
|
+
<div class="ex-mast">
|
|
757
|
+
<span class="name">SpecProof</span>
|
|
758
|
+
<div class="pct">
|
|
759
|
+
90<small>%</small>
|
|
760
|
+
<span class="micro">DOCUMENTED RESPONSES VERIFIED</span>
|
|
761
|
+
</div>
|
|
762
|
+
</div>
|
|
763
|
+
|
|
764
|
+
<div class="ex-summary sp-rule-double">
|
|
765
|
+
<span class="cell"><span class="v">12</span><span class="micro">OPERATIONS</span></span>
|
|
766
|
+
<span class="cell"><span class="v">37/41</span><span class="micro">STATUS PAIRS VERIFIED</span></span>
|
|
767
|
+
<span class="cell"><span class="v">4</span><span class="micro">GAPS</span></span>
|
|
768
|
+
<span class="cell"><span class="v">1</span><span class="micro">UNTESTED ROUTES</span></span>
|
|
769
|
+
<span class="micro compiled">COMPILED 2026-07-13</span>
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
<div class="ex-legend micro">
|
|
773
|
+
<span><span class="sp-mark" data-state="ok"></span> VERIFIED</span>
|
|
774
|
+
<span><span class="sp-mark" data-state="gap"></span> NO TEST</span>
|
|
775
|
+
<span><span class="sp-mark" data-state="undoc"></span> UNDOCUMENTED</span>
|
|
776
|
+
</div>
|
|
777
|
+
|
|
778
|
+
<div class="ex-tag-head">
|
|
779
|
+
<span class="micro">01</span>
|
|
780
|
+
<h3>Orders</h3>
|
|
781
|
+
<span class="desc">Order lifecycle and fulfilment</span>
|
|
782
|
+
<span class="sp-leader" aria-hidden></span>
|
|
783
|
+
<span class="tally"><b>5</b>/6 verified</span>
|
|
784
|
+
</div>
|
|
785
|
+
|
|
786
|
+
<!-- GET /orders -->
|
|
787
|
+
<div class="oprow">
|
|
788
|
+
<button class="optrigger" type="button" aria-expanded="false">
|
|
789
|
+
<span class="sp-method" data-method="get">GET</span>
|
|
790
|
+
<span class="path">/orders</span>
|
|
791
|
+
<span class="sp-leader" aria-hidden></span>
|
|
792
|
+
<span class="sp-marks" aria-hidden>
|
|
793
|
+
<span class="sp-mark" data-state="ok"></span>
|
|
794
|
+
<span class="sp-mark" data-state="ok"></span>
|
|
795
|
+
</span>
|
|
796
|
+
<span class="tally">2/2</span>
|
|
797
|
+
<span class="chev" aria-hidden>▾</span>
|
|
798
|
+
</button>
|
|
799
|
+
<div class="opbody">
|
|
800
|
+
<div class="inner">
|
|
801
|
+
<p class="op-summary">List orders for the authenticated customer.</p>
|
|
802
|
+
<div class="statusrow">
|
|
803
|
+
<span class="sp-code" data-class="2">200</span>
|
|
804
|
+
<span class="desc">A page of orders.</span>
|
|
805
|
+
<span class="sp-leader" aria-hidden></span>
|
|
806
|
+
<span class="count">3 assertions</span>
|
|
807
|
+
<span class="sp-stamp" data-verdict="ok">VERIFIED</span>
|
|
808
|
+
</div>
|
|
809
|
+
<div class="statusrow">
|
|
810
|
+
<span class="sp-code" data-class="4">401</span>
|
|
811
|
+
<span class="desc">Missing or invalid credentials.</span>
|
|
812
|
+
<span class="sp-leader" aria-hidden></span>
|
|
813
|
+
<span class="count">1 assertion</span>
|
|
814
|
+
<span class="sp-stamp" data-verdict="ok">VERIFIED</span>
|
|
815
|
+
</div>
|
|
816
|
+
<div class="opfoot">
|
|
817
|
+
<span>5 tests in <span class="file">tests/orders/list.test.ts</span></span>
|
|
818
|
+
</div>
|
|
819
|
+
</div>
|
|
820
|
+
</div>
|
|
821
|
+
</div>
|
|
822
|
+
|
|
823
|
+
<!-- POST /orders/{id}/cancel — open by default -->
|
|
824
|
+
<div class="oprow" data-open>
|
|
825
|
+
<button class="optrigger" type="button" aria-expanded="true">
|
|
826
|
+
<span class="sp-method" data-method="post">POST</span>
|
|
827
|
+
<span class="path">/orders/<em>{id}</em>/cancel</span>
|
|
828
|
+
<span class="sp-leader" aria-hidden></span>
|
|
829
|
+
<span class="sp-marks" aria-hidden>
|
|
830
|
+
<span class="sp-mark" data-state="ok"></span>
|
|
831
|
+
<span class="sp-mark" data-state="gap"></span>
|
|
832
|
+
<span class="sp-mark" data-state="undoc"></span>
|
|
833
|
+
</span>
|
|
834
|
+
<span class="tally">1/2</span>
|
|
835
|
+
<span class="chev" aria-hidden>▾</span>
|
|
836
|
+
</button>
|
|
837
|
+
<div class="opbody">
|
|
838
|
+
<div class="inner">
|
|
839
|
+
<p class="op-summary">Cancel an order that has not shipped yet.</p>
|
|
840
|
+
|
|
841
|
+
<div class="statusrow">
|
|
842
|
+
<span class="sp-code" data-class="2">200</span>
|
|
843
|
+
<span class="desc">Order cancelled.</span>
|
|
844
|
+
<span class="sp-leader" aria-hidden></span>
|
|
845
|
+
<span class="count">2 assertions</span>
|
|
846
|
+
<button class="sp-stamp" type="button" data-verdict="ok" data-evidence="ev-200" aria-expanded="true" title="Show the test code">VERIFIED ⌕</button>
|
|
847
|
+
</div>
|
|
848
|
+
<div class="evidence" id="ev-200" data-open>
|
|
849
|
+
<figure>
|
|
850
|
+
<figcaption>
|
|
851
|
+
<span class="t">cancels a pending order</span>
|
|
852
|
+
<span class="sp-leader" aria-hidden></span>
|
|
853
|
+
<span class="ln">L15</span>
|
|
854
|
+
</figcaption>
|
|
855
|
+
<pre class="sp-codeblock"><span class="sp-codeline"><span class="sp-lineno">15</span><code>it("cancels a pending order", async () => {</code></span>
|
|
856
|
+
<span class="sp-codeline"><span class="sp-lineno">16</span><code> const res = await request(app).post(`/orders/${order.id}/cancel`);</code></span>
|
|
857
|
+
<span class="sp-codeline" data-hit><span class="sp-lineno">17</span><code> expect(res.status).toBe(200);</code></span>
|
|
858
|
+
<span class="sp-codeline"><span class="sp-lineno">18</span><code> expect(res.body.status).toBe("cancelled");</code></span>
|
|
859
|
+
<span class="sp-codeline"><span class="sp-lineno">19</span><code>});</code></span></pre>
|
|
860
|
+
</figure>
|
|
861
|
+
</div>
|
|
862
|
+
|
|
863
|
+
<div class="statusrow">
|
|
864
|
+
<span class="sp-code" data-class="4">409</span>
|
|
865
|
+
<span class="desc">Order already shipped.</span>
|
|
866
|
+
<span class="sp-leader" aria-hidden></span>
|
|
867
|
+
<span class="sp-stamp" data-verdict="gap">NO TEST</span>
|
|
868
|
+
</div>
|
|
869
|
+
|
|
870
|
+
<div class="statusrow">
|
|
871
|
+
<span class="sp-code" data-class="4">404</span>
|
|
872
|
+
<span class="desc">asserted in tests, but absent from the OpenAPI spec</span>
|
|
873
|
+
<span class="sp-leader" aria-hidden></span>
|
|
874
|
+
<button class="sp-stamp" type="button" data-verdict="undoc" data-evidence="ev-404" aria-expanded="false" title="Show the test code">UNDOCUMENTED ⌕</button>
|
|
875
|
+
</div>
|
|
876
|
+
<div class="evidence" id="ev-404">
|
|
877
|
+
<figure>
|
|
878
|
+
<figcaption>
|
|
879
|
+
<span class="t">returns 404 when the order does not exist</span>
|
|
880
|
+
<span class="sp-leader" aria-hidden></span>
|
|
881
|
+
<span class="ln">L22</span>
|
|
882
|
+
</figcaption>
|
|
883
|
+
<pre class="sp-codeblock"><span class="sp-codeline"><span class="sp-lineno">22</span><code>it("returns 404 when the order does not exist", async () => {</code></span>
|
|
884
|
+
<span class="sp-codeline"><span class="sp-lineno">23</span><code> const res = await request(app).post(`/orders/does-not-exist/cancel`);</code></span>
|
|
885
|
+
<span class="sp-codeline" data-hit><span class="sp-lineno">24</span><code> expect(res.status).toBe(404);</code></span>
|
|
886
|
+
<span class="sp-codeline"><span class="sp-lineno">25</span><code>});</code></span></pre>
|
|
887
|
+
</figure>
|
|
888
|
+
</div>
|
|
889
|
+
|
|
890
|
+
<div class="opfoot">
|
|
891
|
+
<span>4 tests in <span class="file">tests/orders/cancel.test.ts</span></span>
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
|
|
897
|
+
<!-- DELETE /orders/{id} -->
|
|
898
|
+
<div class="oprow" style="border-bottom: none;">
|
|
899
|
+
<button class="optrigger" type="button" aria-expanded="false">
|
|
900
|
+
<span class="sp-method" data-method="delete">DELETE</span>
|
|
901
|
+
<span class="path">/orders/<em>{id}</em></span>
|
|
902
|
+
<span class="sp-leader" aria-hidden></span>
|
|
903
|
+
<span class="sp-marks" aria-hidden>
|
|
904
|
+
<span class="sp-mark" data-state="gap"></span>
|
|
905
|
+
<span class="sp-mark" data-state="gap"></span>
|
|
906
|
+
</span>
|
|
907
|
+
<span class="tally">0/2</span>
|
|
908
|
+
<span class="chev" aria-hidden>▾</span>
|
|
909
|
+
</button>
|
|
910
|
+
<div class="opbody">
|
|
911
|
+
<div class="inner">
|
|
912
|
+
<p class="op-summary">Delete a draft order.</p>
|
|
913
|
+
<div class="statusrow">
|
|
914
|
+
<span class="sp-code" data-class="2">204</span>
|
|
915
|
+
<span class="desc">Order deleted.</span>
|
|
916
|
+
<span class="sp-leader" aria-hidden></span>
|
|
917
|
+
<span class="sp-stamp" data-verdict="gap">NO TEST</span>
|
|
918
|
+
</div>
|
|
919
|
+
<div class="statusrow">
|
|
920
|
+
<span class="sp-code" data-class="4">404</span>
|
|
921
|
+
<span class="desc">No such order.</span>
|
|
922
|
+
<span class="sp-leader" aria-hidden></span>
|
|
923
|
+
<span class="sp-stamp" data-verdict="gap">NO TEST</span>
|
|
924
|
+
</div>
|
|
925
|
+
<div class="opfoot">
|
|
926
|
+
<span class="sp-stamp" data-verdict="gap">NO TEST FILE — THIS OPERATION SHIPS UNTESTED</span>
|
|
927
|
+
</div>
|
|
928
|
+
</div>
|
|
929
|
+
</div>
|
|
930
|
+
</div>
|
|
931
|
+
|
|
932
|
+
</div>
|
|
933
|
+
</div>
|
|
934
|
+
</div>
|
|
935
|
+
</section>
|
|
936
|
+
|
|
937
|
+
<section id="method">
|
|
938
|
+
<div class="wrap">
|
|
939
|
+
<div class="section-head rise">
|
|
940
|
+
<span class="no">02</span>
|
|
941
|
+
<h2>Method</h2>
|
|
942
|
+
<span class="anno">generate → checked-in artifact → contract test</span>
|
|
943
|
+
<span class="sp-leader" aria-hidden></span>
|
|
944
|
+
</div>
|
|
945
|
+
|
|
946
|
+
<div class="steps">
|
|
947
|
+
<div class="step rise" data-accent="get">
|
|
948
|
+
<span class="label">GENERATE</span>
|
|
949
|
+
<div>
|
|
950
|
+
<h3>Read both sources of truth.</h3>
|
|
951
|
+
<p>
|
|
952
|
+
The analyzer reads the target repo's OpenAPI spec — auto-discovered as the shallowest
|
|
953
|
+
<code>openapi*.json</code> / <code>swagger*.json</code>, or pinned with
|
|
954
|
+
<code>SPECPROOF_SPEC</code> — and every test suite titled
|
|
955
|
+
<code>describe("METHOD /path")</code>, matching
|
|
956
|
+
<code>.status).toBe(NNN)</code> assertions to documented responses.
|
|
957
|
+
<code>{param}</code>, <code>[param]</code>, and <code>:param</code> segments are
|
|
958
|
+
treated as equivalent.
|
|
959
|
+
</p>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
|
|
963
|
+
<div class="step rise" data-accent="post">
|
|
964
|
+
<span class="label">COMMIT</span>
|
|
965
|
+
<div>
|
|
966
|
+
<h3>One deterministic, checked-in artifact.</h3>
|
|
967
|
+
<p>
|
|
968
|
+
The result is <code>app/proof.generated.json</code> — regenerated before every
|
|
969
|
+
<code>dev</code> and <code>build</code>, committed alongside your code. The app
|
|
970
|
+
renders only the artifact, so building and deploying the audit needs no target
|
|
971
|
+
checkout at all.
|
|
972
|
+
</p>
|
|
973
|
+
</div>
|
|
974
|
+
</div>
|
|
975
|
+
|
|
976
|
+
<div class="step rise" data-accent="put">
|
|
977
|
+
<span class="label">ENFORCE</span>
|
|
978
|
+
<div>
|
|
979
|
+
<h3>A contract test keeps the proof honest.</h3>
|
|
980
|
+
<p>
|
|
981
|
+
<code>app/proof-contract.test.ts</code> fails CI when the artifact is stale relative
|
|
982
|
+
to the spec or tests, when the audited operations drift from the spec's, or when a
|
|
983
|
+
quoted snippet no longer points at a real <code>it()</code> block. The proof cannot
|
|
984
|
+
silently rot.
|
|
985
|
+
</p>
|
|
986
|
+
</div>
|
|
987
|
+
</div>
|
|
988
|
+
</div>
|
|
989
|
+
</div>
|
|
990
|
+
</section>
|
|
991
|
+
|
|
992
|
+
<section id="quickstart">
|
|
993
|
+
<div class="wrap">
|
|
994
|
+
<div class="section-head rise">
|
|
995
|
+
<span class="no">03</span>
|
|
996
|
+
<h2>Quickstart</h2>
|
|
997
|
+
<span class="anno">requires bun</span>
|
|
998
|
+
<span class="sp-leader" aria-hidden></span>
|
|
999
|
+
</div>
|
|
1000
|
+
|
|
1001
|
+
<div class="code-shell rise">
|
|
1002
|
+
<div class="bar">
|
|
1003
|
+
<span class="micro">TERMINAL</span>
|
|
1004
|
+
<button data-copy="git clone https://github.com/Durable-Quality/specproof.git specproof
|
|
1005
|
+
cd specproof && bun install
|
|
1006
|
+
SPECPROOF_REPO=/path/to/your-repo bun run dev">COPY</button>
|
|
1007
|
+
</div>
|
|
1008
|
+
<pre><span class="c1">$</span> git clone https://github.com/Durable-Quality/specproof.git specproof
|
|
1009
|
+
<span class="c1">$</span> cd specproof && bun install
|
|
1010
|
+
<span class="c1">$</span> SPECPROOF_REPO=/path/to/your-repo bun run dev
|
|
1011
|
+
<span class="c2">→ compiles the proof, then serves it on http://localhost:3001</span></pre>
|
|
1012
|
+
</div>
|
|
1013
|
+
|
|
1014
|
+
<div class="notes rise">
|
|
1015
|
+
<p><span><code>SPECPROOF_REPO</code> defaults to the current working directory — run it from inside the target repo and it needs no configuration at all.</span></p>
|
|
1016
|
+
<p><span>When the target's spec or tests change, <code>bun run generate:proof</code> refreshes the artifact; commit it and CI goes green again.</span></p>
|
|
1017
|
+
<p><span>Conventions, commands, and the drift guard are documented in the <a href="https://github.com/Durable-Quality/specproof#readme">README</a>.</span></p>
|
|
1018
|
+
</div>
|
|
1019
|
+
</div>
|
|
1020
|
+
</section>
|
|
1021
|
+
|
|
1022
|
+
<footer>
|
|
1023
|
+
<div class="wrap">
|
|
1024
|
+
<span class="foot-stamp">AUDITED · NOT ASSERTED</span>
|
|
1025
|
+
<div class="foot-links">
|
|
1026
|
+
<span>MIT licensed</span>
|
|
1027
|
+
<a href="https://github.com/Durable-Quality/specproof">github.com/Durable-Quality/specproof</a>
|
|
1028
|
+
<span>SpecProof</span>
|
|
1029
|
+
</div>
|
|
1030
|
+
</div>
|
|
1031
|
+
</footer>
|
|
1032
|
+
|
|
1033
|
+
<script>
|
|
1034
|
+
// copy buttons
|
|
1035
|
+
document.querySelectorAll('[data-copy]').forEach(function (btn) {
|
|
1036
|
+
btn.addEventListener('click', function () {
|
|
1037
|
+
navigator.clipboard.writeText(btn.getAttribute('data-copy')).then(function () {
|
|
1038
|
+
var original = btn.textContent;
|
|
1039
|
+
btn.textContent = 'COPIED';
|
|
1040
|
+
btn.classList.add('copied');
|
|
1041
|
+
setTimeout(function () {
|
|
1042
|
+
btn.textContent = original;
|
|
1043
|
+
btn.classList.remove('copied');
|
|
1044
|
+
}, 1400);
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
// operation rows expand/collapse (mirrors the app's accordion)
|
|
1050
|
+
document.querySelectorAll('.optrigger').forEach(function (trigger) {
|
|
1051
|
+
trigger.addEventListener('click', function () {
|
|
1052
|
+
var row = trigger.closest('.oprow');
|
|
1053
|
+
var open = row.hasAttribute('data-open');
|
|
1054
|
+
if (open) row.removeAttribute('data-open');
|
|
1055
|
+
else row.setAttribute('data-open', '');
|
|
1056
|
+
trigger.setAttribute('aria-expanded', String(!open));
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
// verdict stamps reveal the test evidence (mirrors the app's sheet)
|
|
1061
|
+
document.querySelectorAll('button.sp-stamp[data-evidence]').forEach(function (stamp) {
|
|
1062
|
+
stamp.addEventListener('click', function () {
|
|
1063
|
+
var panel = document.getElementById(stamp.getAttribute('data-evidence'));
|
|
1064
|
+
var open = panel.hasAttribute('data-open');
|
|
1065
|
+
if (open) panel.removeAttribute('data-open');
|
|
1066
|
+
else panel.setAttribute('data-open', '');
|
|
1067
|
+
stamp.setAttribute('aria-expanded', String(!open));
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
// scroll-triggered rise, same curve and stagger as the app's sp-rise
|
|
1072
|
+
var io = new IntersectionObserver(function (entries) {
|
|
1073
|
+
entries.forEach(function (entry) {
|
|
1074
|
+
if (entry.isIntersecting) {
|
|
1075
|
+
entry.target.classList.add('in');
|
|
1076
|
+
io.unobserve(entry.target);
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
}, { threshold: 0.1 });
|
|
1080
|
+
document.querySelectorAll('.rise').forEach(function (el) { io.observe(el); });
|
|
1081
|
+
</script>
|
|
1082
|
+
</body>
|
|
1083
|
+
</html>
|