@cejel/cejel 0.4.4 → 0.4.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
@@ -79,7 +79,7 @@ if (-not $expected -or $actual -ne $expected.ToLowerInvariant()) {
79
79
  ```
80
80
 
81
81
  > **Windows signing status:** `cejel-Windows-x86_64.exe` is intentionally unsigned in
82
- > 0.4.4 and may trigger Microsoft SmartScreen. The release build removes Node's inherited
82
+ > 0.4.5 and may trigger Microsoft SmartScreen. The release build removes Node's inherited
83
83
  > signature before SEA injection and fails unless Windows reports the result as `NotSigned`;
84
84
  > it does not ship an invalid signature. Before running it, verify `SHA256SUMS` and the
85
85
  > GitHub build-provenance attestation. Each binary also has an attached SPDX SBOM and an
@@ -102,7 +102,7 @@ docker run --rm --network=none -v "$PWD:/w" -w /w -v "$PWD/cejel:/cejel:ro" debi
102
102
  npx @cejel/cejel@latest .
103
103
  ```
104
104
 
105
- > **Distribution note:** Cejel `0.4.4` is the coordinated release version for npm,
105
+ > **Distribution note:** Cejel `0.4.5` is the coordinated release version for npm,
106
106
  > standalone binaries, Docker/OCI, GitHub Action, Homebrew, and MCP Registry.
107
107
 
108
108
  `npx` can reuse a stale cached package. Force the current npm release with the `@latest`
@@ -119,6 +119,16 @@ name while keeping the repository-derived stable slug:
119
119
  npx @cejel/cejel@latest . --name "Customer Portal"
120
120
  ```
121
121
 
122
+ From 0.4.5, use `--product-name` when two checkout directories must emit the
123
+ same identity fields. The supplied value sets both the display name and its slugified stable ID:
124
+
125
+ ```bash
126
+ npx @cejel/cejel@latest . --product-name "Customer Portal"
127
+ ```
128
+
129
+ Product identity is caller context, not scored repository evidence, and is excluded from
130
+ certificate byte-comparison claims.
131
+
122
132
  The npm package is scoped as `@cejel/cejel`; its executable remains the short command `cejel`.
123
133
 
124
134
  **GitHub Action** — score every PR and publish the badge:
@@ -151,16 +161,16 @@ gh attestation verify ./cejel-Windows-x86_64.exe -R BargLabs/cejel
151
161
  This is cryptographically signed provenance. It is distinct from Apple Developer ID or
152
162
  Microsoft Authenticode code-signing.
153
163
 
154
- **Docker / OCI.** The current container release is `0.4.4`:
164
+ **Docker / OCI.** The current container release is `0.4.5`:
155
165
 
156
166
  ```bash
157
- docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.4.4
167
+ docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.4.5
158
168
  ```
159
169
 
160
170
  The image defaults to `cejel-mcp` over stdio. To use the CLI instead:
161
171
 
162
172
  ```bash
163
- docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.4.4 .
173
+ docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.4.5 .
164
174
  ```
165
175
 
166
176
  The OCI image carries an SBOM, maximum-mode build provenance, and a signed registry
@@ -253,9 +263,21 @@ prints that boundary on every successful verification.
253
263
  - `--min-score <n>` — exit non-zero if the overall score is below `n` (0–4), or if Cejel
254
264
  abstains and therefore cannot evaluate the threshold; used by the GitHub Action's optional
255
265
  threshold gate
266
+ - `--product-name <name>` — Set both the caller-context
267
+ display name and slugified product ID so differently named checkouts can emit identical
268
+ certificates.
269
+ - `--name <display>` — override only the displayed project name while leaving the derived slug
270
+ unchanged; retained for compatibility
256
271
  - `--ingest <file|glob>` — fold another scanner's output into the score (repeatable). Accepts
257
272
  SARIF, OpenSSF Scorecard JSON, or the generic Cejel external-signal shape — format is
258
273
  auto-detected. See "Aggregate your scanners" below.
274
+ - `--rubric-pin <version>` — Explicit opt-in only: pin an alternate rubric instead of the
275
+ calibrated public default. It
276
+ accepts the calibrated version or any published prospective rubric
277
+ (`witan-rubric-v18-prospective-*` through `v22`) and fails closed, naming what was accepted, on
278
+ anything else. Omit this flag for the calibrated result every default scan produces — a
279
+ prospective pin carries no precision/recall claim, and the terminal certificate, HTML
280
+ certificate, and Markdown report all state that plainly whenever one is used.
259
281
  - `--quiet` — suppress the terminal certificate (files are still written)
260
282
  - `-h`, `--help` — print usage and exit successfully
261
283
  - `-v`, `--version` — print the version derived from the package manifest and exit successfully
@@ -463,7 +485,7 @@ The OCI image is an alternative when Docker is the preferred execution boundary.
463
485
  the host path with the repository OpenClaw should allow Cejel to read:
464
486
 
465
487
  ```bash
466
- npx -y openclaw@latest mcp set cejel-oci '{"command":"docker","args":["run","--rm","-i","-v","/absolute/path/to/repo:/workspace:ro","ghcr.io/barglabs/cejel:0.4.4"]}'
488
+ npx -y openclaw@latest mcp set cejel-oci '{"command":"docker","args":["run","--rm","-i","-v","/absolute/path/to/repo:/workspace:ro","ghcr.io/barglabs/cejel:0.4.5"]}'
467
489
  npx -y openclaw@latest mcp doctor cejel-oci --probe
468
490
  ```
469
491
 
@@ -5,7 +5,7 @@ import {
5
5
  deriveProductIdentity,
6
6
  runCejelScan,
7
7
  serializeWitanReport
8
- } from "../chunk-PJEKYFVM.js";
8
+ } from "../chunk-4IXESHG7.js";
9
9
  import {
10
10
  listCejelLlmPackFiles
11
11
  } from "../chunk-RW6YJVHZ.js";
@@ -130,7 +130,16 @@ var WitanCriterionMetricSchema = external_exports.object({
130
130
  kind: external_exports.enum(["ratio", "saturating_count"]).optional(),
131
131
  weight: external_exports.number().positive().max(1).default(1),
132
132
  unit: external_exports.string().min(1).max(40).optional(),
133
- description: external_exports.string().min(1).max(300).optional()
133
+ description: external_exports.string().min(1).max(300).optional(),
134
+ presentation: external_exports.object({
135
+ components: external_exports.array(
136
+ external_exports.object({
137
+ label: external_exports.string().min(1).max(120),
138
+ count: external_exports.number().int().nonnegative()
139
+ }).strict()
140
+ ).min(1).optional(),
141
+ condition: external_exports.string().min(1).max(240).optional()
142
+ }).strict().optional()
134
143
  }).strict();
135
144
  var WitanCriterionSignalSchema = external_exports.object({
136
145
  criterionId: WitanCriterionIdSchema,
@@ -367,6 +376,45 @@ var WitanAttestationStatementSchema = external_exports.object({
367
376
  }).strict()
368
377
  }).strict();
369
378
 
379
+ // src/witan/rubric-version.ts
380
+ init_esm_shims();
381
+ var WITAN_RUBRIC_VERSION_V9 = "witan-rubric-v9-2026-07-22";
382
+ var WITAN_RUBRIC_VERSION_V10 = "witan-rubric-v10-2026-07-23";
383
+ var WITAN_RUBRIC_VERSION_V11 = "witan-rubric-v11-2026-07-23";
384
+ var WITAN_RUBRIC_VERSION_V12 = "witan-rubric-v12-2026-07-23";
385
+ var WITAN_RUBRIC_VERSION_V13 = "witan-rubric-v13-2026-07-23";
386
+ var WITAN_RUBRIC_VERSION_V14 = "witan-rubric-v14-2026-07-23";
387
+ var WITAN_RUBRIC_VERSION_V15 = "witan-rubric-v15-2026-07-24";
388
+ var WITAN_RUBRIC_VERSION_V16 = "witan-rubric-v16-2026-07-24";
389
+ var WITAN_RUBRIC_VERSION_V17 = "witan-rubric-v17-2026-07-24";
390
+ var WITAN_RUBRIC_VERSION_V18 = "witan-rubric-v18-prospective-2026-07-25";
391
+ var WITAN_RUBRIC_VERSION_V19 = "witan-rubric-v19-prospective-2026-08-09";
392
+ var WITAN_RUBRIC_VERSION_V20 = "witan-rubric-v20-prospective-2026-08-10";
393
+ var WITAN_RUBRIC_VERSION_V21 = "witan-rubric-v21-prospective-2026-08-10";
394
+ var WITAN_RUBRIC_VERSION_V22 = "witan-rubric-v22-prospective-2026-08-10";
395
+ var WITAN_LAST_CALIBRATED_RUBRIC_VERSION = WITAN_RUBRIC_VERSION_V17;
396
+ var WITAN_PROSPECTIVE_RUBRIC_VERSIONS = Object.freeze([
397
+ WITAN_RUBRIC_VERSION_V18,
398
+ WITAN_RUBRIC_VERSION_V19,
399
+ WITAN_RUBRIC_VERSION_V20,
400
+ WITAN_RUBRIC_VERSION_V21,
401
+ WITAN_RUBRIC_VERSION_V22
402
+ ]);
403
+ var WITAN_SELECTABLE_RUBRIC_VERSIONS = Object.freeze([
404
+ WITAN_LAST_CALIBRATED_RUBRIC_VERSION,
405
+ ...WITAN_PROSPECTIVE_RUBRIC_VERSIONS
406
+ ]);
407
+ function assertSelectableRubricVersion(rubricVersion) {
408
+ if (WITAN_SELECTABLE_RUBRIC_VERSIONS.includes(rubricVersion)) return;
409
+ throw new Error(
410
+ `Cejel: unrecognized rubric version: "${rubricVersion}". Accepted values: ${WITAN_SELECTABLE_RUBRIC_VERSIONS.join(", ")}.`
411
+ );
412
+ }
413
+ function isProspectiveRubricVersion(rubricVersion) {
414
+ return WITAN_PROSPECTIVE_RUBRIC_VERSIONS.includes(rubricVersion);
415
+ }
416
+ var PROSPECTIVE_RUBRIC_NOTICE = "PROSPECTIVE / UNCALIBRATED RUBRIC \u2014 this rubric has not cleared a calibration gate. Scores, verdicts, and findings produced under it carry no precision/recall claim and are not comparable to a calibrated certificate.";
417
+
370
418
  // src/witan/html.ts
371
419
  init_esm_shims();
372
420
 
@@ -476,6 +524,45 @@ init_esm_shims();
476
524
 
477
525
  // src/witan/certificate-presentation.ts
478
526
  init_esm_shims();
527
+ var CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE = "Product name and slug are caller context and are excluded from certificate byte-comparison claims.";
528
+ var CERTIFICATE_METRIC_REGISTRY = {
529
+ test_to_source_ratio: { displayLabel: "Test-to-source file ratio" },
530
+ coverage_percent: { displayLabel: "Static coverage percentage" },
531
+ verification_script_ratio: { displayLabel: "Verification script ratio" },
532
+ non_hollow_test_share: { displayLabel: "Non-hollow test share" },
533
+ secret_cleanliness: { displayLabel: "Secret cleanliness" },
534
+ env_handling_depth: { displayLabel: "Environment handling depth" },
535
+ rls_policy_count: { displayLabel: "RLS policy count" },
536
+ tenant_scope_ratio: { displayLabel: "Tenant-scoped schema ratio" },
537
+ crypto_comparison_hygiene: { displayLabel: "Crypto comparison hygiene" },
538
+ prod_readiness_primitives: { displayLabel: "Production-readiness basic checks" },
539
+ prod_workflow_depth: {
540
+ displayLabel: "Production workflow depth (automated build/test/release pipeline)"
541
+ },
542
+ observability_depth: { displayLabel: "Observability depth" },
543
+ rollback_safety_depth: { displayLabel: "Rollback and migration-safety depth" },
544
+ dependency_automation_ratio: { displayLabel: "Dependency automation ratio" },
545
+ pinned_dependency_ratio: { displayLabel: "Pinned dependency ratio" },
546
+ lockfile_coverage: { displayLabel: "Lockfile coverage" },
547
+ declared_version_range_ratio: { displayLabel: "Declared version range ratio" },
548
+ dependency_count_sanity: { displayLabel: "Dependency count sanity" },
549
+ claim_match_rate: { displayLabel: "Implementation-to-claim-source file ratio" },
550
+ claim_source_depth: { displayLabel: "Claim source depth" },
551
+ reconciliation_artifact_depth: { displayLabel: "Reconciliation artifact depth" },
552
+ pr_trace_primitives: { displayLabel: "PR trace basic checks" },
553
+ pr_merge_ratio: { displayLabel: "Recent commits with recognizable PR references" },
554
+ ci_script_depth: { displayLabel: "CI verification depth" },
555
+ default_branch_ci_depth: {
556
+ displayLabel: "PR-gate CI workflow count (automated build/test pipeline)"
557
+ },
558
+ audit_artifact_depth: { displayLabel: "Audit artifact depth" },
559
+ audit_freshness_depth: { displayLabel: "Audit freshness depth" },
560
+ kill_switch_fail_safe_present: { displayLabel: "Un-overridable kill-switch present" },
561
+ human_gate_documented: { displayLabel: "Human gate documented" },
562
+ fail_closed_privilege_check: { displayLabel: "Fail-closed privilege check present" },
563
+ privilege_escalation_cleanliness: { displayLabel: "Privilege-escalation cleanliness" },
564
+ protected_path_review_gate: { displayLabel: "Protected-path review gate" }
565
+ };
479
566
  var CERTIFICATE_GLOSSARY = [
480
567
  {
481
568
  key: "labels",
@@ -491,188 +578,194 @@ var CERTIFICATE_GLOSSARY = [
491
578
  },
492
579
  {
493
580
  key: "test-to-source-file-ratio",
494
- term: "test-to-source file ratio",
581
+ term: "Test-to-source file ratio",
495
582
  definition: "The number of detected test files compared with detected source files. A larger implementation surface needs a correspondingly visible test surface.",
496
583
  metricNames: ["test_to_source_ratio"]
497
584
  },
498
585
  {
499
586
  key: "static-coverage",
500
- term: "static coverage",
587
+ term: "Static coverage percentage",
501
588
  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.",
502
589
  metricNames: ["coverage_percent"]
503
590
  },
504
591
  {
505
592
  key: "verification-script",
506
- term: "verification script",
507
- definition: "the checks that run automatically: tests, linting, type-checking, build",
593
+ term: "Verification script ratio",
594
+ definition: "A capped scoring input that adds one for each detected test, coverage, lint, or type-check command and one for each detected test-runner configuration file. The raw count is capped at four for scoring. Build is not measured by this metric.",
508
595
  metricNames: ["verification_script_ratio"]
509
596
  },
510
597
  {
511
598
  key: "non-hollow",
512
- term: "non-hollow",
599
+ term: "Non-hollow test share",
513
600
  definition: "test files that actually assert something rather than being empty or skipped",
514
601
  metricNames: ["non_hollow_test_share"]
515
602
  },
516
603
  {
517
604
  key: "secrets",
518
- term: "secrets",
519
- definition: "credentials like API keys and tokens",
605
+ term: "Secret cleanliness",
606
+ definition: "A binary result. Clean means the static repository-tree and reachable-history scan produced no critical secret finding. It is not a count of files containing secrets.",
520
607
  metricNames: ["secret_cleanliness"]
521
608
  },
522
609
  {
523
610
  key: "environment-handling-depth",
524
- term: "environment handling depth",
525
- definition: "The visible controls for keeping environment-specific configuration and credentials out of source. Unsafe configuration handling can expose production access.",
611
+ term: "Environment handling depth",
612
+ definition: "A count of three named practices: an environment template file, a .gitignore rule for environment files, and environment reads in implementation code in a supported language.",
526
613
  metricNames: ["env_handling_depth"]
527
614
  },
528
615
  {
529
616
  key: "rls-policy-count",
530
617
  term: "RLS policy count",
531
- definition: "The number of detected database row-level security policies. Those policies can enforce which rows each tenant or user may reach.",
618
+ definition: "A selected-rubric static count of row-level-security markers. The calibrated detector counts CREATE POLICY, ENABLE ROW LEVEL SECURITY, and FORCE ROW LEVEL SECURITY matches. Prospective native-RLS detection counts repository-native CREATE POLICY definitions.",
532
619
  metricNames: ["rls_policy_count"]
533
620
  },
534
621
  {
535
622
  key: "tenant-scoped-schema-ratio",
536
- term: "tenant-scoped schema ratio",
537
- 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.",
623
+ term: "Tenant-scoped schema ratio",
624
+ definition: "The share of detected data-layer or migration files carrying the selected rubric's tenant-scope marker. Prospective native-RLS detection instead uses repository-native scoped storage files. It is a file-level static proxy, not a table-by-table proof.",
538
625
  metricNames: ["tenant_scope_ratio"]
539
626
  },
540
627
  {
541
628
  key: "crypto-comparison-hygiene",
542
- term: "crypto comparison hygiene",
543
- 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.",
629
+ term: "Crypto comparison hygiene",
630
+ definition: "A conditional binary result shown only when production code exposes a signing, HMAC, or secret-comparison surface. Clean means no detected plain-equality secret comparison or non-canonical JSON signing pattern. Detected constant-time comparison and canonical serialization are positive evidence, but the metric does not require both when only one relevant surface exists.",
544
631
  metricNames: ["crypto_comparison_hygiene"]
545
632
  },
546
633
  {
547
- key: "basic-checks",
548
- term: "basic checks",
549
- 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.",
550
- metricNames: ["prod_readiness_primitives", "pr_trace_primitives"]
634
+ key: "production-readiness-basic-checks",
635
+ term: "Production-readiness basic checks",
636
+ definition: "A count of six named checks: a build or type-check command, a CI workflow, deployment configuration, an environment template, a health or readiness signal, and an error boundary.",
637
+ metricNames: ["prod_readiness_primitives"]
638
+ },
639
+ {
640
+ key: "pr-trace-basic-checks",
641
+ term: "PR trace basic checks",
642
+ definition: "A capped scoring input that counts detected CI workflow files, a pull-request template, and a review-gate record such as CODEOWNERS or branch-protection documentation. The raw count is capped at two for scoring.",
643
+ metricNames: ["pr_trace_primitives"]
551
644
  },
552
645
  {
553
646
  key: "workflow-depth",
554
- term: "workflow depth",
555
- 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.",
647
+ term: "Production workflow depth (automated build/test/release pipeline)",
648
+ definition: "The number of detected CI workflow files plus deployment-configuration files, capped at six for scoring. It counts files, not team policies or distinct pipeline stages.",
556
649
  metricNames: ["prod_workflow_depth"]
557
650
  },
558
651
  {
559
652
  key: "observability-depth",
560
- term: "observability depth",
561
- definition: "The visible health, logging, and error-reporting controls. Operators need evidence that failures can be detected and investigated.",
653
+ term: "Observability depth",
654
+ definition: "The number of implementation files containing a recognized logging, metrics, tracing, or error-reporting marker, capped at four for scoring.",
562
655
  metricNames: ["observability_depth"]
563
656
  },
564
657
  {
565
658
  key: "rollback-and-migration-safety-depth",
566
- term: "rollback and migration-safety depth",
567
- 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.",
659
+ term: "Rollback and migration-safety depth",
660
+ definition: "The number of files under recognized documentation, migration, or script paths containing a rollback, reversal, undo, or migration-safety marker, capped at four for scoring.",
568
661
  metricNames: ["rollback_safety_depth"]
569
662
  },
570
663
  {
571
664
  key: "dependency-automation-ratio",
572
- term: "dependency automation ratio",
665
+ term: "Dependency automation ratio",
573
666
  definition: "The share of expected automated dependency-update and audit controls that are present. Stale or vulnerable packages otherwise depend on manual discovery.",
574
667
  metricNames: ["dependency_automation_ratio"]
575
668
  },
576
669
  {
577
670
  key: "pinned-dependency-ratio",
578
- term: "pinned dependency ratio",
671
+ term: "Pinned dependency ratio",
579
672
  definition: "The share of application dependencies fixed to exact versions. Exact inputs make deployed builds more reproducible.",
580
673
  metricNames: ["pinned_dependency_ratio"]
581
674
  },
582
675
  {
583
676
  key: "lockfile-coverage",
584
- term: "lockfile coverage",
585
- definition: "Whether the package manifests are backed by committed lockfiles. Lockfiles record the exact dependency versions selected for a build.",
677
+ term: "Lockfile coverage",
678
+ definition: "A binary check for at least one detected lockfile in the scanned package or its covering monorepo root. It does not compare each manifest with a separate lockfile.",
586
679
  metricNames: ["lockfile_coverage"]
587
680
  },
588
681
  {
589
682
  key: "declared-version-range-ratio",
590
- term: "declared version range ratio",
591
- definition: "The share of library dependencies with an explicit compatible version range. Unconstrained dependencies can change without a deliberate source change.",
683
+ term: "Declared version range ratio",
684
+ definition: "For a library or CLI, the share of detected dependency specifications carrying any explicit constraint, including an exact version or a compatible range. Bare names, *, latest, and x do not count.",
592
685
  metricNames: ["declared_version_range_ratio"]
593
686
  },
594
687
  {
595
688
  key: "dependency-count-sanity",
596
- term: "dependency count sanity",
597
- definition: "A bounded check for an unusually long list of direct dependencies for a library. Every direct dependency adds maintenance and supply-chain exposure.",
689
+ term: "Dependency count sanity",
690
+ definition: "A low-weight library/CLI metric with full credit through 120 detected dependency specifications across manifests, then linearly declining to zero over the next 240.",
598
691
  metricNames: ["dependency_count_sanity"]
599
692
  },
600
693
  {
601
694
  key: "claim-match-rate",
602
- term: "claim match rate",
603
- definition: "The share of documented product claims that match visible repository evidence. An acceptance claim should be checkable against the delivered revision.",
695
+ term: "Implementation-to-claim-source file ratio",
696
+ definition: "A bounded file-count ratio: detected implementation-file count divided by that count plus detected headline claim-source document count. It does not verify whether individual prose claims are true.",
604
697
  metricNames: ["claim_match_rate"]
605
698
  },
606
699
  {
607
700
  key: "claim-source-depth",
608
- term: "claim source depth",
609
- definition: "How much concrete, checkable claim material is present. Vague or missing claims give a relying party nothing specific to verify.",
701
+ term: "Claim source depth",
702
+ definition: "The number of detected README or headline claim-source documents, capped at four for scoring. It measures visible document depth, not the truth or specificity of their prose.",
610
703
  metricNames: ["claim_source_depth"]
611
704
  },
612
705
  {
613
706
  key: "reconciliation-artifact-depth",
614
- term: "reconciliation artifact depth",
615
- definition: "The visible records connecting stated claims to evidence. A relying party needs to trace what was promised to what was inspected.",
707
+ term: "Reconciliation artifact depth",
708
+ definition: "The number of detected dedicated claim-reality reconciliation artifacts, capped at three for scoring.",
616
709
  metricNames: ["reconciliation_artifact_depth"]
617
710
  },
618
711
  {
619
712
  key: "recent-pr-merge-ratio",
620
- term: "recent PR merge ratio",
621
- definition: "The share of recent visible commits associated with merged pull requests. Review history helps a relying party inspect how changes reached the revision.",
713
+ term: "Recent commits with recognizable PR references",
714
+ definition: "Among up to 12 recent local commit subjects, the share containing \u201Cmerge pull request\u201D, \u201Cpull request\u201D, or a #number reference. A zero means no recognizable reference was found, not that Cejel detected a merge without a PR. When history is empty or unavailable, scoring conservatively uses a denominator of one.",
622
715
  metricNames: ["pr_merge_ratio"]
623
716
  },
624
717
  {
625
718
  key: "ci-verification-depth",
626
719
  term: "CI verification depth",
627
- 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.",
720
+ definition: "A capped scoring input that adds package-script and CI-command detections separately for test, lint, type-check, and build. The same category can therefore contribute once as a package script and once in CI. The raw count is capped at four for scoring.",
628
721
  metricNames: ["ci_script_depth"]
629
722
  },
630
723
  {
631
724
  key: "pr-gate-ci-workflow-count",
632
- term: "PR-gate CI workflow count",
633
- 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.",
725
+ term: "PR-gate CI workflow count (automated build/test pipeline)",
726
+ definition: "The number of detected CI workflow files configured for pull requests or the main or master branch, including a covering monorepo-root workflow, capped at four for scoring.",
634
727
  metricNames: ["default_branch_ci_depth"]
635
728
  },
636
729
  {
637
730
  key: "audit-artifact-depth",
638
- term: "audit artifact depth",
639
- definition: "How many durable audit records such as changelogs, incident notes, or security records are present. Past operational decisions should remain inspectable.",
731
+ term: "Audit artifact depth",
732
+ definition: "The number of files whose paths match recognized changelog, audit, incident, status, runbook, provenance, release-note, or security-policy forms, capped at three for scoring.",
640
733
  metricNames: ["audit_artifact_depth"]
641
734
  },
642
735
  {
643
736
  key: "audit-freshness-depth",
644
- term: "audit freshness depth",
645
- definition: "How recently the visible audit records were maintained. Stale records may not describe the revision being accepted.",
737
+ term: "Audit freshness depth",
738
+ definition: "The share of detected audit-artifact files containing recent, latest, or current, or the applicable reference year. The calibrated rubric uses the scan year. Prospective v19 uses the scanned HEAD committer year when available. This is a text-marker proxy, not a file-history age check.",
646
739
  metricNames: ["audit_freshness_depth"]
647
740
  },
648
741
  {
649
742
  key: "un-overridable-kill-switch",
650
- term: "un-overridable kill-switch",
651
- definition: "A stop control that ordinary automation cannot bypass. Operators need a dependable way to halt privileged behavior.",
743
+ term: "Un-overridable kill-switch present",
744
+ definition: "A positive-only binary check for a named governance or safety toggle whose falsy state immediately returns or throws before lower-priority configuration can proceed. The metric is omitted when no such pattern is detected.",
652
745
  metricNames: ["kill_switch_fail_safe_present"]
653
746
  },
654
747
  {
655
748
  key: "human-gate-documented",
656
- term: "human gate documented",
657
- definition: "A written requirement for a person to approve sensitive operations. High-impact actions should not rely only on unattended automation.",
749
+ term: "Human gate documented",
750
+ definition: "A binary check for documentation saying privileged or credentialed operations are human-executed or human-gated and never agent-run. It is scored only when a privileged-operation-shaped surface exists.",
658
751
  metricNames: ["human_gate_documented"]
659
752
  },
660
753
  {
661
754
  key: "fail-closed-privilege-check",
662
- term: "fail-closed privilege check",
663
- definition: "A permission check that refuses the operation when it cannot reach a confident allow decision. Errors must not silently grant elevated access.",
755
+ term: "Fail-closed privilege check present",
756
+ definition: "A binary check for implementation code that checks role membership and fails closed before setting or elevating a role. It is scored only when a privileged-operation-shaped surface exists.",
664
757
  metricNames: ["fail_closed_privilege_check"]
665
758
  },
666
759
  {
667
760
  key: "privilege-escalation-cleanliness",
668
- term: "privilege-escalation cleanliness",
669
- definition: "The absence of detected patterns that unsafely widen permissions. Unnecessary elevation increases the impact of a mistake or compromise.",
761
+ term: "Privilege-escalation cleanliness",
762
+ definition: "A binary result. Clean means the selected rubric detected no ungated production privilege-escalation pattern. The calibrated detector checks executed role-membership grants and SUPERUSER escalation. Prospective v21 also checks authored administrative SQL and direct database-driver literals for administrative role grants and schema-wide table grants. Tests and fixtures are excluded from the metric.",
670
763
  metricNames: ["privilege_escalation_cleanliness"]
671
764
  },
672
765
  {
673
766
  key: "protected-path-review-gate",
674
- term: "protected-path review gate",
675
- definition: "A rule requiring review before sensitive files or directories can change. Critical controls deserve an explicit second check.",
767
+ term: "Protected-path review gate",
768
+ definition: "A binary check for a CODEOWNERS file or documentation of required review or branch protection. This is the repository-visible proxy for human review of sensitive-path changes.",
676
769
  metricNames: ["protected_path_review_gate"]
677
770
  }
678
771
  ];
@@ -689,14 +782,8 @@ function glossaryEntryForMetric(metric2) {
689
782
  metricNames: [metric2.name]
690
783
  };
691
784
  }
692
- var CERTIFICATE_METRIC_LABELS = {
693
- prod_readiness_primitives: "Production-readiness basic checks",
694
- pr_trace_primitives: "PR trace basic checks",
695
- prod_workflow_depth: "Production workflow depth (automated build/test/release pipeline)",
696
- default_branch_ci_depth: "PR-gate CI workflow count (automated build/test pipeline)"
697
- };
698
785
  function formatCertificateMetricLabel(metric2) {
699
- return CERTIFICATE_METRIC_LABELS[metric2.name] ?? metric2.label;
786
+ return CERTIFICATE_METRIC_REGISTRY[metric2.name]?.displayLabel ?? metric2.label;
700
787
  }
701
788
  function glossaryEntriesForReport(report) {
702
789
  const entries = new Map(CERTIFICATE_GLOSSARY.map((entry) => [entry.key, entry]));
@@ -718,6 +805,62 @@ function labelAlreadyStatesUnit(label, unit) {
718
805
  const escapedUnit = unit.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
719
806
  return new RegExp(`(?:^|[\\s-])${escapedUnit}$`, "i").test(label.trim());
720
807
  }
808
+ var BINARY_METRIC_COPY = {
809
+ secret_cleanliness: {
810
+ positive: "clean \u2014 no critical secret findings detected",
811
+ negative: "not clean \u2014 one or more critical secret findings detected"
812
+ },
813
+ crypto_comparison_hygiene: {
814
+ positive: "clean \u2014 no insecure comparison or serialization pattern detected",
815
+ negative: "not clean \u2014 an insecure comparison or serialization pattern was detected"
816
+ },
817
+ lockfile_coverage: {
818
+ positive: "present \u2014 at least one covering lockfile detected",
819
+ negative: "missing \u2014 no covering lockfile detected"
820
+ },
821
+ kill_switch_fail_safe_present: {
822
+ positive: "present \u2014 a fail-closed safety toggle was detected",
823
+ negative: "missing \u2014 no fail-closed safety toggle detected"
824
+ },
825
+ human_gate_documented: {
826
+ positive: "present \u2014 privileged operations are documented as human-executed or gated",
827
+ negative: "missing \u2014 no human-gate documentation detected"
828
+ },
829
+ fail_closed_privilege_check: {
830
+ positive: "present \u2014 role membership is checked fail-closed before elevation",
831
+ negative: "missing \u2014 no fail-closed pre-elevation role check detected"
832
+ },
833
+ privilege_escalation_cleanliness: {
834
+ positive: "clean \u2014 no ungated production privilege-escalation pattern detected",
835
+ negative: "not clean \u2014 an ungated production privilege-escalation pattern was detected"
836
+ },
837
+ protected_path_review_gate: {
838
+ positive: "present \u2014 CODEOWNERS or a documented required-review policy was detected",
839
+ negative: "missing \u2014 no CODEOWNERS or documented required-review policy detected"
840
+ }
841
+ };
842
+ var CONDITIONAL_METRIC_COPY = {
843
+ crypto_comparison_hygiene: "scored because a signing, HMAC, or secret-comparison surface was detected",
844
+ rls_policy_count: "scored because a multi-tenant data surface was detected",
845
+ tenant_scope_ratio: "scored because a multi-tenant data surface was detected",
846
+ human_gate_documented: "scored because a privileged-operation-shaped surface was detected",
847
+ fail_closed_privilege_check: "scored because a privileged-operation-shaped surface was detected",
848
+ kill_switch_fail_safe_present: "shown because a fail-closed kill switch was detected; this positive-only metric is omitted when absent"
849
+ };
850
+ function appendMetricCondition(metric2, value) {
851
+ const condition = metric2.presentation?.condition ?? CONDITIONAL_METRIC_COPY[metric2.name];
852
+ return condition ? `${value}; ${condition}` : value;
853
+ }
854
+ function formatEnumerableMetric(metric2) {
855
+ const components = metric2.presentation?.components;
856
+ if (!components || metric2.max === void 0) return null;
857
+ const present = components.filter((component) => component.count > 0).map(
858
+ (component) => component.count === 1 ? component.label : `${component.label} (${component.count} detected)`
859
+ );
860
+ const missing = components.filter((component) => component.count === 0).map((component) => component.label);
861
+ const value = metric2.kind === "saturating_count" && metric2.value > metric2.max ? `${formatMetricNumber(metric2.max)}/${formatMetricNumber(metric2.max)} (capped; ${formatMetricNumber(metric2.value)} raw)` : `${formatMetricNumber(metric2.value)}/${formatMetricNumber(metric2.max)}`;
862
+ return `${value} \u2014 present: ${present.length > 0 ? present.join(", ") : "none"}; missing: ${missing.length > 0 ? missing.join(", ") : "none"}`;
863
+ }
721
864
  function formatCertificateMetricValue(criterion, metric2) {
722
865
  if (isCoverageNotMeasured(criterion, metric2)) {
723
866
  return "no coverage report found \u2014 not measured";
@@ -726,15 +869,36 @@ function formatCertificateMetricValue(criterion, metric2) {
726
869
  const tests = formatMetricNumber(metric2.value);
727
870
  const sources = formatMetricNumber(metric2.max);
728
871
  const comparison = `${tests} test file${metric2.value === 1 ? "" : "s"} / ${sources} source file${metric2.max === 1 ? "" : "s"}`;
729
- return metric2.value > metric2.max ? `${comparison} (credit capped at parity)` : comparison;
872
+ return appendMetricCondition(
873
+ metric2,
874
+ metric2.value > metric2.max ? `${comparison} (credit capped at parity)` : comparison
875
+ );
876
+ }
877
+ const binaryCopy = BINARY_METRIC_COPY[metric2.name];
878
+ if (binaryCopy && metric2.max === 1 && (metric2.value === 0 || metric2.value === 1)) {
879
+ return appendMetricCondition(metric2, metric2.value === 1 ? binaryCopy.positive : binaryCopy.negative);
880
+ }
881
+ if (metric2.name === "pr_merge_ratio" && metric2.max !== void 0) {
882
+ const value = metric2.value === 0 ? `none found in bounded recent commit subjects (scoring value 0/${formatMetricNumber(metric2.max)})` : `${formatMetricNumber(metric2.value)} of ${formatMetricNumber(metric2.max)} recent commit subjects contain a recognizable PR reference`;
883
+ return appendMetricCondition(metric2, value);
730
884
  }
885
+ const enumerable = formatEnumerableMetric(metric2);
886
+ if (enumerable) return appendMetricCondition(metric2, enumerable);
731
887
  const displayUnit = metric2.name === "prod_readiness_primitives" || metric2.name === "pr_trace_primitives" ? "checks" : labelAlreadyStatesUnit(formatCertificateMetricLabel(metric2), metric2.unit) ? void 0 : metric2.unit;
732
888
  const unit = displayUnit ? ` ${displayUnit}` : "";
733
- if (metric2.max === void 0) return `${formatMetricNumber(metric2.value)}${unit}`;
889
+ if (metric2.max === void 0) {
890
+ return appendMetricCondition(metric2, `${formatMetricNumber(metric2.value)}${unit}`);
891
+ }
734
892
  if (metric2.kind === "saturating_count" && metric2.value > metric2.max) {
735
- return `${formatMetricNumber(metric2.max)}${unit} (capped; ${formatMetricNumber(metric2.value)} raw)`;
893
+ return appendMetricCondition(
894
+ metric2,
895
+ `${formatMetricNumber(metric2.max)}${unit} (capped; ${formatMetricNumber(metric2.value)} raw)`
896
+ );
736
897
  }
737
- return `${formatMetricNumber(metric2.value)}/${formatMetricNumber(metric2.max)}${unit}`;
898
+ return appendMetricCondition(
899
+ metric2,
900
+ `${formatMetricNumber(metric2.value)}/${formatMetricNumber(metric2.max)}${unit}`
901
+ );
738
902
  }
739
903
  function buildRelyingPartySummary(report) {
740
904
  const applicable = report.criteria.filter((criterion) => criterion.status !== "not_applicable");
@@ -749,6 +913,13 @@ function buildRelyingPartySummary(report) {
749
913
  "No coverage report or configured threshold was found, so static coverage was not measured; Cejel does not run the subject's tests."
750
914
  );
751
915
  }
916
+ if (!report.repo.headSha && report.criteria.some(
917
+ (criterion) => criterion.metrics.some((metric2) => metric2.name === "pr_merge_ratio")
918
+ )) {
919
+ gaps.push(
920
+ "Git history was unavailable, so the recent-commit PR-reference proxy found no commit subjects to inspect; its conservative scoring zero is not a detected merge outcome."
921
+ );
922
+ }
752
923
  const unmeasured = applicable.filter((criterion) => criterion.status === "insufficient_data");
753
924
  if (unmeasured.length > 0) {
754
925
  gaps.push(`The report did not measure ${unmeasured.map((criterion) => criterion.id).join(", ")}.`);
@@ -788,7 +959,7 @@ var METRIC_DERIVED_FINDING_PATTERN = /^[A-Z]\d+ metric-derived score is (\d+\.\d
788
959
  var REMEDIATION_BY_METRIC = {
789
960
  test_to_source_ratio: "add concrete regression tests for the implementation surface",
790
961
  coverage_percent: "configure coverage and publish a measured threshold or report",
791
- verification_script_ratio: "add explicit test, lint, and typecheck verification commands",
962
+ verification_script_ratio: "add the missing test, coverage, lint, or type-check command or a test-runner configuration",
792
963
  non_hollow_test_share: "replace skipped or placeholder tests with live assertions",
793
964
  env_handling_depth: "document and enforce safe environment-secret handling",
794
965
  secret_cleanliness: "remove committed secrets and rotate any exposed credentials",
@@ -806,7 +977,7 @@ var REMEDIATION_BY_METRIC = {
806
977
  claim_match_rate: "align documented product claims with observable repository evidence",
807
978
  claim_source_depth: "document concrete, checkable product claims",
808
979
  reconciliation_artifact_depth: "publish a claim-to-evidence reconciliation record",
809
- pr_merge_ratio: "preserve merged pull-request history in the scanned clone",
980
+ pr_merge_ratio: "preserve recognizable pull-request references in recent commit subjects",
810
981
  pr_trace_primitives: "add pull-request templates and outcome-trace records",
811
982
  ci_script_depth: "run the repository verification commands in CI",
812
983
  default_branch_ci_depth: "run CI on the default branch and pull requests",
@@ -889,10 +1060,14 @@ function renderWitanHtmlReport(report, options = {}) {
889
1060
  <p class="eyebrow">Trust Certificate</p>
890
1061
  <h1>${escapeHtml(report.productDisplayName)}</h1>
891
1062
  <dl class="meta">
1063
+ <div><dt>Product identity</dt><dd>${escapeHtml(CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE)}</dd></div>
892
1064
  ${options.generatedAt ? `<div><dt>Date</dt><dd>${escapeHtml(formatDate(options.generatedAt))}</dd></div>` : ""}
893
1065
  <div><dt>Run</dt><dd>${escapeHtml(renderRepo(report))}</dd></div>
894
1066
  <div><dt>CLI</dt><dd>${escapeHtml(options.cliVersion ? `Cejel ${options.cliVersion}` : "Not recorded")}</dd></div>
895
1067
  <div><dt>Rubric</dt><dd>${escapeHtml(report.rubricVersion)}</dd></div>
1068
+ ${// Gated on the rubric actually being prospective — a calibrated (v17) certificate's
1069
+ // HTML stays byte-identical to before --rubric-pin existed.
1070
+ isProspectiveRubricVersion(report.rubricVersion) ? `<div><dt>Calibration</dt><dd class="prospective-rubric-notice">${escapeHtml(PROSPECTIVE_RUBRIC_NOTICE)}</dd></div>` : ""}
896
1071
  ${contributingSources.length > 0 ? `<div><dt>Sources</dt><dd>Incorporates findings from: ${escapeHtml(contributingSources.join(", "))}<ul class="source-counts">${externalSourceSummaries.map((s) => `<li>${escapeHtml(formatSourceProvenanceLine(s))}</li>`).join("")}</ul></dd></div>` : ""}
897
1072
  </dl>
898
1073
  </section>
@@ -1052,7 +1227,7 @@ function renderNotApplicableGroup(criteria) {
1052
1227
  }
1053
1228
  function renderNotApplicableItem(criterion) {
1054
1229
  const reason = criterion.notes ?? "Not applicable to this repository.";
1055
- return `<li><span class="na-id">${escapeHtml(criterion.id)}</span> ${escapeHtml(criterion.title)} \u2014 <span class="na-reason">${escapeHtml(reason)}</span></li>`;
1230
+ return `<li><span class="na-id">${escapeHtml(criterion.id)}</span> ${escapeHtml(criterion.title)} \u2014 <span class="na-reason">${escapeHtml(reason)}</span><span class="na-reconciliation"><strong>Why no label comparison applies:</strong> this dimension is not applicable to this repository, so it has no weighted numeric score band to compare.</span></li>`;
1056
1231
  }
1057
1232
  function renderCriterionCard(criterion, gitHistoryUnavailable) {
1058
1233
  const evidence = [
@@ -1061,7 +1236,7 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
1061
1236
  ];
1062
1237
  const metrics = criterion.metrics.map((metric2) => renderMetric(criterion, metric2));
1063
1238
  const statusReconciliation = renderStatusReconciliation(criterion);
1064
- 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>` : "";
1239
+ 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-commit PR-reference proxy found no commit subjects to inspect. Its conservative scoring zero is not a detected merge outcome and may undercount ${escapeHtml(criterion.id)}. Re-scan a full Git clone for this signal.</p>` : "";
1065
1240
  return `<article class="criterion">
1066
1241
  <div class="criterion-top">
1067
1242
  <div>
@@ -1082,11 +1257,22 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
1082
1257
  </article>`;
1083
1258
  }
1084
1259
  function renderStatusReconciliation(criterion) {
1085
- if (criterion.status === "not_applicable" || criterion.status === "insufficient_data" || criterion.status === "unverified") {
1086
- return "";
1260
+ if (criterion.status === "not_applicable") {
1261
+ return '<p class="status-explanation"><strong>Why no label comparison applies:</strong> this dimension is not applicable to this repository, so it has no weighted numeric score band to compare.</p>';
1262
+ }
1263
+ if (criterion.status === "insufficient_data") {
1264
+ return '<p class="status-explanation"><strong>Why no label comparison applies:</strong> this dimension has insufficient measurable data, so it has no weighted numeric score band to compare.</p>';
1265
+ }
1266
+ if (criterion.status === "unverified") {
1267
+ return '<p class="status-explanation"><strong>Why no label comparison applies:</strong> the unverified dimension state is a fail-closed or legacy zero state, not a calibrated numeric band.</p>';
1087
1268
  }
1088
1269
  const numericBand = scoreBandForPresentation(criterion.score);
1089
- if (numericBand === criterion.status) return "";
1270
+ if (numericBand === criterion.status) {
1271
+ return `<p class="status-explanation"><strong>How the labels agree:</strong> the ${escapeHtml(criterion.status)} dimension band matches the ${formatScore(criterion.score)}/4.0 weighted score (the ${numericBand} numeric band).</p>`;
1272
+ }
1273
+ if (criterion.status !== "verified" && criterion.status !== "info" && criterion.status !== "warning" && criterion.status !== "critical") {
1274
+ return `<p class="status-explanation"><strong>Why no label comparison applies:</strong> this report contains an unrecognized dimension state (${escapeHtml(String(criterion.status))}), so Cejel will not infer a comparison.</p>`;
1275
+ }
1090
1276
  const entry = CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "labels");
1091
1277
  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>";
1092
1278
  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>`;
@@ -1103,7 +1289,7 @@ function renderMetric(criterion, metric2) {
1103
1289
  const formatted = formatCertificateMetricValue(criterion, metric2);
1104
1290
  const cappedEntry = formatted.includes("capped") ? CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "capped") : void 0;
1105
1291
  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>`;
1106
- if (!cappedEntry) return `${label}<span>${escapeHtml(formatted)}</span>`;
1292
+ if (!cappedEntry) return `${label}<span class="metric-value">${escapeHtml(formatted)}</span>`;
1107
1293
  const cappedTooltipId = `${tooltipId}-capped`;
1108
1294
  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>`;
1109
1295
  }
@@ -1274,6 +1460,8 @@ h2 { font-family: var(--serif); font-size: 30px; font-weight: 400; letter-spacin
1274
1460
  .na-list li { margin: 0; color: var(--muted); }
1275
1461
  .na-id { color: var(--faint); font-family: var(--mono); font-size: 11px; margin-right: 6px; }
1276
1462
  .na-reason { color: var(--muted); }
1463
+ .na-reconciliation { display: block; margin-top: 4px; color: var(--muted); }
1464
+ .na-reconciliation strong { color: var(--text); }
1277
1465
  .criterion { border: 1px solid var(--line); border-radius: 8px; padding: 16px; background: var(--surface-2); }
1278
1466
  .criterion-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; }
1279
1467
  .criterion-id { color: var(--periwinkle); font-family: var(--mono); font-size: 12px; margin-bottom: 4px; }
@@ -1288,7 +1476,10 @@ h3 { font-size: 15px; line-height: 1.35; font-weight: 600; margin-bottom: 0; }
1288
1476
  .criterion-metrics { display: grid; gap: 7px; margin-top: 10px; padding: 0; list-style: none; font-size: 12px; }
1289
1477
  .criterion-metrics li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(238, 244, 247, .08); padding-bottom: 6px; }
1290
1478
  .criterion-metrics strong { color: var(--text); font-weight: 650; }
1291
- .criterion-metrics span { white-space: nowrap; color: var(--muted); }
1479
+ .criterion-metrics span { color: var(--muted); }
1480
+ .criterion-metrics > li > .metric-value {
1481
+ min-width: 0; white-space: normal; overflow-wrap: anywhere; text-align: right;
1482
+ }
1292
1483
  .term-help { position: relative; cursor: help; outline: none; }
1293
1484
  .term-help:focus-visible { box-shadow: 0 0 0 2px var(--periwinkle); border-radius: 3px; }
1294
1485
  .term-tooltip {
@@ -1527,41 +1718,6 @@ function verifyWitanAttestationBinding(statement, report, options = {}) {
1527
1718
  return { valid: errors.length === 0, errors };
1528
1719
  }
1529
1720
 
1530
- // src/witan/rubric-version.ts
1531
- init_esm_shims();
1532
- var WITAN_RUBRIC_VERSION_V9 = "witan-rubric-v9-2026-07-22";
1533
- var WITAN_RUBRIC_VERSION_V10 = "witan-rubric-v10-2026-07-23";
1534
- var WITAN_RUBRIC_VERSION_V11 = "witan-rubric-v11-2026-07-23";
1535
- var WITAN_RUBRIC_VERSION_V12 = "witan-rubric-v12-2026-07-23";
1536
- var WITAN_RUBRIC_VERSION_V13 = "witan-rubric-v13-2026-07-23";
1537
- var WITAN_RUBRIC_VERSION_V14 = "witan-rubric-v14-2026-07-23";
1538
- var WITAN_RUBRIC_VERSION_V15 = "witan-rubric-v15-2026-07-24";
1539
- var WITAN_RUBRIC_VERSION_V16 = "witan-rubric-v16-2026-07-24";
1540
- var WITAN_RUBRIC_VERSION_V17 = "witan-rubric-v17-2026-07-24";
1541
- var WITAN_RUBRIC_VERSION_V18 = "witan-rubric-v18-prospective-2026-07-25";
1542
- var WITAN_RUBRIC_VERSION_V19 = "witan-rubric-v19-prospective-2026-08-09";
1543
- var WITAN_RUBRIC_VERSION_V20 = "witan-rubric-v20-prospective-2026-08-10";
1544
- var WITAN_RUBRIC_VERSION_V21 = "witan-rubric-v21-prospective-2026-08-10";
1545
- var WITAN_RUBRIC_VERSION_V22 = "witan-rubric-v22-prospective-2026-08-10";
1546
- var WITAN_LAST_CALIBRATED_RUBRIC_VERSION = WITAN_RUBRIC_VERSION_V17;
1547
- var WITAN_PROSPECTIVE_RUBRIC_VERSIONS = Object.freeze([
1548
- WITAN_RUBRIC_VERSION_V18,
1549
- WITAN_RUBRIC_VERSION_V19,
1550
- WITAN_RUBRIC_VERSION_V20,
1551
- WITAN_RUBRIC_VERSION_V21,
1552
- WITAN_RUBRIC_VERSION_V22
1553
- ]);
1554
- var WITAN_SELECTABLE_RUBRIC_VERSIONS = Object.freeze([
1555
- WITAN_LAST_CALIBRATED_RUBRIC_VERSION,
1556
- ...WITAN_PROSPECTIVE_RUBRIC_VERSIONS
1557
- ]);
1558
- function assertSelectableRubricVersion(rubricVersion) {
1559
- if (WITAN_SELECTABLE_RUBRIC_VERSIONS.includes(rubricVersion)) return;
1560
- throw new Error(
1561
- `Cejel: unrecognized rubric version: "${rubricVersion}". Accepted values: ${WITAN_SELECTABLE_RUBRIC_VERSIONS.join(", ")}.`
1562
- );
1563
- }
1564
-
1565
1721
  // src/witan/index.ts
1566
1722
  init_esm_shims();
1567
1723
 
@@ -2548,8 +2704,26 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2548
2704
  4,
2549
2705
  0.25,
2550
2706
  "ratio",
2551
- "Measures explicit test/lint/typecheck verification commands (via npm script or CI-invoked tool) plus test runner configuration.",
2552
- "saturating_count"
2707
+ "Counts detected test, coverage, lint, and typecheck commands plus detected test-runner configuration files; raw credit is capped at four.",
2708
+ "saturating_count",
2709
+ {
2710
+ components: [
2711
+ {
2712
+ label: "test command",
2713
+ count: packageScripts.has("test") || Boolean(ciTestWorkflow) ? 1 : 0
2714
+ },
2715
+ { label: "coverage command", count: packageScripts.has("coverage") ? 1 : 0 },
2716
+ {
2717
+ label: "lint command",
2718
+ count: packageScripts.has("lint") || ciHasLintCommand ? 1 : 0
2719
+ },
2720
+ {
2721
+ label: "type-check command",
2722
+ count: packageScripts.has("typecheck") || ciHasTypecheckCommand ? 1 : 0
2723
+ },
2724
+ { label: "test-runner configuration", count: runnerFiles.length }
2725
+ ]
2726
+ }
2553
2727
  ),
2554
2728
  metric(
2555
2729
  "non_hollow_test_share",
@@ -2612,8 +2786,17 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
2612
2786
  4,
2613
2787
  0.25,
2614
2788
  "ratio",
2615
- "Measures explicit test/lint/typecheck verification commands plus test runner configuration.",
2616
- "saturating_count"
2789
+ "Counts detected test, coverage, lint, and typecheck commands plus detected test-runner configuration files; raw credit is capped at four.",
2790
+ "saturating_count",
2791
+ {
2792
+ components: [
2793
+ { label: "test command", count: 0 },
2794
+ { label: "coverage command", count: 0 },
2795
+ { label: "lint command", count: 0 },
2796
+ { label: "type-check command", count: 0 },
2797
+ { label: "test-runner configuration", count: 0 }
2798
+ ]
2799
+ }
2617
2800
  ),
2618
2801
  metric(
2619
2802
  "non_hollow_test_share",
@@ -2855,7 +3038,22 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2855
3038
  3,
2856
3039
  isMultiTenant ? 0.1 : 0.3,
2857
3040
  "practices",
2858
- "Counts three bounded env-handling practices (0\u20133): template file, gitignore rule, env reads in any supported language."
3041
+ "Counts three bounded env-handling practices (0\u20133): template file, gitignore rule, env reads in any supported language.",
3042
+ void 0,
3043
+ {
3044
+ components: [
3045
+ { label: "environment template file", count: envExamples.length > 0 ? 1 : 0 },
3046
+ { label: ".gitignore environment-file rule", count: gitignoreEnvFile ? 1 : 0 },
3047
+ {
3048
+ label: "environment reads in implementation code",
3049
+ count: countFilesContaining(
3050
+ repoPath,
3051
+ repoFiles.filter(isImplementationFile),
3052
+ ENV_READ_PATTERN
3053
+ ) > 0 ? 1 : 0
3054
+ }
3055
+ ]
3056
+ }
2859
3057
  )
2860
3058
  ];
2861
3059
  const isolationMetrics = isMultiTenant ? [
@@ -2867,7 +3065,8 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2867
3065
  0.4,
2868
3066
  "policies",
2869
3067
  useV18NativeRls ? "Counts CREATE POLICY definitions with repository-native USING/WITH CHECK clauses against the repository-native scoped data-layer surface." : "Counts static row-level-security enables, forced-RLS statements, and policy definitions.",
2870
- "saturating_count"
3068
+ "saturating_count",
3069
+ { condition: "scored because a multi-tenant data surface was detected" }
2871
3070
  ),
2872
3071
  metric(
2873
3072
  "tenant_scope_ratio",
@@ -2876,7 +3075,9 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2876
3075
  Math.max(migrationFiles.length, 1),
2877
3076
  0.25,
2878
3077
  "ratio",
2879
- useV18NativeRls ? `Measures repository-native RLS scope identifiers (${nativeRlsInventory?.scopeIdentifiers.join(", ")}) across data-layer files.` : "Measures tenant/studio/workspace scoping evidence across data-layer files."
3078
+ useV18NativeRls ? `Measures repository-native RLS scope identifiers (${nativeRlsInventory?.scopeIdentifiers.join(", ")}) across data-layer files.` : "Measures tenant/studio/workspace scoping evidence across data-layer files.",
3079
+ void 0,
3080
+ { condition: "scored because a multi-tenant data surface was detected" }
2880
3081
  )
2881
3082
  ] : [];
2882
3083
  const cryptoMetrics = cryptoHygiene.hasSurface ? [
@@ -2887,7 +3088,11 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2887
3088
  1,
2888
3089
  0.2,
2889
3090
  "clean",
2890
- "Credits constant-time secret/HMAC comparison and canonical serialization before signing when a signing/HMAC surface is detected in source; only scored when such a surface exists."
3091
+ "Credits constant-time secret/HMAC comparison and canonical serialization before signing when a signing/HMAC surface is detected in source; only scored when such a surface exists.",
3092
+ void 0,
3093
+ {
3094
+ condition: "scored because a signing, HMAC, or secret-comparison surface was detected"
3095
+ }
2891
3096
  )
2892
3097
  ] : [];
2893
3098
  return {
@@ -3181,7 +3386,21 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3181
3386
  6,
3182
3387
  0.55,
3183
3388
  "checks",
3184
- "Counts distinct static production-readiness checks instead of treating presence as enough."
3389
+ "Counts distinct static production-readiness checks instead of treating presence as enough.",
3390
+ void 0,
3391
+ {
3392
+ components: [
3393
+ {
3394
+ label: "build or type-check command",
3395
+ count: packageJson && (scripts.has("build") || scripts.has("typecheck")) ? 1 : 0
3396
+ },
3397
+ { label: "CI workflow", count: workflows.length > 0 ? 1 : 0 },
3398
+ { label: "deployment configuration", count: deployConfigs.length > 0 ? 1 : 0 },
3399
+ { label: "environment template", count: envTemplate ? 1 : 0 },
3400
+ { label: "health or readiness signal", count: healthChecks.length > 0 ? 1 : 0 },
3401
+ { label: "error boundary", count: errorBoundaries.length > 0 ? 1 : 0 }
3402
+ ]
3403
+ }
3185
3404
  ),
3186
3405
  metric(
3187
3406
  "prod_workflow_depth",
@@ -3294,7 +3513,14 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
3294
3513
  2,
3295
3514
  0.25,
3296
3515
  "ratio",
3297
- "Credits automated dependency updates and package-manager audit hooks."
3516
+ "Credits automated dependency updates and package-manager audit hooks.",
3517
+ void 0,
3518
+ {
3519
+ components: [
3520
+ { label: "automated dependency-update configuration", count: hasUpdateConfig ? 1 : 0 },
3521
+ { label: "package-manager audit hook", count: auditConfig ? 1 : 0 }
3522
+ ]
3523
+ }
3298
3524
  );
3299
3525
  const appMetrics = [
3300
3526
  metric(
@@ -3528,7 +3754,14 @@ function collectB2PrTraceEvidence(repoPath, repoFiles) {
3528
3754
  0.8,
3529
3755
  "signals",
3530
3756
  "Measures CI, PR template, and review-gate evidence for pull-request traceability.",
3531
- "saturating_count"
3757
+ "saturating_count",
3758
+ {
3759
+ components: [
3760
+ { label: "CI workflow", count: workflows.length },
3761
+ { label: "pull-request template", count: prTemplate ? 1 : 0 },
3762
+ { label: "review-gate record", count: branchProtectionDoc ? 1 : 0 }
3763
+ ]
3764
+ }
3532
3765
  ),
3533
3766
  // Weight lowered from 0.35 (Phase 3 H1): squash-merge and local-history repos with
3534
3767
  // no "Merge pull request"/"#123" commit-subject convention legitimately score 0/N
@@ -3591,8 +3824,24 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
3591
3824
  4,
3592
3825
  0.45,
3593
3826
  "signals",
3594
- "Counts npm verification scripts plus distinct test/lint/typecheck/build command categories detected anywhere in CI workflows; language-agnostic, counted by category not by file.",
3595
- "saturating_count"
3827
+ "Counts test/lint/typecheck/build package scripts plus those command categories in repository CI and, when applicable, monorepo-root CI; raw credit is capped at four.",
3828
+ "saturating_count",
3829
+ {
3830
+ components: [
3831
+ ...["test", "lint", "typecheck", "build"].map((name) => ({
3832
+ label: `package script: ${name === "typecheck" ? "type-check" : name}`,
3833
+ count: scripts.has(name) ? 1 : 0
3834
+ })),
3835
+ ...CI_COMMAND_CATEGORIES.map((pattern, index) => ({
3836
+ label: `repository CI command: ${["test", "lint", "type-check", "build"][index]}`,
3837
+ count: workflows.some((file) => fileContains(repoPath, file, pattern)) ? 1 : 0
3838
+ })),
3839
+ ...mono ? CI_COMMAND_CATEGORIES.map((pattern, index) => ({
3840
+ label: `monorepo-root CI command: ${["test", "lint", "type-check", "build"][index]}`,
3841
+ count: rootWorkflows.some((file) => fileContains(mono.root, file, pattern)) ? 1 : 0
3842
+ })) : []
3843
+ ]
3844
+ }
3596
3845
  ),
3597
3846
  metric(
3598
3847
  "default_branch_ci_depth",
@@ -3795,7 +4044,11 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
3795
4044
  1,
3796
4045
  0.15,
3797
4046
  "present",
3798
- "Credits a governance/safety toggle that fails closed before any lower-priority config can override it."
4047
+ "Credits a governance/safety toggle that fails closed before any lower-priority config can override it.",
4048
+ void 0,
4049
+ {
4050
+ condition: "shown because a fail-closed kill switch was detected; this positive-only metric is omitted when absent"
4051
+ }
3799
4052
  )
3800
4053
  ] : [];
3801
4054
  const hasPrivilegedOpsSurface = humanGateDoc != null || gatedPrivilegeCheckFile != null || executableFiles.length > 0 || v21ExecutedEscalationFiles.length > 0;
@@ -3812,7 +4065,11 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
3812
4065
  1,
3813
4066
  0.4,
3814
4067
  "present",
3815
- "Credits explicit documentation that privileged/credentialed operations are human-executed, never agent-run."
4068
+ "Credits explicit documentation that privileged/credentialed operations are human-executed, never agent-run.",
4069
+ void 0,
4070
+ {
4071
+ condition: "scored because a privileged-operation-shaped surface was detected"
4072
+ }
3816
4073
  ),
3817
4074
  metric(
3818
4075
  "fail_closed_privilege_check",
@@ -3821,7 +4078,11 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
3821
4078
  1,
3822
4079
  0.3,
3823
4080
  "present",
3824
- "Credits code that checks role membership and fails closed before elevating privilege, instead of attempting elevation blind."
4081
+ "Credits code that checks role membership and fails closed before elevating privilege, instead of attempting elevation blind.",
4082
+ void 0,
4083
+ {
4084
+ condition: "scored because a privileged-operation-shaped surface was detected"
4085
+ }
3825
4086
  )
3826
4087
  ] : [],
3827
4088
  metric(
@@ -6152,16 +6413,29 @@ function evidenceForRelativeAtLine(repoPath, path, kind, label, line) {
6152
6413
  ...contents ? { contentHash: createHash2("sha256").update(contents).digest("hex") } : {}
6153
6414
  };
6154
6415
  }
6155
- function metric(name, label, value, max, weight, unit, description, kind) {
6416
+ function metric(name, label, value, max, weight, unit, description, kind, presentation) {
6417
+ const normalizedValue = Math.max(0, value);
6418
+ if (presentation?.components) {
6419
+ const explainedValue = presentation.components.reduce(
6420
+ (sum, component) => sum + component.count,
6421
+ 0
6422
+ );
6423
+ if (explainedValue !== normalizedValue) {
6424
+ throw new Error(
6425
+ `Cejel invariant: presentation components for ${name} total ${explainedValue}, expected ${normalizedValue}`
6426
+ );
6427
+ }
6428
+ }
6156
6429
  return {
6157
6430
  name,
6158
6431
  label,
6159
- value: Math.max(0, value),
6432
+ value: normalizedValue,
6160
6433
  max,
6161
6434
  ...kind ? { kind } : {},
6162
6435
  weight,
6163
6436
  unit,
6164
- description
6437
+ description,
6438
+ ...presentation ? { presentation } : {}
6165
6439
  };
6166
6440
  }
6167
6441
  function firstMeaningfulLine(contents) {
@@ -7340,9 +7614,9 @@ import { basename as basename2, join as join3 } from "path";
7340
7614
  var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/;
7341
7615
  var FALLBACK_SLUG = "repo";
7342
7616
  var FALLBACK_DISPLAY_NAME = "Repository";
7343
- function deriveProductIdentity(repoPath) {
7617
+ function deriveProductIdentity(repoPath, productName) {
7344
7618
  const packageJsonPath = join3(repoPath, "package.json");
7345
- const raw = readPackageName(packageJsonPath) ?? basename2(repoPath);
7619
+ const raw = productName ?? readPackageName(packageJsonPath) ?? basename2(repoPath);
7346
7620
  const displayName = raw.trim().length > 0 ? raw.trim() : FALLBACK_DISPLAY_NAME;
7347
7621
  return {
7348
7622
  productSlug: slugify(raw),
@@ -7381,7 +7655,7 @@ var METRIC_DERIVED_FINDING_PATTERN2 = /^[A-Z]\d+ metric-derived score is (\d+\.\
7381
7655
  var REMEDIATION_BY_METRIC2 = {
7382
7656
  test_to_source_ratio: "add concrete regression tests for the implementation surface",
7383
7657
  coverage_percent: "configure coverage and publish a measured threshold or report",
7384
- verification_script_ratio: "add explicit test, lint, and typecheck verification commands",
7658
+ verification_script_ratio: "add the missing test, coverage, lint, or type-check command or a test-runner configuration",
7385
7659
  non_hollow_test_share: "replace skipped or placeholder tests with live assertions",
7386
7660
  env_handling_depth: "document and enforce safe environment-secret handling",
7387
7661
  secret_cleanliness: "remove committed secrets and rotate any exposed credentials",
@@ -7399,7 +7673,7 @@ var REMEDIATION_BY_METRIC2 = {
7399
7673
  claim_match_rate: "align documented product claims with observable repository evidence",
7400
7674
  claim_source_depth: "document concrete, checkable product claims",
7401
7675
  reconciliation_artifact_depth: "publish a claim-to-evidence reconciliation record",
7402
- pr_merge_ratio: "preserve merged pull-request history in the scanned clone",
7676
+ pr_merge_ratio: "preserve recognizable pull-request references in recent commit subjects",
7403
7677
  pr_trace_primitives: "add pull-request templates and outcome-trace records",
7404
7678
  ci_script_depth: "run the repository verification commands in CI",
7405
7679
  default_branch_ci_depth: "run CI on the default branch and pull requests",
@@ -7509,7 +7783,7 @@ function buildWitanCliSummary(report) {
7509
7783
 
7510
7784
  // src/scan.ts
7511
7785
  function runCejelScan(options) {
7512
- const identity = deriveProductIdentity(options.repoPath);
7786
+ const identity = deriveProductIdentity(options.repoPath, options.productName);
7513
7787
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
7514
7788
  const report = scoreRepoWithPublicCejel({
7515
7789
  productSlug: identity.productSlug,
@@ -7531,17 +7805,21 @@ function runCejelScan(options) {
7531
7805
  export {
7532
7806
  WitanReportSchema,
7533
7807
  formatExternalSourceLine,
7808
+ CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE,
7534
7809
  CERTIFICATE_GLOSSARY,
7535
7810
  formatCertificateMetricLabel,
7536
7811
  formatCertificateMetricValue,
7537
7812
  buildRelyingPartySummary,
7813
+ WITAN_RUBRIC_VERSION_V22,
7814
+ assertSelectableRubricVersion,
7815
+ isProspectiveRubricVersion,
7816
+ PROSPECTIVE_RUBRIC_NOTICE,
7538
7817
  renderWitanHtmlReport,
7539
7818
  renderWitanBadgeEndpoint,
7540
7819
  renderWitanBadgeSvg,
7541
7820
  serializeWitanReport,
7542
7821
  createWitanAttestation,
7543
7822
  verifyWitanAttestationBinding,
7544
- WITAN_RUBRIC_VERSION_V22,
7545
7823
  deriveProductIdentity,
7546
7824
  runCejelScan
7547
7825
  };
package/dist/index.js CHANGED
@@ -2,21 +2,25 @@
2
2
  import { createRequire as __cejelCreateRequire } from 'node:module';
3
3
  const require = __cejelCreateRequire(import.meta.url);
4
4
  import {
5
+ CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE,
5
6
  CERTIFICATE_GLOSSARY,
7
+ PROSPECTIVE_RUBRIC_NOTICE,
6
8
  WITAN_RUBRIC_VERSION_V22,
7
9
  WitanReportSchema,
10
+ assertSelectableRubricVersion,
8
11
  buildRelyingPartySummary,
9
12
  createWitanAttestation,
10
13
  formatCertificateMetricLabel,
11
14
  formatCertificateMetricValue,
12
15
  formatExternalSourceLine,
16
+ isProspectiveRubricVersion,
13
17
  renderWitanBadgeEndpoint,
14
18
  renderWitanBadgeSvg,
15
19
  renderWitanHtmlReport,
16
20
  runCejelScan,
17
21
  serializeWitanReport,
18
22
  verifyWitanAttestationBinding
19
- } from "./chunk-PJEKYFVM.js";
23
+ } from "./chunk-4IXESHG7.js";
20
24
  import "./chunk-EPK7UTIK.js";
21
25
  import "./chunk-KGFGX3TP.js";
22
26
  import {
@@ -65,10 +69,11 @@ function renderTerminalCertificate(summary, report) {
65
69
  ` Process trust: ${formatScore(summary.processTrustScore)}/4.0`,
66
70
  ""
67
71
  ];
72
+ lines.splice(1, 0, `Product identity: ${CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE}`);
68
73
  if (report) {
69
74
  const relyingPartySummary = buildRelyingPartySummary(report);
70
75
  lines.splice(
71
- 2,
76
+ 3,
72
77
  0,
73
78
  `What was examined: ${relyingPartySummary.examined}`,
74
79
  `What was established: ${relyingPartySummary.established}`,
@@ -155,6 +160,9 @@ function renderTerminalCertificate(summary, report) {
155
160
  for (const entry of CERTIFICATE_GLOSSARY) {
156
161
  lines.push(` ${entry.term} \u2014 ${entry.definition}`);
157
162
  }
163
+ if (report && isProspectiveRubricVersion(report.rubricVersion)) {
164
+ lines.unshift(`Rubric: ${report.rubricVersion}`, PROSPECTIVE_RUBRIC_NOTICE, "");
165
+ }
158
166
  return `${lines.join("\n")}
159
167
  `;
160
168
  }
@@ -178,10 +186,16 @@ var CLI_FLAG_SPECS = [
178
186
  description: "exit nonzero below this overall score",
179
187
  kind: "minScore"
180
188
  },
189
+ {
190
+ tokens: ["--product-name"],
191
+ value: "<name>",
192
+ description: "set caller-context slug and display name for reproducible certificates",
193
+ kind: "productName"
194
+ },
181
195
  {
182
196
  tokens: ["--name"],
183
197
  value: "<display>",
184
- description: "override the project name shown on the certificate",
198
+ description: "override only the displayed project name (legacy compatibility)",
185
199
  kind: "name"
186
200
  },
187
201
  {
@@ -190,6 +204,12 @@ var CLI_FLAG_SPECS = [
190
204
  description: "fold in a SARIF/JSON scanner report (repeatable)",
191
205
  kind: "ingest"
192
206
  },
207
+ {
208
+ tokens: ["--rubric-pin"],
209
+ value: "<version>",
210
+ description: "EXPLICIT OPT-IN: pin a rubric other than the calibrated default (see docs); prospective rubrics carry no calibration claim",
211
+ kind: "rubricPin"
212
+ },
193
213
  {
194
214
  tokens: ["--quiet"],
195
215
  description: "suppress the terminal summary",
@@ -287,10 +307,12 @@ async function runWitanCli(args, rubricVersion) {
287
307
  process.stdout.write(USAGE);
288
308
  return 0;
289
309
  }
310
+ const effectiveRubricVersion = options.rubricPin ?? rubricVersion;
290
311
  const { report, summary, generatedAt } = runCejelScan({
291
312
  repoPath: options.repoPath,
313
+ ...options.productName ? { productName: options.productName } : {},
292
314
  ...options.productDisplayName ? { productDisplayName: options.productDisplayName } : {},
293
- ...rubricVersion ? { rubricVersion } : {},
315
+ ...effectiveRubricVersion ? { rubricVersion: effectiveRubricVersion } : {},
294
316
  ingestPatterns: options.ingestPatterns,
295
317
  warnOnEmptyIngestMatch: !options.quiet
296
318
  });
@@ -425,10 +447,12 @@ function parseArgs(args) {
425
447
  let repoPath;
426
448
  let outDir = DEFAULT_OUT_DIR;
427
449
  let minScore;
450
+ let productName;
428
451
  let productDisplayName;
429
452
  let quiet = false;
430
453
  let showHelp = false;
431
454
  let showVersion = false;
455
+ let rubricPin;
432
456
  const ingestPatterns = [];
433
457
  for (let index = 0; index < args.length; index += 1) {
434
458
  const arg = args[index];
@@ -479,6 +503,19 @@ function parseArgs(args) {
479
503
  index += 1;
480
504
  break;
481
505
  }
506
+ case "productName": {
507
+ const value = args[index + 1];
508
+ if (!value) throw new Error("Missing value for --product-name");
509
+ const trimmed = value.trim();
510
+ if (trimmed.length === 0 || trimmed.length > 120 || hasUnsafeDisplayNameCharacter(trimmed)) {
511
+ throw new Error(
512
+ "--product-name must be a single printable line containing between 1 and 120 characters"
513
+ );
514
+ }
515
+ productName = trimmed;
516
+ index += 1;
517
+ break;
518
+ }
482
519
  case "ingest": {
483
520
  const value = args[index + 1];
484
521
  if (!value) throw new Error("Missing value for --ingest");
@@ -486,6 +523,14 @@ function parseArgs(args) {
486
523
  index += 1;
487
524
  break;
488
525
  }
526
+ case "rubricPin": {
527
+ const value = args[index + 1];
528
+ if (!value) throw new Error("Missing value for --rubric-pin");
529
+ assertSelectableRubricVersion(value);
530
+ rubricPin = value;
531
+ index += 1;
532
+ break;
533
+ }
489
534
  }
490
535
  continue;
491
536
  }
@@ -495,15 +540,20 @@ function parseArgs(args) {
495
540
  throw new Error(`Unexpected positional argument: ${arg}`);
496
541
  }
497
542
  }
543
+ if (productName && productDisplayName) {
544
+ throw new Error("--product-name and --name cannot be used together");
545
+ }
498
546
  return {
499
547
  repoPath: resolve(repoPath ?? "."),
500
548
  outDir,
501
549
  ...minScore != null ? { minScore } : {},
550
+ ...productName ? { productName } : {},
502
551
  ...productDisplayName ? { productDisplayName } : {},
503
552
  quiet,
504
553
  showHelp,
505
554
  showVersion,
506
- ingestPatterns
555
+ ingestPatterns,
556
+ ...rubricPin ? { rubricPin } : {}
507
557
  };
508
558
  }
509
559
  function hasUnsafeDisplayNameCharacter(value) {
package/dist/mcp/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  renderWitanBadgeSvg,
6
6
  renderWitanHtmlReport,
7
7
  runCejelScan
8
- } from "../chunk-PJEKYFVM.js";
8
+ } from "../chunk-4IXESHG7.js";
9
9
  import "../chunk-EPK7UTIK.js";
10
10
  import {
11
11
  ZodFirstPartyTypeKind,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cejel/cejel",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "mcpName": "io.github.BargLabs/cejel",
5
5
  "private": false,
6
6
  "publishConfig": {