@cejel/cejel 0.4.1 → 0.4.3

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
@@ -68,7 +68,7 @@ if (-not $expected -or $actual -ne $expected.ToLowerInvariant()) {
68
68
  ```
69
69
 
70
70
  > **Windows signing status:** `cejel-Windows-x86_64.exe` is intentionally unsigned in
71
- > 0.4.1 and may trigger Microsoft SmartScreen. The release build removes Node's inherited
71
+ > 0.4.3 and may trigger Microsoft SmartScreen. The release build removes Node's inherited
72
72
  > signature before SEA injection and fails unless Windows reports the result as `NotSigned`;
73
73
  > it does not ship an invalid signature. Before running it, verify `SHA256SUMS` and the
74
74
  > GitHub build-provenance attestation. Each binary also has an attached SPDX SBOM and an
@@ -91,7 +91,7 @@ docker run --rm --network=none -v "$PWD:/w" -w /w -v "$PWD/cejel:/cejel:ro" debi
91
91
  npx @cejel/cejel@latest .
92
92
  ```
93
93
 
94
- > **Distribution note:** Cejel `0.4.1` is the coordinated release version for npm,
94
+ > **Distribution note:** Cejel `0.4.3` is the coordinated release version for npm,
95
95
  > standalone binaries, Docker/OCI, GitHub Action, Homebrew, and MCP Registry.
96
96
 
97
97
  `npx` can reuse a stale cached package. Force the current npm release with the `@latest`
@@ -105,7 +105,7 @@ If a root `package.json` still carries a template name, override only the certif
105
105
  name while keeping the repository-derived stable slug:
106
106
 
107
107
  ```bash
108
- npx @cejel/cejel . --name "Customer Portal"
108
+ npx @cejel/cejel@latest . --name "Customer Portal"
109
109
  ```
110
110
 
111
111
  The npm package is scoped as `@cejel/cejel`; its executable remains the short command `cejel`.
@@ -140,16 +140,16 @@ gh attestation verify ./cejel-Windows-x86_64.exe -R BargLabs/cejel
140
140
  This is cryptographically signed provenance. It is distinct from Apple Developer ID or
141
141
  Microsoft Authenticode code-signing.
142
142
 
143
- **Docker / OCI.** The current container release is `0.4.1`:
143
+ **Docker / OCI.** The current container release is `0.4.3`:
144
144
 
145
145
  ```bash
146
- docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.4.1
146
+ docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.4.3
147
147
  ```
148
148
 
149
149
  The image defaults to `cejel-mcp` over stdio. To use the CLI instead:
150
150
 
151
151
  ```bash
152
- docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.4.1 .
152
+ docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.4.3 .
153
153
  ```
154
154
 
155
155
  The OCI image carries an SBOM, maximum-mode build provenance, and a signed registry
@@ -162,7 +162,7 @@ projects, the public Cejel repository itself, and one explicitly labeled transpa
162
162
  snapshot from the private studio monorepo where Cejel was built, with a per-repository
163
163
  evidence report for every row under [`leaderboard/reports/`](./leaderboard/reports/). The
164
164
  board is also hosted at [cejel.dev](https://cejel.dev). Every score is produced by the same
165
- sealed public scorer used by `npx @cejel/cejel .`; no private domain collector contributes.
165
+ sealed public scorer used by `npx @cejel/cejel@latest .`; no private domain collector contributes.
166
166
  For each public-repository row, check out the immutable source commit printed in its report
167
167
  and run the public scorer to reproduce the score, verdict, measured coverage, and evidence.
168
168
  The private Alfred snapshot discloses its limitation instead: its source commit and withheld
@@ -426,7 +426,7 @@ The OCI image is an alternative when Docker is the preferred execution boundary.
426
426
  the host path with the repository OpenClaw should allow Cejel to read:
427
427
 
428
428
  ```bash
429
- openclaw mcp set cejel-oci '{"command":"docker","args":["run","--rm","-i","-v","/absolute/path/to/repo:/workspace:ro","ghcr.io/barglabs/cejel:0.4.1"]}'
429
+ openclaw mcp set cejel-oci '{"command":"docker","args":["run","--rm","-i","-v","/absolute/path/to/repo:/workspace:ro","ghcr.io/barglabs/cejel:0.4.3"]}'
430
430
  openclaw mcp doctor cejel-oci --probe
431
431
  ```
432
432
 
@@ -4531,6 +4531,317 @@ function collectExternalFindings(consumedSignals) {
4531
4531
 
4532
4532
  // src/witan/finding-presentation.ts
4533
4533
  init_esm_shims();
4534
+
4535
+ // src/witan/certificate-presentation.ts
4536
+ init_esm_shims();
4537
+ var CERTIFICATE_GLOSSARY = [
4538
+ {
4539
+ key: "labels",
4540
+ term: "labels / why the labels differ",
4541
+ definition: "Three labels can appear on one line because they answer three different questions. Finding severity describes how serious one issue is. The dimension band summarizes the criterion evidence. The numeric band comes from the weighted score.",
4542
+ metricNames: []
4543
+ },
4544
+ {
4545
+ key: "capped",
4546
+ term: "capped",
4547
+ definition: "Credit stops increasing after the maximum defined by the scoring rubric, even when the raw count is higher. Caps are fixed per rubric version because they are part of what was calibrated. Extra volume therefore cannot outweigh the rest of the rubric.",
4548
+ metricNames: []
4549
+ },
4550
+ {
4551
+ key: "test-to-source-file-ratio",
4552
+ term: "test-to-source file ratio",
4553
+ definition: "The number of detected test files compared with detected source files. A larger implementation surface needs a correspondingly visible test surface.",
4554
+ metricNames: ["test_to_source_ratio"]
4555
+ },
4556
+ {
4557
+ key: "static-coverage",
4558
+ term: "static coverage",
4559
+ definition: "A percentage read from a coverage report or threshold that the repository itself publishes. Cejel does not run the repository's tests. The scoring rubric defines how much credit the published percentage receives.",
4560
+ metricNames: ["coverage_percent"]
4561
+ },
4562
+ {
4563
+ key: "verification-script",
4564
+ term: "verification script",
4565
+ definition: "the checks that run automatically: tests, linting, type-checking, build",
4566
+ metricNames: ["verification_script_ratio"]
4567
+ },
4568
+ {
4569
+ key: "non-hollow",
4570
+ term: "non-hollow",
4571
+ definition: "test files that actually assert something rather than being empty or skipped",
4572
+ metricNames: ["non_hollow_test_share"]
4573
+ },
4574
+ {
4575
+ key: "secrets",
4576
+ term: "secrets",
4577
+ definition: "credentials like API keys and tokens",
4578
+ metricNames: ["secret_cleanliness"]
4579
+ },
4580
+ {
4581
+ key: "environment-handling-depth",
4582
+ term: "environment handling depth",
4583
+ definition: "The visible controls for keeping environment-specific configuration and credentials out of source. Unsafe configuration handling can expose production access.",
4584
+ metricNames: ["env_handling_depth"]
4585
+ },
4586
+ {
4587
+ key: "rls-policy-count",
4588
+ term: "RLS policy count",
4589
+ definition: "The number of detected database row-level security policies. Those policies can enforce which rows each tenant or user may reach.",
4590
+ metricNames: ["rls_policy_count"]
4591
+ },
4592
+ {
4593
+ key: "tenant-scoped-schema-ratio",
4594
+ term: "tenant-scoped schema ratio",
4595
+ definition: "The share of relevant database tables with a visible tenant boundary. Missing tenant scope can allow one customer's data to mix with another's.",
4596
+ metricNames: ["tenant_scope_ratio"]
4597
+ },
4598
+ {
4599
+ key: "crypto-comparison-hygiene",
4600
+ term: "crypto comparison hygiene",
4601
+ definition: "Comparisons of secret values that take the same time whether or not the values match. Attackers therefore cannot recover secrets by measuring response times.",
4602
+ metricNames: ["crypto_comparison_hygiene"]
4603
+ },
4604
+ {
4605
+ key: "basic-checks",
4606
+ term: "basic checks",
4607
+ definition: "In code trust, production-readiness basic checks cover build or type-check commands, automated pipelines, deployment configuration, environment templates, health checks, and error boundaries. In process trust, PR trace basic checks cover automated pipelines, pull-request templates, and review-gate records. Missing checks leave part of the acceptance path unsupported.",
4608
+ metricNames: ["prod_readiness_primitives", "pr_trace_primitives"]
4609
+ },
4610
+ {
4611
+ key: "workflow-depth",
4612
+ term: "workflow depth",
4613
+ definition: "Workflows here are automated build, test, or release pipelines, such as GitHub Actions, not team policy. This measures how many relevant pipeline stages Cejel can see rather than how many workflow files exist. A deeper pipeline checks more of the path to a release.",
4614
+ metricNames: ["prod_workflow_depth"]
4615
+ },
4616
+ {
4617
+ key: "observability-depth",
4618
+ term: "observability depth",
4619
+ definition: "The visible health, logging, and error-reporting controls. Operators need evidence that failures can be detected and investigated.",
4620
+ metricNames: ["observability_depth"]
4621
+ },
4622
+ {
4623
+ key: "rollback-and-migration-safety-depth",
4624
+ term: "rollback and migration-safety depth",
4625
+ definition: "The visible procedures and checks for reversing a release or changing stored data safely. Recovery and data changes are high-risk parts of accepting software.",
4626
+ metricNames: ["rollback_safety_depth"]
4627
+ },
4628
+ {
4629
+ key: "dependency-automation-ratio",
4630
+ term: "dependency automation ratio",
4631
+ definition: "The share of expected automated dependency-update and audit controls that are present. Stale or vulnerable packages otherwise depend on manual discovery.",
4632
+ metricNames: ["dependency_automation_ratio"]
4633
+ },
4634
+ {
4635
+ key: "pinned-dependency-ratio",
4636
+ term: "pinned dependency ratio",
4637
+ definition: "The share of application dependencies fixed to exact versions. Exact inputs make deployed builds more reproducible.",
4638
+ metricNames: ["pinned_dependency_ratio"]
4639
+ },
4640
+ {
4641
+ key: "lockfile-coverage",
4642
+ term: "lockfile coverage",
4643
+ definition: "Whether the package manifests are backed by committed lockfiles. Lockfiles record the exact dependency versions selected for a build.",
4644
+ metricNames: ["lockfile_coverage"]
4645
+ },
4646
+ {
4647
+ key: "declared-version-range-ratio",
4648
+ term: "declared version range ratio",
4649
+ definition: "The share of library dependencies with an explicit compatible version range. Unconstrained dependencies can change without a deliberate source change.",
4650
+ metricNames: ["declared_version_range_ratio"]
4651
+ },
4652
+ {
4653
+ key: "dependency-count-sanity",
4654
+ term: "dependency count sanity",
4655
+ definition: "A bounded check for an unusually long list of direct dependencies for a library. Every direct dependency adds maintenance and supply-chain exposure.",
4656
+ metricNames: ["dependency_count_sanity"]
4657
+ },
4658
+ {
4659
+ key: "claim-match-rate",
4660
+ term: "claim match rate",
4661
+ definition: "The share of documented product claims that match visible repository evidence. An acceptance claim should be checkable against the delivered revision.",
4662
+ metricNames: ["claim_match_rate"]
4663
+ },
4664
+ {
4665
+ key: "claim-source-depth",
4666
+ term: "claim source depth",
4667
+ definition: "How much concrete, checkable claim material is present. Vague or missing claims give a relying party nothing specific to verify.",
4668
+ metricNames: ["claim_source_depth"]
4669
+ },
4670
+ {
4671
+ key: "reconciliation-artifact-depth",
4672
+ term: "reconciliation artifact depth",
4673
+ definition: "The visible records connecting stated claims to evidence. A relying party needs to trace what was promised to what was inspected.",
4674
+ metricNames: ["reconciliation_artifact_depth"]
4675
+ },
4676
+ {
4677
+ key: "recent-pr-merge-ratio",
4678
+ term: "recent PR merge ratio",
4679
+ definition: "The share of recent visible commits associated with merged pull requests. Review history helps a relying party inspect how changes reached the revision.",
4680
+ metricNames: ["pr_merge_ratio"]
4681
+ },
4682
+ {
4683
+ key: "ci-verification-depth",
4684
+ term: "CI verification depth",
4685
+ definition: "The number of test, lint, type-check, and build categories Cejel sees running in continuous integration. Automated gates make checks repeatable for each change.",
4686
+ metricNames: ["ci_script_depth"]
4687
+ },
4688
+ {
4689
+ key: "pr-gate-ci-workflow-count",
4690
+ term: "PR-gate CI workflow count",
4691
+ definition: "Workflows here are automated build and test pipelines, such as GitHub Actions, not team policy. This counts pipelines that visibly run on pull requests or the default branch. Checks that never gate changes cannot support the acceptance decision.",
4692
+ metricNames: ["default_branch_ci_depth"]
4693
+ },
4694
+ {
4695
+ key: "audit-artifact-depth",
4696
+ term: "audit artifact depth",
4697
+ definition: "How many durable audit records such as changelogs, incident notes, or security records are present. Past operational decisions should remain inspectable.",
4698
+ metricNames: ["audit_artifact_depth"]
4699
+ },
4700
+ {
4701
+ key: "audit-freshness-depth",
4702
+ term: "audit freshness depth",
4703
+ definition: "How recently the visible audit records were maintained. Stale records may not describe the revision being accepted.",
4704
+ metricNames: ["audit_freshness_depth"]
4705
+ },
4706
+ {
4707
+ key: "un-overridable-kill-switch",
4708
+ term: "un-overridable kill-switch",
4709
+ definition: "A stop control that ordinary automation cannot bypass. Operators need a dependable way to halt privileged behavior.",
4710
+ metricNames: ["kill_switch_fail_safe_present"]
4711
+ },
4712
+ {
4713
+ key: "human-gate-documented",
4714
+ term: "human gate documented",
4715
+ definition: "A written requirement for a person to approve sensitive operations. High-impact actions should not rely only on unattended automation.",
4716
+ metricNames: ["human_gate_documented"]
4717
+ },
4718
+ {
4719
+ key: "fail-closed-privilege-check",
4720
+ term: "fail-closed privilege check",
4721
+ definition: "A permission check that refuses the operation when it cannot reach a confident allow decision. Errors must not silently grant elevated access.",
4722
+ metricNames: ["fail_closed_privilege_check"]
4723
+ },
4724
+ {
4725
+ key: "privilege-escalation-cleanliness",
4726
+ term: "privilege-escalation cleanliness",
4727
+ definition: "The absence of detected patterns that unsafely widen permissions. Unnecessary elevation increases the impact of a mistake or compromise.",
4728
+ metricNames: ["privilege_escalation_cleanliness"]
4729
+ },
4730
+ {
4731
+ key: "protected-path-review-gate",
4732
+ term: "protected-path review gate",
4733
+ definition: "A rule requiring review before sensitive files or directories can change. Critical controls deserve an explicit second check.",
4734
+ metricNames: ["protected_path_review_gate"]
4735
+ }
4736
+ ];
4737
+ var GLOSSARY_BY_METRIC = new Map(
4738
+ CERTIFICATE_GLOSSARY.flatMap(
4739
+ (entry) => entry.metricNames.map((metricName) => [metricName, entry])
4740
+ )
4741
+ );
4742
+ function glossaryEntryForMetric(metric2) {
4743
+ return GLOSSARY_BY_METRIC.get(metric2.name) ?? {
4744
+ key: `metric-${metric2.name.replace(/[^a-z0-9-]+/gi, "-").toLowerCase()}`,
4745
+ term: formatCertificateMetricLabel(metric2),
4746
+ definition: metric2.description ? `${metric2.description} This measurement contributes to the criterion score.` : "A measured input to this criterion. This measurement contributes to the criterion score.",
4747
+ metricNames: [metric2.name]
4748
+ };
4749
+ }
4750
+ var CERTIFICATE_METRIC_LABELS = {
4751
+ prod_readiness_primitives: "Production-readiness basic checks",
4752
+ pr_trace_primitives: "PR trace basic checks",
4753
+ prod_workflow_depth: "Production workflow depth (automated build/test/release pipeline)",
4754
+ default_branch_ci_depth: "PR-gate CI workflow count (automated build/test pipeline)"
4755
+ };
4756
+ function formatCertificateMetricLabel(metric2) {
4757
+ return CERTIFICATE_METRIC_LABELS[metric2.name] ?? metric2.label;
4758
+ }
4759
+ function glossaryEntriesForReport(report) {
4760
+ const entries = new Map(CERTIFICATE_GLOSSARY.map((entry) => [entry.key, entry]));
4761
+ for (const metric2 of report.criteria.flatMap((criterion) => criterion.metrics)) {
4762
+ const entry = glossaryEntryForMetric(metric2);
4763
+ entries.set(entry.key, entry);
4764
+ }
4765
+ return [...entries.values()];
4766
+ }
4767
+ function isCoverageNotMeasured(criterion, metric2) {
4768
+ if (metric2.name !== "coverage_percent" || metric2.value !== 0) return false;
4769
+ return ![
4770
+ ...criterion.evidence,
4771
+ ...criterion.findings.map((finding) => finding.evidence)
4772
+ ].some((evidence) => evidence.kind === "coverage");
4773
+ }
4774
+ function labelAlreadyStatesUnit(label, unit) {
4775
+ if (!unit) return false;
4776
+ const escapedUnit = unit.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4777
+ return new RegExp(`(?:^|[\\s-])${escapedUnit}$`, "i").test(label.trim());
4778
+ }
4779
+ function formatCertificateMetricValue(criterion, metric2) {
4780
+ if (isCoverageNotMeasured(criterion, metric2)) {
4781
+ return "no coverage report found \u2014 not measured";
4782
+ }
4783
+ if (metric2.name === "test_to_source_ratio" && metric2.max !== void 0) {
4784
+ const tests = formatMetricNumber(metric2.value);
4785
+ const sources = formatMetricNumber(metric2.max);
4786
+ const comparison = `${tests} test file${metric2.value === 1 ? "" : "s"} / ${sources} source file${metric2.max === 1 ? "" : "s"}`;
4787
+ return metric2.value > metric2.max ? `${comparison} (credit capped at parity)` : comparison;
4788
+ }
4789
+ const displayUnit = metric2.name === "prod_readiness_primitives" || metric2.name === "pr_trace_primitives" ? "checks" : labelAlreadyStatesUnit(formatCertificateMetricLabel(metric2), metric2.unit) ? void 0 : metric2.unit;
4790
+ const unit = displayUnit ? ` ${displayUnit}` : "";
4791
+ if (metric2.max === void 0) return `${formatMetricNumber(metric2.value)}${unit}`;
4792
+ if (metric2.kind === "saturating_count" && metric2.value > metric2.max) {
4793
+ return `${formatMetricNumber(metric2.max)}${unit} (capped; ${formatMetricNumber(metric2.value)} raw)`;
4794
+ }
4795
+ return `${formatMetricNumber(metric2.value)}/${formatMetricNumber(metric2.max)}${unit}`;
4796
+ }
4797
+ function buildRelyingPartySummary(report) {
4798
+ const applicable = report.criteria.filter((criterion) => criterion.status !== "not_applicable");
4799
+ const measured = applicable.filter((criterion) => criterion.status !== "insufficient_data");
4800
+ const findings = report.criteria.reduce((sum, criterion) => sum + criterion.findings.length, 0);
4801
+ const revision = report.repo.headSha ? ` at revision ${report.repo.headSha}` : " from the repository snapshot named in this report; no Git revision was available";
4802
+ const a1 = report.criteria.find((criterion) => criterion.id === "A1");
4803
+ const coverageMetric = a1?.metrics.find((metric2) => metric2.name === "coverage_percent");
4804
+ const gaps = [];
4805
+ if (a1 && coverageMetric && isCoverageNotMeasured(a1, coverageMetric)) {
4806
+ gaps.push(
4807
+ "No coverage report or configured threshold was found, so static coverage was not measured; Cejel does not run the subject's tests."
4808
+ );
4809
+ }
4810
+ const unmeasured = applicable.filter((criterion) => criterion.status === "insufficient_data");
4811
+ if (unmeasured.length > 0) {
4812
+ gaps.push(`The report did not measure ${unmeasured.map((criterion) => criterion.id).join(", ")}.`);
4813
+ }
4814
+ const notApplicable = report.criteria.filter((criterion) => criterion.status === "not_applicable");
4815
+ if (notApplicable.length > 0) {
4816
+ gaps.push(
4817
+ `${notApplicable.map((criterion) => criterion.id).join(", ")} ${notApplicable.length === 1 ? "was" : "were"} marked not applicable and therefore not examined.`
4818
+ );
4819
+ }
4820
+ if ((report.scanLimitations?.length ?? 0) > 0) {
4821
+ const count = report.scanLimitations?.length ?? 0;
4822
+ gaps.push(`${count} scan limitation${count === 1 ? "" : "s"} qualifies the readable evidence.`);
4823
+ }
4824
+ if ((report.contentReadSummary?.skipped ?? 0) > 0) {
4825
+ const skipped = report.contentReadSummary?.skipped ?? 0;
4826
+ gaps.push(
4827
+ `${skipped} content entr${skipped === 1 ? "y was" : "ies were"} skipped for the reasons itemized in the certificate.`
4828
+ );
4829
+ }
4830
+ gaps.push(
4831
+ "Anything absent from the measured evidence was not established: no measurement and no finding are not clearance."
4832
+ );
4833
+ return {
4834
+ examined: `Cejel examined the repository evidence recorded for ${report.productDisplayName}${revision} under ${report.rubricVersion}.`,
4835
+ established: `The report established measured results for ${measured.length} of ${applicable.length} applicable rubric dimensions and recorded ${findings} evidence-backed finding${findings === 1 ? "" : "s"}.`,
4836
+ notEstablished: gaps.join(" "),
4837
+ next: "Review the cited evidence and open or unverified items, reproduce the scan at the reported revision when one is available, and obtain missing measurements or resolve limitations before deciding whether to accept the code."
4838
+ };
4839
+ }
4840
+ function formatMetricNumber(value) {
4841
+ return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
4842
+ }
4843
+
4844
+ // src/witan/finding-presentation.ts
4534
4845
  var METRIC_DERIVED_FINDING_PATTERN = /^[A-Z]\d+ metric-derived score is (\d+\.\d)\/4\.0, in the (critical|warning) band — no single finding drove this; it reflects the combined metric weighting below\.$/;
4535
4846
  var REMEDIATION_BY_METRIC = {
4536
4847
  test_to_source_ratio: "add concrete regression tests for the implementation surface",
@@ -4575,18 +4886,18 @@ function renderFindingSummary(criterion, finding) {
4575
4886
  if (drivers.length === 0) {
4576
4887
  return `${criterion.id} dimension band is ${band} at ${score}/4.0. To improve this dimension, add stronger measurable evidence for ${criterion.title.toLowerCase()}.`;
4577
4888
  }
4578
- const measurements = drivers.map(formatMetricMember).join("; ");
4889
+ const measurements = drivers.map((metric2) => formatMetricMember(criterion, metric2)).join("; ");
4579
4890
  const actions = Array.from(
4580
4891
  new Set(
4581
4892
  drivers.map(
4582
- (metric2) => REMEDIATION_BY_METRIC[metric2.name] ?? `raise the measured ${metric2.label.toLowerCase()}`
4893
+ (metric2) => REMEDIATION_BY_METRIC[metric2.name] ?? `raise the measured ${formatCertificateMetricLabel(metric2).toLowerCase()}`
4583
4894
  )
4584
4895
  )
4585
4896
  ).join("; ");
4586
4897
  return `${criterion.id} dimension band is ${band} at ${score}/4.0. Lowest contributing measurements: ${measurements}. To improve: ${actions}.`;
4587
4898
  }
4588
4899
  function selectLowestDrivers(metrics) {
4589
- return [...metrics].filter((metric2) => metric2.max !== void 0).sort((left, right) => {
4900
+ return [...metrics].filter((metric2) => metric2.max !== void 0 && normalizedMetricValue(metric2) < 1).sort((left, right) => {
4590
4901
  const leftRatio = normalizedMetricValue(left);
4591
4902
  const rightRatio = normalizedMetricValue(right);
4592
4903
  if (leftRatio !== rightRatio) return leftRatio - rightRatio;
@@ -4598,17 +4909,8 @@ function normalizedMetricValue(metric2) {
4598
4909
  if (metric2.max === void 0) return 1;
4599
4910
  return Math.min(metric2.value / metric2.max, 1);
4600
4911
  }
4601
- function formatMetricMember(metric2) {
4602
- const unit = metric2.unit ? ` ${metric2.unit}` : "";
4603
- const maximum = metric2.max;
4604
- if (maximum === void 0) return `${metric2.label} ${formatMetricValue(metric2.value)}${unit}`;
4605
- if (metric2.kind === "saturating_count" && metric2.value > maximum) {
4606
- return `${metric2.label} ${formatMetricValue(maximum)}${unit} capped (${formatMetricValue(metric2.value)} raw)`;
4607
- }
4608
- return `${metric2.label} ${formatMetricValue(metric2.value)}/${formatMetricValue(maximum)}${unit}`;
4609
- }
4610
- function formatMetricValue(value) {
4611
- return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
4912
+ function formatMetricMember(criterion, metric2) {
4913
+ return `${formatCertificateMetricLabel(metric2)} ${formatCertificateMetricValue(criterion, metric2)}`;
4612
4914
  }
4613
4915
 
4614
4916
  // src/witan/html.ts
@@ -4624,6 +4926,7 @@ function renderWitanHtmlReport(report, options = {}) {
4624
4926
  const externalFindings = collectExternalFindings(report.consumedSignals ?? []);
4625
4927
  const coverage = computeMeasuredCoverage(report);
4626
4928
  const gitHistoryUnavailable = !report.repo.headSha;
4929
+ const relyingPartySummary = buildRelyingPartySummary(report);
4627
4930
  return `<!doctype html>
4628
4931
  <html lang="en">
4629
4932
  <head>
@@ -4666,6 +4969,8 @@ function renderWitanHtmlReport(report, options = {}) {
4666
4969
  </div>
4667
4970
  </header>
4668
4971
 
4972
+ ${renderRelyingPartySummary(relyingPartySummary)}
4973
+
4669
4974
  ${(report.scanLimitations?.length ?? 0) > 0 ? `<section class="scan-limitations-section evidence-section" aria-label="Scan limitations">
4670
4975
  <h2>Scan limitations</h2>
4671
4976
  <ul>${(report.scanLimitations ?? []).map(
@@ -4698,11 +5003,32 @@ function renderWitanHtmlReport(report, options = {}) {
4698
5003
  <ul>${renderExternalFindingItems(externalFindings).join("")}</ul>
4699
5004
  </article>
4700
5005
  </section>` : ""}
5006
+
5007
+ ${renderGlossary(glossaryEntriesForReport(report))}
4701
5008
  </main>
4702
5009
  </body>
4703
5010
  </html>
4704
5011
  `;
4705
5012
  }
5013
+ function renderRelyingPartySummary(summary) {
5014
+ return `<section class="relying-party-summary" aria-labelledby="relying-party-heading">
5015
+ <h2 id="relying-party-heading">How to read this certificate</h2>
5016
+ <dl>
5017
+ <div><dt>What was examined</dt><dd>${escapeHtml(summary.examined)}</dd></div>
5018
+ <div><dt>What was established</dt><dd>${escapeHtml(summary.established)}</dd></div>
5019
+ <div><dt>What was not established</dt><dd>${escapeHtml(summary.notEstablished)}</dd></div>
5020
+ <div><dt>What to do next</dt><dd>${escapeHtml(summary.next)}</dd></div>
5021
+ </dl>
5022
+ </section>`;
5023
+ }
5024
+ function renderGlossary(entries) {
5025
+ return `<section class="glossary" aria-labelledby="glossary-heading">
5026
+ <h2 id="glossary-heading">Plain-language glossary</h2>
5027
+ <dl>${entries.map(
5028
+ (entry) => `<div><dt id="glossary-${escapeAttribute(entry.key)}">${escapeHtml(entry.term)}</dt><dd>${escapeHtml(entry.definition)}</dd></div>`
5029
+ ).join("")}</dl>
5030
+ </section>`;
5031
+ }
4706
5032
  function renderContentReadSummary(report) {
4707
5033
  const summary = report.contentReadSummary;
4708
5034
  if (!summary || summary.skipped === 0) return "";
@@ -4791,7 +5117,7 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
4791
5117
  ...criterion.evidence.map(renderEvidencePointer),
4792
5118
  ...criterion.findings.map((finding) => renderFindingEvidence(criterion, finding))
4793
5119
  ];
4794
- const metrics = criterion.metrics.map(renderMetric);
5120
+ const metrics = criterion.metrics.map((metric2) => renderMetric(criterion, metric2));
4795
5121
  const statusReconciliation = renderStatusReconciliation(criterion);
4796
5122
  const historyWarning = gitHistoryUnavailable && criterion.metrics.some((metric2) => metric2.name === "pr_merge_ratio") ? `<p class="scan-warning"><strong>Scan limitation:</strong> Git history was unavailable (for example, this may be a source tarball), so the recent PR merge ratio is not a measured repository outcome and its displayed zero may undercount ${escapeHtml(criterion.id)}. Re-scan a full Git clone for this signal.</p>` : "";
4797
5123
  return `<article class="criterion">
@@ -4800,7 +5126,7 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
4800
5126
  <div class="criterion-id">${escapeHtml(criterion.id)}</div>
4801
5127
  <h3>${escapeHtml(criterion.title)}</h3>
4802
5128
  </div>
4803
- ${renderStatusChip(criterion.status)}
5129
+ ${renderStatusChip(criterion.id, criterion.status)}
4804
5130
  </div>
4805
5131
  <div class="criterion-score">${criterion.status === "not_applicable" ? "N/A" : criterion.status === "insufficient_data" ? "No data" : formatScore(criterion.score)}</div>
4806
5132
  ${statusReconciliation}
@@ -4819,7 +5145,9 @@ function renderStatusReconciliation(criterion) {
4819
5145
  }
4820
5146
  const numericBand = scoreBandForPresentation(criterion.score);
4821
5147
  if (numericBand === criterion.status) return "";
4822
- return `<p class="status-explanation"><strong>Why the labels differ:</strong> the ${escapeHtml(criterion.status)} dimension band is calibrated from criterion evidence thresholds and findings, independently of the ${formatScore(criterion.score)}/4.0 weighted score (which falls in the ${numericBand} numeric band).</p>`;
5148
+ const entry = CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "labels");
5149
+ const help = entry ? `<span class="term-help" tabindex="0" aria-describedby="tooltip-${criterion.id}-labels"><strong>Why the labels differ:</strong><span class="term-tooltip" id="tooltip-${criterion.id}-labels" role="tooltip">${escapeHtml(entry.definition)}</span></span>` : "<strong>Why the labels differ:</strong>";
5150
+ return `<p class="status-explanation">${help} the ${escapeHtml(criterion.status)} dimension band is calibrated from criterion evidence thresholds and findings, independently of the ${formatScore(criterion.score)}/4.0 weighted score (which falls in the ${numericBand} numeric band).</p>`;
4823
5151
  }
4824
5152
  function scoreBandForPresentation(score) {
4825
5153
  if (score >= 3.5) return "verified";
@@ -4827,20 +5155,24 @@ function scoreBandForPresentation(score) {
4827
5155
  if (score >= 1.5) return "warning";
4828
5156
  return "critical";
4829
5157
  }
4830
- function renderMetric(metric2) {
4831
- const unit = metric2.unit ? ` ${escapeHtml(metric2.unit)}` : "";
4832
- if (metric2.kind === "saturating_count" && metric2.max !== void 0 && metric2.value > metric2.max) {
4833
- return `<strong>${escapeHtml(metric2.label)}</strong><span>${formatMetricValue2(metric2.max)}${unit} (capped; ${formatMetricValue2(metric2.value)} raw)</span>`;
4834
- }
4835
- const max = metric2.max ? `/${formatMetricValue2(metric2.max)}` : "";
4836
- return `<strong>${escapeHtml(metric2.label)}</strong><span>${formatMetricValue2(metric2.value)}${max}${unit}</span>`;
5158
+ function renderMetric(criterion, metric2) {
5159
+ const entry = glossaryEntryForMetric(metric2);
5160
+ const tooltipId = `tooltip-${criterion.id}-${entry.key}`;
5161
+ const formatted = formatCertificateMetricValue(criterion, metric2);
5162
+ const cappedEntry = formatted.includes("capped") ? CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "capped") : void 0;
5163
+ const label = `<span class="term-help" tabindex="0" aria-describedby="${escapeAttribute(tooltipId)}"><strong>${escapeHtml(formatCertificateMetricLabel(metric2))}</strong><span class="term-tooltip" id="${escapeAttribute(tooltipId)}" role="tooltip">${escapeHtml(entry.definition)}</span></span>`;
5164
+ if (!cappedEntry) return `${label}<span>${escapeHtml(formatted)}</span>`;
5165
+ const cappedTooltipId = `${tooltipId}-capped`;
5166
+ return `${label}<span class="term-help metric-value" tabindex="0" aria-describedby="${escapeAttribute(cappedTooltipId)}">${escapeHtml(formatted)}<span class="term-tooltip" id="${escapeAttribute(cappedTooltipId)}" role="tooltip">${escapeHtml(cappedEntry.definition)}</span></span>`;
4837
5167
  }
4838
5168
  function renderCategoryScore(score, coverage, category) {
4839
5169
  const categoryCoverage = coverage.byCategory.find((entry) => entry.category === category);
4840
5170
  return categoryCoverage !== void 0 && categoryCoverage.measured === 0 || score === null ? "not measured" : formatScore(score);
4841
5171
  }
4842
- function renderStatusChip(status) {
4843
- return `<span class="status" data-status="${status}">dimension band: ${status}</span>`;
5172
+ function renderStatusChip(criterionId, status) {
5173
+ const entry = CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "labels");
5174
+ const tooltipId = `tooltip-${criterionId}-status-labels`;
5175
+ return `<span class="status term-help" data-status="${status}" tabindex="0" aria-describedby="${tooltipId}">dimension band: ${status}${entry ? `<span class="term-tooltip" id="${tooltipId}" role="tooltip">${escapeHtml(entry.definition)}</span>` : ""}</span>`;
4844
5176
  }
4845
5177
  function renderEvidenceListItem(criterion, evidence) {
4846
5178
  return `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${renderEvidencePointer(evidence)}</span></li>`;
@@ -4904,9 +5236,6 @@ function renderMachineVerdict(verdict) {
4904
5236
  function formatScore(score) {
4905
5237
  return score.toFixed(1);
4906
5238
  }
4907
- function formatMetricValue2(value) {
4908
- return Number.isInteger(value) ? value.toString() : value.toFixed(2);
4909
- }
4910
5239
  function formatDate(value) {
4911
5240
  return value.slice(0, 10);
4912
5241
  }
@@ -4982,6 +5311,13 @@ dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
4982
5311
  .subscores { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; color: var(--muted); font-family: var(--mono); font-size: 12px; }
4983
5312
  .coverage-note { margin-top: 8px; color: var(--faint); font-family: var(--mono); font-size: 11px; }
4984
5313
  .source-counts { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
5314
+ .relying-party-summary, .glossary { margin-top: 28px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); padding: 22px; }
5315
+ .relying-party-summary dl { display: grid; gap: 14px; margin: 0; }
5316
+ .relying-party-summary dl div { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 18px; }
5317
+ .relying-party-summary dt, .glossary dt { color: var(--text); }
5318
+ .glossary dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 22px; margin: 0; }
5319
+ .glossary dl div { border-top: 1px solid var(--line); padding-top: 10px; }
5320
+ .glossary dd { margin-top: 4px; font-size: 13px; }
4985
5321
  .trust-grid, .evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }
4986
5322
  .scan-limitations-section { margin-top: 28px; border-color: rgba(231, 191, 114, .44); }
4987
5323
  .scan-limitations-section .scan-warning { list-style: none; margin-left: -18px; }
@@ -5011,6 +5347,19 @@ h3 { font-size: 15px; line-height: 1.35; font-weight: 600; margin-bottom: 0; }
5011
5347
  .criterion-metrics li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(238, 244, 247, .08); padding-bottom: 6px; }
5012
5348
  .criterion-metrics strong { color: var(--text); font-weight: 650; }
5013
5349
  .criterion-metrics span { white-space: nowrap; color: var(--muted); }
5350
+ .term-help { position: relative; cursor: help; outline: none; }
5351
+ .term-help:focus-visible { box-shadow: 0 0 0 2px var(--periwinkle); border-radius: 3px; }
5352
+ .term-tooltip {
5353
+ position: absolute; z-index: 100; left: 0; bottom: calc(100% + 8px); width: min(320px, 75vw);
5354
+ visibility: hidden; opacity: 0; pointer-events: none; white-space: normal;
5355
+ border: 1px solid var(--line-strong); border-radius: 6px; padding: 9px 11px;
5356
+ background: #101b2a; color: var(--text); font-family: var(--sans); font-size: 12px;
5357
+ font-weight: 400; line-height: 1.45; text-transform: none; letter-spacing: normal;
5358
+ box-shadow: 0 10px 28px rgba(0, 0, 0, .55);
5359
+ transition: opacity .12s ease;
5360
+ }
5361
+ .term-help:hover > .term-tooltip, .term-help:focus-within > .term-tooltip { visibility: visible; opacity: 1; }
5362
+ .metric-value .term-tooltip { left: auto; right: 0; }
5014
5363
  .status { flex: none; border-radius: 999px; padding: 4px 9px; font-family: var(--mono); font-size: 11px; border: 1px solid var(--line-strong); }
5015
5364
  .status[data-status="verified"] { color: var(--teal); background: var(--teal-weak); border-color: rgba(47, 200, 166, .42); }
5016
5365
  .status[data-status="info"] { color: var(--periwinkle); background: var(--periwinkle-weak); border-color: rgba(147, 166, 218, .4); }
@@ -5029,6 +5378,7 @@ a:hover { text-decoration: underline; text-underline-offset: 2px; }
5029
5378
  @media (max-width: 840px) {
5030
5379
  .certificate { padding: 28px 18px 48px; }
5031
5380
  .hero-grid, .trust-grid, .evidence-grid { grid-template-columns: 1fr; }
5381
+ .relying-party-summary dl div, .glossary dl { grid-template-columns: 1fr; }
5032
5382
  h1 { font-size: 46px; }
5033
5383
  }
5034
5384
  `;
@@ -6784,7 +7134,7 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors) {
6784
7134
  metrics: [
6785
7135
  metric(
6786
7136
  "prod_readiness_primitives",
6787
- "Production-readiness primitive coverage",
7137
+ "Production-readiness basic checks",
6788
7138
  [
6789
7139
  packageJson && (scripts.has("build") || scripts.has("typecheck")),
6790
7140
  workflows.length > 0,
@@ -6795,8 +7145,8 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors) {
6795
7145
  ].filter(Boolean).length,
6796
7146
  6,
6797
7147
  0.55,
6798
- "primitives",
6799
- "Counts distinct static production-readiness primitives instead of treating presence as enough."
7148
+ "checks",
7149
+ "Counts distinct static production-readiness checks instead of treating presence as enough."
6800
7150
  ),
6801
7151
  metric(
6802
7152
  "prod_workflow_depth",
@@ -7137,7 +7487,7 @@ function collectB2PrTraceEvidence(repoPath, repoFiles) {
7137
7487
  // sat well under half credit here even with a healthy PR-trace posture.
7138
7488
  metric(
7139
7489
  "pr_trace_primitives",
7140
- "PR trace primitive coverage",
7490
+ "PR trace basic checks",
7141
7491
  workflows.length + (prTemplate ? 1 : 0) + (branchProtectionDoc ? 1 : 0),
7142
7492
  2,
7143
7493
  0.8,
@@ -10982,18 +11332,18 @@ function renderFindingSummary2(criterion, finding) {
10982
11332
  if (drivers.length === 0) {
10983
11333
  return `${criterion.id} dimension band is ${band} at ${score}/4.0. To improve this dimension, add stronger measurable evidence for ${criterion.title.toLowerCase()}.`;
10984
11334
  }
10985
- const measurements = drivers.map(formatMetricMember2).join("; ");
11335
+ const measurements = drivers.map((metric2) => formatMetricMember2(criterion, metric2)).join("; ");
10986
11336
  const actions = Array.from(
10987
11337
  new Set(
10988
11338
  drivers.map(
10989
- (metric2) => REMEDIATION_BY_METRIC2[metric2.name] ?? `raise the measured ${metric2.label.toLowerCase()}`
11339
+ (metric2) => REMEDIATION_BY_METRIC2[metric2.name] ?? `raise the measured ${formatCertificateMetricLabel(metric2).toLowerCase()}`
10990
11340
  )
10991
11341
  )
10992
11342
  ).join("; ");
10993
11343
  return `${criterion.id} dimension band is ${band} at ${score}/4.0. Lowest contributing measurements: ${measurements}. To improve: ${actions}.`;
10994
11344
  }
10995
11345
  function selectLowestDrivers2(metrics) {
10996
- return [...metrics].filter((metric2) => metric2.max !== void 0).sort((left, right) => {
11346
+ return [...metrics].filter((metric2) => metric2.max !== void 0 && normalizedMetricValue2(metric2) < 1).sort((left, right) => {
10997
11347
  const leftRatio = normalizedMetricValue2(left);
10998
11348
  const rightRatio = normalizedMetricValue2(right);
10999
11349
  if (leftRatio !== rightRatio) return leftRatio - rightRatio;
@@ -11005,17 +11355,8 @@ function normalizedMetricValue2(metric2) {
11005
11355
  if (metric2.max === void 0) return 1;
11006
11356
  return Math.min(metric2.value / metric2.max, 1);
11007
11357
  }
11008
- function formatMetricMember2(metric2) {
11009
- const unit = metric2.unit ? ` ${metric2.unit}` : "";
11010
- const maximum = metric2.max;
11011
- if (maximum === void 0) return `${metric2.label} ${formatMetricValue3(metric2.value)}${unit}`;
11012
- if (metric2.kind === "saturating_count" && metric2.value > maximum) {
11013
- return `${metric2.label} ${formatMetricValue3(maximum)}${unit} capped (${formatMetricValue3(metric2.value)} raw)`;
11014
- }
11015
- return `${metric2.label} ${formatMetricValue3(metric2.value)}/${formatMetricValue3(maximum)}${unit}`;
11016
- }
11017
- function formatMetricValue3(value) {
11018
- return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
11358
+ function formatMetricMember2(criterion, metric2) {
11359
+ return `${formatCertificateMetricLabel(metric2)} ${formatCertificateMetricValue(criterion, metric2)}`;
11019
11360
  }
11020
11361
 
11021
11362
  // src/summary.ts
@@ -11104,6 +11445,10 @@ export {
11104
11445
  external_exports,
11105
11446
  WitanReportSchema,
11106
11447
  formatExternalSourceLine,
11448
+ CERTIFICATE_GLOSSARY,
11449
+ formatCertificateMetricLabel,
11450
+ formatCertificateMetricValue,
11451
+ buildRelyingPartySummary,
11107
11452
  renderWitanHtmlReport,
11108
11453
  renderWitanBadgeEndpoint,
11109
11454
  renderWitanBadgeSvg,
package/dist/index.js CHANGED
@@ -2,8 +2,12 @@
2
2
  import { createRequire as __cejelCreateRequire } from 'node:module';
3
3
  const require = __cejelCreateRequire(import.meta.url);
4
4
  import {
5
+ CERTIFICATE_GLOSSARY,
5
6
  WitanReportSchema,
7
+ buildRelyingPartySummary,
6
8
  createWitanAttestation,
9
+ formatCertificateMetricLabel,
10
+ formatCertificateMetricValue,
7
11
  formatExternalSourceLine,
8
12
  renderWitanBadgeEndpoint,
9
13
  renderWitanBadgeSvg,
@@ -11,7 +15,7 @@ import {
11
15
  runCejelScan,
12
16
  serializeWitanReport,
13
17
  verifyWitanAttestationBinding
14
- } from "./chunk-HG5NAFH5.js";
18
+ } from "./chunk-ITHPLYKX.js";
15
19
  import {
16
20
  init_esm_shims
17
21
  } from "./chunk-LZVKQV3R.js";
@@ -39,7 +43,7 @@ function renderMinScoreLimitationFailure(summary, minScore) {
39
43
  return `Cejel: cannot evaluate the required minimum ${minScore.toFixed(1)}/4.0 because the scan has limited evidence (${summary.scanLimitations.length} limitation${summary.scanLimitations.length === 1 ? "" : "s"}). Resolve the reported limitations and re-scan.
40
44
  `;
41
45
  }
42
- function renderTerminalCertificate(summary) {
46
+ function renderTerminalCertificate(summary, report) {
43
47
  const abstained = summary.verdict === "Insufficient source" || summary.verdict === "Insufficient evidence";
44
48
  if (!abstained && (summary.overallScore === null || summary.codeTrustScore === null || summary.processTrustScore === null)) {
45
49
  throw new Error("A scored Cejel summary must carry numeric headline scores.");
@@ -58,6 +62,18 @@ function renderTerminalCertificate(summary) {
58
62
  ` Process trust: ${formatScore(summary.processTrustScore)}/4.0`,
59
63
  ""
60
64
  ];
65
+ if (report) {
66
+ const relyingPartySummary = buildRelyingPartySummary(report);
67
+ lines.splice(
68
+ 2,
69
+ 0,
70
+ `What was examined: ${relyingPartySummary.examined}`,
71
+ `What was established: ${relyingPartySummary.established}`,
72
+ `What was not established: ${relyingPartySummary.notEstablished}`,
73
+ `What to do next: ${relyingPartySummary.next}`,
74
+ ""
75
+ );
76
+ }
61
77
  if (summary.scanLimitations.length > 0) {
62
78
  lines.push(
63
79
  "LIMITED EVIDENCE \u2014 the score and verdict are qualified by scan limitations:",
@@ -85,6 +101,20 @@ function renderTerminalCertificate(summary) {
85
101
  }
86
102
  lines.push("");
87
103
  }
104
+ const measurements = report?.criteria.flatMap(
105
+ (criterion) => criterion.metrics.map((metric) => ({
106
+ criterionId: criterion.id,
107
+ label: formatCertificateMetricLabel(metric),
108
+ value: formatCertificateMetricValue(criterion, metric)
109
+ }))
110
+ );
111
+ if (!abstained && (measurements?.length ?? 0) > 0) {
112
+ lines.push("Measurements:");
113
+ for (const measurement of measurements ?? []) {
114
+ lines.push(` ${measurement.criterionId} \u2014 ${measurement.label}: ${measurement.value}`);
115
+ }
116
+ lines.push("");
117
+ }
88
118
  if (abstained) {
89
119
  lines.push("See report.json / certificate.html for the full per-criterion detail.");
90
120
  } else if (summary.topFindings.length === 0) {
@@ -118,6 +148,10 @@ function renderTerminalCertificate(summary) {
118
148
  lines.push(` ...and ${remaining} more \u2014 see report.json for the full list.`);
119
149
  }
120
150
  }
151
+ lines.push("", "Plain-language glossary:");
152
+ for (const entry of CERTIFICATE_GLOSSARY) {
153
+ lines.push(` ${entry.term} \u2014 ${entry.definition}`);
154
+ }
121
155
  return `${lines.join("\n")}
122
156
  `;
123
157
  }
@@ -271,7 +305,7 @@ async function runWitanFreeCli(args) {
271
305
  writeFileSync(join(options.outDir, "badge.svg"), renderWitanBadgeSvg(report), "utf8");
272
306
  writeFileSync(join(options.outDir, "summary.json"), JSON.stringify(summary, null, 2), "utf8");
273
307
  if (!options.quiet) {
274
- process.stdout.write(renderTerminalCertificate(summary));
308
+ process.stdout.write(renderTerminalCertificate(summary, report));
275
309
  process.stdout.write(
276
310
  `
277
311
  Wrote:
package/dist/mcp/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  renderWitanBadgeSvg,
10
10
  renderWitanHtmlReport,
11
11
  runCejelScan
12
- } from "../chunk-HG5NAFH5.js";
12
+ } from "../chunk-ITHPLYKX.js";
13
13
  import {
14
14
  __commonJS,
15
15
  __export,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cejel/cejel",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "mcpName": "io.github.BargLabs/cejel",
5
5
  "private": false,
6
6
  "publishConfig": {