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-leader hidden sm:block" aria-hidden />
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
- <section className="sp-rise" style={{ '--sp-stagger': index + 3 } as React.CSSProperties}>
268
- <header className="mb-1 flex items-baseline gap-4 border-b pb-2">
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
- <h2 className="text-sm font-semibold uppercase tracking-[0.08em]">{tag.tag}</h2>
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="sp-leader" aria-hidden />
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
- </header>
279
- <Accordion type="multiple" className="divide-y divide-[var(--sp-hair)]">
280
- {tag.operations.map((operation) => (
281
- <OperationRow
282
- key={`${operation.method} ${operation.specPath}`}
283
- operation={operation}
284
- onShowEvidence={onShowEvidence}
285
- />
286
- ))}
287
- </Accordion>
288
- </section>
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-4xl flex-col gap-14 px-6 py-16">
319
+ <div className="mx-auto flex max-w-5xl flex-col gap-14 px-6 py-16">
315
320
  {/* masthead */}
316
- <header className="sp-rise" style={{ '--sp-stagger': 0 } as React.CSSProperties}>
317
- <div className="flex flex-wrap items-end justify-between gap-6">
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-5xl font-semibold tabular-nums leading-none">
321
- {Math.round((report.coveredCount / Math.max(report.totalCount, 1)) * 100)}
322
- <span className="text-2xl font-normal text-muted-foreground">%</span>
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
- {/* summary strip */}
332
- <dl
333
- className="sp-rise sp-rule-double flex flex-wrap items-baseline gap-x-10 gap-y-3 border-b py-3"
334
- style={{ '--sp-stagger': 1 } as React.CSSProperties}
335
- >
336
- {facts.map(([label, value]) => (
337
- <div key={label} className="flex items-baseline gap-2.5">
338
- <dd className="text-xl tabular-nums">{value}</dd>
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
- {label.toUpperCase()}
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
- <div className="ml-auto text-[0.65rem] tracking-[0.18em] text-muted-foreground">
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
- report.tags.map((tag, i) => (
379
- <TagSection key={tag.tag} tag={tag} index={i} onShowEvidence={setEvidence} />
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
 
@@ -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.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,