@cejel/cejel 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,19 +5,21 @@ import {
5
5
  describeGitFailure,
6
6
  execGit,
7
7
  isExpectedGitAbsence
8
- } from "./chunk-EPK7UTIK.js";
8
+ } from "./chunk-BZXBLDCJ.js";
9
9
  import {
10
10
  external_exports
11
- } from "./chunk-KGFGX3TP.js";
11
+ } from "./chunk-Y6NO5HAI.js";
12
12
  import {
13
+ MAX_REPOSITORY_CONTENT_BYTES,
13
14
  init_esm_shims,
14
15
  readRepoText,
15
16
  readRepoTextPrefix,
16
17
  recordContentSkip,
18
+ recordContentSkipForCriteria,
17
19
  recordFilesystemSkip,
18
20
  trackContentReads,
19
21
  withContentReadCriterion
20
- } from "./chunk-R4DPQLS2.js";
22
+ } from "./chunk-H55KECVT.js";
21
23
 
22
24
  // src/witan/schemas.ts
23
25
  init_esm_shims();
@@ -117,10 +119,26 @@ var WitanEvidencePointerSchema = external_exports.object({
117
119
  }).strict().refine((value) => Boolean(value.url ?? value.path ?? value.contentHash), {
118
120
  message: "Witan evidence must include a path, url, or content hash."
119
121
  });
122
+ var WitanInventoryScanDerivationSchema = external_exports.object({
123
+ kind: external_exports.literal("inventory-scan"),
124
+ inventoryScope: external_exports.enum(["tracked-scan-eligible", "tracked-complete"]),
125
+ inventoryCount: external_exports.number().int().nonnegative(),
126
+ patternSetId: external_exports.string().min(1).max(180),
127
+ patternCount: external_exports.number().int().positive(),
128
+ matchCount: external_exports.number().int().nonnegative()
129
+ }).strict();
130
+ var WitanFindingDerivationSchema = external_exports.discriminatedUnion("kind", [
131
+ WitanInventoryScanDerivationSchema
132
+ ]);
120
133
  var WitanFindingSchema = external_exports.object({
121
134
  severity: WitanFindingSeveritySchema,
122
135
  summary: external_exports.string().min(1).max(500),
123
- evidence: WitanEvidencePointerSchema
136
+ evidence: WitanEvidencePointerSchema,
137
+ // Absence findings emitted by the native repository detector must name the exact
138
+ // inventory and versioned pattern set whose zero result supports the proposition.
139
+ // This remains optional at the shared schema boundary because positive findings and
140
+ // semantic derivations are separate contracts; native absence emitters require it.
141
+ derivation: WitanFindingDerivationSchema.optional()
124
142
  }).strict();
125
143
  var WitanCriterionMetricSchema = external_exports.object({
126
144
  name: external_exports.string().min(1).max(80),
@@ -130,7 +148,16 @@ var WitanCriterionMetricSchema = external_exports.object({
130
148
  kind: external_exports.enum(["ratio", "saturating_count"]).optional(),
131
149
  weight: external_exports.number().positive().max(1).default(1),
132
150
  unit: external_exports.string().min(1).max(40).optional(),
133
- description: external_exports.string().min(1).max(300).optional()
151
+ description: external_exports.string().min(1).max(300).optional(),
152
+ presentation: external_exports.object({
153
+ components: external_exports.array(
154
+ external_exports.object({
155
+ label: external_exports.string().min(1).max(120),
156
+ count: external_exports.number().int().nonnegative()
157
+ }).strict()
158
+ ).min(1).optional(),
159
+ condition: external_exports.string().min(1).max(240).optional()
160
+ }).strict().optional()
134
161
  }).strict();
135
162
  var WitanCriterionSignalSchema = external_exports.object({
136
163
  criterionId: WitanCriterionIdSchema,
@@ -312,6 +339,7 @@ var WitanReportSchema = external_exports.preprocess((value) => {
312
339
  }, StrictWitanReportSchema);
313
340
  var WITAN_ATTESTATION_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
314
341
  var WITAN_ATTESTATION_PREDICATE_TYPE = "https://cejel.dev/attestations/scan/v1";
342
+ var WITAN_REPORT_FORMAT_VERSION = "1.0";
315
343
  var WitanAttestationOutcomeSchema = external_exports.discriminatedUnion("status", [
316
344
  external_exports.object({
317
345
  status: external_exports.literal("scored"),
@@ -341,6 +369,10 @@ var WitanAttestationStatementSchema = external_exports.object({
341
369
  version: external_exports.string().min(1).max(80)
342
370
  }).strict(),
343
371
  generatedAt: external_exports.string().datetime({ offset: true }),
372
+ // Optional on read for attestations emitted before Evidence Seam v1. New attestations
373
+ // always carry it, keeping report.json byte-stable while giving paired consumers an
374
+ // explicit report contract version.
375
+ reportFormatVersion: external_exports.string().regex(/^1\.\d+$/).optional(),
344
376
  rubricVersion: external_exports.string().min(1).max(120),
345
377
  repository: external_exports.object({
346
378
  productSlug: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/),
@@ -367,13 +399,54 @@ var WitanAttestationStatementSchema = external_exports.object({
367
399
  }).strict()
368
400
  }).strict();
369
401
 
402
+ // src/witan/rubric-version.ts
403
+ init_esm_shims();
404
+ var WITAN_RUBRIC_VERSION_V9 = "witan-rubric-v9-2026-07-22";
405
+ var WITAN_RUBRIC_VERSION_V10 = "witan-rubric-v10-2026-07-23";
406
+ var WITAN_RUBRIC_VERSION_V11 = "witan-rubric-v11-2026-07-23";
407
+ var WITAN_RUBRIC_VERSION_V12 = "witan-rubric-v12-2026-07-23";
408
+ var WITAN_RUBRIC_VERSION_V13 = "witan-rubric-v13-2026-07-23";
409
+ var WITAN_RUBRIC_VERSION_V14 = "witan-rubric-v14-2026-07-23";
410
+ var WITAN_RUBRIC_VERSION_V15 = "witan-rubric-v15-2026-07-24";
411
+ var WITAN_RUBRIC_VERSION_V16 = "witan-rubric-v16-2026-07-24";
412
+ var WITAN_RUBRIC_VERSION_V17 = "witan-rubric-v17-2026-07-24";
413
+ var WITAN_RUBRIC_VERSION_V18 = "witan-rubric-v18-prospective-2026-07-25";
414
+ var WITAN_RUBRIC_VERSION_V19 = "witan-rubric-v19-prospective-2026-08-09";
415
+ var WITAN_RUBRIC_VERSION_V20 = "witan-rubric-v20-prospective-2026-08-10";
416
+ var WITAN_RUBRIC_VERSION_V21 = "witan-rubric-v21-prospective-2026-08-10";
417
+ var WITAN_RUBRIC_VERSION_V22 = "witan-rubric-v22-prospective-2026-08-10";
418
+ var WITAN_LAST_CALIBRATED_RUBRIC_VERSION = WITAN_RUBRIC_VERSION_V17;
419
+ var WITAN_PROSPECTIVE_RUBRIC_VERSIONS = Object.freeze([
420
+ WITAN_RUBRIC_VERSION_V18,
421
+ WITAN_RUBRIC_VERSION_V19,
422
+ WITAN_RUBRIC_VERSION_V20,
423
+ WITAN_RUBRIC_VERSION_V21,
424
+ WITAN_RUBRIC_VERSION_V22
425
+ ]);
426
+ var WITAN_SELECTABLE_RUBRIC_VERSIONS = Object.freeze([
427
+ WITAN_LAST_CALIBRATED_RUBRIC_VERSION,
428
+ ...WITAN_PROSPECTIVE_RUBRIC_VERSIONS
429
+ ]);
430
+ function assertSelectableRubricVersion(rubricVersion) {
431
+ if (WITAN_SELECTABLE_RUBRIC_VERSIONS.includes(rubricVersion)) return;
432
+ throw new Error(
433
+ `Cejel: unrecognized rubric version: "${rubricVersion}". Accepted values: ${WITAN_SELECTABLE_RUBRIC_VERSIONS.join(", ")}.`
434
+ );
435
+ }
436
+ function isProspectiveRubricVersion(rubricVersion) {
437
+ return WITAN_PROSPECTIVE_RUBRIC_VERSIONS.includes(rubricVersion);
438
+ }
439
+ 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.";
440
+
370
441
  // src/witan/html.ts
371
442
  init_esm_shims();
372
443
 
373
444
  // src/witan/abstention.ts
374
445
  init_esm_shims();
375
446
  var WITAN_NO_MEASUREMENT_REASON = "No free-core rubric criterion produced a measurable signal. Cejel abstains rather than publish a numeric zero for an entirely unmeasured repository.";
376
- var WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the complete tracked inventory contains no concrete test file, configured test runner, CI test command, or coverage configuration.";
447
+ var WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the tracked scan-eligible inventory matched no concrete test file, configured test runner, CI test command, or coverage configuration.";
448
+ var WITAN_DIRECTORY_WALK_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the bounded directory walk matched no concrete test file, configured test runner, CI test command, or coverage configuration.";
449
+ var WITAN_LEGACY_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the complete tracked inventory contains no concrete test file, configured test runner, CI test command, or coverage configuration.";
377
450
  function isWitanNoMeasurementAbstention(report) {
378
451
  return report.verdict === "insufficient_source" && report.insufficientSourceReason.startsWith(WITAN_NO_MEASUREMENT_REASON) && report.criteria.every(
379
452
  ({ status }) => status === "not_applicable" || status === "insufficient_data"
@@ -476,6 +549,45 @@ init_esm_shims();
476
549
 
477
550
  // src/witan/certificate-presentation.ts
478
551
  init_esm_shims();
552
+ var CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE = "Product name and slug are caller context and are excluded from certificate byte-comparison claims.";
553
+ var CERTIFICATE_METRIC_REGISTRY = {
554
+ test_to_source_ratio: { displayLabel: "Test-to-source file ratio" },
555
+ coverage_percent: { displayLabel: "Static coverage percentage" },
556
+ verification_script_ratio: { displayLabel: "Verification script ratio" },
557
+ non_hollow_test_share: { displayLabel: "Non-hollow test share" },
558
+ secret_cleanliness: { displayLabel: "Secret cleanliness" },
559
+ env_handling_depth: { displayLabel: "Environment handling depth" },
560
+ rls_policy_count: { displayLabel: "RLS policy count" },
561
+ tenant_scope_ratio: { displayLabel: "Tenant-scoped schema ratio" },
562
+ crypto_comparison_hygiene: { displayLabel: "Crypto comparison hygiene" },
563
+ prod_readiness_primitives: { displayLabel: "Production-readiness basic checks" },
564
+ prod_workflow_depth: {
565
+ displayLabel: "Production workflow depth (automated build/test/release pipeline)"
566
+ },
567
+ observability_depth: { displayLabel: "Observability depth" },
568
+ rollback_safety_depth: { displayLabel: "Rollback and migration-safety depth" },
569
+ dependency_automation_ratio: { displayLabel: "Dependency automation ratio" },
570
+ pinned_dependency_ratio: { displayLabel: "Pinned dependency ratio" },
571
+ lockfile_coverage: { displayLabel: "Lockfile coverage" },
572
+ declared_version_range_ratio: { displayLabel: "Declared version range ratio" },
573
+ dependency_count_sanity: { displayLabel: "Dependency count sanity" },
574
+ claim_match_rate: { displayLabel: "Implementation-to-claim-source file ratio" },
575
+ claim_source_depth: { displayLabel: "Claim source depth" },
576
+ reconciliation_artifact_depth: { displayLabel: "Reconciliation artifact depth" },
577
+ pr_trace_primitives: { displayLabel: "PR trace basic checks" },
578
+ pr_merge_ratio: { displayLabel: "Recent commits with recognizable PR references" },
579
+ ci_script_depth: { displayLabel: "CI verification depth" },
580
+ default_branch_ci_depth: {
581
+ displayLabel: "PR-gate CI workflow count (automated build/test pipeline)"
582
+ },
583
+ audit_artifact_depth: { displayLabel: "Audit artifact depth" },
584
+ audit_freshness_depth: { displayLabel: "Audit freshness depth" },
585
+ kill_switch_fail_safe_present: { displayLabel: "Un-overridable kill-switch present" },
586
+ human_gate_documented: { displayLabel: "Human gate documented" },
587
+ fail_closed_privilege_check: { displayLabel: "Fail-closed privilege check present" },
588
+ privilege_escalation_cleanliness: { displayLabel: "Privilege-escalation cleanliness" },
589
+ protected_path_review_gate: { displayLabel: "Protected-path review gate" }
590
+ };
479
591
  var CERTIFICATE_GLOSSARY = [
480
592
  {
481
593
  key: "labels",
@@ -491,188 +603,194 @@ var CERTIFICATE_GLOSSARY = [
491
603
  },
492
604
  {
493
605
  key: "test-to-source-file-ratio",
494
- term: "test-to-source file ratio",
606
+ term: "Test-to-source file ratio",
495
607
  definition: "The number of detected test files compared with detected source files. A larger implementation surface needs a correspondingly visible test surface.",
496
608
  metricNames: ["test_to_source_ratio"]
497
609
  },
498
610
  {
499
611
  key: "static-coverage",
500
- term: "static coverage",
612
+ term: "Static coverage percentage",
501
613
  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
614
  metricNames: ["coverage_percent"]
503
615
  },
504
616
  {
505
617
  key: "verification-script",
506
- term: "verification script",
507
- definition: "the checks that run automatically: tests, linting, type-checking, build",
618
+ term: "Verification script ratio",
619
+ 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
620
  metricNames: ["verification_script_ratio"]
509
621
  },
510
622
  {
511
623
  key: "non-hollow",
512
- term: "non-hollow",
624
+ term: "Non-hollow test share",
513
625
  definition: "test files that actually assert something rather than being empty or skipped",
514
626
  metricNames: ["non_hollow_test_share"]
515
627
  },
516
628
  {
517
629
  key: "secrets",
518
- term: "secrets",
519
- definition: "credentials like API keys and tokens",
630
+ term: "Secret cleanliness",
631
+ 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
632
  metricNames: ["secret_cleanliness"]
521
633
  },
522
634
  {
523
635
  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.",
636
+ term: "Environment handling depth",
637
+ 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
638
  metricNames: ["env_handling_depth"]
527
639
  },
528
640
  {
529
641
  key: "rls-policy-count",
530
642
  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.",
643
+ 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
644
  metricNames: ["rls_policy_count"]
533
645
  },
534
646
  {
535
647
  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.",
648
+ term: "Tenant-scoped schema ratio",
649
+ 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
650
  metricNames: ["tenant_scope_ratio"]
539
651
  },
540
652
  {
541
653
  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.",
654
+ term: "Crypto comparison hygiene",
655
+ 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
656
  metricNames: ["crypto_comparison_hygiene"]
545
657
  },
546
658
  {
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"]
659
+ key: "production-readiness-basic-checks",
660
+ term: "Production-readiness basic checks",
661
+ 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.",
662
+ metricNames: ["prod_readiness_primitives"]
663
+ },
664
+ {
665
+ key: "pr-trace-basic-checks",
666
+ term: "PR trace basic checks",
667
+ 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.",
668
+ metricNames: ["pr_trace_primitives"]
551
669
  },
552
670
  {
553
671
  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.",
672
+ term: "Production workflow depth (automated build/test/release pipeline)",
673
+ 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
674
  metricNames: ["prod_workflow_depth"]
557
675
  },
558
676
  {
559
677
  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.",
678
+ term: "Observability depth",
679
+ definition: "The number of implementation files containing a recognized logging, metrics, tracing, or error-reporting marker, capped at four for scoring.",
562
680
  metricNames: ["observability_depth"]
563
681
  },
564
682
  {
565
683
  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.",
684
+ term: "Rollback and migration-safety depth",
685
+ 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
686
  metricNames: ["rollback_safety_depth"]
569
687
  },
570
688
  {
571
689
  key: "dependency-automation-ratio",
572
- term: "dependency automation ratio",
690
+ term: "Dependency automation ratio",
573
691
  definition: "The share of expected automated dependency-update and audit controls that are present. Stale or vulnerable packages otherwise depend on manual discovery.",
574
692
  metricNames: ["dependency_automation_ratio"]
575
693
  },
576
694
  {
577
695
  key: "pinned-dependency-ratio",
578
- term: "pinned dependency ratio",
696
+ term: "Pinned dependency ratio",
579
697
  definition: "The share of application dependencies fixed to exact versions. Exact inputs make deployed builds more reproducible.",
580
698
  metricNames: ["pinned_dependency_ratio"]
581
699
  },
582
700
  {
583
701
  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.",
702
+ term: "Lockfile coverage",
703
+ 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
704
  metricNames: ["lockfile_coverage"]
587
705
  },
588
706
  {
589
707
  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.",
708
+ term: "Declared version range ratio",
709
+ 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
710
  metricNames: ["declared_version_range_ratio"]
593
711
  },
594
712
  {
595
713
  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.",
714
+ term: "Dependency count sanity",
715
+ 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
716
  metricNames: ["dependency_count_sanity"]
599
717
  },
600
718
  {
601
719
  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.",
720
+ term: "Implementation-to-claim-source file ratio",
721
+ 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
722
  metricNames: ["claim_match_rate"]
605
723
  },
606
724
  {
607
725
  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.",
726
+ term: "Claim source depth",
727
+ 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
728
  metricNames: ["claim_source_depth"]
611
729
  },
612
730
  {
613
731
  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.",
732
+ term: "Reconciliation artifact depth",
733
+ definition: "The number of detected dedicated claim-reality reconciliation artifacts, capped at three for scoring.",
616
734
  metricNames: ["reconciliation_artifact_depth"]
617
735
  },
618
736
  {
619
737
  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.",
738
+ term: "Recent commits with recognizable PR references",
739
+ 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
740
  metricNames: ["pr_merge_ratio"]
623
741
  },
624
742
  {
625
743
  key: "ci-verification-depth",
626
744
  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.",
745
+ 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
746
  metricNames: ["ci_script_depth"]
629
747
  },
630
748
  {
631
749
  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.",
750
+ term: "PR-gate CI workflow count (automated build/test pipeline)",
751
+ 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
752
  metricNames: ["default_branch_ci_depth"]
635
753
  },
636
754
  {
637
755
  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.",
756
+ term: "Audit artifact depth",
757
+ 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
758
  metricNames: ["audit_artifact_depth"]
641
759
  },
642
760
  {
643
761
  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.",
762
+ term: "Audit freshness depth",
763
+ 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
764
  metricNames: ["audit_freshness_depth"]
647
765
  },
648
766
  {
649
767
  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.",
768
+ term: "Un-overridable kill-switch present",
769
+ 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
770
  metricNames: ["kill_switch_fail_safe_present"]
653
771
  },
654
772
  {
655
773
  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.",
774
+ term: "Human gate documented",
775
+ 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
776
  metricNames: ["human_gate_documented"]
659
777
  },
660
778
  {
661
779
  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.",
780
+ term: "Fail-closed privilege check present",
781
+ 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
782
  metricNames: ["fail_closed_privilege_check"]
665
783
  },
666
784
  {
667
785
  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.",
786
+ term: "Privilege-escalation cleanliness",
787
+ 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
788
  metricNames: ["privilege_escalation_cleanliness"]
671
789
  },
672
790
  {
673
791
  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.",
792
+ term: "Protected-path review gate",
793
+ 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
794
  metricNames: ["protected_path_review_gate"]
677
795
  }
678
796
  ];
@@ -689,14 +807,8 @@ function glossaryEntryForMetric(metric2) {
689
807
  metricNames: [metric2.name]
690
808
  };
691
809
  }
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
810
  function formatCertificateMetricLabel(metric2) {
699
- return CERTIFICATE_METRIC_LABELS[metric2.name] ?? metric2.label;
811
+ return CERTIFICATE_METRIC_REGISTRY[metric2.name]?.displayLabel ?? metric2.label;
700
812
  }
701
813
  function glossaryEntriesForReport(report) {
702
814
  const entries = new Map(CERTIFICATE_GLOSSARY.map((entry) => [entry.key, entry]));
@@ -718,6 +830,62 @@ function labelAlreadyStatesUnit(label, unit) {
718
830
  const escapedUnit = unit.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
719
831
  return new RegExp(`(?:^|[\\s-])${escapedUnit}$`, "i").test(label.trim());
720
832
  }
833
+ var BINARY_METRIC_COPY = {
834
+ secret_cleanliness: {
835
+ positive: "clean \u2014 no critical secret findings detected",
836
+ negative: "not clean \u2014 one or more critical secret findings detected"
837
+ },
838
+ crypto_comparison_hygiene: {
839
+ positive: "clean \u2014 no insecure comparison or serialization pattern detected",
840
+ negative: "not clean \u2014 an insecure comparison or serialization pattern was detected"
841
+ },
842
+ lockfile_coverage: {
843
+ positive: "present \u2014 at least one covering lockfile detected",
844
+ negative: "missing \u2014 no covering lockfile detected"
845
+ },
846
+ kill_switch_fail_safe_present: {
847
+ positive: "present \u2014 a fail-closed safety toggle was detected",
848
+ negative: "missing \u2014 no fail-closed safety toggle detected"
849
+ },
850
+ human_gate_documented: {
851
+ positive: "present \u2014 privileged operations are documented as human-executed or gated",
852
+ negative: "missing \u2014 no human-gate documentation detected"
853
+ },
854
+ fail_closed_privilege_check: {
855
+ positive: "present \u2014 role membership is checked fail-closed before elevation",
856
+ negative: "missing \u2014 no fail-closed pre-elevation role check detected"
857
+ },
858
+ privilege_escalation_cleanliness: {
859
+ positive: "clean \u2014 no ungated production privilege-escalation pattern detected",
860
+ negative: "not clean \u2014 an ungated production privilege-escalation pattern was detected"
861
+ },
862
+ protected_path_review_gate: {
863
+ positive: "present \u2014 CODEOWNERS or a documented required-review policy was detected",
864
+ negative: "missing \u2014 no CODEOWNERS or documented required-review policy detected"
865
+ }
866
+ };
867
+ var CONDITIONAL_METRIC_COPY = {
868
+ crypto_comparison_hygiene: "scored because a signing, HMAC, or secret-comparison surface was detected",
869
+ rls_policy_count: "scored because a multi-tenant data surface was detected",
870
+ tenant_scope_ratio: "scored because a multi-tenant data surface was detected",
871
+ human_gate_documented: "scored because a privileged-operation-shaped surface was detected",
872
+ fail_closed_privilege_check: "scored because a privileged-operation-shaped surface was detected",
873
+ kill_switch_fail_safe_present: "shown because a fail-closed kill switch was detected; this positive-only metric is omitted when absent"
874
+ };
875
+ function appendMetricCondition(metric2, value) {
876
+ const condition = metric2.presentation?.condition ?? CONDITIONAL_METRIC_COPY[metric2.name];
877
+ return condition ? `${value}; ${condition}` : value;
878
+ }
879
+ function formatEnumerableMetric(metric2) {
880
+ const components = metric2.presentation?.components;
881
+ if (!components || metric2.max === void 0) return null;
882
+ const present = components.filter((component) => component.count > 0).map(
883
+ (component) => component.count === 1 ? component.label : `${component.label} (${component.count} detected)`
884
+ );
885
+ const missing = components.filter((component) => component.count === 0).map((component) => component.label);
886
+ 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)}`;
887
+ return `${value} \u2014 present: ${present.length > 0 ? present.join(", ") : "none"}; missing: ${missing.length > 0 ? missing.join(", ") : "none"}`;
888
+ }
721
889
  function formatCertificateMetricValue(criterion, metric2) {
722
890
  if (isCoverageNotMeasured(criterion, metric2)) {
723
891
  return "no coverage report found \u2014 not measured";
@@ -726,15 +894,36 @@ function formatCertificateMetricValue(criterion, metric2) {
726
894
  const tests = formatMetricNumber(metric2.value);
727
895
  const sources = formatMetricNumber(metric2.max);
728
896
  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;
897
+ return appendMetricCondition(
898
+ metric2,
899
+ metric2.value > metric2.max ? `${comparison} (credit capped at parity)` : comparison
900
+ );
730
901
  }
902
+ const binaryCopy = BINARY_METRIC_COPY[metric2.name];
903
+ if (binaryCopy && metric2.max === 1 && (metric2.value === 0 || metric2.value === 1)) {
904
+ return appendMetricCondition(metric2, metric2.value === 1 ? binaryCopy.positive : binaryCopy.negative);
905
+ }
906
+ if (metric2.name === "pr_merge_ratio" && metric2.max !== void 0) {
907
+ 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`;
908
+ return appendMetricCondition(metric2, value);
909
+ }
910
+ const enumerable = formatEnumerableMetric(metric2);
911
+ if (enumerable) return appendMetricCondition(metric2, enumerable);
731
912
  const displayUnit = metric2.name === "prod_readiness_primitives" || metric2.name === "pr_trace_primitives" ? "checks" : labelAlreadyStatesUnit(formatCertificateMetricLabel(metric2), metric2.unit) ? void 0 : metric2.unit;
732
913
  const unit = displayUnit ? ` ${displayUnit}` : "";
733
- if (metric2.max === void 0) return `${formatMetricNumber(metric2.value)}${unit}`;
914
+ if (metric2.max === void 0) {
915
+ return appendMetricCondition(metric2, `${formatMetricNumber(metric2.value)}${unit}`);
916
+ }
734
917
  if (metric2.kind === "saturating_count" && metric2.value > metric2.max) {
735
- return `${formatMetricNumber(metric2.max)}${unit} (capped; ${formatMetricNumber(metric2.value)} raw)`;
918
+ return appendMetricCondition(
919
+ metric2,
920
+ `${formatMetricNumber(metric2.max)}${unit} (capped; ${formatMetricNumber(metric2.value)} raw)`
921
+ );
736
922
  }
737
- return `${formatMetricNumber(metric2.value)}/${formatMetricNumber(metric2.max)}${unit}`;
923
+ return appendMetricCondition(
924
+ metric2,
925
+ `${formatMetricNumber(metric2.value)}/${formatMetricNumber(metric2.max)}${unit}`
926
+ );
738
927
  }
739
928
  function buildRelyingPartySummary(report) {
740
929
  const applicable = report.criteria.filter((criterion) => criterion.status !== "not_applicable");
@@ -749,6 +938,13 @@ function buildRelyingPartySummary(report) {
749
938
  "No coverage report or configured threshold was found, so static coverage was not measured; Cejel does not run the subject's tests."
750
939
  );
751
940
  }
941
+ if (!report.repo.headSha && report.criteria.some(
942
+ (criterion) => criterion.metrics.some((metric2) => metric2.name === "pr_merge_ratio")
943
+ )) {
944
+ gaps.push(
945
+ "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."
946
+ );
947
+ }
752
948
  const unmeasured = applicable.filter((criterion) => criterion.status === "insufficient_data");
753
949
  if (unmeasured.length > 0) {
754
950
  gaps.push(`The report did not measure ${unmeasured.map((criterion) => criterion.id).join(", ")}.`);
@@ -788,7 +984,7 @@ var METRIC_DERIVED_FINDING_PATTERN = /^[A-Z]\d+ metric-derived score is (\d+\.\d
788
984
  var REMEDIATION_BY_METRIC = {
789
985
  test_to_source_ratio: "add concrete regression tests for the implementation surface",
790
986
  coverage_percent: "configure coverage and publish a measured threshold or report",
791
- verification_script_ratio: "add explicit test, lint, and typecheck verification commands",
987
+ verification_script_ratio: "add the missing test, coverage, lint, or type-check command or a test-runner configuration",
792
988
  non_hollow_test_share: "replace skipped or placeholder tests with live assertions",
793
989
  env_handling_depth: "document and enforce safe environment-secret handling",
794
990
  secret_cleanliness: "remove committed secrets and rotate any exposed credentials",
@@ -806,7 +1002,7 @@ var REMEDIATION_BY_METRIC = {
806
1002
  claim_match_rate: "align documented product claims with observable repository evidence",
807
1003
  claim_source_depth: "document concrete, checkable product claims",
808
1004
  reconciliation_artifact_depth: "publish a claim-to-evidence reconciliation record",
809
- pr_merge_ratio: "preserve merged pull-request history in the scanned clone",
1005
+ pr_merge_ratio: "preserve recognizable pull-request references in recent commit subjects",
810
1006
  pr_trace_primitives: "add pull-request templates and outcome-trace records",
811
1007
  ci_script_depth: "run the repository verification commands in CI",
812
1008
  default_branch_ci_depth: "run CI on the default branch and pull requests",
@@ -856,6 +1052,7 @@ function formatMetricMember(criterion, metric2) {
856
1052
  }
857
1053
 
858
1054
  // src/witan/html.ts
1055
+ var CEJEL_CERTIFICATE_FORMAT_VERSION = "1.0";
859
1056
  function renderWitanHtmlReport(report, options = {}) {
860
1057
  const codeCriteria = criteriaByCategory(report, "code_trust");
861
1058
  const processCriteria = criteriaByCategory(report, "process_trust");
@@ -874,6 +1071,7 @@ function renderWitanHtmlReport(report, options = {}) {
874
1071
  <head>
875
1072
  <meta charset="utf-8">
876
1073
  <meta name="viewport" content="width=device-width, initial-scale=1">
1074
+ <meta name="cejel-certificate-format" content="${CEJEL_CERTIFICATE_FORMAT_VERSION}">
877
1075
  <title>Cejel Trust Certificate - ${escapeHtml(report.productDisplayName)}</title>
878
1076
  <style>${CERTIFICATE_CSS}</style>
879
1077
  </head>
@@ -889,10 +1087,14 @@ function renderWitanHtmlReport(report, options = {}) {
889
1087
  <p class="eyebrow">Trust Certificate</p>
890
1088
  <h1>${escapeHtml(report.productDisplayName)}</h1>
891
1089
  <dl class="meta">
1090
+ <div><dt>Product identity</dt><dd>${escapeHtml(CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE)}</dd></div>
892
1091
  ${options.generatedAt ? `<div><dt>Date</dt><dd>${escapeHtml(formatDate(options.generatedAt))}</dd></div>` : ""}
893
1092
  <div><dt>Run</dt><dd>${escapeHtml(renderRepo(report))}</dd></div>
894
1093
  <div><dt>CLI</dt><dd>${escapeHtml(options.cliVersion ? `Cejel ${options.cliVersion}` : "Not recorded")}</dd></div>
895
1094
  <div><dt>Rubric</dt><dd>${escapeHtml(report.rubricVersion)}</dd></div>
1095
+ ${// Gated on the rubric actually being prospective — a calibrated (v17) certificate's
1096
+ // HTML stays byte-identical to before --rubric-pin existed.
1097
+ isProspectiveRubricVersion(report.rubricVersion) ? `<div><dt>Calibration</dt><dd class="prospective-rubric-notice">${escapeHtml(PROSPECTIVE_RUBRIC_NOTICE)}</dd></div>` : ""}
896
1098
  ${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
1099
  </dl>
898
1100
  </section>
@@ -1052,7 +1254,7 @@ function renderNotApplicableGroup(criteria) {
1052
1254
  }
1053
1255
  function renderNotApplicableItem(criterion) {
1054
1256
  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>`;
1257
+ 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
1258
  }
1057
1259
  function renderCriterionCard(criterion, gitHistoryUnavailable) {
1058
1260
  const evidence = [
@@ -1061,7 +1263,7 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
1061
1263
  ];
1062
1264
  const metrics = criterion.metrics.map((metric2) => renderMetric(criterion, metric2));
1063
1265
  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>` : "";
1266
+ 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
1267
  return `<article class="criterion">
1066
1268
  <div class="criterion-top">
1067
1269
  <div>
@@ -1082,11 +1284,22 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
1082
1284
  </article>`;
1083
1285
  }
1084
1286
  function renderStatusReconciliation(criterion) {
1085
- if (criterion.status === "not_applicable" || criterion.status === "insufficient_data" || criterion.status === "unverified") {
1086
- return "";
1287
+ if (criterion.status === "not_applicable") {
1288
+ 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>';
1289
+ }
1290
+ if (criterion.status === "insufficient_data") {
1291
+ 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>';
1292
+ }
1293
+ if (criterion.status === "unverified") {
1294
+ 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
1295
  }
1088
1296
  const numericBand = scoreBandForPresentation(criterion.score);
1089
- if (numericBand === criterion.status) return "";
1297
+ if (numericBand === criterion.status) {
1298
+ 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>`;
1299
+ }
1300
+ if (criterion.status !== "verified" && criterion.status !== "info" && criterion.status !== "warning" && criterion.status !== "critical") {
1301
+ 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>`;
1302
+ }
1090
1303
  const entry = CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "labels");
1091
1304
  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
1305
  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 +1316,7 @@ function renderMetric(criterion, metric2) {
1103
1316
  const formatted = formatCertificateMetricValue(criterion, metric2);
1104
1317
  const cappedEntry = formatted.includes("capped") ? CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "capped") : void 0;
1105
1318
  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>`;
1319
+ if (!cappedEntry) return `${label}<span class="metric-value">${escapeHtml(formatted)}</span>`;
1107
1320
  const cappedTooltipId = `${tooltipId}-capped`;
1108
1321
  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
1322
  }
@@ -1274,6 +1487,8 @@ h2 { font-family: var(--serif); font-size: 30px; font-weight: 400; letter-spacin
1274
1487
  .na-list li { margin: 0; color: var(--muted); }
1275
1488
  .na-id { color: var(--faint); font-family: var(--mono); font-size: 11px; margin-right: 6px; }
1276
1489
  .na-reason { color: var(--muted); }
1490
+ .na-reconciliation { display: block; margin-top: 4px; color: var(--muted); }
1491
+ .na-reconciliation strong { color: var(--text); }
1277
1492
  .criterion { border: 1px solid var(--line); border-radius: 8px; padding: 16px; background: var(--surface-2); }
1278
1493
  .criterion-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; }
1279
1494
  .criterion-id { color: var(--periwinkle); font-family: var(--mono); font-size: 12px; margin-bottom: 4px; }
@@ -1288,7 +1503,10 @@ h3 { font-size: 15px; line-height: 1.35; font-weight: 600; margin-bottom: 0; }
1288
1503
  .criterion-metrics { display: grid; gap: 7px; margin-top: 10px; padding: 0; list-style: none; font-size: 12px; }
1289
1504
  .criterion-metrics li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(238, 244, 247, .08); padding-bottom: 6px; }
1290
1505
  .criterion-metrics strong { color: var(--text); font-weight: 650; }
1291
- .criterion-metrics span { white-space: nowrap; color: var(--muted); }
1506
+ .criterion-metrics span { color: var(--muted); }
1507
+ .criterion-metrics > li > .metric-value {
1508
+ min-width: 0; white-space: normal; overflow-wrap: anywhere; text-align: right;
1509
+ }
1292
1510
  .term-help { position: relative; cursor: help; outline: none; }
1293
1511
  .term-help:focus-visible { box-shadow: 0 0 0 2px var(--periwinkle); border-radius: 3px; }
1294
1512
  .term-tooltip {
@@ -1448,6 +1666,7 @@ function createWitanAttestation(report, options) {
1448
1666
  predicate: {
1449
1667
  tool: { name: "cejel", version: options.toolVersion },
1450
1668
  generatedAt: options.generatedAt,
1669
+ reportFormatVersion: WITAN_REPORT_FORMAT_VERSION,
1451
1670
  rubricVersion: report.rubricVersion,
1452
1671
  repository: {
1453
1672
  productSlug: report.productSlug,
@@ -1527,47 +1746,29 @@ function verifyWitanAttestationBinding(statement, report, options = {}) {
1527
1746
  return { valid: errors.length === 0, errors };
1528
1747
  }
1529
1748
 
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
1749
  // src/witan/index.ts
1566
1750
  init_esm_shims();
1567
1751
 
1568
1752
  // src/witan/markdown.ts
1569
1753
  init_esm_shims();
1570
1754
 
1755
+ // src/presentation-safety.ts
1756
+ init_esm_shims();
1757
+ var PRESENTATION_CONTROL_PATTERN = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
1758
+ function sanitizePresentationLine(value, options) {
1759
+ const withoutControls = value.replace(PRESENTATION_CONTROL_PATTERN, "").trim();
1760
+ const safeValue = withoutControls.length > 0 ? withoutControls : options.fallback;
1761
+ if (safeValue.length <= options.maxLength) return safeValue;
1762
+ const ellipsis = ".".repeat(Math.min(3, options.maxLength));
1763
+ const contentLimit = Math.max(options.maxLength - ellipsis.length, 0);
1764
+ let prefix = "";
1765
+ for (const character of safeValue) {
1766
+ if (prefix.length + character.length > contentLimit) break;
1767
+ prefix += character;
1768
+ }
1769
+ return `${prefix}${ellipsis}`;
1770
+ }
1771
+
1571
1772
  // src/witan/repo-signals.ts
1572
1773
  init_esm_shims();
1573
1774
  import { createHash as createHash2 } from "crypto";
@@ -1589,6 +1790,71 @@ function stripBom(text) {
1589
1790
  function usesV17DetectorClosure(rubricVersion) {
1590
1791
  return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
1591
1792
  }
1793
+ var LEGACY_INVENTORY_ABSENCE_CONTEXT = {
1794
+ rubricGeneration: "legacy",
1795
+ inventoryProvenance: "directory-walk"
1796
+ };
1797
+ var INVENTORY_SCAN_PATTERN_SETS = {
1798
+ a1AuthenticatedAbsence: {
1799
+ id: "cejel.core-a1.test-integrity-surface.v1",
1800
+ count: 4
1801
+ },
1802
+ a1ConcreteTests: {
1803
+ id: "cejel.core-a1.concrete-test-files.v1",
1804
+ count: 16
1805
+ },
1806
+ a1CoverageConfiguration: {
1807
+ id: "cejel.core-a1.coverage-configuration.v1",
1808
+ count: 10
1809
+ },
1810
+ a2CurrentSecret: {
1811
+ id: "cejel.core-a2.current-secret-shape.v1",
1812
+ count: 3
1813
+ },
1814
+ a2RlsPolicy: {
1815
+ id: "cejel.core-a2.rls-policy.v1",
1816
+ count: 3
1817
+ },
1818
+ a3CiOrReleaseDeploy: {
1819
+ id: "cejel.core-a3.ci-or-release-deploy.v1",
1820
+ count: 12
1821
+ },
1822
+ a4Lockfile: {
1823
+ id: "cejel.core-a4.lockfile.v1",
1824
+ count: 11
1825
+ },
1826
+ a5ClaimRealityArtifact: {
1827
+ id: "cejel.core-a5.claim-reality-artifact.v1",
1828
+ count: 3
1829
+ }
1830
+ };
1831
+ function inventoryScanDerivation(inventoryCount, patternSet, matchCount) {
1832
+ return {
1833
+ kind: "inventory-scan",
1834
+ inventoryScope: "tracked-scan-eligible",
1835
+ inventoryCount,
1836
+ patternSetId: patternSet.id,
1837
+ patternCount: patternSet.count,
1838
+ matchCount
1839
+ };
1840
+ }
1841
+ function inventoryAbsenceFinding(options) {
1842
+ const { context, summaries } = options;
1843
+ const summary = context.rubricGeneration === "legacy" ? summaries.legacy : context.inventoryProvenance === "tracked-scan-eligible" ? summaries.currentTracked : summaries.currentDirectoryWalk;
1844
+ const emitsDerivation = context.rubricGeneration === "current" && context.inventoryProvenance === "tracked-scan-eligible";
1845
+ return {
1846
+ severity: options.severity,
1847
+ summary,
1848
+ evidence: options.evidence,
1849
+ ...emitsDerivation ? {
1850
+ derivation: inventoryScanDerivation(
1851
+ options.inventoryCount,
1852
+ options.patternSet,
1853
+ options.matchCount
1854
+ )
1855
+ } : {}
1856
+ };
1857
+ }
1592
1858
  function buildWitanInputFromRepo(options) {
1593
1859
  const tracked = trackContentReads(options.repoPath, () => buildWitanInputFromRepoUntracked(options));
1594
1860
  const affectedCriteria = tracked.affectedCriteria;
@@ -1627,6 +1893,10 @@ function buildWitanInputFromRepoUntracked(options) {
1627
1893
  const inventoryFiles = fileInventory.inventoryFiles;
1628
1894
  const ignoredTargetReason = repoFiles.length === 0 ? explainIgnoredScanTarget(options.repoPath) : void 0;
1629
1895
  const usesV17Detectors = usesV17DetectorClosure(rubricVersion);
1896
+ const inventoryAbsenceContext = {
1897
+ rubricGeneration: usesV17Detectors ? "current" : "legacy",
1898
+ inventoryProvenance: fileInventory.repoFileInventoryScope === "tracked-scan-eligible" ? "tracked-scan-eligible" : "directory-walk"
1899
+ };
1630
1900
  const usesV18NativeRls = rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
1631
1901
  const usesV19CommitYear = rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
1632
1902
  const usesV20A3ExplicitGaps = rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
@@ -1668,6 +1938,7 @@ function buildWitanInputFromRepoUntracked(options) {
1668
1938
  usesV21ExecutedEscalations,
1669
1939
  usesV22PackageStartEntrypoint,
1670
1940
  reviewableSourceProof,
1941
+ inventoryAbsenceContext,
1671
1942
  scanLimitations
1672
1943
  );
1673
1944
  const archetype = usesV17Detectors && reviewableSourceProof?.passes === true && isV17RescuableAbstention(gatedArchetype.abstentionKind) && coreSignals.some(isMeasuredCriterionSignal) ? {
@@ -1677,7 +1948,10 @@ function buildWitanInputFromRepoUntracked(options) {
1677
1948
  } : gatedArchetype;
1678
1949
  return {
1679
1950
  productSlug: options.productSlug,
1680
- productDisplayName: options.productDisplayName,
1951
+ productDisplayName: sanitizePresentationLine(options.productDisplayName, {
1952
+ fallback: "Repository",
1953
+ maxLength: 120
1954
+ }),
1681
1955
  repo: {
1682
1956
  ...headSha ? { headSha } : {}
1683
1957
  },
@@ -2274,7 +2548,7 @@ function buildNotApplicableSignal(criterionId, reason) {
2274
2548
  notApplicable: true
2275
2549
  };
2276
2550
  }
2277
- function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, reviewableSourceProof, scanLimitations = /* @__PURE__ */ new Set()) {
2551
+ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
2278
2552
  const signals = [];
2279
2553
  const mono = resolveMonorepoContext(repoPath, scanLimitations);
2280
2554
  const collectCriterion = (criterionId, collect) => withContentReadCriterion(criterionId, collect);
@@ -2286,7 +2560,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2286
2560
  useV27Detectors,
2287
2561
  useV33Detectors,
2288
2562
  useV36Detectors,
2289
- reviewableSourceProof
2563
+ reviewableSourceProof,
2564
+ inventoryAbsenceContext
2290
2565
  )
2291
2566
  );
2292
2567
  const a2Signal = collectCriterion(
@@ -2300,6 +2575,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2300
2575
  useV39Detectors,
2301
2576
  useV47Detectors,
2302
2577
  useV18NativeRls,
2578
+ inventoryAbsenceContext,
2303
2579
  scanLimitations
2304
2580
  )
2305
2581
  );
@@ -2310,7 +2586,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2310
2586
  repoFiles,
2311
2587
  useV27Detectors,
2312
2588
  useV20A3ExplicitGaps,
2313
- useV22PackageStartEntrypoint
2589
+ useV22PackageStartEntrypoint,
2590
+ inventoryAbsenceContext
2314
2591
  )
2315
2592
  );
2316
2593
  const a4Signal = collectCriterion(
@@ -2320,12 +2597,18 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2320
2597
  repoFiles,
2321
2598
  mono,
2322
2599
  useV27Detectors,
2323
- useV47Detectors
2600
+ useV47Detectors,
2601
+ inventoryAbsenceContext
2324
2602
  )
2325
2603
  );
2326
2604
  const a5Signal = collectCriterion(
2327
2605
  "A5",
2328
- () => collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors)
2606
+ () => collectA5ClaimRealityEvidence(
2607
+ repoPath,
2608
+ repoFiles,
2609
+ useV27Detectors,
2610
+ inventoryAbsenceContext
2611
+ )
2329
2612
  );
2330
2613
  const b1Signal = buildNotApplicableSignal(
2331
2614
  "B1",
@@ -2376,7 +2659,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2376
2659
  }
2377
2660
  return signals;
2378
2661
  }
2379
- function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, reviewableSourceProof) {
2662
+ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
2380
2663
  const evidence = [];
2381
2664
  const findings = [];
2382
2665
  const allTestFiles = findConcreteTestFiles(
@@ -2473,7 +2756,12 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2473
2756
  }
2474
2757
  }
2475
2758
  if (evidence.length === 0 && findings.length === 0 && configuredRunnerFiles.length === 0) {
2476
- return reviewableSourceProof?.passes === true && reviewableSourceProof.firstReadablePath ? buildA1AuthenticatedAbsenceSignal(repoPath, reviewableSourceProof) : null;
2759
+ return reviewableSourceProof?.passes === true && reviewableSourceProof.firstReadablePath ? buildA1AuthenticatedAbsenceSignal(
2760
+ repoPath,
2761
+ reviewableSourceProof,
2762
+ repoFiles.length,
2763
+ inventoryAbsenceContext
2764
+ ) : null;
2477
2765
  }
2478
2766
  if (testFiles.length === 0) {
2479
2767
  const fallback = configuredRunnerFiles[0] ? evidenceForRelative(
@@ -2483,11 +2771,21 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2483
2771
  "Configured test runner"
2484
2772
  ) : useV33Detectors ? void 0 : evidence[0] ?? findings[0]?.evidence;
2485
2773
  if (fallback) {
2486
- findings.push({
2487
- severity: "warning",
2488
- summary: "A test runner is configured, but no concrete test files were detected.",
2489
- evidence: fallback
2490
- });
2774
+ findings.push(
2775
+ inventoryAbsenceFinding({
2776
+ severity: "warning",
2777
+ summaries: {
2778
+ legacy: "A test runner is configured, but no concrete test files were detected.",
2779
+ currentTracked: "A test runner is configured, but the tracked scan-eligible inventory matched no concrete test file.",
2780
+ currentDirectoryWalk: "A test runner is configured, but the bounded directory walk matched no concrete test file."
2781
+ },
2782
+ evidence: fallback,
2783
+ context: inventoryAbsenceContext,
2784
+ inventoryCount: repoFiles.length,
2785
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a1ConcreteTests,
2786
+ matchCount: 0
2787
+ })
2788
+ );
2491
2789
  }
2492
2790
  } else if (coverageFiles.length === 0 && (!useV33Detectors || configuredRunnerFiles.length > 0)) {
2493
2791
  if (isLeanTestToolchain) {
@@ -2505,11 +2803,21 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2505
2803
  } else {
2506
2804
  const firstEvidence = evidence[0];
2507
2805
  if (!firstEvidence) return null;
2508
- findings.push({
2509
- severity: "info",
2510
- summary: "Test suite files are present, but no coverage configuration was detected.",
2511
- evidence: firstEvidence
2512
- });
2806
+ findings.push(
2807
+ inventoryAbsenceFinding({
2808
+ severity: "info",
2809
+ summaries: {
2810
+ legacy: "Test suite files are present, but no coverage configuration was detected.",
2811
+ currentTracked: "Test suite files are present, but the tracked scan-eligible inventory matched no coverage configuration.",
2812
+ currentDirectoryWalk: "Test suite files are present, but the bounded directory walk matched no coverage configuration."
2813
+ },
2814
+ evidence: firstEvidence,
2815
+ context: inventoryAbsenceContext,
2816
+ inventoryCount: repoFiles.length,
2817
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a1CoverageConfiguration,
2818
+ matchCount: 0
2819
+ })
2820
+ );
2513
2821
  }
2514
2822
  }
2515
2823
  const nonHollowShare = measureNonHollowTestShare(
@@ -2548,8 +2856,26 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2548
2856
  4,
2549
2857
  0.25,
2550
2858
  "ratio",
2551
- "Measures explicit test/lint/typecheck verification commands (via npm script or CI-invoked tool) plus test runner configuration.",
2552
- "saturating_count"
2859
+ "Counts detected test, coverage, lint, and typecheck commands plus detected test-runner configuration files; raw credit is capped at four.",
2860
+ "saturating_count",
2861
+ {
2862
+ components: [
2863
+ {
2864
+ label: "test command",
2865
+ count: packageScripts.has("test") || Boolean(ciTestWorkflow) ? 1 : 0
2866
+ },
2867
+ { label: "coverage command", count: packageScripts.has("coverage") ? 1 : 0 },
2868
+ {
2869
+ label: "lint command",
2870
+ count: packageScripts.has("lint") || ciHasLintCommand ? 1 : 0
2871
+ },
2872
+ {
2873
+ label: "type-check command",
2874
+ count: packageScripts.has("typecheck") || ciHasTypecheckCommand ? 1 : 0
2875
+ },
2876
+ { label: "test-runner configuration", count: runnerFiles.length }
2877
+ ]
2878
+ }
2553
2879
  ),
2554
2880
  metric(
2555
2881
  "non_hollow_test_share",
@@ -2564,7 +2890,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2564
2890
  notes: "A1 is detected from real test files, test runner configuration, and optional coverage configuration."
2565
2891
  };
2566
2892
  }
2567
- function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
2893
+ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inventoryAbsenceContext) {
2568
2894
  const sourcePath = proof.firstReadablePath;
2569
2895
  if (!sourcePath) {
2570
2896
  throw new Error("Cejel invariant: a passing reviewable-source proof must have a readable path");
@@ -2579,11 +2905,19 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
2579
2905
  criterionId: "A1",
2580
2906
  positiveEvidence: [anchor],
2581
2907
  findings: [
2582
- {
2908
+ inventoryAbsenceFinding({
2583
2909
  severity: "critical",
2584
- summary: WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY,
2585
- evidence: anchor
2586
- }
2910
+ summaries: {
2911
+ legacy: WITAN_LEGACY_AUTHENTICATED_A1_ABSENCE_SUMMARY,
2912
+ currentTracked: WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY,
2913
+ currentDirectoryWalk: WITAN_DIRECTORY_WALK_AUTHENTICATED_A1_ABSENCE_SUMMARY
2914
+ },
2915
+ evidence: anchor,
2916
+ context: inventoryAbsenceContext,
2917
+ inventoryCount,
2918
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a1AuthenticatedAbsence,
2919
+ matchCount: 0
2920
+ })
2587
2921
  ],
2588
2922
  metrics: [
2589
2923
  metric(
@@ -2612,8 +2946,17 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
2612
2946
  4,
2613
2947
  0.25,
2614
2948
  "ratio",
2615
- "Measures explicit test/lint/typecheck verification commands plus test runner configuration.",
2616
- "saturating_count"
2949
+ "Counts detected test, coverage, lint, and typecheck commands plus detected test-runner configuration files; raw credit is capped at four.",
2950
+ "saturating_count",
2951
+ {
2952
+ components: [
2953
+ { label: "test command", count: 0 },
2954
+ { label: "coverage command", count: 0 },
2955
+ { label: "lint command", count: 0 },
2956
+ { label: "type-check command", count: 0 },
2957
+ { label: "test-runner configuration", count: 0 }
2958
+ ]
2959
+ }
2617
2960
  ),
2618
2961
  metric(
2619
2962
  "non_hollow_test_share",
@@ -2628,7 +2971,7 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
2628
2971
  notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files.`
2629
2972
  };
2630
2973
  }
2631
- function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, scanLimitations) {
2974
+ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, inventoryAbsenceContext, scanLimitations) {
2632
2975
  const evidence = [];
2633
2976
  const findings = [];
2634
2977
  const gitignore = repoFiles.find((file) => basename(file) === ".gitignore");
@@ -2781,16 +3124,26 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2781
3124
  )
2782
3125
  });
2783
3126
  } else if (committedEnvFilePath) {
2784
- findings.push({
2785
- severity: useV33Detectors ? "info" : "warning",
2786
- summary: "A non-template .env file is committed in the current repository tree; no secret-shaped value was detected.",
2787
- evidence: evidenceForRelative(
2788
- repoPath,
2789
- committedEnvFilePath,
2790
- "secret_scan",
2791
- "Committed .env file (no confirmed secret value found)"
2792
- )
2793
- });
3127
+ findings.push(
3128
+ inventoryAbsenceFinding({
3129
+ severity: useV33Detectors ? "info" : "warning",
3130
+ summaries: {
3131
+ legacy: "A non-template .env file is committed in the current repository tree; no secret-shaped value was detected.",
3132
+ currentTracked: "A tracked non-template .env file is present, while the bounded scan over the tracked scan-eligible inventory matched no secret-shaped value.",
3133
+ currentDirectoryWalk: "A non-template .env file is present in the bounded directory walk, which matched no secret-shaped value."
3134
+ },
3135
+ evidence: evidenceForRelative(
3136
+ repoPath,
3137
+ committedEnvFilePath,
3138
+ "secret_scan",
3139
+ "Committed .env file (no confirmed secret value found)"
3140
+ ),
3141
+ context: inventoryAbsenceContext,
3142
+ inventoryCount: repoFiles.length,
3143
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a2CurrentSecret,
3144
+ matchCount: 0
3145
+ })
3146
+ );
2794
3147
  }
2795
3148
  if (historySecretScan?.evidence) {
2796
3149
  const historyPath = historySecretScan.evidence.path ?? "";
@@ -2822,16 +3175,26 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2822
3175
  if (hasTenantWithoutRlsGap) {
2823
3176
  const gapEvidence = rlsMigration ?? tenantScopedFile ?? tenantWithoutRlsPremiseFiles[0];
2824
3177
  if (gapEvidence) {
2825
- findings.push({
2826
- severity: "warning",
2827
- summary: "Tenant-scoped schema detected but no row-level-security policies found \u2014 isolation gap.",
2828
- evidence: evidenceForRelative(
2829
- repoPath,
2830
- gapEvidence,
2831
- "artifact",
2832
- "Tenant schema without RLS policies"
2833
- )
2834
- });
3178
+ findings.push(
3179
+ inventoryAbsenceFinding({
3180
+ severity: "warning",
3181
+ summaries: {
3182
+ legacy: "Tenant-scoped schema detected but no row-level-security policies found \u2014 isolation gap.",
3183
+ currentTracked: "A tenant-scoped schema signal is present, while the tracked scan-eligible inventory matched no row-level-security policy.",
3184
+ currentDirectoryWalk: "A tenant-scoped schema signal is present, while the bounded directory walk matched no row-level-security policy."
3185
+ },
3186
+ evidence: evidenceForRelative(
3187
+ repoPath,
3188
+ gapEvidence,
3189
+ "artifact",
3190
+ "Tenant schema without RLS policies"
3191
+ ),
3192
+ context: inventoryAbsenceContext,
3193
+ inventoryCount: repoFiles.length,
3194
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a2RlsPolicy,
3195
+ matchCount: 0
3196
+ })
3197
+ );
2835
3198
  }
2836
3199
  }
2837
3200
  evidence.push(...cryptoHygiene.evidence);
@@ -2855,7 +3218,22 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2855
3218
  3,
2856
3219
  isMultiTenant ? 0.1 : 0.3,
2857
3220
  "practices",
2858
- "Counts three bounded env-handling practices (0\u20133): template file, gitignore rule, env reads in any supported language."
3221
+ "Counts three bounded env-handling practices (0\u20133): template file, gitignore rule, env reads in any supported language.",
3222
+ void 0,
3223
+ {
3224
+ components: [
3225
+ { label: "environment template file", count: envExamples.length > 0 ? 1 : 0 },
3226
+ { label: ".gitignore environment-file rule", count: gitignoreEnvFile ? 1 : 0 },
3227
+ {
3228
+ label: "environment reads in implementation code",
3229
+ count: countFilesContaining(
3230
+ repoPath,
3231
+ repoFiles.filter(isImplementationFile),
3232
+ ENV_READ_PATTERN
3233
+ ) > 0 ? 1 : 0
3234
+ }
3235
+ ]
3236
+ }
2859
3237
  )
2860
3238
  ];
2861
3239
  const isolationMetrics = isMultiTenant ? [
@@ -2867,7 +3245,8 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2867
3245
  0.4,
2868
3246
  "policies",
2869
3247
  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"
3248
+ "saturating_count",
3249
+ { condition: "scored because a multi-tenant data surface was detected" }
2871
3250
  ),
2872
3251
  metric(
2873
3252
  "tenant_scope_ratio",
@@ -2876,7 +3255,9 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2876
3255
  Math.max(migrationFiles.length, 1),
2877
3256
  0.25,
2878
3257
  "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."
3258
+ useV18NativeRls ? `Measures repository-native RLS scope identifiers (${nativeRlsInventory?.scopeIdentifiers.join(", ")}) across data-layer files.` : "Measures tenant/studio/workspace scoping evidence across data-layer files.",
3259
+ void 0,
3260
+ { condition: "scored because a multi-tenant data surface was detected" }
2880
3261
  )
2881
3262
  ] : [];
2882
3263
  const cryptoMetrics = cryptoHygiene.hasSurface ? [
@@ -2887,7 +3268,11 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2887
3268
  1,
2888
3269
  0.2,
2889
3270
  "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."
3271
+ "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.",
3272
+ void 0,
3273
+ {
3274
+ condition: "scored because a signing, HMAC, or secret-comparison surface was detected"
3275
+ }
2891
3276
  )
2892
3277
  ] : [];
2893
3278
  return {
@@ -2994,7 +3379,7 @@ function collectCryptoHygieneEvidence(repoPath, implFiles, useV39Detectors = fal
2994
3379
  }
2995
3380
  return { evidence, findings, hasSurface, clean: hasSurface && !insecureFound };
2996
3381
  }
2997
- function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, useV20ExplicitGaps = false, useV22PackageStartEntrypoint = false) {
3382
+ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, useV20ExplicitGaps = false, useV22PackageStartEntrypoint = false, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
2998
3383
  const v20DirectHttpEntrypoint = useV20ExplicitGaps ? findV20DirectHttpServerEntrypointFile(repoPath, repoFiles) : null;
2999
3384
  const v22PackageStartHttpEntrypoint = useV22PackageStartEntrypoint ? findV22PackageStartHttpServerEntrypointFile(repoPath, repoFiles) : null;
3000
3385
  const v20RuntimeContainer = useV20ExplicitGaps ? findRuntimeContainerEntrypointFile(repoPath, repoFiles, true) : null;
@@ -3156,11 +3541,21 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3156
3541
  if (!workflow && releaseDeployConfigs.length === 0) {
3157
3542
  const firstEvidence = evidence[0];
3158
3543
  if (!firstEvidence) return null;
3159
- findings.push({
3160
- severity: "info",
3161
- summary: "A deployable service surface exists, but no CI or release-deployment automation was detected.",
3162
- evidence: firstEvidence
3163
- });
3544
+ findings.push(
3545
+ inventoryAbsenceFinding({
3546
+ severity: "info",
3547
+ summaries: {
3548
+ legacy: "A deployable service surface exists, but no CI or release-deployment automation was detected.",
3549
+ currentTracked: "A deployable service signal is present, while the tracked scan-eligible inventory matched no CI workflow or release-deployment automation.",
3550
+ currentDirectoryWalk: "A deployable service signal is present, while the bounded directory walk matched no CI workflow or release-deployment automation."
3551
+ },
3552
+ evidence: firstEvidence,
3553
+ context: inventoryAbsenceContext,
3554
+ inventoryCount: repoFiles.length,
3555
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a3CiOrReleaseDeploy,
3556
+ matchCount: 0
3557
+ })
3558
+ );
3164
3559
  }
3165
3560
  return {
3166
3561
  criterionId: "A3",
@@ -3181,7 +3576,21 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3181
3576
  6,
3182
3577
  0.55,
3183
3578
  "checks",
3184
- "Counts distinct static production-readiness checks instead of treating presence as enough."
3579
+ "Counts distinct static production-readiness checks instead of treating presence as enough.",
3580
+ void 0,
3581
+ {
3582
+ components: [
3583
+ {
3584
+ label: "build or type-check command",
3585
+ count: packageJson && (scripts.has("build") || scripts.has("typecheck")) ? 1 : 0
3586
+ },
3587
+ { label: "CI workflow", count: workflows.length > 0 ? 1 : 0 },
3588
+ { label: "deployment configuration", count: deployConfigs.length > 0 ? 1 : 0 },
3589
+ { label: "environment template", count: envTemplate ? 1 : 0 },
3590
+ { label: "health or readiness signal", count: healthChecks.length > 0 ? 1 : 0 },
3591
+ { label: "error boundary", count: errorBoundaries.length > 0 ? 1 : 0 }
3592
+ ]
3593
+ }
3185
3594
  ),
3186
3595
  metric(
3187
3596
  "prod_workflow_depth",
@@ -3216,7 +3625,7 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3216
3625
  ]
3217
3626
  };
3218
3627
  }
3219
- function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors, useV47Detectors) {
3628
+ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors, useV47Detectors, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
3220
3629
  const evidence = [];
3221
3630
  const findings = [];
3222
3631
  const manifests = repoFiles.filter(isDependencyManifest);
@@ -3269,11 +3678,26 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
3269
3678
  const deployableServiceFiles = useV47Detectors ? findDeployableServicePremiseFiles(repoPath, repoFiles, true) : [];
3270
3679
  const isAppOrService = (useV47Detectors ? deployableServiceFiles.length > 0 : isDeployableService(repoPath, repoFiles, useV27Detectors)) || packagedApplicationFiles.length > 0;
3271
3680
  if (isAppOrService && !hasLockfile && manifest) {
3272
- findings.push({
3273
- severity: "critical",
3274
- summary: "Dependency manifest is present without a detected lockfile \u2014 non-reproducible installs.",
3275
- evidence: evidenceForRelative(repoPath, manifest, "dependency_report", "Dependency manifest")
3276
- });
3681
+ findings.push(
3682
+ inventoryAbsenceFinding({
3683
+ severity: "critical",
3684
+ summaries: {
3685
+ legacy: "Dependency manifest is present without a detected lockfile \u2014 non-reproducible installs.",
3686
+ currentTracked: "Dependency manifest is present, but the tracked scan-eligible inventory matched no recognized lockfile.",
3687
+ currentDirectoryWalk: "Dependency manifest is present, but the bounded directory walk matched no recognized lockfile."
3688
+ },
3689
+ evidence: evidenceForRelative(
3690
+ repoPath,
3691
+ manifest,
3692
+ "dependency_report",
3693
+ "Dependency manifest"
3694
+ ),
3695
+ context: inventoryAbsenceContext,
3696
+ inventoryCount: repoFiles.length + (mono?.sharedFiles.length ?? 0),
3697
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a4Lockfile,
3698
+ matchCount: 0
3699
+ })
3700
+ );
3277
3701
  }
3278
3702
  if (manifest && hasSuspiciousDependencies(repoPath, manifest, useV47Detectors)) {
3279
3703
  findings.push({
@@ -3294,7 +3718,14 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
3294
3718
  2,
3295
3719
  0.25,
3296
3720
  "ratio",
3297
- "Credits automated dependency updates and package-manager audit hooks."
3721
+ "Credits automated dependency updates and package-manager audit hooks.",
3722
+ void 0,
3723
+ {
3724
+ components: [
3725
+ { label: "automated dependency-update configuration", count: hasUpdateConfig ? 1 : 0 },
3726
+ { label: "package-manager audit hook", count: auditConfig ? 1 : 0 }
3727
+ ]
3728
+ }
3298
3729
  );
3299
3730
  const appMetrics = [
3300
3731
  metric(
@@ -3374,7 +3805,7 @@ function hasDeclaredVersionConstraint(dependency) {
3374
3805
  if (/[~^<>=!]/.test(trimmed)) return true;
3375
3806
  return /^v?\d/.test(trimmed);
3376
3807
  }
3377
- function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors) {
3808
+ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
3378
3809
  const reconciliationArtifacts = useV27Detectors ? findClaimRealityReconciliationArtifacts(repoPath, repoFiles) : [];
3379
3810
  const claimSourceFiles = useV27Detectors ? findClaimSourceFiles(repoFiles) : repoFiles.filter(
3380
3811
  (file) => /^(?:README|readme)\.md$/.test(file) || /^docs\/[^/]+\.(?:md|mdx)$/.test(file)
@@ -3439,15 +3870,23 @@ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors) {
3439
3870
  criterionId: "A5",
3440
3871
  positiveEvidence: evidence,
3441
3872
  findings: reconciliationArtifacts.length > 0 ? [] : [
3442
- {
3873
+ inventoryAbsenceFinding({
3443
3874
  // Proxy-only A5: cap at warning (never critical) — a static proxy is lower-confidence
3444
3875
  // than a curated registry or reconciliation artifact. Downgraded to info when the repo
3445
3876
  // documents honest negative space: that scoping IS a form of claim-reality discipline,
3446
3877
  // not an additional gap on top of the missing dedicated artifact.
3447
3878
  severity: negativeSpaceDoc ? "info" : "warning",
3448
- summary: negativeSpaceDoc ? "Claim source and implementation files are present; no dedicated claim-reality report artifact was supplied, but the repo explicitly documents what it does NOT cover/protect against \u2014 honest scoping, not overclaiming." : "Claim source and implementation files are present, but no dedicated claim-reality report artifact was supplied.",
3449
- evidence: firstEvidence
3450
- }
3879
+ summaries: {
3880
+ legacy: negativeSpaceDoc ? "Claim source and implementation files are present; no dedicated claim-reality report artifact was supplied, but the repo explicitly documents what it does NOT cover/protect against \u2014 honest scoping, not overclaiming." : "Claim source and implementation files are present, but no dedicated claim-reality report artifact was supplied.",
3881
+ currentTracked: negativeSpaceDoc ? "Claim-source and implementation-file signals are present, while the tracked scan-eligible inventory matched no dedicated claim-reality report artifact; the repo separately documents what it does NOT cover or protect against." : "Claim-source and implementation-file signals are present, while the tracked scan-eligible inventory matched no dedicated claim-reality report artifact.",
3882
+ currentDirectoryWalk: negativeSpaceDoc ? "Claim-source and implementation-file signals are present, while the bounded directory walk matched no dedicated claim-reality report artifact; the repo separately documents what it does NOT cover or protect against." : "Claim-source and implementation-file signals are present, while the bounded directory walk matched no dedicated claim-reality report artifact."
3883
+ },
3884
+ evidence: firstEvidence,
3885
+ context: inventoryAbsenceContext,
3886
+ inventoryCount: repoFiles.length,
3887
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a5ClaimRealityArtifact,
3888
+ matchCount: 0
3889
+ })
3451
3890
  ],
3452
3891
  // Recalibrated (goal_cejel_launch_hardening_combined_2026-07-06, Phase 3 H1):
3453
3892
  // reconciliation_artifact_depth is usually 0 on the generic-proxy path, so its prior 0.3
@@ -3528,7 +3967,14 @@ function collectB2PrTraceEvidence(repoPath, repoFiles) {
3528
3967
  0.8,
3529
3968
  "signals",
3530
3969
  "Measures CI, PR template, and review-gate evidence for pull-request traceability.",
3531
- "saturating_count"
3970
+ "saturating_count",
3971
+ {
3972
+ components: [
3973
+ { label: "CI workflow", count: workflows.length },
3974
+ { label: "pull-request template", count: prTemplate ? 1 : 0 },
3975
+ { label: "review-gate record", count: branchProtectionDoc ? 1 : 0 }
3976
+ ]
3977
+ }
3532
3978
  ),
3533
3979
  // Weight lowered from 0.35 (Phase 3 H1): squash-merge and local-history repos with
3534
3980
  // no "Merge pull request"/"#123" commit-subject convention legitimately score 0/N
@@ -3591,8 +4037,24 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
3591
4037
  4,
3592
4038
  0.45,
3593
4039
  "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"
4040
+ "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.",
4041
+ "saturating_count",
4042
+ {
4043
+ components: [
4044
+ ...["test", "lint", "typecheck", "build"].map((name) => ({
4045
+ label: `package script: ${name === "typecheck" ? "type-check" : name}`,
4046
+ count: scripts.has(name) ? 1 : 0
4047
+ })),
4048
+ ...CI_COMMAND_CATEGORIES.map((pattern, index) => ({
4049
+ label: `repository CI command: ${["test", "lint", "type-check", "build"][index]}`,
4050
+ count: workflows.some((file) => fileContains(repoPath, file, pattern)) ? 1 : 0
4051
+ })),
4052
+ ...mono ? CI_COMMAND_CATEGORIES.map((pattern, index) => ({
4053
+ label: `monorepo-root CI command: ${["test", "lint", "type-check", "build"][index]}`,
4054
+ count: rootWorkflows.some((file) => fileContains(mono.root, file, pattern)) ? 1 : 0
4055
+ })) : []
4056
+ ]
4057
+ }
3596
4058
  ),
3597
4059
  metric(
3598
4060
  "default_branch_ci_depth",
@@ -3772,7 +4234,7 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
3772
4234
  const isV21ExecutedShape = v21ExecutedEscalationFileSet.has(file);
3773
4235
  findings.push({
3774
4236
  severity: "critical",
3775
- summary: isV21ExecutedShape ? "An authored SQL artifact contains, or a direct database-driver call executes, an administrative role grant, SUPERUSER escalation, or schema-wide table privilege grant with no documented human gate." : "Role-membership GRANT or SUPERUSER escalation executes in code with no documented human gate.",
4237
+ summary: isV21ExecutedShape ? "This file contains an authored administrative SQL statement \u2014 role-membership grant, SUPERUSER escalation, or schema-wide table privilege grant \u2014 or executes one through a direct database-driver call, and does not itself contain text matching the human-gate marker pattern." : "This file executes a role-membership GRANT or SUPERUSER escalation and does not itself contain text matching the human-gate marker pattern.",
3776
4238
  evidence: evidenceForRelative(
3777
4239
  repoPath,
3778
4240
  file,
@@ -3795,7 +4257,11 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
3795
4257
  1,
3796
4258
  0.15,
3797
4259
  "present",
3798
- "Credits a governance/safety toggle that fails closed before any lower-priority config can override it."
4260
+ "Credits a governance/safety toggle that fails closed before any lower-priority config can override it.",
4261
+ void 0,
4262
+ {
4263
+ condition: "shown because a fail-closed kill switch was detected; this positive-only metric is omitted when absent"
4264
+ }
3799
4265
  )
3800
4266
  ] : [];
3801
4267
  const hasPrivilegedOpsSurface = humanGateDoc != null || gatedPrivilegeCheckFile != null || executableFiles.length > 0 || v21ExecutedEscalationFiles.length > 0;
@@ -3812,7 +4278,11 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
3812
4278
  1,
3813
4279
  0.4,
3814
4280
  "present",
3815
- "Credits explicit documentation that privileged/credentialed operations are human-executed, never agent-run."
4281
+ "Credits explicit documentation that privileged/credentialed operations are human-executed, never agent-run.",
4282
+ void 0,
4283
+ {
4284
+ condition: "scored because a privileged-operation-shaped surface was detected"
4285
+ }
3816
4286
  ),
3817
4287
  metric(
3818
4288
  "fail_closed_privilege_check",
@@ -3821,7 +4291,11 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
3821
4291
  1,
3822
4292
  0.3,
3823
4293
  "present",
3824
- "Credits code that checks role membership and fails closed before elevating privilege, instead of attempting elevation blind."
4294
+ "Credits code that checks role membership and fails closed before elevating privilege, instead of attempting elevation blind.",
4295
+ void 0,
4296
+ {
4297
+ condition: "scored because a privileged-operation-shaped surface was detected"
4298
+ }
3825
4299
  )
3826
4300
  ] : [],
3827
4301
  metric(
@@ -4087,10 +4561,20 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
4087
4561
  continue;
4088
4562
  }
4089
4563
  try {
4090
- if (!lstatSync(fullPath).isFile()) {
4564
+ const stat = lstatSync(fullPath);
4565
+ if (!stat.isFile()) {
4091
4566
  recordContentSkip(fullPath, "non_regular_file", true);
4092
4567
  continue;
4093
4568
  }
4569
+ if (stat.size > MAX_REPOSITORY_CONTENT_BYTES) {
4570
+ const affectedCriteria = affectedCriteriaForUnavailablePath(file);
4571
+ if (affectedCriteria.length > 0) {
4572
+ recordContentSkipForCriteria(fullPath, "too_large", affectedCriteria, true);
4573
+ } else {
4574
+ recordContentSkip(fullPath, "too_large", true);
4575
+ }
4576
+ continue;
4577
+ }
4094
4578
  } catch (error) {
4095
4579
  recordFilesystemSkip(fullPath, error, false, true);
4096
4580
  continue;
@@ -4102,6 +4586,39 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
4102
4586
  }
4103
4587
  return files.sort();
4104
4588
  }
4589
+ function affectedCriteriaForUnavailablePath(file) {
4590
+ const affected = /* @__PURE__ */ new Set();
4591
+ if (isDependencyManifest(file) || isLockfile(file)) affected.add("A4");
4592
+ if (isCiWorkflow(file)) {
4593
+ affected.add("A1");
4594
+ affected.add("A3");
4595
+ affected.add("B2");
4596
+ affected.add("B3");
4597
+ }
4598
+ if (isDeployConfig(file)) {
4599
+ affected.add("A3");
4600
+ affected.add("B2");
4601
+ }
4602
+ if (isTestFile(file)) {
4603
+ affected.add("A1");
4604
+ affected.add("A2");
4605
+ }
4606
+ if (isImplementationFile(file)) {
4607
+ affected.add("A1");
4608
+ affected.add("A2");
4609
+ affected.add("A3");
4610
+ affected.add("B1");
4611
+ affected.add("B6");
4612
+ }
4613
+ if (/\.(?:md|mdx|rst|txt)$/i.test(file)) {
4614
+ affected.add("A5");
4615
+ affected.add("B4");
4616
+ affected.add("B5");
4617
+ affected.add("B6");
4618
+ }
4619
+ if (isAuditFile(file)) affected.add("B5");
4620
+ return [...affected];
4621
+ }
4105
4622
  function directoryInventory(repoPath) {
4106
4623
  const files = [];
4107
4624
  const rootDevice = statSync(repoPath).dev;
@@ -4114,6 +4631,7 @@ function collectRepoFileInventory(repoPath) {
4114
4631
  return {
4115
4632
  repoFiles: parseGitTrackedFiles(repoPath, tracked.stdout, false),
4116
4633
  inventoryFiles: parseGitTrackedFiles(repoPath, tracked.stdout, true),
4634
+ repoFileInventoryScope: "tracked-scan-eligible",
4117
4635
  scanLimitations: []
4118
4636
  };
4119
4637
  }
@@ -4221,8 +4739,13 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
4221
4739
  continue;
4222
4740
  }
4223
4741
  const repoRelativePath = relative(repoPath, fullPath);
4224
- if (size > 512e3) {
4225
- recordContentSkip(fullPath, "too_large");
4742
+ if (size > MAX_REPOSITORY_CONTENT_BYTES) {
4743
+ const affectedCriteria = affectedCriteriaForUnavailablePath(repoRelativePath);
4744
+ if (affectedCriteria.length > 0) {
4745
+ recordContentSkipForCriteria(fullPath, "too_large", affectedCriteria);
4746
+ } else {
4747
+ recordContentSkip(fullPath, "too_large");
4748
+ }
4226
4749
  continue;
4227
4750
  }
4228
4751
  if (!isPotentialContentPath(repoRelativePath)) {
@@ -6152,16 +6675,29 @@ function evidenceForRelativeAtLine(repoPath, path, kind, label, line) {
6152
6675
  ...contents ? { contentHash: createHash2("sha256").update(contents).digest("hex") } : {}
6153
6676
  };
6154
6677
  }
6155
- function metric(name, label, value, max, weight, unit, description, kind) {
6678
+ function metric(name, label, value, max, weight, unit, description, kind, presentation) {
6679
+ const normalizedValue = Math.max(0, value);
6680
+ if (presentation?.components) {
6681
+ const explainedValue = presentation.components.reduce(
6682
+ (sum, component) => sum + component.count,
6683
+ 0
6684
+ );
6685
+ if (explainedValue !== normalizedValue) {
6686
+ throw new Error(
6687
+ `Cejel invariant: presentation components for ${name} total ${explainedValue}, expected ${normalizedValue}`
6688
+ );
6689
+ }
6690
+ }
6156
6691
  return {
6157
6692
  name,
6158
6693
  label,
6159
- value: Math.max(0, value),
6694
+ value: normalizedValue,
6160
6695
  max,
6161
6696
  ...kind ? { kind } : {},
6162
6697
  weight,
6163
6698
  unit,
6164
- description
6699
+ description,
6700
+ ...presentation ? { presentation } : {}
6165
6701
  };
6166
6702
  }
6167
6703
  function firstMeaningfulLine(contents) {
@@ -6201,12 +6737,11 @@ function hasEnvPathInGitHistory(repoPath, scanLimitations) {
6201
6737
 
6202
6738
  // src/witan/public-scan.ts
6203
6739
  init_esm_shims();
6204
- import { resolve as resolve2 } from "path";
6205
6740
 
6206
6741
  // src/witan/ingest.ts
6207
6742
  init_esm_shims();
6208
- import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
6209
- import { join as join2 } from "path";
6743
+ import { lstatSync as lstatSync3, opendirSync, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
6744
+ import { join as join2, resolve as resolve3 } from "path";
6210
6745
 
6211
6746
  // src/witan/finding-limits.ts
6212
6747
  init_esm_shims();
@@ -6230,49 +6765,118 @@ function clampFinding(finding) {
6230
6765
  // src/witan/generic-adapter.ts
6231
6766
  init_esm_shims();
6232
6767
  import { readFileSync } from "fs";
6233
- var VALID_DIMENSIONS = /* @__PURE__ */ new Set([
6234
- "A1",
6235
- "A2",
6236
- "A3",
6237
- "A4",
6238
- "A5",
6239
- "B1",
6240
- "B2",
6241
- "B3",
6242
- "B4",
6243
- "B5",
6244
- "B6"
6245
- ]);
6246
- var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning", "info"]);
6768
+
6769
+ // src/witan/ingest-files.ts
6770
+ init_esm_shims();
6771
+ import { lstatSync as lstatSync2, realpathSync as realpathSync2 } from "fs";
6772
+ import { dirname as dirname2, isAbsolute, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
6773
+ var MAX_INGEST_DOCUMENT_BYTES = 8 * 1024 * 1024;
6774
+ function isContainedPath(candidate, directory) {
6775
+ const relativePath = relative2(directory, candidate);
6776
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep2}`) && !isAbsolute(relativePath);
6777
+ }
6778
+ function isMissingIngestPath(error) {
6779
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
6780
+ }
6781
+ function resolveIngestFilePath(filePath, inputDirectory = dirname2(filePath)) {
6782
+ const lexicalDirectory = resolve2(inputDirectory);
6783
+ const lexicalCandidate = resolve2(filePath);
6784
+ const directoryStat = lstatSync2(lexicalDirectory);
6785
+ if (directoryStat.isSymbolicLink()) {
6786
+ throw new Error(`Cejel: refusing symlinked ingest directory: ${inputDirectory}`);
6787
+ }
6788
+ if (!directoryStat.isDirectory()) {
6789
+ throw new Error(`Cejel: refusing non-directory ingest boundary: ${inputDirectory}`);
6790
+ }
6791
+ const candidateStat = lstatSync2(lexicalCandidate);
6792
+ if (candidateStat.isSymbolicLink()) {
6793
+ throw new Error(`Cejel: refusing symlinked ingest file: ${filePath}`);
6794
+ }
6795
+ if (!candidateStat.isFile()) {
6796
+ throw new Error(`Cejel: refusing non-regular ingest file: ${filePath}`);
6797
+ }
6798
+ if (candidateStat.size > MAX_INGEST_DOCUMENT_BYTES) {
6799
+ throw new Error(
6800
+ `Cejel: ingest file exceeds the ${MAX_INGEST_DOCUMENT_BYTES.toLocaleString("en-US")} byte budget: ${filePath}`
6801
+ );
6802
+ }
6803
+ const resolvedDirectory = realpathSync2(lexicalDirectory);
6804
+ const resolvedCandidate = realpathSync2(lexicalCandidate);
6805
+ if (!isContainedPath(resolvedCandidate, resolvedDirectory)) {
6806
+ throw new Error(`Cejel: refusing ingest file outside its resolved input directory: ${filePath}`);
6807
+ }
6808
+ return resolvedCandidate;
6809
+ }
6810
+ function isResolvedIngestPathContained(candidate, directory) {
6811
+ return isContainedPath(candidate, directory);
6812
+ }
6813
+
6814
+ // src/witan/generic-adapter.ts
6815
+ var CEJEL_GENERIC_INGEST_CONTRACT_VERSION = "1.0";
6816
+ var CEJEL_GENERIC_INGEST_CONTRACT_MAJOR = 1;
6817
+ var GenericFindingV1Schema = external_exports.object({
6818
+ ruleId: external_exports.string().min(1),
6819
+ severity: external_exports.enum(["critical", "warning", "info"]),
6820
+ message: external_exports.string().min(1),
6821
+ location: external_exports.string().min(1).optional()
6822
+ }).passthrough();
6823
+ var GenericSignalV1Schema = external_exports.object({
6824
+ dimension: external_exports.enum(["A1", "A2", "A3", "A4", "A5", "B1", "B2", "B3", "B4", "B5", "B6"]),
6825
+ weight: external_exports.number().min(0).max(1).default(0.5),
6826
+ findings: external_exports.array(GenericFindingV1Schema).min(1)
6827
+ }).passthrough();
6828
+ var GenericSignalDocumentV1Schema = external_exports.object({
6829
+ version: external_exports.string().regex(/^1\.\d+$/),
6830
+ tool: external_exports.string().trim().min(1),
6831
+ signals: external_exports.array(GenericSignalV1Schema)
6832
+ }).passthrough();
6247
6833
  function isGenericSignalDocument(raw) {
6248
6834
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
6249
6835
  const doc = raw;
6250
6836
  return typeof doc.tool === "string" && doc.tool.length > 0 && Array.isArray(doc.signals);
6251
6837
  }
6838
+ function assertSupportedContractVersion(version) {
6839
+ if (typeof version !== "string") {
6840
+ throw new Error(
6841
+ `Cejel: generic ingest contract version is required; expected "version": "${CEJEL_GENERIC_INGEST_CONTRACT_VERSION}". Refusing to guess an unversioned contract.`
6842
+ );
6843
+ }
6844
+ const match = /^(\d+)\.(\d+)$/.exec(version);
6845
+ if (!match) {
6846
+ throw new Error(
6847
+ `Cejel: invalid generic ingest contract version "${version}"; expected <major>.<minor> (current ${CEJEL_GENERIC_INGEST_CONTRACT_VERSION}).`
6848
+ );
6849
+ }
6850
+ const major = Number(match[1]);
6851
+ if (major !== CEJEL_GENERIC_INGEST_CONTRACT_MAJOR) {
6852
+ throw new Error(
6853
+ `Cejel: unsupported generic ingest contract major version ${major} (declared "${version}"); this Cejel build supports major ${CEJEL_GENERIC_INGEST_CONTRACT_MAJOR}. Refusing to guess.`
6854
+ );
6855
+ }
6856
+ }
6857
+ function formatValidationIssues(error) {
6858
+ return error.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "<document>"}: ${issue.message}`).join("; ");
6859
+ }
6252
6860
  function parseGenericJson(raw) {
6253
6861
  if (!isGenericSignalDocument(raw)) return [];
6254
- const source = raw.tool ?? "unknown";
6255
- const signals = [];
6256
- for (const signal of raw.signals ?? []) {
6257
- const dimension = signal.dimension;
6258
- if (!dimension || !VALID_DIMENSIONS.has(dimension)) continue;
6259
- const weight = typeof signal.weight === "number" && Number.isFinite(signal.weight) ? Math.min(Math.max(signal.weight, 0), 1) : 0.5;
6260
- const findings = [];
6261
- for (const finding of signal.findings ?? []) {
6262
- const severity = finding.severity;
6263
- if (!severity || !VALID_SEVERITIES.has(severity)) continue;
6264
- const ruleId = finding.ruleId ?? "unknown";
6265
- findings.push({
6266
- ruleId,
6267
- severity,
6268
- message: finding.message ?? ruleId,
6269
- ...finding.location ? { location: finding.location } : {}
6270
- });
6271
- }
6272
- if (findings.length === 0) continue;
6273
- signals.push({ source, dimension, weight, findings });
6862
+ assertSupportedContractVersion(raw.version);
6863
+ const parsed = GenericSignalDocumentV1Schema.safeParse(raw);
6864
+ if (!parsed.success) {
6865
+ throw new Error(
6866
+ `Cejel: invalid generic ingest contract v${raw.version}: ${formatValidationIssues(parsed.error)}`
6867
+ );
6274
6868
  }
6275
- return signals;
6869
+ return parsed.data.signals.map((signal) => ({
6870
+ source: parsed.data.tool,
6871
+ dimension: signal.dimension,
6872
+ weight: signal.weight,
6873
+ findings: signal.findings.map((finding) => ({
6874
+ ruleId: finding.ruleId,
6875
+ severity: finding.severity,
6876
+ message: finding.message,
6877
+ ...finding.location ? { location: finding.location } : {}
6878
+ }))
6879
+ }));
6276
6880
  }
6277
6881
 
6278
6882
  // src/witan/sarif-adapter.ts
@@ -6534,30 +7138,35 @@ function rawToolName(raw) {
6534
7138
  }
6535
7139
  var MAX_INGEST_SOURCE_LENGTH = 120;
6536
7140
  function sanitizeIngestSource(source) {
6537
- const withoutControls = source.replace(/[\p{Cc}\p{Cf}]/gu, "").trim();
6538
- const safeSource = withoutControls.length > 0 ? withoutControls : "unknown";
6539
- const codePoints = Array.from(safeSource);
6540
- if (codePoints.length <= MAX_INGEST_SOURCE_LENGTH) return safeSource;
6541
- return `${codePoints.slice(0, MAX_INGEST_SOURCE_LENGTH - 3).join("")}...`;
7141
+ return sanitizePresentationLine(source, {
7142
+ fallback: "unknown",
7143
+ maxLength: MAX_INGEST_SOURCE_LENGTH
7144
+ });
6542
7145
  }
6543
7146
  function parseIngestFile(filePath, options = {}) {
7147
+ const resolvedFilePath = resolveIngestFilePath(filePath);
6544
7148
  let raw;
6545
7149
  try {
6546
- raw = JSON.parse(stripBom(readFileSync4(filePath, "utf8")));
7150
+ raw = JSON.parse(stripBom(readFileSync4(resolvedFilePath, "utf8")));
6547
7151
  } catch (error) {
6548
7152
  const message = error instanceof Error ? error.message : String(error);
6549
7153
  throw new Error(`Cejel: could not parse ingest file as JSON: ${filePath} (${message})`);
6550
7154
  }
7155
+ const rawCount = countRawEntries(raw);
7156
+ if (options.maxFindingCandidates !== void 0 && rawCount > options.maxFindingCandidates) {
7157
+ throw new Error(
7158
+ `Cejel: ${rawCount.toLocaleString("en-US")} ingest finding candidates exceed the ${options.maxFindingCandidates.toLocaleString("en-US")} remaining retained ingest finding budget; refusing before adapter materialization.`
7159
+ );
7160
+ }
6551
7161
  let signals;
6552
7162
  if (looksLikeSarif(raw)) signals = parseSarifJson(raw, options.extraSarifDimensionRules);
6553
7163
  else if (looksLikeScorecard(raw)) signals = parseScorecardJson(raw);
6554
7164
  else if (isGenericSignalDocument(raw)) signals = parseGenericJson(raw);
6555
7165
  if (!signals) {
6556
7166
  throw new Error(
6557
- `Cejel: unrecognized ingest file format: ${filePath} \u2014 expected SARIF (a "runs" array), OpenSSF Scorecard JSON (a "checks" array), or the generic Cejel external-signal shape (a "tool" string + "signals" array). See the cejel README "Aggregate your scanners".`
7167
+ `Cejel: unrecognized ingest file format: ${filePath} \u2014 expected SARIF (a "runs" array), OpenSSF Scorecard JSON (a "checks" array), or the versioned generic Cejel external-signal shape (a "version" string + "tool" string + "signals" array). See the cejel README "Aggregate your scanners".`
6558
7168
  );
6559
7169
  }
6560
- const rawCount = countRawEntries(raw);
6561
7170
  const mappedCount = signals.reduce((sum, signal) => sum + signal.findings.length, 0);
6562
7171
  if (rawCount > 0 && mappedCount === 0) {
6563
7172
  process.stderr.write(
@@ -6572,30 +7181,83 @@ function parseIngestFile(filePath, options = {}) {
6572
7181
  findings: signal.findings.map(clampFinding)
6573
7182
  }));
6574
7183
  }
6575
- function expandIngestPattern(pattern) {
7184
+ function expandIngestPattern(pattern, maxMatches = Number.POSITIVE_INFINITY) {
6576
7185
  if (!pattern.includes("*")) return [pattern];
6577
7186
  const lastSlash = pattern.lastIndexOf("/");
6578
7187
  const dir = lastSlash === -1 ? "." : pattern.slice(0, lastSlash);
6579
7188
  const filePattern = lastSlash === -1 ? pattern : pattern.slice(lastSlash + 1);
6580
7189
  const regexSource = filePattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
6581
7190
  const regex = new RegExp(`^${regexSource}$`);
6582
- let entries;
7191
+ const entries = [];
6583
7192
  try {
6584
- entries = readdirSync2(dir);
6585
- } catch {
7193
+ const directoryStat = lstatSync3(resolve3(dir));
7194
+ if (directoryStat.isSymbolicLink()) {
7195
+ throw new Error(`Cejel: refusing symlinked ingest directory: ${dir}`);
7196
+ }
7197
+ if (!directoryStat.isDirectory()) return [];
7198
+ const directory = opendirSync(dir);
7199
+ try {
7200
+ for (; ; ) {
7201
+ const entry = directory.readSync();
7202
+ if (!entry) break;
7203
+ if (!regex.test(entry.name)) continue;
7204
+ entries.push(entry.name);
7205
+ if (entries.length >= maxMatches) break;
7206
+ }
7207
+ } finally {
7208
+ directory.closeSync();
7209
+ }
7210
+ } catch (error) {
7211
+ if (!isMissingIngestPath(error)) throw error;
6586
7212
  return [];
6587
7213
  }
6588
- return entries.filter((name) => regex.test(name)).sort().map((name) => join2(dir, name));
7214
+ return entries.sort().map((name) => {
7215
+ const candidate = join2(dir, name);
7216
+ resolveIngestFilePath(candidate, dir);
7217
+ return candidate;
7218
+ });
6589
7219
  }
6590
- function discoverIngestInputs(repoPath) {
7220
+ function discoverIngestInputs(repoPath, maxMatches = Number.POSITIVE_INFINITY) {
7221
+ const resolvedRoot = realpathSync3(repoPath);
7222
+ const cejelDir = join2(repoPath, ".cejel");
6591
7223
  const inputsDir = join2(repoPath, ".cejel", "inputs");
6592
- let entries;
7224
+ const entries = [];
6593
7225
  try {
6594
- entries = readdirSync2(inputsDir);
6595
- } catch {
7226
+ const cejelStat = lstatSync3(cejelDir);
7227
+ if (cejelStat.isSymbolicLink()) {
7228
+ throw new Error(`Cejel: refusing symlinked ingest path component: ${cejelDir}`);
7229
+ }
7230
+ if (!cejelStat.isDirectory()) {
7231
+ throw new Error(`Cejel: refusing non-directory ingest path component: ${cejelDir}`);
7232
+ }
7233
+ const inputsStat = lstatSync3(inputsDir);
7234
+ if (inputsStat.isSymbolicLink()) {
7235
+ throw new Error(`Cejel: refusing symlinked ingest path component: ${inputsDir}`);
7236
+ }
7237
+ if (!inputsStat.isDirectory()) {
7238
+ throw new Error(`Cejel: refusing non-directory ingest path component: ${inputsDir}`);
7239
+ }
7240
+ const resolvedInputs = realpathSync3(inputsDir);
7241
+ if (!isResolvedIngestPathContained(resolvedInputs, resolvedRoot)) {
7242
+ throw new Error(`Cejel: refusing auto-discovery directory outside the repository root.`);
7243
+ }
7244
+ const directory = opendirSync(inputsDir);
7245
+ try {
7246
+ for (; ; ) {
7247
+ const entry = directory.readSync();
7248
+ if (!entry) break;
7249
+ if (!/\.(sarif|json)$/i.test(entry.name)) continue;
7250
+ entries.push(entry.name);
7251
+ if (entries.length >= maxMatches) break;
7252
+ }
7253
+ } finally {
7254
+ directory.closeSync();
7255
+ }
7256
+ } catch (error) {
7257
+ if (!isMissingIngestPath(error)) throw error;
6596
7258
  return [];
6597
7259
  }
6598
- return entries.filter((name) => /\.(sarif|json)$/i.test(name)).sort().map((name) => join2(inputsDir, name));
7260
+ return entries.sort().map((name) => resolveIngestFilePath(join2(inputsDir, name), inputsDir));
6599
7261
  }
6600
7262
 
6601
7263
  // src/witan/scoring.ts
@@ -7007,7 +7669,11 @@ function scoreMetrics(metrics) {
7007
7669
  function capScoreForFindings(score, findings, rubricVersion, criterionId) {
7008
7670
  if (findings.some((finding) => finding.severity === "critical")) {
7009
7671
  const criticalFindings = findings.filter(({ severity }) => severity === "critical");
7010
- const authenticatedA1Absence = usesV17DetectorClosure2(rubricVersion) && criterionId === "A1" && criticalFindings.length === 1 && criticalFindings[0]?.summary === WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY;
7672
+ const authenticatedA1Absence = usesV17DetectorClosure2(rubricVersion) && criterionId === "A1" && criticalFindings.length === 1 && [
7673
+ WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY,
7674
+ WITAN_DIRECTORY_WALK_AUTHENTICATED_A1_ABSENCE_SUMMARY,
7675
+ WITAN_LEGACY_AUTHENTICATED_A1_ABSENCE_SUMMARY
7676
+ ].includes(criticalFindings[0]?.summary ?? "");
7011
7677
  return authenticatedA1Absence ? Math.min(score, 1.4) : 1.4;
7012
7678
  }
7013
7679
  if (findings.some((finding) => finding.severity === "warning")) return Math.min(score, 2.4);
@@ -7279,6 +7945,8 @@ function roundScore(score) {
7279
7945
  }
7280
7946
 
7281
7947
  // src/witan/public-scan.ts
7948
+ var MAX_INGEST_DOCUMENTS = 128;
7949
+ var MAX_RETAINED_INGEST_FINDINGS = 1e4;
7282
7950
  function scoreRepoWithPublicCejel(options) {
7283
7951
  if (options.rubricVersion !== void 0) {
7284
7952
  assertSelectableRubricVersion(options.rubricVersion);
@@ -7306,44 +7974,69 @@ function scoreRepoWithPublicCejel(options) {
7306
7974
  }
7307
7975
  function resolvePublicIngestSignals(options) {
7308
7976
  const seen = /* @__PURE__ */ new Set();
7309
- const explicitFiles = [];
7977
+ const documents = [];
7978
+ const addDocument = (file, provenance) => {
7979
+ const resolved = resolveIngestFilePath(file);
7980
+ if (seen.has(resolved)) return;
7981
+ seen.add(resolved);
7982
+ documents.push({ file: resolved, provenance });
7983
+ if (documents.length > MAX_INGEST_DOCUMENTS) {
7984
+ throw new Error(
7985
+ `Cejel: ${documents.length} unique ingest documents exceed the ${MAX_INGEST_DOCUMENTS} ingest document budget; refusing to produce a partial certificate.`
7986
+ );
7987
+ }
7988
+ };
7310
7989
  for (const pattern of options.ingestPatterns ?? []) {
7311
- const matches = expandIngestPattern(pattern);
7990
+ const matches = expandIngestPattern(pattern, MAX_INGEST_DOCUMENTS + 1);
7312
7991
  if (matches.length === 0 && options.warnOnEmptyIngestMatch) {
7313
7992
  process.stderr.write(`Cejel: --ingest pattern matched no files: ${pattern}
7314
7993
  `);
7315
7994
  }
7316
7995
  for (const match of matches) {
7317
- const resolved = resolve2(match);
7318
- if (seen.has(resolved)) continue;
7319
- seen.add(resolved);
7320
- explicitFiles.push(match);
7996
+ addDocument(match, "operator_supplied");
7321
7997
  }
7322
7998
  }
7323
- const signals = explicitFiles.flatMap(
7324
- (file) => parseIngestFile(file, { provenance: "operator_supplied" })
7325
- );
7326
- if (!options.autoDiscoverIngest) return signals;
7327
- for (const discovered of discoverIngestInputs(options.repoPath)) {
7328
- const resolved = resolve2(discovered);
7329
- if (seen.has(resolved)) continue;
7330
- seen.add(resolved);
7331
- signals.push(...parseIngestFile(discovered, { provenance: "auto_discovered" }));
7999
+ if (options.autoDiscoverIngest) {
8000
+ for (const discovered of discoverIngestInputs(
8001
+ options.repoPath,
8002
+ MAX_INGEST_DOCUMENTS + 1
8003
+ )) {
8004
+ addDocument(discovered, "auto_discovered");
8005
+ }
8006
+ }
8007
+ const signals = [];
8008
+ let retainedFindings = 0;
8009
+ for (const document of documents) {
8010
+ const parsed = parseIngestFile(document.file, {
8011
+ maxFindingCandidates: MAX_RETAINED_INGEST_FINDINGS - retainedFindings,
8012
+ provenance: document.provenance
8013
+ });
8014
+ const documentFindings = parsed.reduce((count, signal) => count + signal.findings.length, 0);
8015
+ retainedFindings += documentFindings;
8016
+ if (retainedFindings > MAX_RETAINED_INGEST_FINDINGS) {
8017
+ throw new Error(
8018
+ `Cejel: ${retainedFindings.toLocaleString("en-US")} retained findings exceed the ${MAX_RETAINED_INGEST_FINDINGS.toLocaleString("en-US")} retained ingest finding budget; refusing to truncate evidence.`
8019
+ );
8020
+ }
8021
+ signals.push(...parsed);
7332
8022
  }
7333
8023
  return signals;
7334
8024
  }
7335
8025
 
7336
8026
  // src/product-identity.ts
7337
8027
  init_esm_shims();
7338
- import { existsSync, readFileSync as readFileSync5 } from "fs";
8028
+ import { existsSync, lstatSync as lstatSync4, readFileSync as readFileSync5 } from "fs";
7339
8029
  import { basename as basename2, join as join3 } from "path";
7340
8030
  var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/;
7341
8031
  var FALLBACK_SLUG = "repo";
7342
8032
  var FALLBACK_DISPLAY_NAME = "Repository";
7343
- function deriveProductIdentity(repoPath) {
8033
+ function deriveProductIdentity(repoPath, productName) {
7344
8034
  const packageJsonPath = join3(repoPath, "package.json");
7345
- const raw = readPackageName(packageJsonPath) ?? basename2(repoPath);
7346
- const displayName = raw.trim().length > 0 ? raw.trim() : FALLBACK_DISPLAY_NAME;
8035
+ const raw = productName ?? readPackageName(packageJsonPath) ?? basename2(repoPath);
8036
+ const displayName = sanitizePresentationLine(raw, {
8037
+ fallback: FALLBACK_DISPLAY_NAME,
8038
+ maxLength: 120
8039
+ });
7347
8040
  return {
7348
8041
  productSlug: slugify(raw),
7349
8042
  productDisplayName: displayName
@@ -7352,6 +8045,8 @@ function deriveProductIdentity(repoPath) {
7352
8045
  function readPackageName(packageJsonPath) {
7353
8046
  if (!existsSync(packageJsonPath)) return void 0;
7354
8047
  try {
8048
+ const stat = lstatSync4(packageJsonPath);
8049
+ if (!stat.isFile() || stat.size > MAX_REPOSITORY_CONTENT_BYTES) return void 0;
7355
8050
  const parsed = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
7356
8051
  if (typeof parsed === "object" && parsed !== null && "name" in parsed) {
7357
8052
  const name = parsed.name;
@@ -7381,7 +8076,7 @@ var METRIC_DERIVED_FINDING_PATTERN2 = /^[A-Z]\d+ metric-derived score is (\d+\.\
7381
8076
  var REMEDIATION_BY_METRIC2 = {
7382
8077
  test_to_source_ratio: "add concrete regression tests for the implementation surface",
7383
8078
  coverage_percent: "configure coverage and publish a measured threshold or report",
7384
- verification_script_ratio: "add explicit test, lint, and typecheck verification commands",
8079
+ verification_script_ratio: "add the missing test, coverage, lint, or type-check command or a test-runner configuration",
7385
8080
  non_hollow_test_share: "replace skipped or placeholder tests with live assertions",
7386
8081
  env_handling_depth: "document and enforce safe environment-secret handling",
7387
8082
  secret_cleanliness: "remove committed secrets and rotate any exposed credentials",
@@ -7399,7 +8094,7 @@ var REMEDIATION_BY_METRIC2 = {
7399
8094
  claim_match_rate: "align documented product claims with observable repository evidence",
7400
8095
  claim_source_depth: "document concrete, checkable product claims",
7401
8096
  reconciliation_artifact_depth: "publish a claim-to-evidence reconciliation record",
7402
- pr_merge_ratio: "preserve merged pull-request history in the scanned clone",
8097
+ pr_merge_ratio: "preserve recognizable pull-request references in recent commit subjects",
7403
8098
  pr_trace_primitives: "add pull-request templates and outcome-trace records",
7404
8099
  ci_script_depth: "run the repository verification commands in CI",
7405
8100
  default_branch_ci_depth: "run CI on the default branch and pull requests",
@@ -7509,7 +8204,7 @@ function buildWitanCliSummary(report) {
7509
8204
 
7510
8205
  // src/scan.ts
7511
8206
  function runCejelScan(options) {
7512
- const identity = deriveProductIdentity(options.repoPath);
8207
+ const identity = deriveProductIdentity(options.repoPath, options.productName);
7513
8208
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
7514
8209
  const report = scoreRepoWithPublicCejel({
7515
8210
  productSlug: identity.productSlug,
@@ -7531,17 +8226,21 @@ function runCejelScan(options) {
7531
8226
  export {
7532
8227
  WitanReportSchema,
7533
8228
  formatExternalSourceLine,
8229
+ CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE,
7534
8230
  CERTIFICATE_GLOSSARY,
7535
8231
  formatCertificateMetricLabel,
7536
8232
  formatCertificateMetricValue,
7537
8233
  buildRelyingPartySummary,
8234
+ WITAN_RUBRIC_VERSION_V22,
8235
+ assertSelectableRubricVersion,
8236
+ isProspectiveRubricVersion,
8237
+ PROSPECTIVE_RUBRIC_NOTICE,
7538
8238
  renderWitanHtmlReport,
7539
8239
  renderWitanBadgeEndpoint,
7540
8240
  renderWitanBadgeSvg,
7541
8241
  serializeWitanReport,
7542
8242
  createWitanAttestation,
7543
8243
  verifyWitanAttestationBinding,
7544
- WITAN_RUBRIC_VERSION_V22,
7545
8244
  deriveProductIdentity,
7546
8245
  runCejelScan
7547
8246
  };