@cejel/cejel 0.1.4 → 0.1.5

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/README.md CHANGED
@@ -20,6 +20,10 @@ certificate + badge over all of them. See "Aggregate your scanners" below.
20
20
  > monorepo — the ten-product studio it was built inside — which it currently scores
21
21
  > 3.1/4.0 ("Conditional"). We score ourselves before asking you to score yourself.
22
22
 
23
+ The reusable Barg Labs listing mark is kept at [`brand/barg-icon.png`](./brand/barg-icon.png)
24
+ for directories and registries that accept a custom image. GitHub Actions Marketplace uses
25
+ GitHub's predefined `action.yml` branding icons instead.
26
+
23
27
  ## Install
24
28
 
25
29
  No account, no key, no signup.
@@ -27,7 +31,15 @@ No account, no key, no signup.
27
31
  **Single-file binary.** One file. No Node, no npm, no `node_modules`, nothing installed.
28
32
 
29
33
  ```bash
30
- curl -fsSL https://github.com/BargLabs/cejel/releases/latest/download/cejel-$(uname -s)-$(uname -m) -o cejel
34
+ asset="cejel-$(uname -s)-$(uname -m)"
35
+ curl -fsSL "https://github.com/BargLabs/cejel/releases/latest/download/$asset" -o "$asset"
36
+ curl -fsSL https://github.com/BargLabs/cejel/releases/latest/download/SHA256SUMS -o SHA256SUMS
37
+ if command -v sha256sum >/dev/null; then
38
+ grep " $asset$" SHA256SUMS | sha256sum -c -
39
+ else
40
+ grep " $asset$" SHA256SUMS | shasum -a 256 -c -
41
+ fi
42
+ mv "$asset" cejel
31
43
  chmod +x cejel
32
44
  ./cejel .
33
45
  ```
@@ -41,15 +53,13 @@ product, and you can falsify it in ten seconds:
41
53
  docker run --rm --network=none -v "$PWD:/w" -w /w -v "$PWD/cejel:/cejel:ro" debian:stable-slim /cejel .
42
54
  ```
43
55
 
44
- **npm (scoped package).**
56
+ **npm.**
45
57
 
46
58
  ```bash
47
- npx --yes --package @cejel/cejel cejel .
59
+ npx @cejel/cejel .
48
60
  ```
49
61
 
50
- The package is published under the `@cejel` scope while the unscoped name is under npm
51
- registry review. The installed executable remains `cejel`. For a global install, run
52
- `npm install --global @cejel/cejel`, then invoke `cejel .`.
62
+ The npm package is scoped as `@cejel/cejel`; its executable remains the short command `cejel`.
53
63
 
54
64
  **GitHub Action** — score every PR and publish the badge:
55
65
 
@@ -67,9 +77,9 @@ pnpm install && pnpm build
67
77
  node dist/index.js .
68
78
  ```
69
79
 
70
- Released binaries: `cejel-Darwin-arm64`, `cejel-Linux-x86_64`. A Homebrew tap and the
71
- remaining architectures are in progress; they are not listed here because they do not exist
72
- yet.
80
+ Released binaries: `cejel-Darwin-arm64`, `cejel-Darwin-x86_64`, `cejel-Linux-aarch64`, and
81
+ `cejel-Linux-x86_64`. The release also carries `SHA256SUMS`; each binary is executed against
82
+ the source build and with networking denied before attachment.
73
83
 
74
84
  ## Leaderboard
75
85
 
@@ -78,7 +88,7 @@ corpus scored and published in the open — elite OSS projects, Cejel itself, an
78
88
  studio monorepo Cejel was built inside — with a per-repository evidence report for every
79
89
  row under [`leaderboard/reports/`](./leaderboard/reports/). The board is also hosted at
80
90
  [cejel.dev](https://cejel.dev). Every score on this board is produced by the same sealed
81
- public scorer used by `npx cejel .` — check out the source commit printed in the
91
+ public scorer used by `npx @cejel/cejel .` — check out the source commit printed in the
82
92
  report, run the tool, and you will get this number. A required guard does exactly that for
83
93
  every corpus row and compares score, verdict, measured coverage, and evidence. No private
84
94
  domain collector contributes to any published score, ours included; nobody is exempt.
@@ -123,7 +133,8 @@ better-evidenced rows.
123
133
  Scores the current directory with sensible defaults: no flags, no signup, fully offline.
124
134
  Prints a concise terminal certificate and writes to `.cejel/`:
125
135
 
126
- - `report.json` — the full structured report
136
+ - `report.json` — the full structured report; scored runs carry numeric headline scores and a
137
+ score-band `verdict`, while abstained runs carry `null` scores and `verdict: "insufficient_source"`
127
138
  - `attestation.json` — an unsigned in-toto statement binding the report digest, repository
128
139
  revision, rubric, and scored-or-abstained outcome; ready for an external signer
129
140
  - `certificate.html` — a self-contained HTML certificate (no external assets)
@@ -141,8 +152,9 @@ or provenance system signs it, `assurance.status` is `unsigned` and `issuer` is
141
152
  - `<path>` — repo to score (default: current directory)
142
153
  - `--out <dir>` — where to write report/certificate/badge files (default: `.cejel`;
143
154
  `--out-dir` remains available as a compatibility alias)
144
- - `--min-score <n>` — exit non-zero if the overall score is below `n` (0–4); used by the
145
- GitHub Action's optional threshold gate
155
+ - `--min-score <n>` — exit non-zero if the overall score is below `n` (0–4), or if Cejel
156
+ abstains and therefore cannot evaluate the threshold; used by the GitHub Action's optional
157
+ threshold gate
146
158
  - `--ingest <file|glob>` — fold another scanner's output into the score (repeatable). Accepts
147
159
  SARIF, OpenSSF Scorecard JSON, or the generic Cejel external-signal shape — format is
148
160
  auto-detected. See "Aggregate your scanners" below.
@@ -168,9 +180,10 @@ honestly rather than guessing:
168
180
  - **Not yet recognised** — other ecosystems, including COBOL, Fortran, and MATLAB. Cejel does
169
181
  not score these as "source" at all: a repository with zero
170
182
  recognised-extension files gets the `unrecognised_ecosystem` archetype, an explicit
171
- `insufficient_data` status, and **no verdict** — never a confident numeric score, and never
172
- the word "Unverified" for the sole reason that cejel cannot read the language. The
173
- certificate states plainly which of the 11 dimensions were and were not measured.
183
+ `insufficient_data` criterion status, `null` headline scores, and an explicit machine verdict
184
+ of `insufficient_source` — never a numeric score-band verdict, and never the word
185
+ "Unverified" for the sole reason that cejel cannot read the language. The certificate states
186
+ plainly which of the 11 dimensions were and were not measured.
174
187
 
175
188
  This list will grow. It will never be "any codebase" — that claim is a promise the parser
176
189
  cannot keep, and an honest support matrix is worth more than a marketing line the code
@@ -278,7 +291,7 @@ Add it to an MCP client config:
278
291
  "mcpServers": {
279
292
  "cejel": {
280
293
  "command": "npx",
281
- "args": ["-y", "--package=cejel", "cejel-mcp"]
294
+ "args": ["-y", "--package=@cejel/cejel", "cejel-mcp"]
282
295
  }
283
296
  }
284
297
  }
@@ -299,7 +312,7 @@ and the server writes no files.
299
312
  ## What "offline" means here
300
313
 
301
314
  Scoring a repo — `cejel .` itself, and the Action's scoring step — makes zero network
302
- calls: no telemetry, no signup, no model call. Fetching the `cejel` package the
315
+ calls: no telemetry, no signup, no model call. Fetching the `@cejel/cejel` package the
303
316
  first time (like any npm-distributed CLI, including this Action's own dependency install)
304
317
  does need network; that's a one-time install cost, not part of the scoring guarantee.
305
318
 
@@ -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
 
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  renderWitanHtmlReport,
8
8
  runCejelScan,
9
9
  serializeWitanReport
10
- } from "./chunk-SQNTNX5U.js";
10
+ } from "./chunk-A7NZKJ5H.js";
11
11
 
12
12
  // src/index.ts
13
13
  import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
@@ -16,11 +16,15 @@ import { pathToFileURL } from "url";
16
16
 
17
17
  // src/terminal.ts
18
18
  function renderTerminalCertificate(summary) {
19
- const lines = summary.insufficientSourceReason ? [
19
+ const abstained = summary.verdict === "Insufficient source";
20
+ if (!abstained && (summary.overallScore === null || summary.codeTrustScore === null || summary.processTrustScore === null)) {
21
+ throw new Error("A scored Cejel summary must carry numeric headline scores.");
22
+ }
23
+ const lines = abstained ? [
20
24
  `Cejel Trust Certificate \u2014 ${summary.productDisplayName}`,
21
25
  "",
22
26
  "Insufficient source to certify.",
23
- ` ${summary.insufficientSourceReason}`,
27
+ ` ${summary.insufficientSourceReason ?? "No ratable source was found."}`,
24
28
  ""
25
29
  ] : [
26
30
  `Cejel Trust Certificate \u2014 ${summary.productDisplayName}`,
@@ -37,7 +41,7 @@ function renderTerminalCertificate(summary) {
37
41
  }
38
42
  lines.push("");
39
43
  }
40
- if (summary.insufficientSourceReason) {
44
+ if (abstained) {
41
45
  lines.push("See report.json / certificate.html for the full per-criterion detail.");
42
46
  } else if (summary.topFindings.length === 0) {
43
47
  lines.push("No evidence-backed findings.");
@@ -51,7 +55,7 @@ function renderTerminalCertificate(summary) {
51
55
  lines.push(` ...and ${remaining} more \u2014 see the written report for the full list.`);
52
56
  }
53
57
  }
54
- if (!summary.insufficientSourceReason && summary.externalFindingCount > 0) {
58
+ if (!abstained && summary.externalFindingCount > 0) {
55
59
  lines.push(
56
60
  "",
57
61
  `External findings (${summary.externalFindingCount} total, attributed to tool + criterion):`
@@ -70,6 +74,7 @@ function renderTerminalCertificate(summary) {
70
74
  `;
71
75
  }
72
76
  function formatScore(score) {
77
+ if (score === null) throw new Error("A scored Cejel summary must carry numeric scores.");
73
78
  return score.toFixed(1);
74
79
  }
75
80
 
@@ -117,6 +122,9 @@ var CLI_FLAG_KIND_BY_TOKEN = new Map(
117
122
  CLI_FLAG_SPECS.flatMap((spec) => spec.tokens.map((token) => [token, spec.kind]))
118
123
  );
119
124
  function cliVersion() {
125
+ if (typeof __CEJEL_SEA_VERSION__ === "string" && __CEJEL_SEA_VERSION__.length > 0) {
126
+ return __CEJEL_SEA_VERSION__;
127
+ }
120
128
  const manifest = JSON.parse(
121
129
  readFileSync(new URL("../package.json", import.meta.url), "utf8")
122
130
  );
@@ -191,12 +199,21 @@ Wrote:
191
199
  `
192
200
  );
193
201
  }
194
- if (options.minScore != null && report.overallScore < options.minScore) {
195
- process.stderr.write(
196
- `Cejel: overall score ${report.overallScore.toFixed(1)}/4.0 is below the required minimum ${options.minScore.toFixed(1)}/4.0
202
+ if (options.minScore != null) {
203
+ if (report.verdict === "insufficient_source") {
204
+ process.stderr.write(
205
+ `Cejel: cannot evaluate the required minimum ${options.minScore.toFixed(1)}/4.0 because this repository has insufficient source.
197
206
  `
198
- );
199
- return 1;
207
+ );
208
+ return 1;
209
+ }
210
+ if (report.overallScore < options.minScore) {
211
+ process.stderr.write(
212
+ `Cejel: overall score ${report.overallScore.toFixed(1)}/4.0 is below the required minimum ${options.minScore.toFixed(1)}/4.0
213
+ `
214
+ );
215
+ return 1;
216
+ }
200
217
  }
201
218
  return 0;
202
219
  }
package/dist/mcp/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  renderWitanBadgeSvg,
11
11
  renderWitanHtmlReport,
12
12
  runCejelScan
13
- } from "../chunk-SQNTNX5U.js";
13
+ } from "../chunk-A7NZKJ5H.js";
14
14
 
15
15
  // node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/codegen/code.js
16
16
  var require_code = __commonJS({
@@ -17061,7 +17061,7 @@ function createCejelMcpServer(identity) {
17061
17061
  });
17062
17062
  server.tool(
17063
17063
  "scan",
17064
- `Score a repository's engineering-trust signals (tests, secrets, isolation, claim-vs-reality, CI discipline) with the ${identity.packageName} trust-certificate scan \u2014 the exact same offline, deterministic scoring as running the ${identity.packageName} CLI on the path. Returns the trust cert as JSON: overall + code/process sub-scores (0-4), verdict band, and top findings. No network, no telemetry, no signup. After a scan, the full HTML certificate and SVG badge are readable as the resources ${certificateUri} and ${badgeUri}.`,
17064
+ `Score a repository's engineering-trust signals (tests, secrets, isolation, claim-vs-reality, CI discipline) with the ${identity.packageName} trust-certificate scan \u2014 the exact same offline, deterministic scoring as running the ${identity.packageName} CLI on the path. Returns the trust cert as JSON: overall + code/process sub-scores (0-4) for a scored repository, or null scores with an insufficient-source verdict when Cejel cannot read it; plus the verdict and top findings. No network, no telemetry, no signup. After a scan, the full HTML certificate and SVG badge are readable as the resources ${certificateUri} and ${badgeUri}.`,
17065
17065
  {
17066
17066
  path: external_exports.string().min(1).describe("Path to the repository to score (absolute, or relative to the server CWD)"),
17067
17067
  format: external_exports.enum(["summary", "json"]).optional().describe(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cejel/cejel",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "license": "AGPL-3.0-only",
24
24
  "author": "Barg Labs",
25
- "homepage": "https://github.com/BargLabs/cejel",
25
+ "homepage": "https://cejel.dev",
26
26
  "bugs": {
27
27
  "url": "https://github.com/BargLabs/cejel/issues"
28
28
  },
@@ -45,6 +45,7 @@
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsup",
48
+ "build:sea-js": "tsup --config tsup.public-sea.config.ts",
48
49
  "prepublishOnly": "pnpm run build",
49
50
  "prepack": "pnpm run build",
50
51
  "dev": "tsx src/index.ts",
@@ -54,6 +55,7 @@
54
55
  "devDependencies": {
55
56
  "@modelcontextprotocol/sdk": "^1.0.0",
56
57
  "@types/node": "^25.9.1",
58
+ "postject": "^1.0.0-alpha.6",
57
59
  "tsup": "^8.0.0",
58
60
  "tsx": "^4.0.0",
59
61
  "typescript": "^5.5.0",