@cejel/cejel 0.4.0 → 0.4.2

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.0 and may trigger Microsoft SmartScreen. The release build removes Node's inherited
71
+ > 0.4.2 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.0` is the coordinated release version for npm,
94
+ > **Distribution note:** Cejel `0.4.2` 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`
@@ -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.0`:
143
+ **Docker / OCI.** The current container release is `0.4.2`:
144
144
 
145
145
  ```bash
146
- docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.4.0
146
+ docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.4.2
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.0 .
152
+ docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.4.2 .
153
153
  ```
154
154
 
155
155
  The OCI image carries an SBOM, maximum-mode build provenance, and a signed registry
@@ -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.0"]}'
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.2"]}'
430
430
  openclaw mcp doctor cejel-oci --probe
431
431
  ```
432
432
 
@@ -4236,12 +4236,12 @@ var WitanContentReadSummarySchema = external_exports.object({
4236
4236
  }
4237
4237
  });
4238
4238
  var WitanRepoRefSchema = external_exports.object({
4239
+ // Retained for backward-compatible verification of reports emitted through v0.4.0.
4240
+ // New reports omit the checkout path so their bytes are independent of run location.
4239
4241
  path: external_exports.string().min(1).max(700).optional(),
4240
4242
  url: external_exports.string().url().optional(),
4241
4243
  headSha: external_exports.string().min(7).max(64).optional()
4242
- }).strict().refine((value) => Boolean(value.path ?? value.url), {
4243
- message: "Witan repo reference must include a path or url."
4244
- });
4244
+ }).strict();
4245
4245
  var WitanReportInputSchema = external_exports.object({
4246
4246
  productSlug: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/),
4247
4247
  productDisplayName: external_exports.string().min(1).max(120),
@@ -4531,6 +4531,312 @@ 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 formatCertificateMetricValue(criterion, metric2) {
4775
+ if (isCoverageNotMeasured(criterion, metric2)) {
4776
+ return "no coverage report found \u2014 not measured";
4777
+ }
4778
+ if (metric2.name === "test_to_source_ratio" && metric2.max !== void 0) {
4779
+ const tests = formatMetricNumber(metric2.value);
4780
+ const sources = formatMetricNumber(metric2.max);
4781
+ const comparison = `${tests} test file${metric2.value === 1 ? "" : "s"} / ${sources} source file${metric2.max === 1 ? "" : "s"}`;
4782
+ return metric2.value > metric2.max ? `${comparison} (credit capped at parity)` : comparison;
4783
+ }
4784
+ const displayUnit = metric2.name === "prod_readiness_primitives" || metric2.name === "pr_trace_primitives" ? "checks" : metric2.unit;
4785
+ const unit = displayUnit ? ` ${displayUnit}` : "";
4786
+ if (metric2.max === void 0) return `${formatMetricNumber(metric2.value)}${unit}`;
4787
+ if (metric2.kind === "saturating_count" && metric2.value > metric2.max) {
4788
+ return `${formatMetricNumber(metric2.max)}${unit} (capped; ${formatMetricNumber(metric2.value)} raw)`;
4789
+ }
4790
+ return `${formatMetricNumber(metric2.value)}/${formatMetricNumber(metric2.max)}${unit}`;
4791
+ }
4792
+ function buildRelyingPartySummary(report) {
4793
+ const applicable = report.criteria.filter((criterion) => criterion.status !== "not_applicable");
4794
+ const measured = applicable.filter((criterion) => criterion.status !== "insufficient_data");
4795
+ const findings = report.criteria.reduce((sum, criterion) => sum + criterion.findings.length, 0);
4796
+ const revision = report.repo.headSha ? ` at revision ${report.repo.headSha}` : " from the repository snapshot named in this report; no Git revision was available";
4797
+ const a1 = report.criteria.find((criterion) => criterion.id === "A1");
4798
+ const coverageMetric = a1?.metrics.find((metric2) => metric2.name === "coverage_percent");
4799
+ const gaps = [];
4800
+ if (a1 && coverageMetric && isCoverageNotMeasured(a1, coverageMetric)) {
4801
+ gaps.push(
4802
+ "No coverage report or configured threshold was found, so static coverage was not measured; Cejel does not run the subject's tests."
4803
+ );
4804
+ }
4805
+ const unmeasured = applicable.filter((criterion) => criterion.status === "insufficient_data");
4806
+ if (unmeasured.length > 0) {
4807
+ gaps.push(`The report did not measure ${unmeasured.map((criterion) => criterion.id).join(", ")}.`);
4808
+ }
4809
+ const notApplicable = report.criteria.filter((criterion) => criterion.status === "not_applicable");
4810
+ if (notApplicable.length > 0) {
4811
+ gaps.push(
4812
+ `${notApplicable.map((criterion) => criterion.id).join(", ")} ${notApplicable.length === 1 ? "was" : "were"} marked not applicable and therefore not examined.`
4813
+ );
4814
+ }
4815
+ if ((report.scanLimitations?.length ?? 0) > 0) {
4816
+ const count = report.scanLimitations?.length ?? 0;
4817
+ gaps.push(`${count} scan limitation${count === 1 ? "" : "s"} qualifies the readable evidence.`);
4818
+ }
4819
+ if ((report.contentReadSummary?.skipped ?? 0) > 0) {
4820
+ const skipped = report.contentReadSummary?.skipped ?? 0;
4821
+ gaps.push(
4822
+ `${skipped} content entr${skipped === 1 ? "y was" : "ies were"} skipped for the reasons itemized in the certificate.`
4823
+ );
4824
+ }
4825
+ gaps.push(
4826
+ "Anything absent from the measured evidence was not established: no measurement and no finding are not clearance."
4827
+ );
4828
+ return {
4829
+ examined: `Cejel examined the repository evidence recorded for ${report.productDisplayName}${revision} under ${report.rubricVersion}.`,
4830
+ established: `The report established measured results for ${measured.length} of ${applicable.length} applicable rubric dimensions and recorded ${findings} evidence-backed finding${findings === 1 ? "" : "s"}.`,
4831
+ notEstablished: gaps.join(" "),
4832
+ 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."
4833
+ };
4834
+ }
4835
+ function formatMetricNumber(value) {
4836
+ return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
4837
+ }
4838
+
4839
+ // src/witan/finding-presentation.ts
4534
4840
  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
4841
  var REMEDIATION_BY_METRIC = {
4536
4842
  test_to_source_ratio: "add concrete regression tests for the implementation surface",
@@ -4575,11 +4881,11 @@ function renderFindingSummary(criterion, finding) {
4575
4881
  if (drivers.length === 0) {
4576
4882
  return `${criterion.id} dimension band is ${band} at ${score}/4.0. To improve this dimension, add stronger measurable evidence for ${criterion.title.toLowerCase()}.`;
4577
4883
  }
4578
- const measurements = drivers.map(formatMetricMember).join("; ");
4884
+ const measurements = drivers.map((metric2) => formatMetricMember(criterion, metric2)).join("; ");
4579
4885
  const actions = Array.from(
4580
4886
  new Set(
4581
4887
  drivers.map(
4582
- (metric2) => REMEDIATION_BY_METRIC[metric2.name] ?? `raise the measured ${metric2.label.toLowerCase()}`
4888
+ (metric2) => REMEDIATION_BY_METRIC[metric2.name] ?? `raise the measured ${formatCertificateMetricLabel(metric2).toLowerCase()}`
4583
4889
  )
4584
4890
  )
4585
4891
  ).join("; ");
@@ -4598,17 +4904,8 @@ function normalizedMetricValue(metric2) {
4598
4904
  if (metric2.max === void 0) return 1;
4599
4905
  return Math.min(metric2.value / metric2.max, 1);
4600
4906
  }
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(/\.$/, "");
4907
+ function formatMetricMember(criterion, metric2) {
4908
+ return `${formatCertificateMetricLabel(metric2)} ${formatCertificateMetricValue(criterion, metric2)}`;
4612
4909
  }
4613
4910
 
4614
4911
  // src/witan/html.ts
@@ -4624,6 +4921,7 @@ function renderWitanHtmlReport(report, options = {}) {
4624
4921
  const externalFindings = collectExternalFindings(report.consumedSignals ?? []);
4625
4922
  const coverage = computeMeasuredCoverage(report);
4626
4923
  const gitHistoryUnavailable = !report.repo.headSha;
4924
+ const relyingPartySummary = buildRelyingPartySummary(report);
4627
4925
  return `<!doctype html>
4628
4926
  <html lang="en">
4629
4927
  <head>
@@ -4666,6 +4964,8 @@ function renderWitanHtmlReport(report, options = {}) {
4666
4964
  </div>
4667
4965
  </header>
4668
4966
 
4967
+ ${renderRelyingPartySummary(relyingPartySummary)}
4968
+
4669
4969
  ${(report.scanLimitations?.length ?? 0) > 0 ? `<section class="scan-limitations-section evidence-section" aria-label="Scan limitations">
4670
4970
  <h2>Scan limitations</h2>
4671
4971
  <ul>${(report.scanLimitations ?? []).map(
@@ -4698,11 +4998,32 @@ function renderWitanHtmlReport(report, options = {}) {
4698
4998
  <ul>${renderExternalFindingItems(externalFindings).join("")}</ul>
4699
4999
  </article>
4700
5000
  </section>` : ""}
5001
+
5002
+ ${renderGlossary(glossaryEntriesForReport(report))}
4701
5003
  </main>
4702
5004
  </body>
4703
5005
  </html>
4704
5006
  `;
4705
5007
  }
5008
+ function renderRelyingPartySummary(summary) {
5009
+ return `<section class="relying-party-summary" aria-labelledby="relying-party-heading">
5010
+ <h2 id="relying-party-heading">How to read this certificate</h2>
5011
+ <dl>
5012
+ <div><dt>What was examined</dt><dd>${escapeHtml(summary.examined)}</dd></div>
5013
+ <div><dt>What was established</dt><dd>${escapeHtml(summary.established)}</dd></div>
5014
+ <div><dt>What was not established</dt><dd>${escapeHtml(summary.notEstablished)}</dd></div>
5015
+ <div><dt>What to do next</dt><dd>${escapeHtml(summary.next)}</dd></div>
5016
+ </dl>
5017
+ </section>`;
5018
+ }
5019
+ function renderGlossary(entries) {
5020
+ return `<section class="glossary" aria-labelledby="glossary-heading">
5021
+ <h2 id="glossary-heading">Plain-language glossary</h2>
5022
+ <dl>${entries.map(
5023
+ (entry) => `<div><dt id="glossary-${escapeAttribute(entry.key)}">${escapeHtml(entry.term)}</dt><dd>${escapeHtml(entry.definition)}</dd></div>`
5024
+ ).join("")}</dl>
5025
+ </section>`;
5026
+ }
4706
5027
  function renderContentReadSummary(report) {
4707
5028
  const summary = report.contentReadSummary;
4708
5029
  if (!summary || summary.skipped === 0) return "";
@@ -4791,7 +5112,7 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
4791
5112
  ...criterion.evidence.map(renderEvidencePointer),
4792
5113
  ...criterion.findings.map((finding) => renderFindingEvidence(criterion, finding))
4793
5114
  ];
4794
- const metrics = criterion.metrics.map(renderMetric);
5115
+ const metrics = criterion.metrics.map((metric2) => renderMetric(criterion, metric2));
4795
5116
  const statusReconciliation = renderStatusReconciliation(criterion);
4796
5117
  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
5118
  return `<article class="criterion">
@@ -4800,7 +5121,7 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
4800
5121
  <div class="criterion-id">${escapeHtml(criterion.id)}</div>
4801
5122
  <h3>${escapeHtml(criterion.title)}</h3>
4802
5123
  </div>
4803
- ${renderStatusChip(criterion.status)}
5124
+ ${renderStatusChip(criterion.id, criterion.status)}
4804
5125
  </div>
4805
5126
  <div class="criterion-score">${criterion.status === "not_applicable" ? "N/A" : criterion.status === "insufficient_data" ? "No data" : formatScore(criterion.score)}</div>
4806
5127
  ${statusReconciliation}
@@ -4819,7 +5140,9 @@ function renderStatusReconciliation(criterion) {
4819
5140
  }
4820
5141
  const numericBand = scoreBandForPresentation(criterion.score);
4821
5142
  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>`;
5143
+ const entry = CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "labels");
5144
+ 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>";
5145
+ 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
5146
  }
4824
5147
  function scoreBandForPresentation(score) {
4825
5148
  if (score >= 3.5) return "verified";
@@ -4827,20 +5150,24 @@ function scoreBandForPresentation(score) {
4827
5150
  if (score >= 1.5) return "warning";
4828
5151
  return "critical";
4829
5152
  }
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>`;
5153
+ function renderMetric(criterion, metric2) {
5154
+ const entry = glossaryEntryForMetric(metric2);
5155
+ const tooltipId = `tooltip-${criterion.id}-${entry.key}`;
5156
+ const formatted = formatCertificateMetricValue(criterion, metric2);
5157
+ const cappedEntry = formatted.includes("capped") ? CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "capped") : void 0;
5158
+ 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>`;
5159
+ if (!cappedEntry) return `${label}<span>${escapeHtml(formatted)}</span>`;
5160
+ const cappedTooltipId = `${tooltipId}-capped`;
5161
+ 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
5162
  }
4838
5163
  function renderCategoryScore(score, coverage, category) {
4839
5164
  const categoryCoverage = coverage.byCategory.find((entry) => entry.category === category);
4840
5165
  return categoryCoverage !== void 0 && categoryCoverage.measured === 0 || score === null ? "not measured" : formatScore(score);
4841
5166
  }
4842
- function renderStatusChip(status) {
4843
- return `<span class="status" data-status="${status}">dimension band: ${status}</span>`;
5167
+ function renderStatusChip(criterionId, status) {
5168
+ const entry = CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "labels");
5169
+ const tooltipId = `tooltip-${criterionId}-status-labels`;
5170
+ 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
5171
  }
4845
5172
  function renderEvidenceListItem(criterion, evidence) {
4846
5173
  return `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${renderEvidencePointer(evidence)}</span></li>`;
@@ -4875,7 +5202,7 @@ function renderPathEvidenceTarget(evidence) {
4875
5202
  return [pathTarget, hashTarget].filter(Boolean).join(" \xB7 ");
4876
5203
  }
4877
5204
  function renderRepo(report) {
4878
- const repo = report.repo.path ?? report.repo.url ?? "unknown";
5205
+ const repo = report.repo.path ?? report.repo.url ?? report.productSlug;
4879
5206
  return report.repo.headSha ? `${repo} @ ${report.repo.headSha}` : repo;
4880
5207
  }
4881
5208
  function renderVerdict(score) {
@@ -4904,9 +5231,6 @@ function renderMachineVerdict(verdict) {
4904
5231
  function formatScore(score) {
4905
5232
  return score.toFixed(1);
4906
5233
  }
4907
- function formatMetricValue2(value) {
4908
- return Number.isInteger(value) ? value.toString() : value.toFixed(2);
4909
- }
4910
5234
  function formatDate(value) {
4911
5235
  return value.slice(0, 10);
4912
5236
  }
@@ -4982,6 +5306,13 @@ dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
4982
5306
  .subscores { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; color: var(--muted); font-family: var(--mono); font-size: 12px; }
4983
5307
  .coverage-note { margin-top: 8px; color: var(--faint); font-family: var(--mono); font-size: 11px; }
4984
5308
  .source-counts { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
5309
+ .relying-party-summary, .glossary { margin-top: 28px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); padding: 22px; }
5310
+ .relying-party-summary dl { display: grid; gap: 14px; margin: 0; }
5311
+ .relying-party-summary dl div { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 18px; }
5312
+ .relying-party-summary dt, .glossary dt { color: var(--text); }
5313
+ .glossary dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 22px; margin: 0; }
5314
+ .glossary dl div { border-top: 1px solid var(--line); padding-top: 10px; }
5315
+ .glossary dd { margin-top: 4px; font-size: 13px; }
4985
5316
  .trust-grid, .evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }
4986
5317
  .scan-limitations-section { margin-top: 28px; border-color: rgba(231, 191, 114, .44); }
4987
5318
  .scan-limitations-section .scan-warning { list-style: none; margin-left: -18px; }
@@ -5011,6 +5342,18 @@ h3 { font-size: 15px; line-height: 1.35; font-weight: 600; margin-bottom: 0; }
5011
5342
  .criterion-metrics li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(238, 244, 247, .08); padding-bottom: 6px; }
5012
5343
  .criterion-metrics strong { color: var(--text); font-weight: 650; }
5013
5344
  .criterion-metrics span { white-space: nowrap; color: var(--muted); }
5345
+ .term-help { position: relative; cursor: help; outline: none; }
5346
+ .term-help:focus-visible { box-shadow: 0 0 0 2px var(--periwinkle); border-radius: 3px; }
5347
+ .term-tooltip {
5348
+ position: absolute; z-index: 10; left: 0; bottom: calc(100% + 8px); width: min(320px, 75vw);
5349
+ visibility: hidden; opacity: 0; pointer-events: none; white-space: normal;
5350
+ border: 1px solid var(--line-strong); border-radius: 6px; padding: 9px 11px;
5351
+ background: #101b2a; color: var(--text); font-family: var(--sans); font-size: 12px;
5352
+ font-weight: 400; line-height: 1.45; text-transform: none; letter-spacing: normal;
5353
+ transition: opacity .12s ease;
5354
+ }
5355
+ .term-help:hover > .term-tooltip, .term-help:focus-within > .term-tooltip { visibility: visible; opacity: 1; }
5356
+ .metric-value .term-tooltip { left: auto; right: 0; }
5014
5357
  .status { flex: none; border-radius: 999px; padding: 4px 9px; font-family: var(--mono); font-size: 11px; border: 1px solid var(--line-strong); }
5015
5358
  .status[data-status="verified"] { color: var(--teal); background: var(--teal-weak); border-color: rgba(47, 200, 166, .42); }
5016
5359
  .status[data-status="info"] { color: var(--periwinkle); background: var(--periwinkle-weak); border-color: rgba(147, 166, 218, .4); }
@@ -5029,6 +5372,7 @@ a:hover { text-decoration: underline; text-underline-offset: 2px; }
5029
5372
  @media (max-width: 840px) {
5030
5373
  .certificate { padding: 28px 18px 48px; }
5031
5374
  .hero-grid, .trust-grid, .evidence-grid { grid-template-columns: 1fr; }
5375
+ .relying-party-summary dl div, .glossary dl { grid-template-columns: 1fr; }
5032
5376
  h1 { font-size: 46px; }
5033
5377
  }
5034
5378
  `;
@@ -5359,7 +5703,6 @@ function buildWitanInputFromRepoUntracked(options) {
5359
5703
  productSlug: options.productSlug,
5360
5704
  productDisplayName: options.productDisplayName,
5361
5705
  repo: {
5362
- path: options.repoPath,
5363
5706
  ...headSha ? { headSha } : {}
5364
5707
  },
5365
5708
  generatedAt,
@@ -10983,11 +11326,11 @@ function renderFindingSummary2(criterion, finding) {
10983
11326
  if (drivers.length === 0) {
10984
11327
  return `${criterion.id} dimension band is ${band} at ${score}/4.0. To improve this dimension, add stronger measurable evidence for ${criterion.title.toLowerCase()}.`;
10985
11328
  }
10986
- const measurements = drivers.map(formatMetricMember2).join("; ");
11329
+ const measurements = drivers.map((metric2) => formatMetricMember2(criterion, metric2)).join("; ");
10987
11330
  const actions = Array.from(
10988
11331
  new Set(
10989
11332
  drivers.map(
10990
- (metric2) => REMEDIATION_BY_METRIC2[metric2.name] ?? `raise the measured ${metric2.label.toLowerCase()}`
11333
+ (metric2) => REMEDIATION_BY_METRIC2[metric2.name] ?? `raise the measured ${formatCertificateMetricLabel(metric2).toLowerCase()}`
10991
11334
  )
10992
11335
  )
10993
11336
  ).join("; ");
@@ -11006,17 +11349,8 @@ function normalizedMetricValue2(metric2) {
11006
11349
  if (metric2.max === void 0) return 1;
11007
11350
  return Math.min(metric2.value / metric2.max, 1);
11008
11351
  }
11009
- function formatMetricMember2(metric2) {
11010
- const unit = metric2.unit ? ` ${metric2.unit}` : "";
11011
- const maximum = metric2.max;
11012
- if (maximum === void 0) return `${metric2.label} ${formatMetricValue3(metric2.value)}${unit}`;
11013
- if (metric2.kind === "saturating_count" && metric2.value > maximum) {
11014
- return `${metric2.label} ${formatMetricValue3(maximum)}${unit} capped (${formatMetricValue3(metric2.value)} raw)`;
11015
- }
11016
- return `${metric2.label} ${formatMetricValue3(metric2.value)}/${formatMetricValue3(maximum)}${unit}`;
11017
- }
11018
- function formatMetricValue3(value) {
11019
- return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
11352
+ function formatMetricMember2(criterion, metric2) {
11353
+ return `${formatCertificateMetricLabel(metric2)} ${formatCertificateMetricValue(criterion, metric2)}`;
11020
11354
  }
11021
11355
 
11022
11356
  // src/summary.ts
@@ -11105,6 +11439,10 @@ export {
11105
11439
  external_exports,
11106
11440
  WitanReportSchema,
11107
11441
  formatExternalSourceLine,
11442
+ CERTIFICATE_GLOSSARY,
11443
+ formatCertificateMetricLabel,
11444
+ formatCertificateMetricValue,
11445
+ buildRelyingPartySummary,
11108
11446
  renderWitanHtmlReport,
11109
11447
  renderWitanBadgeEndpoint,
11110
11448
  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-PSYXFAVU.js";
18
+ } from "./chunk-WLOMKEFR.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-PSYXFAVU.js";
12
+ } from "../chunk-WLOMKEFR.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.0",
3
+ "version": "0.4.2",
4
4
  "mcpName": "io.github.BargLabs/cejel",
5
5
  "private": false,
6
6
  "publishConfig": {