@cejel/cejel 0.1.4 → 0.1.6

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.
@@ -29,450 +29,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
29
  mod
30
30
  ));
31
31
 
32
- // src/witan/external-findings.ts
33
- var EXTERNAL_FINDINGS_DISPLAY_LIMIT = 10;
34
- var SEVERITY_RANK = {
35
- critical: 0,
36
- warning: 1,
37
- info: 2
38
- };
39
- function formatExternalSourceLabel(source) {
40
- return source.startsWith("sarif:") ? source.slice("sarif:".length) : source;
41
- }
42
- function summarizeExternalSources(consumedSignals) {
43
- const bySource = /* @__PURE__ */ new Map();
44
- for (const signal of consumedSignals) {
45
- const entry = bySource.get(signal.source) ?? { findingCount: 0, dimensions: /* @__PURE__ */ new Set() };
46
- entry.findingCount += signal.findingCount;
47
- entry.dimensions.add(signal.dimension);
48
- bySource.set(signal.source, entry);
49
- }
50
- return Array.from(bySource.entries()).map(([source, { findingCount, dimensions }]) => ({
51
- source,
52
- label: formatExternalSourceLabel(source),
53
- findingCount,
54
- dimensions: Array.from(dimensions).sort()
55
- })).sort((a, b) => a.source.localeCompare(b.source));
56
- }
57
- function formatExternalSourceLine(summary) {
58
- const noun = summary.findingCount === 1 ? "finding" : "findings";
59
- return `${summary.label}: ${summary.findingCount} ${noun} ingested (folded into ${summary.dimensions.join(", ")})`;
60
- }
61
- function collectExternalFindings(consumedSignals) {
62
- const findings = [];
63
- for (const signal of consumedSignals) {
64
- for (const finding of signal.findings) {
65
- findings.push({
66
- source: signal.source,
67
- label: formatExternalSourceLabel(signal.source),
68
- dimension: signal.dimension,
69
- severity: finding.severity,
70
- ruleId: finding.ruleId,
71
- message: finding.message,
72
- ...finding.location ? { location: finding.location } : {}
73
- });
74
- }
75
- }
76
- return findings.sort((a, b) => {
77
- const bySeverity = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
78
- if (bySeverity !== 0) return bySeverity;
79
- const bySource = a.source.localeCompare(b.source);
80
- if (bySource !== 0) return bySource;
81
- return a.ruleId.localeCompare(b.ruleId);
82
- });
83
- }
84
-
85
- // src/witan/html.ts
86
- function renderWitanHtmlReport(report) {
87
- const codeCriteria = criteriaByCategory(report, "code_trust");
88
- const processCriteria = criteriaByCategory(report, "process_trust");
89
- const verifiedEvidence = report.criteria.flatMap(
90
- (criterion) => criterion.status === "verified" ? criterion.evidence.map((evidence) => renderEvidenceListItem(criterion, evidence)) : []
91
- );
92
- const openItems = report.criteria.flatMap(renderOpenItems);
93
- const contributingSources = renderContributingSources(report.consumedSignals ?? []);
94
- const externalSourceSummaries = summarizeExternalSources(report.consumedSignals ?? []);
95
- const externalFindings = collectExternalFindings(report.consumedSignals ?? []);
96
- return `<!doctype html>
97
- <html lang="en">
98
- <head>
99
- <meta charset="utf-8">
100
- <meta name="viewport" content="width=device-width, initial-scale=1">
101
- <title>Cejel Trust Certificate - ${escapeHtml(report.productDisplayName)}</title>
102
- <style>${CERTIFICATE_CSS}</style>
103
- </head>
104
- <body>
105
- <main class="certificate">
106
- <header class="hero">
107
- <div class="brand">
108
- <span class="brand-mark" aria-hidden="true">C</span>
109
- <span class="brand-name">Cejel</span>
110
- </div>
111
- <div class="hero-grid">
112
- <section>
113
- <p class="eyebrow">Trust Certificate</p>
114
- <h1>${escapeHtml(report.productDisplayName)}</h1>
115
- <dl class="meta">
116
- <div><dt>Date</dt><dd>${escapeHtml(formatDate(report.generatedAt))}</dd></div>
117
- <div><dt>Run</dt><dd>${escapeHtml(renderRepo(report))}</dd></div>
118
- <div><dt>Rubric</dt><dd>${escapeHtml(report.rubricVersion)}</dd></div>
119
- ${contributingSources.length > 0 ? `<div><dt>Sources</dt><dd>Incorporates findings from: ${escapeHtml(contributingSources.join(", "))}<ul class="source-counts">${externalSourceSummaries.map((s) => `<li>${escapeHtml(formatExternalSourceLine(s))}</li>`).join("")}</ul></dd></div>` : ""}
120
- </dl>
121
- </section>
122
- <aside class="score-panel" aria-label="Summary trust score">
123
- <div class="score-badge">Summary</div>
124
- ${report.insufficientSourceReason ? `<div class="verdict">Insufficient source</div>
125
- <p class="insufficient-note">${escapeHtml(report.insufficientSourceReason)}</p>` : `<div class="score">${formatScore(report.overallScore)}</div>
126
- <div class="score-unit">/ 4.0 overall</div>
127
- <div class="verdict">${escapeHtml(renderVerdict(report.overallScore))}</div>
128
- <div class="subscores">
129
- <span>Code ${formatScore(report.codeTrustScore)}</span>
130
- <span>Process ${formatScore(report.processTrustScore)}</span>
131
- </div>`}
132
- </aside>
133
- </div>
134
- </header>
135
-
136
- <section class="trust-grid" aria-label="Rubric criteria">
137
- ${renderCriterionColumn("Code trust", codeCriteria)}
138
- ${renderCriterionColumn("Process trust", processCriteria)}
139
- </section>
140
-
141
- <section class="evidence-grid" aria-label="Evidence and gaps">
142
- <article class="evidence-section">
143
- <h2>Verified evidence</h2>
144
- <ul>${verifiedEvidence.length > 0 ? verifiedEvidence.join("") : "<li>No verified evidence supplied.</li>"}</ul>
145
- </article>
146
- <article class="evidence-section">
147
- <h2>Open / unverified</h2>
148
- <ul>${openItems.length > 0 ? openItems.join("") : "<li>No open or unverified criteria supplied.</li>"}</ul>
149
- </article>
150
- </section>
151
-
152
- ${externalFindings.length > 0 ? `<section class="external-findings-section" aria-label="External findings">
153
- <article class="evidence-section">
154
- <h2>External findings</h2>
155
- <p class="external-note">Findings surfaced by ingested external scanners, attributed to the tool and the cejel criterion they were folded into \u2014 distinct from cejel's own findings above.</p>
156
- <ul>${renderExternalFindingItems(externalFindings).join("")}</ul>
157
- </article>
158
- </section>` : ""}
159
- </main>
160
- </body>
161
- </html>
162
- `;
163
- }
164
- function renderContributingSources(signals) {
165
- return Array.from(new Set(signals.map((s) => s.source))).sort();
166
- }
167
- function renderExternalFindingItems(findings) {
168
- const shown = findings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT);
169
- const items = shown.map(
170
- (f) => `<li><strong>${escapeHtml(f.label)} \u2192 ${escapeHtml(f.dimension)}</strong><span>${escapeHtml(f.severity)}: ${escapeHtml(f.ruleId)} \u2014 ${escapeHtml(f.message)}${f.location ? ` (${escapeHtml(f.location)})` : ""}</span></li>`
171
- );
172
- const remaining = findings.length - shown.length;
173
- if (remaining > 0) {
174
- items.push(`<li><em>...and ${remaining} more \u2014 see report.json for the full list.</em></li>`);
175
- }
176
- return items;
177
- }
178
- function criteriaByCategory(report, category) {
179
- return report.criteria.filter((criterion) => criterion.category === category);
180
- }
181
- function renderCriterionColumn(title, criteria) {
182
- const applicable = criteria.filter((criterion) => criterion.status !== "not_applicable");
183
- const notApplicable = criteria.filter((criterion) => criterion.status === "not_applicable");
184
- return `<section class="criteria-column">
185
- <h2>${escapeHtml(title)}</h2>
186
- <div class="criteria-list">
187
- ${applicable.map(renderCriterionCard).join("")}
188
- </div>
189
- ${notApplicable.length > 0 ? renderNotApplicableGroup(notApplicable) : ""}
190
- </section>`;
191
- }
192
- function renderNotApplicableGroup(criteria) {
193
- return `<div class="na-group">
194
- <h3 class="na-heading">Not applicable to this repository</h3>
195
- <ul class="na-list">
196
- ${criteria.map(renderNotApplicableItem).join("")}
197
- </ul>
198
- </div>`;
199
- }
200
- function renderNotApplicableItem(criterion) {
201
- const reason = criterion.notes ?? "Not applicable to this repository.";
202
- return `<li><span class="na-id">${escapeHtml(criterion.id)}</span> ${escapeHtml(criterion.title)} \u2014 <span class="na-reason">${escapeHtml(reason)}</span></li>`;
203
- }
204
- function renderCriterionCard(criterion) {
205
- const evidence = [
206
- ...criterion.evidence.map(renderEvidencePointer),
207
- ...criterion.findings.map((finding) => renderFindingEvidence(finding))
208
- ];
209
- const metrics = criterion.metrics.map(renderMetric);
210
- return `<article class="criterion">
211
- <div class="criterion-top">
212
- <div>
213
- <div class="criterion-id">${escapeHtml(criterion.id)}</div>
214
- <h3>${escapeHtml(criterion.title)}</h3>
215
- </div>
216
- ${renderStatusChip(criterion.status)}
217
- </div>
218
- <div class="criterion-score">${criterion.status === "not_applicable" ? "N/A" : criterion.status === "insufficient_data" ? "No data" : formatScore(criterion.score)}</div>
219
- <ul class="criterion-metrics">
220
- ${metrics.length > 0 ? metrics.map((item) => `<li>${item}</li>`).join("") : "<li>No measured depth metrics supplied.</li>"}
221
- </ul>
222
- <ul class="criterion-evidence">
223
- ${evidence.length > 0 ? evidence.map((item) => `<li>${item}</li>`).join("") : "<li>No concrete evidence supplied.</li>"}
224
- </ul>
225
- </article>`;
226
- }
227
- function renderMetric(metric2) {
228
- const max = metric2.max ? `/${formatMetricValue(metric2.max)}` : "";
229
- const unit = metric2.unit ? ` ${escapeHtml(metric2.unit)}` : "";
230
- return `<strong>${escapeHtml(metric2.label)}</strong><span>${formatMetricValue(metric2.value)}${max}${unit}</span>`;
231
- }
232
- function renderStatusChip(status) {
233
- return `<span class="status" data-status="${status}">${status}</span>`;
234
- }
235
- function renderEvidenceListItem(criterion, evidence) {
236
- return `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${renderEvidencePointer(evidence)}</span></li>`;
237
- }
238
- function renderOpenItems(criterion) {
239
- const findingItems = criterion.findings.map(
240
- (finding) => `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${escapeHtml(finding.severity)}: ${escapeHtml(finding.summary)} (${renderEvidencePointer(finding.evidence)})</span></li>`
241
- );
242
- if ((criterion.status === "unverified" || criterion.status === "insufficient_data") && criterion.evidence.length === 0 && criterion.findings.length === 0) {
243
- const message = criterion.status === "insufficient_data" ? "Insufficient data \u2014 no measurable signal; excluded from composite (unmeasured, not inapplicable)." : "No concrete evidence supplied.";
244
- return [
245
- `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${escapeHtml(message)}</span></li>`
246
- ];
247
- }
248
- if (criterion.status === "verified") return [];
249
- return findingItems;
250
- }
251
- function renderFindingEvidence(finding) {
252
- return `${escapeHtml(finding.severity)}: ${escapeHtml(finding.summary)} (${renderEvidencePointer(
253
- finding.evidence
254
- )})`;
255
- }
256
- function renderEvidencePointer(evidence) {
257
- const target = evidence.url ?? renderPathEvidenceTarget(evidence);
258
- const label = escapeHtml(evidence.label);
259
- if (evidence.url) {
260
- return `<a href="${escapeAttribute(evidence.url)}">${label}</a>`;
261
- }
262
- return [label, target ? `<code>${escapeHtml(target)}</code>` : void 0].filter(Boolean).join(" ");
263
- }
264
- function renderPathEvidenceTarget(evidence) {
265
- const pathTarget = evidence.path ? `${evidence.path}${evidence.line ? `:${evidence.line}` : ""}` : void 0;
266
- const hashTarget = evidence.contentHash ? `sha256:${evidence.contentHash.slice(0, 12)}` : void 0;
267
- return [pathTarget, hashTarget].filter(Boolean).join(" \xB7 ");
268
- }
269
- function renderRepo(report) {
270
- const repo = report.repo.path ?? report.repo.url ?? "unknown";
271
- return report.repo.headSha ? `${repo} @ ${report.repo.headSha}` : repo;
272
- }
273
- function renderVerdict(score) {
274
- if (score >= 3.5) return "Verified";
275
- if (score >= 2.5) return "Conditional";
276
- if (score >= 1.5) return "At risk";
277
- return "Unverified";
278
- }
279
- function renderReportVerdict(report) {
280
- if (report.insufficientSourceReason) return "Insufficient source";
281
- return renderVerdict(report.overallScore);
282
- }
283
- function formatScore(score) {
284
- return score.toFixed(1);
285
- }
286
- function formatMetricValue(value) {
287
- return Number.isInteger(value) ? value.toString() : value.toFixed(2);
288
- }
289
- function formatDate(value) {
290
- return value.slice(0, 10);
291
- }
292
- function escapeHtml(value) {
293
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
294
- }
295
- function escapeAttribute(value) {
296
- return escapeHtml(value);
297
- }
298
- var CERTIFICATE_CSS = `
299
- :root {
300
- color-scheme: dark;
301
- --steel: #16212a;
302
- --surface: #1d2b35;
303
- --surface-2: #273946;
304
- --line: rgba(238, 244, 247, .13);
305
- --line-strong: rgba(238, 244, 247, .22);
306
- --text: #eef4f7;
307
- --muted: #b7c5cc;
308
- --faint: #8799a4;
309
- --teal: #2fc8a6;
310
- --teal-weak: rgba(47, 200, 166, .14);
311
- --periwinkle: #93a6da;
312
- --periwinkle-weak: rgba(147, 166, 218, .16);
313
- --warn: #e7bf72;
314
- --warn-weak: rgba(231, 191, 114, .16);
315
- --danger: #f09a8f;
316
- --danger-weak: rgba(240, 154, 143, .14);
317
- --quiet: rgba(238, 244, 247, .08);
318
- --serif: "Instrument Serif", Georgia, "Times New Roman", serif;
319
- --sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
320
- --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
321
- }
322
- * { box-sizing: border-box; }
323
- body {
324
- margin: 0;
325
- background: var(--steel);
326
- color: var(--text);
327
- font-family: var(--sans);
328
- line-height: 1.55;
329
- -webkit-font-smoothing: antialiased;
330
- }
331
- .certificate { max-width: 1120px; margin: 0 auto; padding: 44px 32px 64px; }
332
- .hero { border-bottom: 1px solid var(--line); padding-bottom: 38px; }
333
- .brand { display: flex; align-items: center; gap: 12px; margin-bottom: 44px; }
334
- .brand-mark {
335
- display: inline-flex; align-items: center; justify-content: center;
336
- width: 34px; height: 34px; border-radius: 8px;
337
- background: var(--teal); color: #06231d; font-family: var(--serif); font-size: 24px;
338
- }
339
- .brand-name { font-family: var(--serif); font-size: 24px; }
340
- .hero-grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 36px; align-items: end; }
341
- .eyebrow {
342
- margin: 0 0 14px; color: var(--teal); font-family: var(--mono);
343
- font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
344
- }
345
- h1, h2, h3, p { margin-top: 0; }
346
- h1 { margin-bottom: 24px; font-family: var(--serif); font-size: 64px; line-height: 1; font-weight: 400; letter-spacing: 0; }
347
- .meta { display: grid; gap: 10px; margin: 0; max-width: 780px; }
348
- .meta div { display: grid; grid-template-columns: 86px minmax(0, 1fr); gap: 16px; }
349
- dt { color: var(--faint); font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .12em; }
350
- dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
351
- .score-panel { border: 1px solid var(--line-strong); border-radius: 8px; padding: 24px; background: var(--surface); }
352
- .score-badge {
353
- display: inline-flex; margin-bottom: 14px; border: 1px solid rgba(147, 166, 218, .38);
354
- border-radius: 999px; padding: 5px 10px; color: var(--periwinkle);
355
- background: var(--periwinkle-weak); font-family: var(--mono); font-size: 12px;
356
- }
357
- .score { display: inline; font-family: var(--mono); font-size: 56px; line-height: 1; color: var(--teal); }
358
- .score-unit { display: inline; margin-left: 6px; color: var(--faint); font-family: var(--mono); }
359
- .verdict { margin-top: 12px; font-family: var(--serif); font-size: 30px; color: var(--periwinkle); }
360
- .insufficient-note { margin-top: 10px; color: var(--muted); font-size: 13px; line-height: 1.5; }
361
- .subscores { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; color: var(--muted); font-family: var(--mono); font-size: 12px; }
362
- .source-counts { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
363
- .trust-grid, .evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }
364
- .external-findings-section { margin-top: 18px; }
365
- .external-note { margin: 0 0 14px; color: var(--muted); font-size: 13px; }
366
- .criteria-column, .evidence-section { border: 1px solid var(--line); border-radius: 8px; background: var(--surface); padding: 22px; }
367
- h2 { font-family: var(--serif); font-size: 30px; font-weight: 400; letter-spacing: 0; margin-bottom: 18px; }
368
- .criteria-list { display: grid; gap: 12px; }
369
- .na-group { margin-top: 16px; padding-top: 14px; border-top: 1px dashed var(--line-strong); }
370
- .na-heading { font-family: var(--sans); font-size: 12px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); margin: 0 0 10px; }
371
- .na-list { display: grid; gap: 8px; font-size: 13px; }
372
- .na-list li { margin: 0; color: var(--muted); }
373
- .na-id { color: var(--faint); font-family: var(--mono); font-size: 11px; margin-right: 6px; }
374
- .na-reason { color: var(--muted); }
375
- .criterion { border: 1px solid var(--line); border-radius: 8px; padding: 16px; background: var(--surface-2); }
376
- .criterion-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; }
377
- .criterion-id { color: var(--periwinkle); font-family: var(--mono); font-size: 12px; margin-bottom: 4px; }
378
- h3 { font-size: 15px; line-height: 1.35; font-weight: 600; margin-bottom: 0; }
379
- .criterion-score { margin-top: 12px; color: var(--text); font-family: var(--mono); font-size: 24px; }
380
- .criterion-metrics { display: grid; gap: 7px; margin-top: 10px; padding: 0; list-style: none; font-size: 12px; }
381
- .criterion-metrics li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(238, 244, 247, .08); padding-bottom: 6px; }
382
- .criterion-metrics strong { color: var(--text); font-weight: 650; }
383
- .criterion-metrics span { white-space: nowrap; color: var(--muted); }
384
- .status { flex: none; border-radius: 999px; padding: 4px 9px; font-family: var(--mono); font-size: 11px; border: 1px solid var(--line-strong); }
385
- .status[data-status="verified"] { color: var(--teal); background: var(--teal-weak); border-color: rgba(47, 200, 166, .42); }
386
- .status[data-status="info"] { color: var(--periwinkle); background: var(--periwinkle-weak); border-color: rgba(147, 166, 218, .4); }
387
- .status[data-status="warning"] { color: var(--warn); background: var(--warn-weak); border-color: rgba(231, 191, 114, .44); }
388
- .status[data-status="critical"] { color: var(--danger); background: var(--danger-weak); border-color: rgba(240, 154, 143, .46); }
389
- .status[data-status="unverified"] { color: var(--muted); background: var(--quiet); }
390
- .status[data-status="insufficient_data"] { color: var(--muted); background: var(--quiet); border-style: dashed; }
391
- ul { margin: 0; padding-left: 18px; }
392
- li { margin: 8px 0; color: var(--muted); }
393
- .criterion-evidence { margin-top: 12px; font-size: 13px; }
394
- .evidence-section li strong { display: block; color: var(--text); font-size: 14px; }
395
- .evidence-section li span { display: block; margin-top: 2px; font-size: 13px; }
396
- code { color: var(--periwinkle); font-family: var(--mono); font-size: .92em; overflow-wrap: anywhere; }
397
- a { color: var(--teal); text-decoration: none; }
398
- a:hover { text-decoration: underline; text-underline-offset: 2px; }
399
- @media (max-width: 840px) {
400
- .certificate { padding: 28px 18px 48px; }
401
- .hero-grid, .trust-grid, .evidence-grid { grid-template-columns: 1fr; }
402
- h1 { font-size: 46px; }
403
- }
404
- `;
405
-
406
- // src/witan/badge.ts
407
- var VERDICT_COLOR = {
408
- Verified: "brightgreen",
409
- Conditional: "yellow",
410
- "At risk": "orange",
411
- Unverified: "red",
412
- "Insufficient source": "lightgrey"
413
- };
414
- var VERDICT_SVG_FILL = {
415
- Verified: "#2fc86a",
416
- Conditional: "#dfb92f",
417
- "At risk": "#e08a3c",
418
- Unverified: "#d64545",
419
- "Insufficient source": "#8c8c8c"
420
- };
421
- function badgeMessage(report, verdict) {
422
- if (report.insufficientSourceReason) return "unrated: no source";
423
- return `${formatScore2(report.overallScore)}/4.0 ${verdict.toLowerCase()}`;
424
- }
425
- function renderWitanBadgeEndpoint(report) {
426
- const verdict = renderReportVerdict(report);
427
- return {
428
- schemaVersion: 1,
429
- label: "cejel trust",
430
- message: badgeMessage(report, verdict),
431
- color: VERDICT_COLOR[verdict] ?? "lightgrey"
432
- };
433
- }
434
- function renderWitanBadgeSvg(report) {
435
- const verdict = renderReportVerdict(report);
436
- const label = "cejel trust";
437
- const message = badgeMessage(report, verdict);
438
- const fill = VERDICT_SVG_FILL[verdict] ?? "#8c8c8c";
439
- const charWidth = 6.5;
440
- const padding = 10;
441
- const labelWidth = Math.round(label.length * charWidth + padding);
442
- const messageWidth = Math.round(message.length * charWidth + padding);
443
- const totalWidth = labelWidth + messageWidth;
444
- const height = 20;
445
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${height}" role="img" aria-label="${escapeXml(label)}: ${escapeXml(message)}">
446
- <title>${escapeXml(label)}: ${escapeXml(message)}</title>
447
- <linearGradient id="witan-badge-smooth" x2="0" y2="100%">
448
- <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
449
- <stop offset="1" stop-opacity=".1"/>
450
- </linearGradient>
451
- <clipPath id="witan-badge-round">
452
- <rect width="${totalWidth}" height="${height}" rx="3" fill="#fff"/>
453
- </clipPath>
454
- <g clip-path="url(#witan-badge-round)">
455
- <rect width="${labelWidth}" height="${height}" fill="#555"/>
456
- <rect x="${labelWidth}" width="${messageWidth}" height="${height}" fill="${fill}"/>
457
- <rect width="${totalWidth}" height="${height}" fill="url(#witan-badge-smooth)"/>
458
- </g>
459
- <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
460
- <text x="${labelWidth / 2}" y="14">${escapeXml(label)}</text>
461
- <text x="${labelWidth + messageWidth / 2}" y="14">${escapeXml(message)}</text>
462
- </g>
463
- </svg>
464
- `;
465
- }
466
- function formatScore2(score) {
467
- return score.toFixed(1);
468
- }
469
- function escapeXml(value) {
470
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
471
- }
472
-
473
- // src/witan/attestation.ts
474
- import { createHash } from "crypto";
475
-
476
32
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
477
33
  var external_exports = {};
478
34
  __export(external_exports, {
@@ -4686,30 +4242,70 @@ var WitanConsumedSignalSummarySchema = external_exports.object({
4686
4242
  // this field always carries every finding.
4687
4243
  findings: external_exports.array(WitanInputSignalFindingSchema).default([])
4688
4244
  }).strict();
4689
- var WitanReportSchema = external_exports.object({
4245
+ var WitanScoredVerdictSchema = external_exports.enum([
4246
+ "verified",
4247
+ "conditional",
4248
+ "at_risk",
4249
+ "unverified"
4250
+ ]);
4251
+ var WitanReportVerdictSchema = external_exports.union([
4252
+ WitanScoredVerdictSchema,
4253
+ external_exports.literal("insufficient_source")
4254
+ ]);
4255
+ function witanVerdictForScore(score) {
4256
+ if (score >= 3.5) return "verified";
4257
+ if (score >= 2.5) return "conditional";
4258
+ if (score >= 1.5) return "at_risk";
4259
+ return "unverified";
4260
+ }
4261
+ var WitanReportCommonSchema = external_exports.object({
4690
4262
  productSlug: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/),
4691
4263
  productDisplayName: external_exports.string().min(1).max(120),
4692
4264
  repo: WitanRepoRefSchema,
4693
4265
  generatedAt: external_exports.string().datetime({ offset: true }),
4694
4266
  rubricVersion: external_exports.string().min(1).max(120),
4695
- codeTrustScore: external_exports.number().min(0).max(4),
4696
- processTrustScore: external_exports.number().min(0).max(4),
4697
- overallScore: external_exports.number().min(0).max(4),
4698
4267
  // Rubric-driven: length matches whatever rubric produced this report, not a fixed enum.
4699
4268
  criteria: external_exports.array(WitanCriterionScoreSchema).min(1),
4700
4269
  consumedSignals: external_exports.array(WitanConsumedSignalSummarySchema).optional(),
4270
+ archetype: WitanRepoArchetypeSchema.optional()
4271
+ });
4272
+ var WitanScoredReportSchema = WitanReportCommonSchema.extend({
4273
+ verdict: WitanScoredVerdictSchema,
4274
+ codeTrustScore: external_exports.number().min(0).max(4),
4275
+ processTrustScore: external_exports.number().min(0).max(4),
4276
+ overallScore: external_exports.number().min(0).max(4),
4701
4277
  // Present only when the rubric has more than two criterion categories — the legacy
4702
4278
  // codeTrustScore/processTrustScore pair can't losslessly represent 3+ category buckets.
4703
4279
  categoryScores: external_exports.record(external_exports.string(), external_exports.number().min(0).max(4)).optional(),
4704
- // Repo-archetype metadata (see WitanRepoArchetypeSchema above). Both fields are omitted for
4705
- // callers that don't classify an archetype (e.g. the trading rubric). insufficientSourceReason
4706
- // is present only for the non-source archetypes ('docs_only' | 'binary_only' |
4707
- // 'unrecognised_ecosystem' | 'empty') — its presence is what presentation layers
4708
- // (badge/terminal/verdict) key off to show an explicit insufficient-source verdict instead of
4709
- // a confident numeric score.
4710
- archetype: WitanRepoArchetypeSchema.optional(),
4711
- insufficientSourceReason: external_exports.string().min(1).max(2e3).optional()
4280
+ insufficientSourceReason: external_exports.never().optional()
4712
4281
  }).strict();
4282
+ var WitanAbstainedReportSchema = WitanReportCommonSchema.extend({
4283
+ verdict: external_exports.literal("insufficient_source"),
4284
+ // Null is deliberate and explicit: omission is ambiguous, while zero is a real score.
4285
+ codeTrustScore: external_exports.null(),
4286
+ processTrustScore: external_exports.null(),
4287
+ overallScore: external_exports.null(),
4288
+ categoryScores: external_exports.never().optional(),
4289
+ insufficientSourceReason: external_exports.string().min(1).max(2e3)
4290
+ }).strict();
4291
+ var StrictWitanReportSchema = external_exports.union([WitanScoredReportSchema, WitanAbstainedReportSchema]).superRefine((report, context) => {
4292
+ if (report.verdict !== "insufficient_source" && report.verdict !== witanVerdictForScore(report.overallScore)) {
4293
+ context.addIssue({
4294
+ code: external_exports.ZodIssueCode.custom,
4295
+ path: ["verdict"],
4296
+ message: `verdict does not match overallScore ${report.overallScore}`
4297
+ });
4298
+ }
4299
+ });
4300
+ var WitanReportSchema = external_exports.preprocess((value) => {
4301
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && !("verdict" in value) && !("insufficientSourceReason" in value) && typeof value.overallScore === "number") {
4302
+ return {
4303
+ ...value,
4304
+ verdict: witanVerdictForScore(value.overallScore)
4305
+ };
4306
+ }
4307
+ return value;
4308
+ }, StrictWitanReportSchema);
4713
4309
  var WITAN_ATTESTATION_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
4714
4310
  var WITAN_ATTESTATION_PREDICATE_TYPE = "https://cejel.dev/attestations/scan/v1";
4715
4311
  var WitanAttestationOutcomeSchema = external_exports.discriminatedUnion("status", [
@@ -4761,7 +4357,459 @@ var WitanAttestationStatementSchema = external_exports.object({
4761
4357
  }).strict()
4762
4358
  }).strict();
4763
4359
 
4360
+ // src/witan/external-findings.ts
4361
+ var EXTERNAL_FINDINGS_DISPLAY_LIMIT = 10;
4362
+ var SEVERITY_RANK = {
4363
+ critical: 0,
4364
+ warning: 1,
4365
+ info: 2
4366
+ };
4367
+ function formatExternalSourceLabel(source) {
4368
+ return source.startsWith("sarif:") ? source.slice("sarif:".length) : source;
4369
+ }
4370
+ function summarizeExternalSources(consumedSignals) {
4371
+ const bySource = /* @__PURE__ */ new Map();
4372
+ for (const signal of consumedSignals) {
4373
+ const entry = bySource.get(signal.source) ?? { findingCount: 0, dimensions: /* @__PURE__ */ new Set() };
4374
+ entry.findingCount += signal.findingCount;
4375
+ entry.dimensions.add(signal.dimension);
4376
+ bySource.set(signal.source, entry);
4377
+ }
4378
+ return Array.from(bySource.entries()).map(([source, { findingCount, dimensions }]) => ({
4379
+ source,
4380
+ label: formatExternalSourceLabel(source),
4381
+ findingCount,
4382
+ dimensions: Array.from(dimensions).sort()
4383
+ })).sort((a, b) => a.source.localeCompare(b.source));
4384
+ }
4385
+ function formatExternalSourceLine(summary) {
4386
+ const noun = summary.findingCount === 1 ? "finding" : "findings";
4387
+ return `${summary.label}: ${summary.findingCount} ${noun} ingested (folded into ${summary.dimensions.join(", ")})`;
4388
+ }
4389
+ function collectExternalFindings(consumedSignals) {
4390
+ const findings = [];
4391
+ for (const signal of consumedSignals) {
4392
+ for (const finding of signal.findings) {
4393
+ findings.push({
4394
+ source: signal.source,
4395
+ label: formatExternalSourceLabel(signal.source),
4396
+ dimension: signal.dimension,
4397
+ severity: finding.severity,
4398
+ ruleId: finding.ruleId,
4399
+ message: finding.message,
4400
+ ...finding.location ? { location: finding.location } : {}
4401
+ });
4402
+ }
4403
+ }
4404
+ return findings.sort((a, b) => {
4405
+ const bySeverity = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
4406
+ if (bySeverity !== 0) return bySeverity;
4407
+ const bySource = a.source.localeCompare(b.source);
4408
+ if (bySource !== 0) return bySource;
4409
+ return a.ruleId.localeCompare(b.ruleId);
4410
+ });
4411
+ }
4412
+
4413
+ // src/witan/html.ts
4414
+ function renderWitanHtmlReport(report) {
4415
+ const codeCriteria = criteriaByCategory(report, "code_trust");
4416
+ const processCriteria = criteriaByCategory(report, "process_trust");
4417
+ const verifiedEvidence = report.criteria.flatMap(
4418
+ (criterion) => criterion.status === "verified" ? criterion.evidence.map((evidence) => renderEvidenceListItem(criterion, evidence)) : []
4419
+ );
4420
+ const openItems = report.criteria.flatMap(renderOpenItems);
4421
+ const contributingSources = renderContributingSources(report.consumedSignals ?? []);
4422
+ const externalSourceSummaries = summarizeExternalSources(report.consumedSignals ?? []);
4423
+ const externalFindings = collectExternalFindings(report.consumedSignals ?? []);
4424
+ return `<!doctype html>
4425
+ <html lang="en">
4426
+ <head>
4427
+ <meta charset="utf-8">
4428
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4429
+ <title>Cejel Trust Certificate - ${escapeHtml(report.productDisplayName)}</title>
4430
+ <style>${CERTIFICATE_CSS}</style>
4431
+ </head>
4432
+ <body>
4433
+ <main class="certificate">
4434
+ <header class="hero">
4435
+ <div class="brand">
4436
+ <span class="brand-mark" aria-hidden="true">C</span>
4437
+ <span class="brand-name">Cejel</span>
4438
+ </div>
4439
+ <div class="hero-grid">
4440
+ <section>
4441
+ <p class="eyebrow">Trust Certificate</p>
4442
+ <h1>${escapeHtml(report.productDisplayName)}</h1>
4443
+ <dl class="meta">
4444
+ <div><dt>Date</dt><dd>${escapeHtml(formatDate(report.generatedAt))}</dd></div>
4445
+ <div><dt>Run</dt><dd>${escapeHtml(renderRepo(report))}</dd></div>
4446
+ <div><dt>Rubric</dt><dd>${escapeHtml(report.rubricVersion)}</dd></div>
4447
+ ${contributingSources.length > 0 ? `<div><dt>Sources</dt><dd>Incorporates findings from: ${escapeHtml(contributingSources.join(", "))}<ul class="source-counts">${externalSourceSummaries.map((s) => `<li>${escapeHtml(formatExternalSourceLine(s))}</li>`).join("")}</ul></dd></div>` : ""}
4448
+ </dl>
4449
+ </section>
4450
+ <aside class="score-panel" aria-label="Summary trust score">
4451
+ <div class="score-badge">Summary</div>
4452
+ ${report.verdict === "insufficient_source" ? `<div class="verdict">Insufficient source</div>
4453
+ <p class="insufficient-note">${escapeHtml(report.insufficientSourceReason)}</p>` : `<div class="score">${formatScore(report.overallScore)}</div>
4454
+ <div class="score-unit">/ 4.0 overall</div>
4455
+ <div class="verdict">${escapeHtml(renderVerdict(report.overallScore))}</div>
4456
+ <div class="subscores">
4457
+ <span>Code ${formatScore(report.codeTrustScore)}</span>
4458
+ <span>Process ${formatScore(report.processTrustScore)}</span>
4459
+ </div>`}
4460
+ </aside>
4461
+ </div>
4462
+ </header>
4463
+
4464
+ <section class="trust-grid" aria-label="Rubric criteria">
4465
+ ${renderCriterionColumn("Code trust", codeCriteria)}
4466
+ ${renderCriterionColumn("Process trust", processCriteria)}
4467
+ </section>
4468
+
4469
+ <section class="evidence-grid" aria-label="Evidence and gaps">
4470
+ <article class="evidence-section">
4471
+ <h2>Verified evidence</h2>
4472
+ <ul>${verifiedEvidence.length > 0 ? verifiedEvidence.join("") : "<li>No verified evidence supplied.</li>"}</ul>
4473
+ </article>
4474
+ <article class="evidence-section">
4475
+ <h2>Open / unverified</h2>
4476
+ <ul>${openItems.length > 0 ? openItems.join("") : "<li>No open or unverified criteria supplied.</li>"}</ul>
4477
+ </article>
4478
+ </section>
4479
+
4480
+ ${externalFindings.length > 0 ? `<section class="external-findings-section" aria-label="External findings">
4481
+ <article class="evidence-section">
4482
+ <h2>External findings</h2>
4483
+ <p class="external-note">Findings surfaced by ingested external scanners, attributed to the tool and the cejel criterion they were folded into \u2014 distinct from cejel's own findings above.</p>
4484
+ <ul>${renderExternalFindingItems(externalFindings).join("")}</ul>
4485
+ </article>
4486
+ </section>` : ""}
4487
+ </main>
4488
+ </body>
4489
+ </html>
4490
+ `;
4491
+ }
4492
+ function renderContributingSources(signals) {
4493
+ return Array.from(new Set(signals.map((s) => s.source))).sort();
4494
+ }
4495
+ function renderExternalFindingItems(findings) {
4496
+ const shown = findings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT);
4497
+ const items = shown.map(
4498
+ (f) => `<li><strong>${escapeHtml(f.label)} \u2192 ${escapeHtml(f.dimension)}</strong><span>${escapeHtml(f.severity)}: ${escapeHtml(f.ruleId)} \u2014 ${escapeHtml(f.message)}${f.location ? ` (${escapeHtml(f.location)})` : ""}</span></li>`
4499
+ );
4500
+ const remaining = findings.length - shown.length;
4501
+ if (remaining > 0) {
4502
+ items.push(`<li><em>...and ${remaining} more \u2014 see report.json for the full list.</em></li>`);
4503
+ }
4504
+ return items;
4505
+ }
4506
+ function criteriaByCategory(report, category) {
4507
+ return report.criteria.filter((criterion) => criterion.category === category);
4508
+ }
4509
+ function renderCriterionColumn(title, criteria) {
4510
+ const applicable = criteria.filter((criterion) => criterion.status !== "not_applicable");
4511
+ const notApplicable = criteria.filter((criterion) => criterion.status === "not_applicable");
4512
+ return `<section class="criteria-column">
4513
+ <h2>${escapeHtml(title)}</h2>
4514
+ <div class="criteria-list">
4515
+ ${applicable.map(renderCriterionCard).join("")}
4516
+ </div>
4517
+ ${notApplicable.length > 0 ? renderNotApplicableGroup(notApplicable) : ""}
4518
+ </section>`;
4519
+ }
4520
+ function renderNotApplicableGroup(criteria) {
4521
+ return `<div class="na-group">
4522
+ <h3 class="na-heading">Not applicable to this repository</h3>
4523
+ <ul class="na-list">
4524
+ ${criteria.map(renderNotApplicableItem).join("")}
4525
+ </ul>
4526
+ </div>`;
4527
+ }
4528
+ function renderNotApplicableItem(criterion) {
4529
+ const reason = criterion.notes ?? "Not applicable to this repository.";
4530
+ return `<li><span class="na-id">${escapeHtml(criterion.id)}</span> ${escapeHtml(criterion.title)} \u2014 <span class="na-reason">${escapeHtml(reason)}</span></li>`;
4531
+ }
4532
+ function renderCriterionCard(criterion) {
4533
+ const evidence = [
4534
+ ...criterion.evidence.map(renderEvidencePointer),
4535
+ ...criterion.findings.map((finding) => renderFindingEvidence(finding))
4536
+ ];
4537
+ const metrics = criterion.metrics.map(renderMetric);
4538
+ return `<article class="criterion">
4539
+ <div class="criterion-top">
4540
+ <div>
4541
+ <div class="criterion-id">${escapeHtml(criterion.id)}</div>
4542
+ <h3>${escapeHtml(criterion.title)}</h3>
4543
+ </div>
4544
+ ${renderStatusChip(criterion.status)}
4545
+ </div>
4546
+ <div class="criterion-score">${criterion.status === "not_applicable" ? "N/A" : criterion.status === "insufficient_data" ? "No data" : formatScore(criterion.score)}</div>
4547
+ <ul class="criterion-metrics">
4548
+ ${metrics.length > 0 ? metrics.map((item) => `<li>${item}</li>`).join("") : "<li>No measured depth metrics supplied.</li>"}
4549
+ </ul>
4550
+ <ul class="criterion-evidence">
4551
+ ${evidence.length > 0 ? evidence.map((item) => `<li>${item}</li>`).join("") : "<li>No concrete evidence supplied.</li>"}
4552
+ </ul>
4553
+ </article>`;
4554
+ }
4555
+ function renderMetric(metric2) {
4556
+ const max = metric2.max ? `/${formatMetricValue(metric2.max)}` : "";
4557
+ const unit = metric2.unit ? ` ${escapeHtml(metric2.unit)}` : "";
4558
+ return `<strong>${escapeHtml(metric2.label)}</strong><span>${formatMetricValue(metric2.value)}${max}${unit}</span>`;
4559
+ }
4560
+ function renderStatusChip(status) {
4561
+ return `<span class="status" data-status="${status}">${status}</span>`;
4562
+ }
4563
+ function renderEvidenceListItem(criterion, evidence) {
4564
+ return `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${renderEvidencePointer(evidence)}</span></li>`;
4565
+ }
4566
+ function renderOpenItems(criterion) {
4567
+ const findingItems = criterion.findings.map(
4568
+ (finding) => `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${escapeHtml(finding.severity)}: ${escapeHtml(finding.summary)} (${renderEvidencePointer(finding.evidence)})</span></li>`
4569
+ );
4570
+ if ((criterion.status === "unverified" || criterion.status === "insufficient_data") && criterion.evidence.length === 0 && criterion.findings.length === 0) {
4571
+ const message = criterion.status === "insufficient_data" ? "Insufficient data \u2014 no measurable signal; excluded from composite (unmeasured, not inapplicable)." : "No concrete evidence supplied.";
4572
+ return [
4573
+ `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${escapeHtml(message)}</span></li>`
4574
+ ];
4575
+ }
4576
+ if (criterion.status === "verified") return [];
4577
+ return findingItems;
4578
+ }
4579
+ function renderFindingEvidence(finding) {
4580
+ return `${escapeHtml(finding.severity)}: ${escapeHtml(finding.summary)} (${renderEvidencePointer(
4581
+ finding.evidence
4582
+ )})`;
4583
+ }
4584
+ function renderEvidencePointer(evidence) {
4585
+ const target = evidence.url ?? renderPathEvidenceTarget(evidence);
4586
+ const label = escapeHtml(evidence.label);
4587
+ if (evidence.url) {
4588
+ return `<a href="${escapeAttribute(evidence.url)}">${label}</a>`;
4589
+ }
4590
+ return [label, target ? `<code>${escapeHtml(target)}</code>` : void 0].filter(Boolean).join(" ");
4591
+ }
4592
+ function renderPathEvidenceTarget(evidence) {
4593
+ const pathTarget = evidence.path ? `${evidence.path}${evidence.line ? `:${evidence.line}` : ""}` : void 0;
4594
+ const hashTarget = evidence.contentHash ? `sha256:${evidence.contentHash.slice(0, 12)}` : void 0;
4595
+ return [pathTarget, hashTarget].filter(Boolean).join(" \xB7 ");
4596
+ }
4597
+ function renderRepo(report) {
4598
+ const repo = report.repo.path ?? report.repo.url ?? "unknown";
4599
+ return report.repo.headSha ? `${repo} @ ${report.repo.headSha}` : repo;
4600
+ }
4601
+ function renderVerdict(score) {
4602
+ return renderMachineVerdict(witanVerdictForScore(score));
4603
+ }
4604
+ function renderReportVerdict(report) {
4605
+ return renderMachineVerdict(report.verdict);
4606
+ }
4607
+ function renderMachineVerdict(verdict) {
4608
+ switch (verdict) {
4609
+ case "verified":
4610
+ return "Verified";
4611
+ case "conditional":
4612
+ return "Conditional";
4613
+ case "at_risk":
4614
+ return "At risk";
4615
+ case "unverified":
4616
+ return "Unverified";
4617
+ case "insufficient_source":
4618
+ return "Insufficient source";
4619
+ }
4620
+ }
4621
+ function formatScore(score) {
4622
+ return score.toFixed(1);
4623
+ }
4624
+ function formatMetricValue(value) {
4625
+ return Number.isInteger(value) ? value.toString() : value.toFixed(2);
4626
+ }
4627
+ function formatDate(value) {
4628
+ return value.slice(0, 10);
4629
+ }
4630
+ function escapeHtml(value) {
4631
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
4632
+ }
4633
+ function escapeAttribute(value) {
4634
+ return escapeHtml(value);
4635
+ }
4636
+ var CERTIFICATE_CSS = `
4637
+ :root {
4638
+ color-scheme: dark;
4639
+ --steel: #16212a;
4640
+ --surface: #1d2b35;
4641
+ --surface-2: #273946;
4642
+ --line: rgba(238, 244, 247, .13);
4643
+ --line-strong: rgba(238, 244, 247, .22);
4644
+ --text: #eef4f7;
4645
+ --muted: #b7c5cc;
4646
+ --faint: #8799a4;
4647
+ --teal: #2fc8a6;
4648
+ --teal-weak: rgba(47, 200, 166, .14);
4649
+ --periwinkle: #93a6da;
4650
+ --periwinkle-weak: rgba(147, 166, 218, .16);
4651
+ --warn: #e7bf72;
4652
+ --warn-weak: rgba(231, 191, 114, .16);
4653
+ --danger: #f09a8f;
4654
+ --danger-weak: rgba(240, 154, 143, .14);
4655
+ --quiet: rgba(238, 244, 247, .08);
4656
+ --serif: "Instrument Serif", Georgia, "Times New Roman", serif;
4657
+ --sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
4658
+ --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
4659
+ }
4660
+ * { box-sizing: border-box; }
4661
+ body {
4662
+ margin: 0;
4663
+ background: var(--steel);
4664
+ color: var(--text);
4665
+ font-family: var(--sans);
4666
+ line-height: 1.55;
4667
+ -webkit-font-smoothing: antialiased;
4668
+ }
4669
+ .certificate { max-width: 1120px; margin: 0 auto; padding: 44px 32px 64px; }
4670
+ .hero { border-bottom: 1px solid var(--line); padding-bottom: 38px; }
4671
+ .brand { display: flex; align-items: center; gap: 12px; margin-bottom: 44px; }
4672
+ .brand-mark {
4673
+ display: inline-flex; align-items: center; justify-content: center;
4674
+ width: 34px; height: 34px; border-radius: 8px;
4675
+ background: var(--teal); color: #06231d; font-family: var(--serif); font-size: 24px;
4676
+ }
4677
+ .brand-name { font-family: var(--serif); font-size: 24px; }
4678
+ .hero-grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 36px; align-items: end; }
4679
+ .eyebrow {
4680
+ margin: 0 0 14px; color: var(--teal); font-family: var(--mono);
4681
+ font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
4682
+ }
4683
+ h1, h2, h3, p { margin-top: 0; }
4684
+ h1 { margin-bottom: 24px; font-family: var(--serif); font-size: 64px; line-height: 1; font-weight: 400; letter-spacing: 0; }
4685
+ .meta { display: grid; gap: 10px; margin: 0; max-width: 780px; }
4686
+ .meta div { display: grid; grid-template-columns: 86px minmax(0, 1fr); gap: 16px; }
4687
+ dt { color: var(--faint); font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .12em; }
4688
+ dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
4689
+ .score-panel { border: 1px solid var(--line-strong); border-radius: 8px; padding: 24px; background: var(--surface); }
4690
+ .score-badge {
4691
+ display: inline-flex; margin-bottom: 14px; border: 1px solid rgba(147, 166, 218, .38);
4692
+ border-radius: 999px; padding: 5px 10px; color: var(--periwinkle);
4693
+ background: var(--periwinkle-weak); font-family: var(--mono); font-size: 12px;
4694
+ }
4695
+ .score { display: inline; font-family: var(--mono); font-size: 56px; line-height: 1; color: var(--teal); }
4696
+ .score-unit { display: inline; margin-left: 6px; color: var(--faint); font-family: var(--mono); }
4697
+ .verdict { margin-top: 12px; font-family: var(--serif); font-size: 30px; color: var(--periwinkle); }
4698
+ .insufficient-note { margin-top: 10px; color: var(--muted); font-size: 13px; line-height: 1.5; }
4699
+ .subscores { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; color: var(--muted); font-family: var(--mono); font-size: 12px; }
4700
+ .source-counts { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
4701
+ .trust-grid, .evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }
4702
+ .external-findings-section { margin-top: 18px; }
4703
+ .external-note { margin: 0 0 14px; color: var(--muted); font-size: 13px; }
4704
+ .criteria-column, .evidence-section { border: 1px solid var(--line); border-radius: 8px; background: var(--surface); padding: 22px; }
4705
+ h2 { font-family: var(--serif); font-size: 30px; font-weight: 400; letter-spacing: 0; margin-bottom: 18px; }
4706
+ .criteria-list { display: grid; gap: 12px; }
4707
+ .na-group { margin-top: 16px; padding-top: 14px; border-top: 1px dashed var(--line-strong); }
4708
+ .na-heading { font-family: var(--sans); font-size: 12px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); margin: 0 0 10px; }
4709
+ .na-list { display: grid; gap: 8px; font-size: 13px; }
4710
+ .na-list li { margin: 0; color: var(--muted); }
4711
+ .na-id { color: var(--faint); font-family: var(--mono); font-size: 11px; margin-right: 6px; }
4712
+ .na-reason { color: var(--muted); }
4713
+ .criterion { border: 1px solid var(--line); border-radius: 8px; padding: 16px; background: var(--surface-2); }
4714
+ .criterion-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; }
4715
+ .criterion-id { color: var(--periwinkle); font-family: var(--mono); font-size: 12px; margin-bottom: 4px; }
4716
+ h3 { font-size: 15px; line-height: 1.35; font-weight: 600; margin-bottom: 0; }
4717
+ .criterion-score { margin-top: 12px; color: var(--text); font-family: var(--mono); font-size: 24px; }
4718
+ .criterion-metrics { display: grid; gap: 7px; margin-top: 10px; padding: 0; list-style: none; font-size: 12px; }
4719
+ .criterion-metrics li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(238, 244, 247, .08); padding-bottom: 6px; }
4720
+ .criterion-metrics strong { color: var(--text); font-weight: 650; }
4721
+ .criterion-metrics span { white-space: nowrap; color: var(--muted); }
4722
+ .status { flex: none; border-radius: 999px; padding: 4px 9px; font-family: var(--mono); font-size: 11px; border: 1px solid var(--line-strong); }
4723
+ .status[data-status="verified"] { color: var(--teal); background: var(--teal-weak); border-color: rgba(47, 200, 166, .42); }
4724
+ .status[data-status="info"] { color: var(--periwinkle); background: var(--periwinkle-weak); border-color: rgba(147, 166, 218, .4); }
4725
+ .status[data-status="warning"] { color: var(--warn); background: var(--warn-weak); border-color: rgba(231, 191, 114, .44); }
4726
+ .status[data-status="critical"] { color: var(--danger); background: var(--danger-weak); border-color: rgba(240, 154, 143, .46); }
4727
+ .status[data-status="unverified"] { color: var(--muted); background: var(--quiet); }
4728
+ .status[data-status="insufficient_data"] { color: var(--muted); background: var(--quiet); border-style: dashed; }
4729
+ ul { margin: 0; padding-left: 18px; }
4730
+ li { margin: 8px 0; color: var(--muted); }
4731
+ .criterion-evidence { margin-top: 12px; font-size: 13px; }
4732
+ .evidence-section li strong { display: block; color: var(--text); font-size: 14px; }
4733
+ .evidence-section li span { display: block; margin-top: 2px; font-size: 13px; }
4734
+ code { color: var(--periwinkle); font-family: var(--mono); font-size: .92em; overflow-wrap: anywhere; }
4735
+ a { color: var(--teal); text-decoration: none; }
4736
+ a:hover { text-decoration: underline; text-underline-offset: 2px; }
4737
+ @media (max-width: 840px) {
4738
+ .certificate { padding: 28px 18px 48px; }
4739
+ .hero-grid, .trust-grid, .evidence-grid { grid-template-columns: 1fr; }
4740
+ h1 { font-size: 46px; }
4741
+ }
4742
+ `;
4743
+
4744
+ // src/witan/badge.ts
4745
+ var VERDICT_COLOR = {
4746
+ Verified: "brightgreen",
4747
+ Conditional: "yellow",
4748
+ "At risk": "orange",
4749
+ Unverified: "red",
4750
+ "Insufficient source": "lightgrey"
4751
+ };
4752
+ var VERDICT_SVG_FILL = {
4753
+ Verified: "#2fc86a",
4754
+ Conditional: "#dfb92f",
4755
+ "At risk": "#e08a3c",
4756
+ Unverified: "#d64545",
4757
+ "Insufficient source": "#8c8c8c"
4758
+ };
4759
+ function badgeMessage(report, verdict) {
4760
+ if (report.verdict === "insufficient_source") return "unrated: no source";
4761
+ return `${formatScore2(report.overallScore)}/4.0 ${verdict.toLowerCase()}`;
4762
+ }
4763
+ function renderWitanBadgeEndpoint(report) {
4764
+ const verdict = renderReportVerdict(report);
4765
+ return {
4766
+ schemaVersion: 1,
4767
+ label: "cejel trust",
4768
+ message: badgeMessage(report, verdict),
4769
+ color: VERDICT_COLOR[verdict] ?? "lightgrey"
4770
+ };
4771
+ }
4772
+ function renderWitanBadgeSvg(report) {
4773
+ const verdict = renderReportVerdict(report);
4774
+ const label = "cejel trust";
4775
+ const message = badgeMessage(report, verdict);
4776
+ const fill = VERDICT_SVG_FILL[verdict] ?? "#8c8c8c";
4777
+ const charWidth = 6.5;
4778
+ const padding = 10;
4779
+ const labelWidth = Math.round(label.length * charWidth + padding);
4780
+ const messageWidth = Math.round(message.length * charWidth + padding);
4781
+ const totalWidth = labelWidth + messageWidth;
4782
+ const height = 20;
4783
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${height}" role="img" aria-label="${escapeXml(label)}: ${escapeXml(message)}">
4784
+ <title>${escapeXml(label)}: ${escapeXml(message)}</title>
4785
+ <linearGradient id="witan-badge-smooth" x2="0" y2="100%">
4786
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
4787
+ <stop offset="1" stop-opacity=".1"/>
4788
+ </linearGradient>
4789
+ <clipPath id="witan-badge-round">
4790
+ <rect width="${totalWidth}" height="${height}" rx="3" fill="#fff"/>
4791
+ </clipPath>
4792
+ <g clip-path="url(#witan-badge-round)">
4793
+ <rect width="${labelWidth}" height="${height}" fill="#555"/>
4794
+ <rect x="${labelWidth}" width="${messageWidth}" height="${height}" fill="${fill}"/>
4795
+ <rect width="${totalWidth}" height="${height}" fill="url(#witan-badge-smooth)"/>
4796
+ </g>
4797
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
4798
+ <text x="${labelWidth / 2}" y="14">${escapeXml(label)}</text>
4799
+ <text x="${labelWidth + messageWidth / 2}" y="14">${escapeXml(message)}</text>
4800
+ </g>
4801
+ </svg>
4802
+ `;
4803
+ }
4804
+ function formatScore2(score) {
4805
+ return score.toFixed(1);
4806
+ }
4807
+ function escapeXml(value) {
4808
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4809
+ }
4810
+
4764
4811
  // src/witan/attestation.ts
4812
+ import { createHash } from "crypto";
4765
4813
  var ATTESTATION_LIMITATIONS = [
4766
4814
  "Cejel measures observable repository signals; it is not a security or compliance guarantee.",
4767
4815
  "This statement is self-generated and unsigned until an external signer binds their identity to it."
@@ -4774,7 +4822,7 @@ function hashWitanReport(report) {
4774
4822
  }
4775
4823
  function createWitanAttestation(report, options) {
4776
4824
  const reportSha256 = hashWitanReport(report);
4777
- const outcome = report.insufficientSourceReason ? {
4825
+ const outcome = report.verdict === "insufficient_source" ? {
4778
4826
  status: "abstained",
4779
4827
  reason: report.insufficientSourceReason
4780
4828
  } : {
@@ -7755,20 +7803,22 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
7755
7803
  const overallScore = roundScore(
7756
7804
  categoryOrder.reduce((sum, category) => sum + (categoryScoreMap[category] ?? 0), 0) / Math.max(categoryOrder.length, 1)
7757
7805
  );
7806
+ const abstained = parsedInput.insufficientSourceReason != null;
7758
7807
  return WitanReportSchema.parse({
7759
7808
  productSlug: parsedInput.productSlug,
7760
7809
  productDisplayName: parsedInput.productDisplayName,
7761
7810
  repo: parsedInput.repo,
7762
7811
  generatedAt: parsedInput.generatedAt,
7763
7812
  rubricVersion: parsedInput.rubricVersion,
7764
- codeTrustScore,
7765
- processTrustScore,
7766
- overallScore,
7813
+ verdict: abstained ? "insufficient_source" : witanVerdictForScore(overallScore),
7814
+ codeTrustScore: abstained ? null : codeTrustScore,
7815
+ processTrustScore: abstained ? null : processTrustScore,
7816
+ overallScore: abstained ? null : overallScore,
7767
7817
  criteria,
7768
7818
  ...consumedSignals.length > 0 ? { consumedSignals } : {},
7769
7819
  // Only surface the full per-category map for rubrics with more than two buckets —
7770
7820
  // the default two-category rubric is fully represented by codeTrustScore/processTrustScore.
7771
- ...categoryOrder.length > 2 ? { categoryScores: categoryScoreMap } : {},
7821
+ ...!abstained && categoryOrder.length > 2 ? { categoryScores: categoryScoreMap } : {},
7772
7822
  // Pass through repo-archetype metadata verbatim — createWitanReport does not compute or
7773
7823
  // alter it (see classifyRepoArchetype in repo-signals.ts); per-criterion scoring above is
7774
7824
  // unaffected by archetype, only presentation layers (badge/terminal/verdict) key off it.
@@ -8084,21 +8134,33 @@ function buildWitanCliSummary(report) {
8084
8134
  ).sort();
8085
8135
  const externalSources = summarizeExternalSources(report.consumedSignals ?? []);
8086
8136
  const allExternalFindings = collectExternalFindings(report.consumedSignals ?? []);
8087
- return {
8137
+ const common = {
8088
8138
  productSlug: report.productSlug,
8089
8139
  productDisplayName: report.productDisplayName,
8090
8140
  generatedAt: report.generatedAt,
8091
- overallScore: report.overallScore,
8092
- codeTrustScore: report.codeTrustScore,
8093
- processTrustScore: report.processTrustScore,
8094
- verdict: renderReportVerdict(report),
8095
8141
  findingCount: allFindings.length,
8096
8142
  topFindings: sorted.slice(0, TOP_FINDINGS_LIMIT),
8097
8143
  contributingSources,
8098
8144
  externalSources,
8099
8145
  externalFindingCount: allExternalFindings.length,
8100
- topExternalFindings: allExternalFindings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT),
8101
- ...report.insufficientSourceReason ? { insufficientSourceReason: report.insufficientSourceReason } : {}
8146
+ topExternalFindings: allExternalFindings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT)
8147
+ };
8148
+ if (report.verdict === "insufficient_source") {
8149
+ return {
8150
+ ...common,
8151
+ overallScore: null,
8152
+ codeTrustScore: null,
8153
+ processTrustScore: null,
8154
+ verdict: "Insufficient source",
8155
+ insufficientSourceReason: report.insufficientSourceReason
8156
+ };
8157
+ }
8158
+ return {
8159
+ ...common,
8160
+ overallScore: report.overallScore,
8161
+ codeTrustScore: report.codeTrustScore,
8162
+ processTrustScore: report.processTrustScore,
8163
+ verdict: renderReportVerdict(report)
8102
8164
  };
8103
8165
  }
8104
8166