specproof 0.3.0 → 0.4.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.
|
@@ -228,8 +228,7 @@ function OperationRow({
|
|
|
228
228
|
{operation.method}
|
|
229
229
|
</span>
|
|
230
230
|
<PathInk specPath={operation.specPath} />
|
|
231
|
-
<span className="sp-
|
|
232
|
-
<span className="sp-marks shrink-0" aria-hidden>
|
|
231
|
+
<span className="sp-marks ml-auto shrink-0" aria-hidden>
|
|
233
232
|
{operation.statuses.map((status) => (
|
|
234
233
|
<span key={status.code} className="sp-mark" data-state={verdictOf(status)} />
|
|
235
234
|
))}
|
|
@@ -264,28 +263,33 @@ function TagSection({
|
|
|
264
263
|
onShowEvidence: (evidence: Evidence) => void;
|
|
265
264
|
}) {
|
|
266
265
|
return (
|
|
267
|
-
<
|
|
268
|
-
|
|
266
|
+
<AccordionItem
|
|
267
|
+
value={tag.tag}
|
|
268
|
+
className="sp-rise border-b-0"
|
|
269
|
+
style={{ '--sp-stagger': index + 3 } as React.CSSProperties}
|
|
270
|
+
>
|
|
271
|
+
<AccordionTrigger className="items-baseline gap-4 border-b py-0 pb-2 hover:no-underline">
|
|
269
272
|
<span className="text-[0.65rem] tracking-[0.2em] text-muted-foreground">
|
|
270
273
|
{String(index + 1).padStart(2, '0')}
|
|
271
274
|
</span>
|
|
272
|
-
<
|
|
275
|
+
<span className="text-sm font-semibold uppercase tracking-[0.08em]">{tag.tag}</span>
|
|
273
276
|
<span className="hidden text-xs text-muted-foreground sm:block">{tag.description}</span>
|
|
274
|
-
<span className="
|
|
275
|
-
<span className="text-sm tabular-nums text-muted-foreground">
|
|
277
|
+
<span className="ml-auto text-sm font-normal tabular-nums text-muted-foreground">
|
|
276
278
|
<span className="text-foreground">{tag.coveredCount}</span>/{tag.totalCount} verified
|
|
277
279
|
</span>
|
|
278
|
-
</
|
|
279
|
-
<
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
280
|
+
</AccordionTrigger>
|
|
281
|
+
<AccordionContent className="pb-0 pt-1">
|
|
282
|
+
<Accordion type="multiple" className="divide-y divide-[var(--sp-hair)]">
|
|
283
|
+
{tag.operations.map((operation) => (
|
|
284
|
+
<OperationRow
|
|
285
|
+
key={`${operation.method} ${operation.specPath}`}
|
|
286
|
+
operation={operation}
|
|
287
|
+
onShowEvidence={onShowEvidence}
|
|
288
|
+
/>
|
|
289
|
+
))}
|
|
290
|
+
</Accordion>
|
|
291
|
+
</AccordionContent>
|
|
292
|
+
</AccordionItem>
|
|
289
293
|
);
|
|
290
294
|
}
|
|
291
295
|
|
|
@@ -302,6 +306,7 @@ export function CoverageProof({
|
|
|
302
306
|
}) {
|
|
303
307
|
const [evidence, setEvidence] = useState<Evidence | null>(null);
|
|
304
308
|
const gapCount = report.totalCount - report.coveredCount;
|
|
309
|
+
const verifiedPct = Math.round((report.coveredCount / Math.max(report.totalCount, 1)) * 100);
|
|
305
310
|
const facts: Array<[string, string]> = [
|
|
306
311
|
['operations', String(report.operationCount)],
|
|
307
312
|
['status pairs verified', `${report.coveredCount}/${report.totalCount}`],
|
|
@@ -311,56 +316,43 @@ export function CoverageProof({
|
|
|
311
316
|
|
|
312
317
|
return (
|
|
313
318
|
<div className="proof-root proof-page min-h-screen">
|
|
314
|
-
<div className="mx-auto flex max-w-
|
|
319
|
+
<div className="mx-auto flex max-w-5xl flex-col gap-14 px-6 py-16">
|
|
315
320
|
{/* masthead */}
|
|
316
|
-
<header
|
|
317
|
-
|
|
321
|
+
<header
|
|
322
|
+
className="sp-rise flex flex-col gap-8"
|
|
323
|
+
style={{ '--sp-stagger': 0 } as React.CSSProperties}
|
|
324
|
+
>
|
|
325
|
+
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
318
326
|
<h1 className="text-5xl font-semibold tracking-tight">SpecProof</h1>
|
|
319
327
|
<div className="text-right">
|
|
320
|
-
<div className="text-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
</div>
|
|
324
|
-
<div className="mt-2 text-[0.65rem] tracking-[0.14em] text-muted-foreground">
|
|
325
|
-
DOCUMENTED RESPONSES VERIFIED
|
|
328
|
+
<div className="text-2xl font-semibold tracking-tight">{report.repoName}</div>
|
|
329
|
+
<div className="mt-2 text-[0.65rem] tracking-[0.18em] tabular-nums text-muted-foreground">
|
|
330
|
+
{new Date(compiledAt).toISOString().slice(0, 16).replace('T', ' ')} UTC
|
|
326
331
|
</div>
|
|
327
332
|
</div>
|
|
328
333
|
</div>
|
|
329
|
-
</header>
|
|
330
334
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
335
|
+
{/* report metadata */}
|
|
336
|
+
<dl className="sp-rule-double flex flex-wrap items-end justify-between gap-x-8 gap-y-4 border-b py-4">
|
|
337
|
+
{facts.map(([label, value]) => (
|
|
338
|
+
<div key={label}>
|
|
339
|
+
<dt className="text-[0.65rem] tracking-[0.18em] text-muted-foreground">
|
|
340
|
+
{label.toUpperCase()}
|
|
341
|
+
</dt>
|
|
342
|
+
<dd className="mt-2 text-2xl font-semibold tabular-nums">{value}</dd>
|
|
343
|
+
</div>
|
|
344
|
+
))}
|
|
345
|
+
<div className="text-right">
|
|
339
346
|
<dt className="text-[0.65rem] tracking-[0.18em] text-muted-foreground">
|
|
340
|
-
|
|
347
|
+
RESPONSES VERIFIED
|
|
341
348
|
</dt>
|
|
349
|
+
<dd className="mt-2 text-2xl font-semibold tabular-nums">
|
|
350
|
+
{verifiedPct}
|
|
351
|
+
<span className="text-base font-normal text-muted-foreground">%</span>
|
|
352
|
+
</dd>
|
|
342
353
|
</div>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
COMPILED {new Date(compiledAt).toISOString().slice(0, 10)}
|
|
346
|
-
</div>
|
|
347
|
-
</dl>
|
|
348
|
-
|
|
349
|
-
{/* legend */}
|
|
350
|
-
<div
|
|
351
|
-
className="sp-rise -mt-8 flex flex-wrap items-center gap-x-6 gap-y-2 text-[0.65rem] tracking-[0.14em] text-muted-foreground"
|
|
352
|
-
style={{ '--sp-stagger': 2 } as React.CSSProperties}
|
|
353
|
-
>
|
|
354
|
-
<span className="flex items-center gap-2">
|
|
355
|
-
<span className="sp-mark" data-state="ok" /> VERIFIED
|
|
356
|
-
</span>
|
|
357
|
-
<span className="flex items-center gap-2">
|
|
358
|
-
<span className="sp-mark" data-state="gap" /> NO TEST
|
|
359
|
-
</span>
|
|
360
|
-
<span className="flex items-center gap-2">
|
|
361
|
-
<span className="sp-mark" data-state="undoc" /> UNDOCUMENTED
|
|
362
|
-
</span>
|
|
363
|
-
</div>
|
|
354
|
+
</dl>
|
|
355
|
+
</header>
|
|
364
356
|
|
|
365
357
|
{/* tag sections */}
|
|
366
358
|
{report.operationCount === 0 ? (
|
|
@@ -375,9 +367,15 @@ export function CoverageProof({
|
|
|
375
367
|
</div>
|
|
376
368
|
</section>
|
|
377
369
|
) : (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
370
|
+
<Accordion
|
|
371
|
+
type="multiple"
|
|
372
|
+
defaultValue={report.tags.map((tag) => tag.tag)}
|
|
373
|
+
className="flex flex-col gap-14"
|
|
374
|
+
>
|
|
375
|
+
{report.tags.map((tag, i) => (
|
|
376
|
+
<TagSection key={tag.tag} tag={tag} index={i} onShowEvidence={setEvidence} />
|
|
377
|
+
))}
|
|
378
|
+
</Accordion>
|
|
381
379
|
)}
|
|
382
380
|
</div>
|
|
383
381
|
|
package/lib/api-test-coverage.ts
CHANGED
|
@@ -127,6 +127,8 @@ export interface TagCoverage {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
export interface CoverageReport {
|
|
130
|
+
/** Display name of the audited repo (its package.json "name", falling back to the directory name) */
|
|
131
|
+
repoName: string;
|
|
130
132
|
tags: TagCoverage[];
|
|
131
133
|
operationCount: number;
|
|
132
134
|
/** documented (path, method, status) pairs with assertions */
|
|
@@ -136,6 +138,19 @@ export interface CoverageReport {
|
|
|
136
138
|
untestedOperations: number;
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
/** The audited repo's display name: its package.json "name", falling back to the directory name. */
|
|
142
|
+
export function resolveRepoName(repoRoot: string): string {
|
|
143
|
+
try {
|
|
144
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as {
|
|
145
|
+
name?: string;
|
|
146
|
+
};
|
|
147
|
+
if (pkg.name) return pkg.name;
|
|
148
|
+
} catch {
|
|
149
|
+
// no readable package.json — fall through to the directory name
|
|
150
|
+
}
|
|
151
|
+
return path.basename(repoRoot);
|
|
152
|
+
}
|
|
153
|
+
|
|
139
154
|
// ============================================================================
|
|
140
155
|
// Test-file parsing
|
|
141
156
|
// ============================================================================
|
|
@@ -378,6 +393,7 @@ export function buildCoverageReport(repoRoot: string = TARGET_REPO_ROOT): Covera
|
|
|
378
393
|
|
|
379
394
|
const operations = tags.flatMap((t) => t.operations);
|
|
380
395
|
return {
|
|
396
|
+
repoName: resolveRepoName(repoRoot),
|
|
381
397
|
tags,
|
|
382
398
|
operationCount: operations.length,
|
|
383
399
|
coveredCount: tags.reduce((n, t) => n + t.coveredCount, 0),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specproof",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -18,6 +18,7 @@ import { fileURLToPath } from 'url';
|
|
|
18
18
|
|
|
19
19
|
import {
|
|
20
20
|
buildCoverageReport,
|
|
21
|
+
resolveRepoName,
|
|
21
22
|
resolveSpecPath,
|
|
22
23
|
TARGET_REPO_ROOT,
|
|
23
24
|
type CoverageReport,
|
|
@@ -83,6 +84,7 @@ export function runGenerate(options: GenerateOptions = {}): number {
|
|
|
83
84
|
// no proof at all. Write an empty one: the app needs the file to build,
|
|
84
85
|
// and an empty report renders the "no API definition" state.
|
|
85
86
|
const empty: CoverageReport = {
|
|
87
|
+
repoName: resolveRepoName(TARGET_REPO_ROOT),
|
|
86
88
|
tags: [],
|
|
87
89
|
operationCount: 0,
|
|
88
90
|
coveredCount: 0,
|