@cejel/cejel 0.1.9 → 0.1.10

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.
@@ -4365,6 +4365,18 @@ var WitanAttestationStatementSchema = external_exports.object({
4365
4365
  }).strict()
4366
4366
  }).strict();
4367
4367
 
4368
+ // src/witan/abstention.ts
4369
+ 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.";
4370
+ 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.";
4371
+ function isWitanNoMeasurementAbstention(report) {
4372
+ return report.verdict === "insufficient_source" && report.insufficientSourceReason === WITAN_NO_MEASUREMENT_REASON && report.criteria.every(
4373
+ ({ status }) => status === "not_applicable" || status === "insufficient_data"
4374
+ );
4375
+ }
4376
+ function renderWitanAbstentionLabel(report) {
4377
+ return isWitanNoMeasurementAbstention(report) ? "Insufficient evidence" : "Insufficient source";
4378
+ }
4379
+
4368
4380
  // src/witan/coverage.ts
4369
4381
  var LOW_CONFIDENCE_COVERAGE_THRESHOLD = 0.5;
4370
4382
  function isMeasuredCriterionStatus(status) {
@@ -4451,6 +4463,87 @@ function collectExternalFindings(consumedSignals) {
4451
4463
  });
4452
4464
  }
4453
4465
 
4466
+ // src/witan/finding-presentation.ts
4467
+ var METRIC_DERIVED_FINDING_PATTERN = /^[A-Z]\d+ metric-derived score is (\d+\.\d)\/4\.0, in the (critical|warning) band — no single finding drove this; it reflects the combined metric weighting below\.$/;
4468
+ var REMEDIATION_BY_METRIC = {
4469
+ test_to_source_ratio: "add concrete regression tests for the implementation surface",
4470
+ coverage_percent: "configure coverage and publish a measured threshold or report",
4471
+ verification_script_ratio: "add explicit test, lint, and typecheck verification commands",
4472
+ non_hollow_test_share: "replace skipped or placeholder tests with live assertions",
4473
+ env_handling_depth: "document and enforce safe environment-secret handling",
4474
+ secret_cleanliness: "remove committed secrets and rotate any exposed credentials",
4475
+ tenant_scope_ratio: "apply tenant scoping consistently across data access",
4476
+ rls_policy_count: "add database row-level security policies where the data model requires them",
4477
+ crypto_comparison_hygiene: "use constant-time comparison for signature or MAC checks",
4478
+ prod_readiness_primitives: "add the missing deployment-readiness controls",
4479
+ observability_depth: "add health checks, structured logging, and error boundaries",
4480
+ rollback_safety_depth: "document and test rollback or recovery procedures",
4481
+ pinned_dependency_ratio: "pin application dependencies that determine the deployed artifact",
4482
+ lockfile_coverage: "commit the package-manager lockfile used by CI and deployment",
4483
+ dependency_automation_ratio: "enable automated dependency updates and an audit command",
4484
+ declared_version_range_ratio: "declare an explicit compatible version for every dependency",
4485
+ dependency_count_sanity: "review and reduce unnecessary direct dependencies",
4486
+ claim_match_rate: "align documented product claims with observable repository evidence",
4487
+ claim_source_depth: "document concrete, checkable product claims",
4488
+ reconciliation_artifact_depth: "publish a claim-to-evidence reconciliation record",
4489
+ pr_merge_ratio: "preserve merged pull-request history in the scanned clone",
4490
+ pr_trace_primitives: "add pull-request templates and outcome-trace records",
4491
+ ci_script_depth: "run the repository verification commands in CI",
4492
+ default_branch_ci_depth: "run CI on the default branch and pull requests",
4493
+ prod_workflow_depth: "add a production or release verification workflow",
4494
+ audit_artifact_depth: "publish changelog, incident, security, or audit records",
4495
+ audit_freshness_depth: "keep the audit trail current",
4496
+ human_gate_documented: "document which privileged operations require human approval",
4497
+ protected_path_review_gate: "require review for changes to protected paths",
4498
+ fail_closed_privilege_check: "make privileged-operation checks fail closed",
4499
+ kill_switch_fail_safe_present: "add a fail-safe kill switch for privileged automation",
4500
+ privilege_escalation_cleanliness: "remove unsafe privilege-escalation patterns"
4501
+ };
4502
+ function renderFindingSummary(criterion, finding) {
4503
+ const match = METRIC_DERIVED_FINDING_PATTERN.exec(finding.summary);
4504
+ if (!match) return finding.summary;
4505
+ const score = match[1] ?? criterion.score.toFixed(1);
4506
+ const band = match[2] ?? criterion.status;
4507
+ const drivers = selectLowestDrivers(criterion.metrics);
4508
+ if (drivers.length === 0) {
4509
+ return `${criterion.id} dimension band is ${band} at ${score}/4.0. To improve this dimension, add stronger measurable evidence for ${criterion.title.toLowerCase()}.`;
4510
+ }
4511
+ const measurements = drivers.map(formatMetricMember).join("; ");
4512
+ const actions = Array.from(
4513
+ new Set(
4514
+ drivers.map(
4515
+ (metric2) => REMEDIATION_BY_METRIC[metric2.name] ?? `raise the measured ${metric2.label.toLowerCase()}`
4516
+ )
4517
+ )
4518
+ ).join("; ");
4519
+ return `${criterion.id} dimension band is ${band} at ${score}/4.0. Lowest contributing measurements: ${measurements}. To improve: ${actions}.`;
4520
+ }
4521
+ function selectLowestDrivers(metrics) {
4522
+ return [...metrics].filter((metric2) => metric2.max !== void 0).sort((left, right) => {
4523
+ const leftRatio = normalizedMetricValue(left);
4524
+ const rightRatio = normalizedMetricValue(right);
4525
+ if (leftRatio !== rightRatio) return leftRatio - rightRatio;
4526
+ if (left.weight !== right.weight) return right.weight - left.weight;
4527
+ return left.name.localeCompare(right.name);
4528
+ }).slice(0, 2);
4529
+ }
4530
+ function normalizedMetricValue(metric2) {
4531
+ if (metric2.max === void 0) return 1;
4532
+ return Math.min(metric2.value / metric2.max, 1);
4533
+ }
4534
+ function formatMetricMember(metric2) {
4535
+ const unit = metric2.unit ? ` ${metric2.unit}` : "";
4536
+ const maximum = metric2.max;
4537
+ if (maximum === void 0) return `${metric2.label} ${formatMetricValue(metric2.value)}${unit}`;
4538
+ if (metric2.kind === "saturating_count" && metric2.value > maximum) {
4539
+ return `${metric2.label} ${formatMetricValue(maximum)}${unit} capped (${formatMetricValue(metric2.value)} raw)`;
4540
+ }
4541
+ return `${metric2.label} ${formatMetricValue(metric2.value)}/${formatMetricValue(maximum)}${unit}`;
4542
+ }
4543
+ function formatMetricValue(value) {
4544
+ return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
4545
+ }
4546
+
4454
4547
  // src/witan/html.ts
4455
4548
  function renderWitanHtmlReport(report) {
4456
4549
  const codeCriteria = criteriaByCategory(report, "code_trust");
@@ -4491,7 +4584,7 @@ function renderWitanHtmlReport(report) {
4491
4584
  </section>
4492
4585
  <aside class="score-panel" aria-label="Summary trust score">
4493
4586
  <div class="score-badge">Summary</div>
4494
- ${report.verdict === "insufficient_source" ? `<div class="verdict">Insufficient source</div>
4587
+ ${report.verdict === "insufficient_source" ? `<div class="verdict">${escapeHtml(renderWitanAbstentionLabel(report))}</div>
4495
4588
  <p class="insufficient-note">${escapeHtml(report.insufficientSourceReason)}</p>` : `<div class="score">${formatScore(report.overallScore)}</div>
4496
4589
  <div class="score-unit">/ 4.0 overall</div>
4497
4590
  <div class="verdict">${escapeHtml(renderVerdict(report.overallScore))}</div>
@@ -4576,7 +4669,7 @@ function renderNotApplicableItem(criterion) {
4576
4669
  function renderCriterionCard(criterion) {
4577
4670
  const evidence = [
4578
4671
  ...criterion.evidence.map(renderEvidencePointer),
4579
- ...criterion.findings.map((finding) => renderFindingEvidence(finding))
4672
+ ...criterion.findings.map((finding) => renderFindingEvidence(criterion, finding))
4580
4673
  ];
4581
4674
  const metrics = criterion.metrics.map(renderMetric);
4582
4675
  return `<article class="criterion">
@@ -4599,24 +4692,24 @@ function renderCriterionCard(criterion) {
4599
4692
  function renderMetric(metric2) {
4600
4693
  const unit = metric2.unit ? ` ${escapeHtml(metric2.unit)}` : "";
4601
4694
  if (metric2.kind === "saturating_count" && metric2.max !== void 0 && metric2.value > metric2.max) {
4602
- return `<strong>${escapeHtml(metric2.label)}</strong><span>${formatMetricValue(metric2.max)}${unit} (capped; ${formatMetricValue(metric2.value)} raw)</span>`;
4695
+ return `<strong>${escapeHtml(metric2.label)}</strong><span>${formatMetricValue2(metric2.max)}${unit} (capped; ${formatMetricValue2(metric2.value)} raw)</span>`;
4603
4696
  }
4604
- const max = metric2.max ? `/${formatMetricValue(metric2.max)}` : "";
4605
- return `<strong>${escapeHtml(metric2.label)}</strong><span>${formatMetricValue(metric2.value)}${max}${unit}</span>`;
4697
+ const max = metric2.max ? `/${formatMetricValue2(metric2.max)}` : "";
4698
+ return `<strong>${escapeHtml(metric2.label)}</strong><span>${formatMetricValue2(metric2.value)}${max}${unit}</span>`;
4606
4699
  }
4607
4700
  function renderCategoryScore(score, coverage, category) {
4608
4701
  const categoryCoverage = coverage.byCategory.find((entry) => entry.category === category);
4609
4702
  return categoryCoverage !== void 0 && categoryCoverage.measured === 0 || score === null ? "not measured" : formatScore(score);
4610
4703
  }
4611
4704
  function renderStatusChip(status) {
4612
- return `<span class="status" data-status="${status}">${status}</span>`;
4705
+ return `<span class="status" data-status="${status}">dimension band: ${status}</span>`;
4613
4706
  }
4614
4707
  function renderEvidenceListItem(criterion, evidence) {
4615
4708
  return `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${renderEvidencePointer(evidence)}</span></li>`;
4616
4709
  }
4617
4710
  function renderOpenItems(criterion) {
4618
4711
  const findingItems = criterion.findings.map(
4619
- (finding) => `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>${escapeHtml(finding.severity)}: ${escapeHtml(finding.summary)} (${renderEvidencePointer(finding.evidence)})</span></li>`
4712
+ (finding) => `<li><strong>${escapeHtml(criterion.id)} - ${escapeHtml(criterion.title)}</strong><span>finding severity ${escapeHtml(finding.severity)}: ${escapeHtml(renderFindingSummary(criterion, finding))} (${renderEvidencePointer(finding.evidence)})</span></li>`
4620
4713
  );
4621
4714
  if ((criterion.status === "unverified" || criterion.status === "insufficient_data") && criterion.evidence.length === 0 && criterion.findings.length === 0) {
4622
4715
  const message = criterion.status === "insufficient_data" ? "Insufficient data \u2014 no measurable signal; excluded from composite (unmeasured, not inapplicable)." : "No concrete evidence supplied.";
@@ -4627,10 +4720,8 @@ function renderOpenItems(criterion) {
4627
4720
  if (criterion.status === "verified") return [];
4628
4721
  return findingItems;
4629
4722
  }
4630
- function renderFindingEvidence(finding) {
4631
- return `${escapeHtml(finding.severity)}: ${escapeHtml(finding.summary)} (${renderEvidencePointer(
4632
- finding.evidence
4633
- )})`;
4723
+ function renderFindingEvidence(criterion, finding) {
4724
+ return `finding severity ${escapeHtml(finding.severity)}: ${escapeHtml(renderFindingSummary(criterion, finding))} (${renderEvidencePointer(finding.evidence)})`;
4634
4725
  }
4635
4726
  function renderEvidencePointer(evidence) {
4636
4727
  const target = evidence.url ?? renderPathEvidenceTarget(evidence);
@@ -4653,6 +4744,9 @@ function renderVerdict(score) {
4653
4744
  return renderMachineVerdict(witanVerdictForScore(score));
4654
4745
  }
4655
4746
  function renderReportVerdict(report) {
4747
+ if (report.verdict === "insufficient_source") {
4748
+ return renderWitanAbstentionLabel(report);
4749
+ }
4656
4750
  return renderMachineVerdict(report.verdict);
4657
4751
  }
4658
4752
  function renderMachineVerdict(verdict) {
@@ -4672,7 +4766,7 @@ function renderMachineVerdict(verdict) {
4672
4766
  function formatScore(score) {
4673
4767
  return score.toFixed(1);
4674
4768
  }
4675
- function formatMetricValue(value) {
4769
+ function formatMetricValue2(value) {
4676
4770
  return Number.isInteger(value) ? value.toString() : value.toFixed(2);
4677
4771
  }
4678
4772
  function formatDate(value) {
@@ -4799,16 +4893,19 @@ var VERDICT_COLOR = {
4799
4893
  Conditional: "yellow",
4800
4894
  "At risk": "orange",
4801
4895
  Unverified: "red",
4802
- "Insufficient source": "lightgrey"
4896
+ "Insufficient source": "lightgrey",
4897
+ "Insufficient evidence": "lightgrey"
4803
4898
  };
4804
4899
  var VERDICT_SVG_FILL = {
4805
4900
  Verified: "#2fc86a",
4806
4901
  Conditional: "#dfb92f",
4807
4902
  "At risk": "#e08a3c",
4808
4903
  Unverified: "#d64545",
4809
- "Insufficient source": "#8c8c8c"
4904
+ "Insufficient source": "#8c8c8c",
4905
+ "Insufficient evidence": "#8c8c8c"
4810
4906
  };
4811
4907
  function badgeMessage(report, verdict) {
4908
+ if (isWitanNoMeasurementAbstention(report)) return "unrated: no measurable evidence";
4812
4909
  if (report.verdict === "insufficient_source") return "unrated: no source";
4813
4910
  return `${formatScore2(report.overallScore)}/4.0 ${verdict.toLowerCase()}`;
4814
4911
  }
@@ -4988,6 +5085,8 @@ var WITAN_RUBRIC_VERSION_V12 = "witan-rubric-v12-2026-07-23";
4988
5085
  var WITAN_RUBRIC_VERSION_V13 = "witan-rubric-v13-2026-07-23";
4989
5086
  var WITAN_RUBRIC_VERSION_V14 = "witan-rubric-v14-2026-07-23";
4990
5087
  var WITAN_RUBRIC_VERSION_V15 = "witan-rubric-v15-2026-07-24";
5088
+ var WITAN_RUBRIC_VERSION_V16 = "witan-rubric-v16-2026-07-24";
5089
+ var WITAN_RUBRIC_VERSION_V17 = "witan-rubric-v17-2026-07-24";
4991
5090
 
4992
5091
  // src/witan/repo-signals.ts
4993
5092
  function buildWitanInputFromRepo(options) {
@@ -4997,25 +5096,44 @@ function buildWitanInputFromRepo(options) {
4997
5096
  const headSha = readGitHead(options.repoPath);
4998
5097
  const repoFiles = listRepoFiles(options.repoPath);
4999
5098
  const inventoryFiles = listRepoInventory(options.repoPath, repoFiles);
5099
+ const ignoredTargetReason = repoFiles.length === 0 ? explainIgnoredScanTarget(options.repoPath) : void 0;
5100
+ const usesV17Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V17;
5000
5101
  const structuralArchetype = classifyRepoArchetype(inventoryFiles, rubricVersion);
5001
- const readableArchetype = rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 ? applyReadableSourceRepresentationGate(
5102
+ const readableArchetype = rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17 ? applyReadableSourceRepresentationGate(
5002
5103
  options.repoPath,
5003
5104
  inventoryFiles,
5004
5105
  structuralArchetype,
5005
5106
  headSha,
5006
5107
  rubricVersion
5007
5108
  ) : structuralArchetype;
5008
- const archetype = rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 ? applySemanticSourceRepresentationGate(
5109
+ const gatedArchetype = rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17 ? applySemanticSourceRepresentationGate(
5009
5110
  options.repoPath,
5010
5111
  inventoryFiles,
5011
5112
  readableArchetype,
5012
5113
  rubricVersion
5013
5114
  ) : readableArchetype;
5014
- const usesV8Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V8 || rubricVersion === WITAN_RUBRIC_VERSION_V92 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15;
5015
- const usesV33Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V92 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15;
5016
- const usesV36Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15;
5017
- const usesV39Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15;
5018
- const usesV47Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V15;
5115
+ const usesV8Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V8 || rubricVersion === WITAN_RUBRIC_VERSION_V92 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17;
5116
+ const usesV33Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V92 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17;
5117
+ const usesV36Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17;
5118
+ const usesV39Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17;
5119
+ const usesV47Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17;
5120
+ const reviewableSourceProof = usesV17Detectors ? buildReviewableSourceProof(options.repoPath, inventoryFiles, headSha, rubricVersion) : void 0;
5121
+ const coreSignals = collectRepoSignals(
5122
+ options.repoPath,
5123
+ generatedAt,
5124
+ repoFiles,
5125
+ usesV8Detectors,
5126
+ usesV33Detectors,
5127
+ usesV36Detectors,
5128
+ usesV39Detectors,
5129
+ usesV47Detectors,
5130
+ reviewableSourceProof
5131
+ );
5132
+ const archetype = usesV17Detectors && reviewableSourceProof?.passes === true && isV17RescuableAbstention(gatedArchetype.abstentionKind) && coreSignals.some(isMeasuredCriterionSignal) ? {
5133
+ archetype: isLikelyMonorepo(inventoryFiles) ? "monorepo" : "source",
5134
+ sourceFileCount: gatedArchetype.sourceFileCount,
5135
+ totalFileCount: gatedArchetype.totalFileCount
5136
+ } : gatedArchetype;
5019
5137
  return {
5020
5138
  productSlug: options.productSlug,
5021
5139
  productDisplayName: options.productDisplayName,
@@ -5026,23 +5144,21 @@ function buildWitanInputFromRepo(options) {
5026
5144
  generatedAt,
5027
5145
  ...options.rubricVersion ? { rubricVersion: options.rubricVersion } : {},
5028
5146
  archetype: archetype.archetype,
5029
- ...archetype.insufficientSourceReason ? { insufficientSourceReason: archetype.insufficientSourceReason } : {},
5147
+ ...ignoredTargetReason ?? archetype.insufficientSourceReason ? { insufficientSourceReason: ignoredTargetReason ?? archetype.insufficientSourceReason } : {},
5030
5148
  signals: [
5031
- ...collectRepoSignals(
5032
- options.repoPath,
5033
- generatedAt,
5034
- repoFiles,
5035
- usesV8Detectors,
5036
- usesV33Detectors,
5037
- usesV36Detectors,
5038
- usesV39Detectors,
5039
- usesV47Detectors
5040
- ),
5149
+ ...coreSignals,
5041
5150
  ...(options.domainCollectors ?? []).map((collect) => collect(options.repoPath, repoFiles)),
5042
5151
  ...options.additionalSignals ?? []
5043
5152
  ]
5044
5153
  };
5045
5154
  }
5155
+ function isV17RescuableAbstention(kind) {
5156
+ return kind === "artifact_dominance" || kind === "docs_source_role" || kind === "readable_representation" || kind === "semantic_auxiliary";
5157
+ }
5158
+ function isMeasuredCriterionSignal(signal) {
5159
+ if (signal.notApplicable === true) return false;
5160
+ return (signal.metrics?.length ?? 0) > 0 || (signal.positiveEvidence?.length ?? 0) > 0 || (signal.findings?.length ?? 0) > 0;
5161
+ }
5046
5162
  var SOURCE_LANGUAGES = [
5047
5163
  {
5048
5164
  name: "JS/TS",
@@ -5118,13 +5234,14 @@ var SOURCE_EXTENSION_PATTERN_V10 = new RegExp(
5118
5234
  "i"
5119
5235
  );
5120
5236
  function isRecognizedSourcePath(path, rubricVersion = WITAN_RUBRIC_VERSION) {
5121
- return (rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 ? SOURCE_EXTENSION_PATTERN_V10 : SOURCE_EXTENSION_PATTERN).test(path);
5237
+ return (rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17 ? SOURCE_EXTENSION_PATTERN_V10 : SOURCE_EXTENSION_PATTERN).test(path);
5122
5238
  }
5123
5239
  var BUNDLED_BINARY_EXTENSION_PATTERN = /\.(vsix|zip|tgz|whl|exe|dmg|pkg|msi|jar|war|apk|ipa|dll|dylib|ttf|otf|woff2?|eot|eps)$/i;
5124
5240
  var DOCS_OR_MEDIA_EXTENSION_PATTERN = /\.(md|mdx|rst|png|jpe?g|gif|svg|webp|ico|pdf)$/i;
5125
5241
  var ARTIFACT_DOMINANCE_EXTENSION_PATTERN = /\.(vsix|zip|tgz|whl|exe|dmg|pkg|msi|jar|war|apk|ipa|dll|dylib|ttf|otf|woff2?|eot|eps|png|jpe?g|gif|svg|webp|ico|pdf)$/i;
5126
5242
  var DOC_DISTRIBUTION_COMPANION_PATTERN = /(^|\/)(LICENSE|LICENCE|NOTICE|COPYING|AUTHORS|CONTRIBUTORS|CODE_OF_CONDUCT)(\.[A-Za-z0-9]+)?$/i;
5127
5243
  var GENERATED_OR_VENDOR_PATH_PATTERN = /(^|\/)(vendor|vendors|dist|build|generated|gen|coverage|node_modules|site-packages|\.venv|venv|\.next|__pycache__|\.terraform|public\/assets|static\/assets)(\/|$)|(^|\/)[^/]+\.min\.(?:js|css)$/i;
5244
+ var V17_BUILD_OR_DEPENDENCY_CACHE_PATH_PATTERN = /(^|\/)(?:target|_build|\.build|DerivedData|Pods|Carthage\/Build|\.cache|\.gradle|\.m2|\.yarn\/cache|\.pnpm-store|\.dart_tool|\.tox|\.pytest_cache|\.ruff_cache|\.mypy_cache|\.turbo|\.parcel-cache|\.angular\/cache|\.svelte-kit|\.nuxt|bazel-(?:bin|out|testlogs)|buck-out)(\/|$)/i;
5128
5245
  var INDEPENDENT_CATALOG_PATH_PATTERN = /(^|\/)(solutions?|exercises?|challenges?|algorithms?)(\/|$)/i;
5129
5246
  var KNOWN_NON_SOURCE_EXTENSION_PATTERN = /\.(json|jsonc|json5|ya?ml|toml|txt|lock|xml|csv|tsv|ini|cfg|conf|cnf|properties|env|sum|mod|graphql|gql|proto|sarif|editorconfig|gitignore|gitattributes|npmrc|nvmrc|dockerignore|prettierignore|prettierrc|log|stderr|snap|grit|po|mo|dbf|shp|shx|egg|pem|key|csr|ai|txt\.license|pf|in|case|common|fortls)$/i;
5130
5247
  var V13_NON_SOURCE_EXTENSION_PATTERN = /\.(?:\d+|class|py[co]|gz|bz2|xz|7z|rar|mat|npy|npz|h5|hdf5|onnx|pt|pth|ckpt|safetensors|crt|cer|der|p12|pfx|diff|patch|plist|pbxproj|xcworkspacedata|xcbuild|xcplayground|xcscheme|xcsettings|xcconfig|xcprivacy|xcstrings|csproj|fsproj|vbproj|sln|slnf|props|targets|projitems|shproj|pubxml|resw|resources|storyboard|xib|entitlements|modulemap|podspec|nuspec|appxmanifest|webmanifest|manifest|map|fls|aux|bbl|bib|ocm|tdb|db|sqlite|sqlcipher\d*|bin|dat|pdb|prj|resolved|sim|input|list|fig|drawio|puml|sketch|icns|flac|wav|mp3|mp4|mov|avi|mkv|nv21|xls[xb]?|mex[a-z0-9_]*|gitconfig|profile|yapf|config|desktop|typed|example|bak|_|test(?:_[a-z0-9]+)*_location_override|xamltest|tld(?:_[a-z0-9]+)?|xsd|cmake|gradle|mk|am|pro|upri|dockerfile|dockergen|makefile|alpine|android|debian|ubuntu\d+|cuda\d+_ubuntu\d+|manylinux[a-z0-9_]*)$/i;
@@ -5148,6 +5265,7 @@ function classifyRepoArchetype(repoFiles, rubricVersion = WITAN_RUBRIC_VERSION)
5148
5265
  archetype: "empty",
5149
5266
  sourceFileCount: 0,
5150
5267
  totalFileCount: 0,
5268
+ abstentionKind: "empty",
5151
5269
  insufficientSourceReason: "Repository has no tracked files \u2014 there is nothing to certify."
5152
5270
  };
5153
5271
  }
@@ -5159,6 +5277,7 @@ function classifyRepoArchetype(repoFiles, rubricVersion = WITAN_RUBRIC_VERSION)
5159
5277
  archetype: "generated_only",
5160
5278
  sourceFileCount,
5161
5279
  totalFileCount,
5280
+ abstentionKind: "generated_only",
5162
5281
  insufficientSourceReason: `${generatedSourceFiles.length} of ${sourceFileCount} source-shaped file(s) are under generated, vendor, build, distribution, coverage, or bundled-asset paths; only ${sourceFileCount - generatedSourceFiles.length} authored source-shaped file(s) remain. Cejel abstains because generated/vendor output is not a reviewable implementation surface. ${INGEST_POINTER}`
5163
5282
  };
5164
5283
  }
@@ -5170,10 +5289,11 @@ function classifyRepoArchetype(repoFiles, rubricVersion = WITAN_RUBRIC_VERSION)
5170
5289
  archetype: "non_cohesive_source",
5171
5290
  sourceFileCount,
5172
5291
  totalFileCount,
5292
+ abstentionKind: "independent_catalog",
5173
5293
  insufficientSourceReason: `${catalogSourceFiles.length} of ${sourceFileCount} source-shaped file(s) are under independent solution, exercise, challenge, or algorithm catalog paths; only ${sourceFileCount - catalogSourceFiles.length} source-shaped file(s) remain outside that catalog. Cejel abstains because the repository does not expose one cohesive product implementation to score. ${INGEST_POINTER}`
5174
5294
  };
5175
5295
  }
5176
- if (rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15) {
5296
+ if (rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17) {
5177
5297
  const authoredSourceFiles = sourceFiles.filter(isAuthoredProductionPath);
5178
5298
  const documentationFiles = repoFiles.filter(
5179
5299
  (file) => DOCS_OR_MEDIA_EXTENSION_PATTERN.test(file)
@@ -5183,6 +5303,7 @@ function classifyRepoArchetype(repoFiles, rubricVersion = WITAN_RUBRIC_VERSION)
5183
5303
  archetype: "docs_only",
5184
5304
  sourceFileCount,
5185
5305
  totalFileCount,
5306
+ abstentionKind: "docs_source_role",
5186
5307
  insufficientSourceReason: `${sourceFileCount} source-shaped file(s) are tests/fixtures around a documentation tree; no authored product source remains. Cejel abstains because documentation tests do not support a product-source headline score. ${INGEST_POINTER}`
5187
5308
  };
5188
5309
  }
@@ -5190,11 +5311,12 @@ function classifyRepoArchetype(repoFiles, rubricVersion = WITAN_RUBRIC_VERSION)
5190
5311
  (file) => ARTIFACT_DOMINANCE_EXTENSION_PATTERN.test(file)
5191
5312
  );
5192
5313
  const materialFiles = authoredSourceFiles.length + artifactFiles.length;
5193
- if (artifactFiles.length >= 10 && (authoredSourceFiles.length <= 10 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15) && materialFiles > 0 && authoredSourceFiles.length / materialFiles <= 0.1) {
5314
+ if (artifactFiles.length >= 10 && (authoredSourceFiles.length <= 10 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17) && materialFiles > 0 && authoredSourceFiles.length / materialFiles <= 0.1) {
5194
5315
  return {
5195
5316
  archetype: "binary_only",
5196
5317
  sourceFileCount,
5197
5318
  totalFileCount,
5319
+ abstentionKind: "artifact_dominance",
5198
5320
  insufficientSourceReason: `${authoredSourceFiles.length} authored source-shaped file(s) accompany ${artifactFiles.length} packaged, font, image, or media artifact(s); the source is sparse tooling rather than a representative product implementation. Cejel abstains because incidental tooling cannot support a product-source headline score. ${INGEST_POINTER}`
5199
5321
  };
5200
5322
  }
@@ -5208,6 +5330,7 @@ function classifyRepoArchetype(repoFiles, rubricVersion = WITAN_RUBRIC_VERSION)
5208
5330
  archetype: "binary_only",
5209
5331
  sourceFileCount,
5210
5332
  totalFileCount,
5333
+ abstentionKind: "binary_distribution",
5211
5334
  insufficientSourceReason: `${sourceFileCount} source file(s) found among ${totalFileCount} tracked file(s); repo appears to be a binary/bundled-distribution tree (e.g. ${sample ? basename(sample) : "a packaged artifact"}) \u2014 cejel rates source, not binaries. ${INGEST_POINTER}`
5212
5335
  };
5213
5336
  }
@@ -5215,11 +5338,12 @@ function classifyRepoArchetype(repoFiles, rubricVersion = WITAN_RUBRIC_VERSION)
5215
5338
  const docsDistributionFiles = repoFiles.filter(
5216
5339
  (file) => DOCS_OR_MEDIA_EXTENSION_PATTERN.test(file) || DOC_DISTRIBUTION_COMPANION_PATTERN.test(file)
5217
5340
  );
5218
- if (docsFiles.length > 0 && (docsDistributionFiles.length === totalFileCount || (rubricVersion === WITAN_RUBRIC_VERSION_V92 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15) && unrecognisedSourceFiles.length === 0)) {
5341
+ if (docsFiles.length > 0 && (docsDistributionFiles.length === totalFileCount || (rubricVersion === WITAN_RUBRIC_VERSION_V92 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17) && unrecognisedSourceFiles.length === 0)) {
5219
5342
  return {
5220
5343
  archetype: "docs_only",
5221
5344
  sourceFileCount,
5222
5345
  totalFileCount,
5346
+ abstentionKind: "docs_distribution",
5223
5347
  insufficientSourceReason: `${sourceFileCount} source file(s) found among ${totalFileCount} tracked file(s); repo appears to be a docs/distribution tree (README/markdown/media), not a source tree \u2014 cejel rates source, not docs. ${INGEST_POINTER}`
5224
5348
  };
5225
5349
  }
@@ -5333,6 +5457,33 @@ function isRepresentativeSourceText(contents) {
5333
5457
  function representativeSourceRank(rankKey, file) {
5334
5458
  return createHash2("sha256").update(`${rankKey}:${file}`).digest("hex");
5335
5459
  }
5460
+ function buildReviewableSourceProof(repoPath, repoFiles, headSha, rubricVersion) {
5461
+ const sourceShapedFiles = repoFiles.filter(
5462
+ (path) => isRecognizedSourcePath(path, rubricVersion) || isUnrecognisedSourcePath(path, rubricVersion) || /\.ipynb$/i.test(path)
5463
+ );
5464
+ const eligibleSourceFiles = sourceShapedFiles.filter(
5465
+ (path) => !GENERATED_OR_VENDOR_PATH_PATTERN.test(path) && !V17_BUILD_OR_DEPENDENCY_CACHE_PATH_PATTERN.test(path) && (SOURCE_EXTENSION_PATTERN.test(path) || /\.m$/i.test(path))
5466
+ );
5467
+ const sample = selectProportionalRepresentativeSourceFiles(
5468
+ eligibleSourceFiles,
5469
+ `witan-v17-reviewable-source:${headSha ?? "unversioned"}`,
5470
+ READABLE_SOURCE_REPRESENTATION_SAMPLE_LIMIT_V13
5471
+ );
5472
+ const readablePaths = sample.filter((file) => {
5473
+ const contents = readRepresentativeSourceText(repoPath, file);
5474
+ return contents !== null && isRepresentativeSourceText(contents);
5475
+ });
5476
+ const ratableShare = sourceShapedFiles.length === 0 ? 0 : eligibleSourceFiles.length / sourceShapedFiles.length;
5477
+ const readableShare = sample.length === 0 ? 0 : readablePaths.length / sample.length;
5478
+ return {
5479
+ sourceShapedFileCount: sourceShapedFiles.length,
5480
+ eligibleSourceFileCount: eligibleSourceFiles.length,
5481
+ sampledFileCount: sample.length,
5482
+ readableSampledFileCount: readablePaths.length,
5483
+ ...readablePaths[0] ? { firstReadablePath: readablePaths[0] } : {},
5484
+ passes: eligibleSourceFiles.length > 0 && ratableShare >= 0.2 && readablePaths.length > 0 && (readableShare >= 0.75 || readablePaths.length >= 8)
5485
+ };
5486
+ }
5336
5487
  function applyReadableSourceRepresentationGate(repoPath, repoFiles, classification, headSha, rubricVersion) {
5337
5488
  if (classification.archetype !== "source" && classification.archetype !== "monorepo") {
5338
5489
  return classification;
@@ -5379,6 +5530,7 @@ function applyReadableSourceRepresentationGate(repoPath, repoFiles, classificati
5379
5530
  archetype: "non_cohesive_source",
5380
5531
  sourceFileCount: classification.sourceFileCount,
5381
5532
  totalFileCount: classification.totalFileCount,
5533
+ abstentionKind: "readable_representation",
5382
5534
  insufficientSourceReason: `Readable source representation covers ${readableCountLabel} of ${authoredSourceFiles.length} authored source-shaped file(s) (${readablePct}%); this is below the ${thresholdPct}% representation threshold. Cejel abstains because unreadable, compressed, binary-shaped, or opaque source families cannot support a representative headline score. ${INGEST_POINTER}`
5383
5535
  };
5384
5536
  }
@@ -5413,6 +5565,7 @@ function applySemanticSourceRepresentationGate(repoPath, repoFiles, classificati
5413
5565
  archetype: "non_cohesive_source",
5414
5566
  sourceFileCount: classification.sourceFileCount,
5415
5567
  totalFileCount: classification.totalFileCount,
5568
+ abstentionKind: "semantic_auxiliary",
5416
5569
  insufficientSourceReason: `${detail} provide no representative product implementation. Cejel abstains because auxiliary metadata or payload files cannot support a product-source headline score. ${INGEST_POINTER}`
5417
5570
  };
5418
5571
  }
@@ -5423,6 +5576,7 @@ function applySemanticSourceRepresentationGate(repoPath, repoFiles, classificati
5423
5576
  archetype: "non_cohesive_source",
5424
5577
  sourceFileCount: classification.sourceFileCount,
5425
5578
  totalFileCount: classification.totalFileCount,
5579
+ abstentionKind: "semantic_auxiliary",
5426
5580
  insufficientSourceReason: `${auxiliaryCount} of ${authoredSourceFiles.length} authored source-shaped file(s) are localization, style, package, or redirect auxiliary material. Cejel abstains because an auxiliary-dominated tree does not expose representative product implementation for a headline score. ${INGEST_POINTER}`
5427
5581
  };
5428
5582
  }
@@ -5469,7 +5623,7 @@ function readRepresentativeSourceText(repoPath, file) {
5469
5623
  }
5470
5624
  }
5471
5625
  function sourceDominanceThreshold(rubricVersion) {
5472
- if (rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15) {
5626
+ if (rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17) {
5473
5627
  return SOURCE_DOMINANCE_RATIO_THRESHOLD_V11;
5474
5628
  }
5475
5629
  if (rubricVersion === WITAN_RUBRIC_VERSION_V10) return SOURCE_DOMINANCE_RATIO_THRESHOLD_V10;
@@ -5483,7 +5637,7 @@ function isUnrecognisedSourcePath(file, rubricVersion = WITAN_RUBRIC_VERSION) {
5483
5637
  const base = basename(file);
5484
5638
  if (!/\.([a-zA-Z0-9_]+)$/.test(base)) return false;
5485
5639
  if (KNOWN_NON_SOURCE_EXTENSION_PATTERN.test(base)) return false;
5486
- if ((rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15) && V13_NON_SOURCE_EXTENSION_PATTERN.test(base)) {
5640
+ if ((rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17) && V13_NON_SOURCE_EXTENSION_PATTERN.test(base)) {
5487
5641
  return false;
5488
5642
  }
5489
5643
  if (DOCS_OR_MEDIA_EXTENSION_PATTERN.test(base)) return false;
@@ -5512,6 +5666,7 @@ function unrecognisedEcosystemResult(sourceFileCount, totalFileCount, unrecognis
5512
5666
  archetype: "unrecognised_ecosystem",
5513
5667
  sourceFileCount,
5514
5668
  totalFileCount,
5669
+ abstentionKind: "unrecognised_ecosystem",
5515
5670
  insufficientSourceReason: `Cejel does not yet read this repository's dominant source language(s) (${sample}) \u2014 ${measuredClause}. Cejel abstains from a verdict rather than score a repository whose recognised source is incidental rather than dominant; the Criterion Profile and Measured coverage below show exactly which dimensions were and were not measured. ${INGEST_POINTER}`
5516
5671
  };
5517
5672
  }
@@ -5532,6 +5687,32 @@ function assertRepoPathExists(repoPath) {
5532
5687
  throw new Error(`Cejel: path is not a directory: ${repoPath}`);
5533
5688
  }
5534
5689
  }
5690
+ function explainIgnoredScanTarget(repoPath) {
5691
+ let top;
5692
+ try {
5693
+ top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
5694
+ cwd: repoPath,
5695
+ encoding: "utf8",
5696
+ stdio: ["ignore", "pipe", "ignore"]
5697
+ }).trim();
5698
+ } catch {
5699
+ return void 0;
5700
+ }
5701
+ if (!top) return void 0;
5702
+ const topResolved = realpathSync(top);
5703
+ const targetResolved = realpathSync(repoPath);
5704
+ const target = relative(topResolved, targetResolved).split(sep).join("/");
5705
+ if (!target || target.startsWith("../")) return void 0;
5706
+ try {
5707
+ execFileSync("git", ["check-ignore", "--quiet", "--", `${target}/`], {
5708
+ cwd: topResolved,
5709
+ stdio: "ignore"
5710
+ });
5711
+ } catch {
5712
+ return void 0;
5713
+ }
5714
+ return `The requested scan path (${target}) is excluded by this repository's Git ignore rules, so Cejel found no tracked source to evaluate. Scan a tracked project or intentionally add the path to version control; Cejel abstains rather than score an ignored or vendored working tree.`;
5715
+ }
5535
5716
  function buildNotApplicableSignal(criterionId, reason) {
5536
5717
  return {
5537
5718
  criterionId,
@@ -5542,7 +5723,7 @@ function buildNotApplicableSignal(criterionId, reason) {
5542
5723
  notApplicable: true
5543
5724
  };
5544
5725
  }
5545
- function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors) {
5726
+ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, reviewableSourceProof) {
5546
5727
  const signals = [];
5547
5728
  const mono = resolveMonorepoContext(repoPath);
5548
5729
  const a1Signal = collectA1TestIntegrityEvidence(
@@ -5550,7 +5731,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
5550
5731
  repoFiles,
5551
5732
  useV27Detectors,
5552
5733
  useV33Detectors,
5553
- useV36Detectors
5734
+ useV36Detectors,
5735
+ reviewableSourceProof
5554
5736
  );
5555
5737
  const a2Signal = collectA2IsolationEvidence(
5556
5738
  repoPath,
@@ -5599,10 +5781,14 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
5599
5781
  }
5600
5782
  return signals;
5601
5783
  }
5602
- function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors) {
5784
+ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, reviewableSourceProof) {
5603
5785
  const evidence = [];
5604
5786
  const findings = [];
5605
- const allTestFiles = findConcreteTestFiles(repoPath, repoFiles);
5787
+ const allTestFiles = findConcreteTestFiles(
5788
+ repoPath,
5789
+ repoFiles,
5790
+ reviewableSourceProof ? WITAN_RUBRIC_VERSION_V17 : void 0
5791
+ );
5606
5792
  const testFiles = allTestFiles.slice(0, 8);
5607
5793
  const sourceFiles = repoFiles.filter(isSourceFile);
5608
5794
  const runnerFiles = repoFiles.filter(
@@ -5691,7 +5877,9 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
5691
5877
  });
5692
5878
  }
5693
5879
  }
5694
- if (evidence.length === 0 && findings.length === 0) return null;
5880
+ if (evidence.length === 0 && findings.length === 0 && configuredRunnerFiles.length === 0) {
5881
+ return reviewableSourceProof?.passes === true && reviewableSourceProof.firstReadablePath ? buildA1AuthenticatedAbsenceSignal(repoPath, reviewableSourceProof) : null;
5882
+ }
5695
5883
  if (testFiles.length === 0) {
5696
5884
  const fallback = configuredRunnerFiles[0] ? evidenceForRelative(
5697
5885
  repoPath,
@@ -5729,7 +5917,11 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
5729
5917
  });
5730
5918
  }
5731
5919
  }
5732
- const nonHollowShare = measureNonHollowTestShare(repoPath, allTestFiles);
5920
+ const nonHollowShare = measureNonHollowTestShare(
5921
+ repoPath,
5922
+ allTestFiles,
5923
+ reviewableSourceProof !== void 0
5924
+ );
5733
5925
  return {
5734
5926
  criterionId: "A1",
5735
5927
  positiveEvidence: evidence,
@@ -5777,6 +5969,70 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
5777
5969
  notes: "A1 is detected from real test files, test runner configuration, and optional coverage configuration."
5778
5970
  };
5779
5971
  }
5972
+ function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
5973
+ const sourcePath = proof.firstReadablePath;
5974
+ if (!sourcePath) {
5975
+ throw new Error("Cejel invariant: a passing reviewable-source proof must have a readable path");
5976
+ }
5977
+ const anchor = evidenceForRelative(
5978
+ repoPath,
5979
+ sourcePath,
5980
+ "artifact",
5981
+ `Reviewable source anchor (${proof.readableSampledFileCount}/${proof.sampledFileCount} sampled files readable; ${proof.eligibleSourceFileCount}/${proof.sourceShapedFileCount} source-shaped files criterion-ratable)`
5982
+ );
5983
+ return {
5984
+ criterionId: "A1",
5985
+ positiveEvidence: [anchor],
5986
+ findings: [
5987
+ {
5988
+ severity: "critical",
5989
+ summary: WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY,
5990
+ evidence: anchor
5991
+ }
5992
+ ],
5993
+ metrics: [
5994
+ metric(
5995
+ "test_to_source_ratio",
5996
+ "Test-to-source file ratio",
5997
+ 0,
5998
+ proof.eligibleSourceFileCount,
5999
+ 0.3,
6000
+ "ratio",
6001
+ "Measures how much concrete test surface exists relative to criterion-ratable implementation source.",
6002
+ "saturating_count"
6003
+ ),
6004
+ metric(
6005
+ "coverage_percent",
6006
+ "Static coverage percentage",
6007
+ 0,
6008
+ 100,
6009
+ 0.3,
6010
+ "percent",
6011
+ "Uses a static coverage report value or configured threshold when present, without running tests."
6012
+ ),
6013
+ metric(
6014
+ "verification_script_ratio",
6015
+ "Verification script ratio",
6016
+ 0,
6017
+ 4,
6018
+ 0.25,
6019
+ "ratio",
6020
+ "Measures explicit test/lint/typecheck verification commands plus test runner configuration.",
6021
+ "saturating_count"
6022
+ ),
6023
+ metric(
6024
+ "non_hollow_test_share",
6025
+ "Non-hollow test share",
6026
+ 0,
6027
+ 1,
6028
+ 0.15,
6029
+ "ratio",
6030
+ "No concrete test files were present to establish non-hollow test behavior."
6031
+ )
6032
+ ],
6033
+ notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files.`
6034
+ };
6035
+ }
5780
6036
  function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors) {
5781
6037
  const evidence = [];
5782
6038
  const findings = [];
@@ -6923,12 +7179,20 @@ function isV47AuthoredProductionPath(path) {
6923
7179
  var TEST_DIRECTORY_SOURCE_PATTERN = new RegExp(
6924
7180
  `(^|/)(?:__tests__|tests?|specs?|[^/]+(?:Tests|Specs))/(?:.*\\.)?(?:${SOURCE_LANGUAGES.flatMap((language) => language.extensions).join("|")})$`
6925
7181
  );
6926
- function isTestFile(file) {
6927
- return TEST_DIRECTORY_SOURCE_PATTERN.test(file) || // AVA's default discovery includes test.js and test-*.js at the scanned package root.
7182
+ var TEST_DIRECTORY_SOURCE_PATTERN_V17 = new RegExp(
7183
+ `(^|/)(?:__tests__|tests?|specs?|[^/]+(?:Tests|Specs))/(?:.*\\.)?(?:${[
7184
+ ...SOURCE_LANGUAGES_V10.flatMap((language) => language.extensions),
7185
+ "bats"
7186
+ ].join("|")})$`,
7187
+ "i"
7188
+ );
7189
+ function isTestFile(file, rubricVersion = WITAN_RUBRIC_VERSION) {
7190
+ return rubricVersion === WITAN_RUBRIC_VERSION_V17 && (TEST_DIRECTORY_SOURCE_PATTERN_V17.test(file) || /\.tftest\.hcl$/i.test(file) || /\.bats$/i.test(file) || /(^|\/)test[^/]*\.m$/i.test(file) || /_test\.m$/i.test(file)) || TEST_DIRECTORY_SOURCE_PATTERN.test(file) || // AVA's default discovery includes test.js and test-*.js at the scanned package root.
6928
7191
  // Keep this root-anchored: nested files need an existing test-directory/suffix convention.
6929
7192
  /^test(?:-[^/]+)?\.[cm]?[jt]sx?$/.test(file) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(file) || /(^|\/)test_.*\.py$/.test(file) || /_test\.go$/.test(file) || /(^|\/)test_.*\.(cpp|cc|cxx)$/.test(file) || /_(test|tests)\.(cpp|cc|cxx)$/.test(file) || /Tests?\.(java|kt)$/.test(file) || /(_test|_spec)\.(rs|rb|php|swift|kt)$/.test(file);
6930
7193
  }
6931
7194
  var NAME_SHAPED_TEST_FILE_PATTERN = /^test(?:-[^/]+)?\.[cm]?[jt]sx?$|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)__tests__\/|(^|\/)test_[^/]*\.py$|(^|\/)tests?\.py$|_test\.go$|(^|\/)test_[^/]*\.(cpp|cc|cxx)$|_(test|tests)\.(cpp|cc|cxx)$|Tests?\.(java|kt)$|(_test|_spec)\.(rs|rb|php|swift|kt)$/;
7195
+ var NAME_SHAPED_TEST_FILE_PATTERN_V17 = /\.tftest\.hcl$|\.bats$|(^|\/)test[^/]*\.m$|_test\.m$/i;
6932
7196
  var TEST_RUNNER_PATTERN = /\b(vitest|jest|mocha|ava|tap|pytest|go test|cargo test|rspec|phpunit|gradle test|mvn test|node\s+--test|node:test)\b/i;
6933
7197
  var CI_TEST_COMMAND_PATTERN = /\b(pytest|go\s+test|cargo\s+test|mvn\s+test|gradle\s+test|dotnet\s+test|ctest|make\s+test|jest|vitest|mocha|rspec|phpunit|pnpm\s+(?:run\s+)?test|npm\s+(?:run\s+)?test|yarn\s+(?:run\s+)?test|node\s+--test|node:test)\b|--target[=\s]+test\b/i;
6934
7198
  var CI_LINT_COMMAND_PATTERN = /\b(eslint|pylint|flake8|black|ruff)\b/i;
@@ -7214,7 +7478,7 @@ var JS_TEST_ASSERTION_PATTERN = /(expect\(|assert\.|should\.|toEqual\(|toBe\(|as
7214
7478
  var CPP_TEST_ASSERTION_PATTERN = /\bEXPECT_|ASSERT_EQ\b|ASSERT_TRUE\b|ASSERT_FALSE\b|\bREQUIRE\s*\(|\bCHECK\s*\(|\bTEST\s*\(|\bTEST_F\s*\(|\bTEST_CASE\s*\(|\bSECTION\s*\(/;
7215
7479
  var MULTI_ECOSYSTEM_TEST_ASSERTION_PATTERN = /\.assert[A-Z_]\w*\s*\(|\bassert[A-Z]\w*\s*\(|\bpytest\.raises\b|\bself\.fail\s*\(|\bt\.(?:Error|Errorf|Fatal|Fatalf|Fail|FailNow)\b|\brequire\.[A-Z]\w*\s*\(|\b(?:debug_)?assert(?:_eq|_ne)?!\s*\(|\bassert_\w+\b|\$this->assert\w+\s*\(/;
7216
7480
  var SUITE_LEVEL_SKIP_PATTERN = /\b(?:describe|suite|context)\.skip\s*\(/;
7217
- function measureNonHollowTestShare(repoPath, files) {
7481
+ function measureNonHollowTestShare(repoPath, files, useV17Detectors = false) {
7218
7482
  let nonHollowCount = 0;
7219
7483
  let ratedCount = 0;
7220
7484
  for (const file of files) {
@@ -7225,7 +7489,7 @@ function measureNonHollowTestShare(repoPath, files) {
7225
7489
  if (nonHollow) {
7226
7490
  nonHollowCount += 1;
7227
7491
  ratedCount += 1;
7228
- } else if (NAME_SHAPED_TEST_FILE_PATTERN.test(file)) {
7492
+ } else if (NAME_SHAPED_TEST_FILE_PATTERN.test(file) || useV17Detectors && NAME_SHAPED_TEST_FILE_PATTERN_V17.test(file)) {
7229
7493
  ratedCount += 1;
7230
7494
  }
7231
7495
  }
@@ -7557,9 +7821,12 @@ function findCoverageConfigFiles(repoPath, repoFiles, authoredOnly = false) {
7557
7821
  (file) => (!authoredOnly || isAuthoredProductionPath(file)) && isCoverageConfig(repoPath, file)
7558
7822
  );
7559
7823
  }
7560
- function findConcreteTestFiles(repoPath, repoFiles) {
7824
+ function findConcreteTestFiles(repoPath, repoFiles, rubricVersion = WITAN_RUBRIC_VERSION) {
7561
7825
  const contentCppTests = collectContentBasedCppTestFiles(repoPath, repoFiles);
7562
- return [...repoFiles.filter(isTestFile), ...contentCppTests.filter((file) => !isTestFile(file))];
7826
+ return [
7827
+ ...repoFiles.filter((file) => isTestFile(file, rubricVersion)),
7828
+ ...contentCppTests.filter((file) => !isTestFile(file, rubricVersion))
7829
+ ];
7563
7830
  }
7564
7831
  function findConfiguredTestRunnerFiles(repoPath, repoFiles) {
7565
7832
  const configured = repoFiles.filter(
@@ -7686,8 +7953,439 @@ function isCoverageConfig(repoPath, file) {
7686
7953
  if (/(^|\/)pyproject\.toml$/.test(file)) {
7687
7954
  return fileContains(repoPath, file, /^\[tool\.coverage(?:\.|\])/m);
7688
7955
  }
7956
+ if (/(^|\/)package\.json$/.test(file)) {
7957
+ return packageJsonHasCoverageTooling(repoPath, file);
7958
+ }
7959
+ return false;
7960
+ }
7961
+ function packageJsonHasCoverageTooling(repoPath, file) {
7962
+ const fullPath = join(repoPath, file);
7963
+ if (!isRegularFile(fullPath)) return false;
7964
+ const parsed = parseJsonObject(readFileSync(fullPath, "utf8"));
7965
+ if (!parsed) return false;
7966
+ const scripts = parsed.scripts;
7967
+ if (typeof scripts === "object" && scripts !== null && !Array.isArray(scripts)) {
7968
+ for (const command of Object.values(scripts)) {
7969
+ if (typeof command === "string" && commandInvokesCoverageTool(command)) {
7970
+ return true;
7971
+ }
7972
+ }
7973
+ }
7974
+ for (const key of ["nyc", "c8", "istanbul"]) {
7975
+ const config = parsed[key];
7976
+ if (typeof config === "object" && config !== null && !Array.isArray(config)) return true;
7977
+ }
7978
+ return false;
7979
+ }
7980
+ function commandInvokesCoverageTool(command) {
7981
+ return normalizedCommandInvokesCoverageTool(normalizeCoverageCommandPrefixes(command), 0);
7982
+ }
7983
+ function normalizedCommandInvokesCoverageTool(normalized, depth) {
7984
+ const segments = normalized.splitShellOperators ? splitShellCommandSegments(normalized.segment) : [normalized.segment];
7985
+ return segments.some((rawSegment) => {
7986
+ const candidate = normalizeCoverageCommandPrefixes(rawSegment);
7987
+ if (/^(?:nyc|c8|istanbul)(?:\s|$)/iu.test(candidate.segment)) return true;
7988
+ return depth < 4 && candidate.splitShellOperators && candidate.segment !== rawSegment.trim() && normalizedCommandInvokesCoverageTool(candidate, depth + 1);
7989
+ });
7990
+ }
7991
+ function splitShellCommandSegments(command) {
7992
+ const segments = [];
7993
+ let start = 0;
7994
+ let quote = null;
7995
+ let escaped = false;
7996
+ for (let index = 0; index < command.length; index += 1) {
7997
+ const character = command[index];
7998
+ if (escaped) {
7999
+ escaped = false;
8000
+ continue;
8001
+ }
8002
+ if (character === "\\" && quote !== "'") {
8003
+ escaped = true;
8004
+ continue;
8005
+ }
8006
+ if (quote) {
8007
+ if (character === quote) quote = null;
8008
+ continue;
8009
+ }
8010
+ if (character === "'" || character === '"') {
8011
+ quote = character;
8012
+ continue;
8013
+ }
8014
+ if (character === "#" && (index === 0 || /[\s;|&()]/u.test(command[index - 1] ?? ""))) {
8015
+ segments.push(command.slice(start, index));
8016
+ const newline = command.indexOf("\n", index + 1);
8017
+ if (newline < 0) return segments;
8018
+ index = newline;
8019
+ start = newline + 1;
8020
+ continue;
8021
+ }
8022
+ const isBoundary = character === ";" || character === "|" || character === "&" || character === "\n";
8023
+ if (!isBoundary) continue;
8024
+ segments.push(command.slice(start, index));
8025
+ if ((character === "|" || character === "&") && command[index + 1] === character) index += 1;
8026
+ start = index + 1;
8027
+ }
8028
+ segments.push(command.slice(start));
8029
+ return segments;
8030
+ }
8031
+ function normalizeCoverageCommandPrefixes(rawSegment) {
8032
+ let segment = rawSegment.trim();
8033
+ let unwrapQuotedShellCommand = false;
8034
+ let splitShellOperators = true;
8035
+ for (let index = 0; index < 8; index += 1) {
8036
+ const before = segment;
8037
+ segment = stripOuterShellGroup(segment);
8038
+ segment = segment.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+)*/u, "");
8039
+ segment = segment.replace(
8040
+ /^(?:npx|npm\s+exec|pnpm\s+(?:exec|dlx)|yarn\s+(?:exec|dlx))(?:\s+--(?:no-install|yes))*(?:\s+--)?\s+/iu,
8041
+ ""
8042
+ );
8043
+ segment = segment.replace(/^(?:command(?:\s+-p)?(?:\s+--)?|time(?:\s+-p)?)\s+/iu, "");
8044
+ const explicitShellCommand = extractExplicitShellCommand(segment);
8045
+ if (explicitShellCommand !== null) {
8046
+ segment = explicitShellCommand;
8047
+ unwrapQuotedShellCommand = false;
8048
+ splitShellOperators = true;
8049
+ }
8050
+ const environmentPrefix = stripEnvironmentCommandPrefix(segment);
8051
+ segment = environmentPrefix.segment;
8052
+ unwrapQuotedShellCommand ||= environmentPrefix.unwrapQuotedShellCommand;
8053
+ if (environmentPrefix.splitShellOperators !== null) {
8054
+ splitShellOperators = environmentPrefix.splitShellOperators;
8055
+ }
8056
+ if (segment === before) break;
8057
+ }
8058
+ return {
8059
+ segment: unwrapQuotedShellCommand ? segment.replace(/^(["'])(.*)\1$/u, "$2").trim() : segment,
8060
+ splitShellOperators
8061
+ };
8062
+ }
8063
+ function extractExplicitShellCommand(segment) {
8064
+ const shellExecutable = /^(?:(?:\/(?:usr\/)?bin\/)?(?<shell>(?:ba|da|k|z)?sh))(?=\s|$)/iu.exec(
8065
+ segment
8066
+ );
8067
+ if (!shellExecutable) return null;
8068
+ const shell = shellExecutable.groups?.shell?.toLowerCase();
8069
+ if (!shell) return null;
8070
+ let offset = shellExecutable[0].length;
8071
+ let noExecute = false;
8072
+ let dumpStrings = false;
8073
+ for (let optionCount = 0; optionCount < 32; optionCount += 1) {
8074
+ const option = scanShellWord(segment, offset);
8075
+ if (!option || !option.word.startsWith("-") && !option.word.startsWith("+")) return null;
8076
+ offset = option.end;
8077
+ if (option.word === "--") return null;
8078
+ const zshLongOption = shell === "zsh" ? /^(?<sign>--|\+-)(?<name>[A-Za-z][A-Za-z_-]*)$/u.exec(option.word) : null;
8079
+ if (zshLongOption) {
8080
+ const optionName = zshLongOption.groups?.name?.replaceAll("-", "");
8081
+ if (!optionName) return null;
8082
+ const resolvedSetOption = resolveSupportedShellSetOption(shell, optionName);
8083
+ if (!resolvedSetOption && !isSupportedZshLongInvocationOption(optionName)) return null;
8084
+ if (resolvedSetOption?.name === "noexec") {
8085
+ noExecute = zshLongOption.groups?.sign === "--" !== resolvedSetOption.inverted;
8086
+ }
8087
+ continue;
8088
+ }
8089
+ if (option.word === "--rcfile" || option.word === "--init-file") {
8090
+ const operand = scanShellWord(segment, offset);
8091
+ if (shell !== "bash" || !operand) return null;
8092
+ offset = operand.end;
8093
+ continue;
8094
+ }
8095
+ if (shell === "bash" && /^--(?:debug|debugger|login|noediting|noprofile|norc|posix|protected|restricted|verbose)$/u.test(
8096
+ option.word
8097
+ )) {
8098
+ continue;
8099
+ }
8100
+ const cluster = parseSupportedShellOptionCluster(shell, option.word);
8101
+ if (!cluster) return null;
8102
+ if (cluster.changesNoExecute) noExecute = cluster.sign === "-";
8103
+ if (cluster.enablesDumpStrings) dumpStrings = true;
8104
+ if (cluster.operandKind) {
8105
+ const operand = scanShellWord(segment, offset);
8106
+ const resolvedSetOperand = operand && cluster.operandKind === "set" ? resolveSupportedShellSetOption(shell, operand.word) : null;
8107
+ const operandIsZshInvocationOption = shell === "zsh" && operand !== null && cluster.operandKind === "set" && isSupportedZshLongInvocationOption(operand.word);
8108
+ const operandIsSupported = cluster.operandKind === "set" ? resolvedSetOperand !== null || operandIsZshInvocationOption : shell === "bash" && operand && BASH_SHOPT_OPTIONS.has(operand.word);
8109
+ if (!operand || !operandIsSupported) return null;
8110
+ if (resolvedSetOperand?.name === "noexec") {
8111
+ noExecute = cluster.sign === "-" !== resolvedSetOperand.inverted;
8112
+ }
8113
+ offset = operand.end;
8114
+ }
8115
+ if (cluster.hasCommand) {
8116
+ return noExecute || dumpStrings ? null : readShellWord(segment, offset);
8117
+ }
8118
+ }
8119
+ return null;
8120
+ }
8121
+ var BASH_SET_OPTIONS = /* @__PURE__ */ new Set([
8122
+ "allexport",
8123
+ "braceexpand",
8124
+ "emacs",
8125
+ "errexit",
8126
+ "errtrace",
8127
+ "functrace",
8128
+ "hashall",
8129
+ "histexpand",
8130
+ "history",
8131
+ "ignoreeof",
8132
+ "keyword",
8133
+ "monitor",
8134
+ "noclobber",
8135
+ "noexec",
8136
+ "noglob",
8137
+ "nolog",
8138
+ "notify",
8139
+ "nounset",
8140
+ "onecmd",
8141
+ "physical",
8142
+ "pipefail",
8143
+ "posix",
8144
+ "privileged",
8145
+ "verbose",
8146
+ "vi",
8147
+ "xtrace"
8148
+ ]);
8149
+ var BASH_SHOPT_OPTIONS = /* @__PURE__ */ new Set([
8150
+ "autocd",
8151
+ "cdable_vars",
8152
+ "cdspell",
8153
+ "checkhash",
8154
+ "checkjobs",
8155
+ "checkwinsize",
8156
+ "cmdhist",
8157
+ "complete_fullquote",
8158
+ "direxpand",
8159
+ "dirspell",
8160
+ "dotglob",
8161
+ "execfail",
8162
+ "expand_aliases",
8163
+ "extdebug",
8164
+ "extglob",
8165
+ "extquote",
8166
+ "failglob",
8167
+ "force_fignore",
8168
+ "globasciiranges",
8169
+ "globskipdots",
8170
+ "globstar",
8171
+ "gnu_errfmt",
8172
+ "histappend",
8173
+ "histreedit",
8174
+ "histverify",
8175
+ "hostcomplete",
8176
+ "huponexit",
8177
+ "inherit_errexit",
8178
+ "interactive_comments",
8179
+ "lastpipe",
8180
+ "lithist",
8181
+ "localvar_inherit",
8182
+ "localvar_unset",
8183
+ "login_shell",
8184
+ "mailwarn",
8185
+ "nocaseglob",
8186
+ "nocasematch",
8187
+ "no_empty_cmd_completion",
8188
+ "nullglob",
8189
+ "progcomp",
8190
+ "progcomp_alias",
8191
+ "promptvars",
8192
+ "restricted_shell",
8193
+ "shift_verbose",
8194
+ "sourcepath",
8195
+ "varredir_close",
8196
+ "xpg_echo"
8197
+ ]);
8198
+ var POSIX_SET_OPTIONS = /* @__PURE__ */ new Set([
8199
+ "allexport",
8200
+ "errexit",
8201
+ "ignoreeof",
8202
+ "noclobber",
8203
+ "noexec",
8204
+ "noglob",
8205
+ "nounset",
8206
+ "verbose",
8207
+ "vi",
8208
+ "xtrace"
8209
+ ]);
8210
+ var ZSH_LONG_INVOCATION_OPTIONS = /* @__PURE__ */ new Set([
8211
+ "interactive",
8212
+ "login",
8213
+ "privileged",
8214
+ "shinstdin",
8215
+ "stdin"
8216
+ ]);
8217
+ function isSupportedShellSetOption(shell, option) {
8218
+ if (shell === "bash") return BASH_SET_OPTIONS.has(option);
8219
+ if (shell === "sh" || shell === "dash") return POSIX_SET_OPTIONS.has(option);
8220
+ if (shell === "ksh" || shell === "zsh") {
8221
+ return POSIX_SET_OPTIONS.has(option) || option === "pipefail";
8222
+ }
7689
8223
  return false;
7690
8224
  }
8225
+ function normalizeShellSetOption(shell, option) {
8226
+ if (shell === "zsh") return option.toLowerCase().replaceAll("_", "");
8227
+ return option;
8228
+ }
8229
+ function isSupportedZshLongInvocationOption(option) {
8230
+ const normalized = normalizeShellSetOption("zsh", option);
8231
+ if (ZSH_LONG_INVOCATION_OPTIONS.has(normalized)) return true;
8232
+ return normalized.startsWith("no") && !normalized.startsWith("nono") && ZSH_LONG_INVOCATION_OPTIONS.has(normalized.slice(2));
8233
+ }
8234
+ function resolveSupportedShellSetOption(shell, option) {
8235
+ const normalized = normalizeShellSetOption(shell, option);
8236
+ if (shell === "zsh" && normalized === "exec") {
8237
+ return { name: "noexec", inverted: true };
8238
+ }
8239
+ if (isSupportedShellSetOption(shell, normalized)) {
8240
+ return { name: normalized, inverted: false };
8241
+ }
8242
+ if (shell === "zsh" && normalized.startsWith("no") && !normalized.startsWith("nono") && isSupportedShellSetOption(shell, normalized.slice(2))) {
8243
+ return { name: normalized.slice(2), inverted: true };
8244
+ }
8245
+ return null;
8246
+ }
8247
+ function parseSupportedShellOptionCluster(shell, option) {
8248
+ if (!/^[+-][A-Za-z]+$/u.test(option)) return null;
8249
+ const sign = option[0];
8250
+ let letters = option.slice(1);
8251
+ let operandKind = null;
8252
+ const operandLetter = letters.at(-1);
8253
+ if (operandLetter === "o" || operandLetter === "O") {
8254
+ if (operandLetter === "O" && shell !== "bash") return null;
8255
+ letters = letters.slice(0, -1);
8256
+ if (letters.includes("c")) return null;
8257
+ operandKind = operandLetter === "o" ? "set" : "shopt";
8258
+ }
8259
+ const supportedLetters = shell === "bash" ? "abcefhkmnptuvxBCEHPTilrsD" : shell === "sh" ? "abCcefimnsuvx" : shell === "dash" ? "abCcefilmnsuvx" : "acefilmnpsuvx";
8260
+ if (![...letters].every((letter) => supportedLetters.includes(letter))) return null;
8261
+ return {
8262
+ sign,
8263
+ hasCommand: letters.includes("c"),
8264
+ changesNoExecute: letters.includes("n"),
8265
+ enablesDumpStrings: shell === "bash" && letters.includes("D"),
8266
+ operandKind
8267
+ };
8268
+ }
8269
+ function scanShellWord(value, start) {
8270
+ let result = "";
8271
+ let quote = null;
8272
+ let escaped = false;
8273
+ let index = start;
8274
+ while (index < value.length && /\s/u.test(value[index] ?? "")) index += 1;
8275
+ const wordStart = index;
8276
+ for (; index < value.length; index += 1) {
8277
+ const character = value[index];
8278
+ if (escaped) {
8279
+ if (character !== "\n") result += character;
8280
+ escaped = false;
8281
+ continue;
8282
+ }
8283
+ if (character === "\\" && quote !== "'") {
8284
+ const nextCharacter = value[index + 1];
8285
+ const escapesNext = quote !== '"' || nextCharacter === "$" || nextCharacter === "`" || nextCharacter === '"' || nextCharacter === "\\" || nextCharacter === "\n";
8286
+ if (escapesNext) {
8287
+ escaped = true;
8288
+ } else {
8289
+ result += character;
8290
+ }
8291
+ continue;
8292
+ }
8293
+ if (quote) {
8294
+ if (character === quote) {
8295
+ quote = null;
8296
+ } else {
8297
+ result += character;
8298
+ }
8299
+ continue;
8300
+ }
8301
+ if (character === "'" || character === '"') {
8302
+ quote = character;
8303
+ continue;
8304
+ }
8305
+ if (/\s/u.test(character ?? "")) break;
8306
+ result += character;
8307
+ }
8308
+ if (escaped || quote || index === wordStart) return null;
8309
+ return { word: result, end: index };
8310
+ }
8311
+ function readShellWord(value, start) {
8312
+ return scanShellWord(value, start)?.word ?? null;
8313
+ }
8314
+ function stripOuterShellGroup(segment) {
8315
+ if (!segment.startsWith("(") || segment.startsWith("((")) return segment;
8316
+ let depth = 0;
8317
+ let quote = null;
8318
+ let escaped = false;
8319
+ for (let index = 0; index < segment.length; index += 1) {
8320
+ const character = segment[index];
8321
+ if (escaped) {
8322
+ escaped = false;
8323
+ continue;
8324
+ }
8325
+ if (character === "\\" && quote !== "'") {
8326
+ escaped = true;
8327
+ continue;
8328
+ }
8329
+ if (quote) {
8330
+ if (character === quote) quote = null;
8331
+ continue;
8332
+ }
8333
+ if (character === "'" || character === '"') {
8334
+ quote = character;
8335
+ continue;
8336
+ }
8337
+ if (character === "(") {
8338
+ depth += 1;
8339
+ continue;
8340
+ }
8341
+ if (character !== ")") continue;
8342
+ depth -= 1;
8343
+ if (depth < 0) return segment;
8344
+ if (depth === 0) {
8345
+ if (segment.slice(index + 1).trim().length > 0) return segment;
8346
+ return segment.slice(1, index).trim();
8347
+ }
8348
+ }
8349
+ return segment;
8350
+ }
8351
+ function stripEnvironmentCommandPrefix(segment) {
8352
+ const wrapper = /^(cross-env(?:-shell)?|env)(?:\s|$)/iu.exec(segment);
8353
+ if (!wrapper) {
8354
+ return { segment, unwrapQuotedShellCommand: false, splitShellOperators: null };
8355
+ }
8356
+ let remainder = segment.slice(wrapper[0].length).trimStart();
8357
+ let unwrapQuotedShellCommand = wrapper[1]?.toLowerCase() === "cross-env-shell";
8358
+ let splitShellOperators = true;
8359
+ while (remainder.length > 0) {
8360
+ const splitString = /^(?:-S|--split-string)(?:=|\s+)(?:"([^"]*)"|'([^']*)'|([^\s]+))(?:\s+|$)/u.exec(remainder);
8361
+ if (splitString) {
8362
+ const splitCommand = splitString[1] ?? splitString[2] ?? splitString[3] ?? "";
8363
+ remainder = `${splitCommand} ${remainder.slice(splitString[0].length)}`.trim();
8364
+ unwrapQuotedShellCommand = false;
8365
+ splitShellOperators = false;
8366
+ continue;
8367
+ }
8368
+ const assignment = /^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)(?:\s+|$)/u.exec(
8369
+ remainder
8370
+ );
8371
+ if (assignment) {
8372
+ remainder = remainder.slice(assignment[0].length).trimStart();
8373
+ continue;
8374
+ }
8375
+ const optionWithValue = /^(?:-u|--unset|-C|--chdir)(?:=|\s+)(?:"[^"]*"|'[^']*'|[^\s]+)(?:\s+|$)/u.exec(remainder);
8376
+ if (optionWithValue) {
8377
+ remainder = remainder.slice(optionWithValue[0].length).trimStart();
8378
+ continue;
8379
+ }
8380
+ const flag = /^(?:--ignore-environment|-i|--)(?:\s+|$)/u.exec(remainder);
8381
+ if (flag) {
8382
+ remainder = remainder.slice(flag[0].length).trimStart();
8383
+ continue;
8384
+ }
8385
+ break;
8386
+ }
8387
+ return { segment: remainder, unwrapQuotedShellCommand, splitShellOperators };
8388
+ }
7691
8389
  function isCiWorkflow(file) {
7692
8390
  return /^\.github\/workflows\/.+\.ya?ml$/.test(file) || /^\.gitlab-ci\.ya?ml$/.test(file) || /^\.circleci\/config\.ya?ml$/.test(file) || /^azure-pipelines\.ya?ml$/.test(file) || /^Jenkinsfile$/.test(file);
7693
8391
  }
@@ -8985,7 +9683,15 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
8985
9683
  const overallScore = roundScore(
8986
9684
  categoryOrder.reduce((sum, category) => sum + (categoryScoreMap[category] ?? 0), 0) / Math.max(categoryOrder.length, 1)
8987
9685
  );
8988
- const abstained = parsedInput.insufficientSourceReason != null;
9686
+ const hasMeasuredCriterion = criteria.some(
9687
+ ({ status }) => status !== "not_applicable" && status !== "insufficient_data"
9688
+ );
9689
+ const isFreeCoreRubric = rubric.length === WITAN_RUBRIC.length && rubric.every(
9690
+ ({ id, category }, index) => id === WITAN_RUBRIC[index]?.id && category === WITAN_RUBRIC[index]?.category
9691
+ );
9692
+ const noMeasurementAbstention = (parsedInput.rubricVersion === WITAN_RUBRIC_VERSION_V16 || parsedInput.rubricVersion === WITAN_RUBRIC_VERSION_V17) && isFreeCoreRubric && !hasMeasuredCriterion;
9693
+ const insufficientSourceReason = parsedInput.insufficientSourceReason ?? (noMeasurementAbstention ? WITAN_NO_MEASUREMENT_REASON : void 0);
9694
+ const abstained = insufficientSourceReason != null;
8989
9695
  return WitanReportSchema.parse({
8990
9696
  productSlug: parsedInput.productSlug,
8991
9697
  productDisplayName: parsedInput.productDisplayName,
@@ -9005,7 +9711,7 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
9005
9711
  // alter it (see classifyRepoArchetype in repo-signals.ts); per-criterion scoring above is
9006
9712
  // unaffected by archetype, only presentation layers (badge/terminal/verdict) key off it.
9007
9713
  ...parsedInput.archetype ? { archetype: parsedInput.archetype } : {},
9008
- ...parsedInput.insufficientSourceReason ? { insufficientSourceReason: parsedInput.insufficientSourceReason } : {}
9714
+ ...insufficientSourceReason ? { insufficientSourceReason } : {}
9009
9715
  });
9010
9716
  }
9011
9717
  function applySignalAdjustment(nativeScore, dimension, signals) {
@@ -9051,7 +9757,7 @@ function statusForScore(score) {
9051
9757
  }
9052
9758
  function statusAfterInputAdjustment(nativeStatus, nativeScore, adjustment, rubricVersion) {
9053
9759
  if (adjustment === 0) return nativeStatus;
9054
- if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V92 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15) {
9760
+ if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V92 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && rubricVersion !== WITAN_RUBRIC_VERSION_V17) {
9055
9761
  return statusForScore(roundScore(Math.max(0, nativeScore - adjustment)));
9056
9762
  }
9057
9763
  const statusFloor = {
@@ -9095,7 +9801,7 @@ function mergeSignalsByCriterion(signals) {
9095
9801
  return merged;
9096
9802
  }
9097
9803
  function usesMetricScoring(rubricVersion) {
9098
- return rubricVersion === WITAN_RUBRIC_VERSION_V1 || rubricVersion === WITAN_RUBRIC_VERSION_V2 || rubricVersion === WITAN_RUBRIC_VERSION_V3 || rubricVersion === WITAN_RUBRIC_VERSION_V4 || rubricVersion === WITAN_RUBRIC_VERSION_V5 || rubricVersion === WITAN_RUBRIC_VERSION_V6 || rubricVersion === WITAN_RUBRIC_VERSION_V7 || rubricVersion === WITAN_RUBRIC_VERSION_V8 || rubricVersion === WITAN_RUBRIC_VERSION_V92 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0;
9804
+ return rubricVersion === WITAN_RUBRIC_VERSION_V1 || rubricVersion === WITAN_RUBRIC_VERSION_V2 || rubricVersion === WITAN_RUBRIC_VERSION_V3 || rubricVersion === WITAN_RUBRIC_VERSION_V4 || rubricVersion === WITAN_RUBRIC_VERSION_V5 || rubricVersion === WITAN_RUBRIC_VERSION_V6 || rubricVersion === WITAN_RUBRIC_VERSION_V7 || rubricVersion === WITAN_RUBRIC_VERSION_V8 || rubricVersion === WITAN_RUBRIC_VERSION_V92 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0;
9099
9805
  }
9100
9806
  function unmeasuredStatus(rubricVersion) {
9101
9807
  return rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0 ? "unverified" : "insufficient_data";
@@ -9136,7 +9842,12 @@ function scoreCriterion(signal, rubricVersion) {
9136
9842
  }
9137
9843
  if (usesMetricScoring(rubricVersion)) {
9138
9844
  const measuredMetrics = metrics.length > 0 ? metrics : fallbackMetricsForEvidence(evidence, findings);
9139
- const score = capScoreForFindings(scoreMetrics(measuredMetrics), findings);
9845
+ const score = capScoreForFindings(
9846
+ scoreMetrics(measuredMetrics),
9847
+ findings,
9848
+ rubricVersion,
9849
+ signal.criterionId
9850
+ );
9140
9851
  const status = statusForScoreAndFindings(
9141
9852
  signal.criterionId,
9142
9853
  score,
@@ -9218,16 +9929,20 @@ function scoreMetrics(metrics) {
9218
9929
  if (totalWeight === 0) return 0;
9219
9930
  return roundScore(weightedTotal / totalWeight * 4);
9220
9931
  }
9221
- function capScoreForFindings(score, findings) {
9222
- if (findings.some((finding) => finding.severity === "critical")) return 1.4;
9932
+ function capScoreForFindings(score, findings, rubricVersion, criterionId) {
9933
+ if (findings.some((finding) => finding.severity === "critical")) {
9934
+ const criticalFindings = findings.filter(({ severity }) => severity === "critical");
9935
+ const authenticatedA1Absence = rubricVersion === WITAN_RUBRIC_VERSION_V17 && criterionId === "A1" && criticalFindings.length === 1 && criticalFindings[0]?.summary === WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY;
9936
+ return authenticatedA1Absence ? Math.min(score, 1.4) : 1.4;
9937
+ }
9223
9938
  if (findings.some((finding) => finding.severity === "warning")) return Math.min(score, 2.4);
9224
9939
  if (findings.some((finding) => finding.severity === "info")) return Math.min(score, 3.4);
9225
9940
  return score;
9226
9941
  }
9227
9942
  function statusForScoreAndFindings(criterionId, score, findings, metrics, rubricVersion) {
9228
9943
  if (findings.some((finding) => finding.severity === "critical")) return "critical";
9229
- if (rubricVersion === WITAN_RUBRIC_VERSION_V8 || rubricVersion === WITAN_RUBRIC_VERSION_V92 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15) {
9230
- const calibrated = rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 ? calibratedCriterionStateV11(criterionId, metrics) : rubricVersion === WITAN_RUBRIC_VERSION_V92 || rubricVersion === WITAN_RUBRIC_VERSION_V10 ? calibratedCriterionStateV9(criterionId, metrics) : calibratedCriterionStateV8(criterionId, metrics);
9944
+ if (rubricVersion === WITAN_RUBRIC_VERSION_V8 || rubricVersion === WITAN_RUBRIC_VERSION_V92 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17) {
9945
+ const calibrated = rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17 ? calibratedCriterionStateV11(criterionId, metrics) : rubricVersion === WITAN_RUBRIC_VERSION_V92 || rubricVersion === WITAN_RUBRIC_VERSION_V10 ? calibratedCriterionStateV9(criterionId, metrics) : calibratedCriterionStateV8(criterionId, metrics);
9231
9946
  if (calibrated && calibrated !== "verified") return calibrated;
9232
9947
  if (calibrated === "verified" && !findings.some((finding) => finding.severity === "warning")) {
9233
9948
  return "verified";
@@ -9453,7 +10168,7 @@ function calibratedCriterionStateV11(criterionId, metrics) {
9453
10168
  return void 0;
9454
10169
  }
9455
10170
  function ensureFindingsExplainStatus(criterionId, score, status, findings, evidence, rubricVersion) {
9456
- if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V92 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && findings.length > 0) {
10171
+ if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V92 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && rubricVersion !== WITAN_RUBRIC_VERSION_V17 && findings.length > 0) {
9457
10172
  return [...findings];
9458
10173
  }
9459
10174
  if (status !== "critical" && status !== "warning") return [...findings];
@@ -9560,6 +10275,87 @@ function slugify(raw) {
9560
10275
  return FALLBACK_SLUG;
9561
10276
  }
9562
10277
 
10278
+ // src/finding-presentation.ts
10279
+ var METRIC_DERIVED_FINDING_PATTERN2 = /^[A-Z]\d+ metric-derived score is (\d+\.\d)\/4\.0, in the (critical|warning) band — no single finding drove this; it reflects the combined metric weighting below\.$/;
10280
+ var REMEDIATION_BY_METRIC2 = {
10281
+ test_to_source_ratio: "add concrete regression tests for the implementation surface",
10282
+ coverage_percent: "configure coverage and publish a measured threshold or report",
10283
+ verification_script_ratio: "add explicit test, lint, and typecheck verification commands",
10284
+ non_hollow_test_share: "replace skipped or placeholder tests with live assertions",
10285
+ env_handling_depth: "document and enforce safe environment-secret handling",
10286
+ secret_cleanliness: "remove committed secrets and rotate any exposed credentials",
10287
+ tenant_scope_ratio: "apply tenant scoping consistently across data access",
10288
+ rls_policy_count: "add database row-level security policies where the data model requires them",
10289
+ crypto_comparison_hygiene: "use constant-time comparison for signature or MAC checks",
10290
+ prod_readiness_primitives: "add the missing deployment-readiness controls",
10291
+ observability_depth: "add health checks, structured logging, and error boundaries",
10292
+ rollback_safety_depth: "document and test rollback or recovery procedures",
10293
+ pinned_dependency_ratio: "pin application dependencies that determine the deployed artifact",
10294
+ lockfile_coverage: "commit the package-manager lockfile used by CI and deployment",
10295
+ dependency_automation_ratio: "enable automated dependency updates and an audit command",
10296
+ declared_version_range_ratio: "declare an explicit compatible version for every dependency",
10297
+ dependency_count_sanity: "review and reduce unnecessary direct dependencies",
10298
+ claim_match_rate: "align documented product claims with observable repository evidence",
10299
+ claim_source_depth: "document concrete, checkable product claims",
10300
+ reconciliation_artifact_depth: "publish a claim-to-evidence reconciliation record",
10301
+ pr_merge_ratio: "preserve merged pull-request history in the scanned clone",
10302
+ pr_trace_primitives: "add pull-request templates and outcome-trace records",
10303
+ ci_script_depth: "run the repository verification commands in CI",
10304
+ default_branch_ci_depth: "run CI on the default branch and pull requests",
10305
+ prod_workflow_depth: "add a production or release verification workflow",
10306
+ audit_artifact_depth: "publish changelog, incident, security, or audit records",
10307
+ audit_freshness_depth: "keep the audit trail current",
10308
+ human_gate_documented: "document which privileged operations require human approval",
10309
+ protected_path_review_gate: "require review for changes to protected paths",
10310
+ fail_closed_privilege_check: "make privileged-operation checks fail closed",
10311
+ kill_switch_fail_safe_present: "add a fail-safe kill switch for privileged automation",
10312
+ privilege_escalation_cleanliness: "remove unsafe privilege-escalation patterns"
10313
+ };
10314
+ function renderFindingSummary2(criterion, finding) {
10315
+ const match = METRIC_DERIVED_FINDING_PATTERN2.exec(finding.summary);
10316
+ if (!match) return finding.summary;
10317
+ const score = match[1] ?? criterion.score.toFixed(1);
10318
+ const band = match[2] ?? criterion.status;
10319
+ const drivers = selectLowestDrivers2(criterion.metrics);
10320
+ if (drivers.length === 0) {
10321
+ return `${criterion.id} dimension band is ${band} at ${score}/4.0. To improve this dimension, add stronger measurable evidence for ${criterion.title.toLowerCase()}.`;
10322
+ }
10323
+ const measurements = drivers.map(formatMetricMember2).join("; ");
10324
+ const actions = Array.from(
10325
+ new Set(
10326
+ drivers.map(
10327
+ (metric2) => REMEDIATION_BY_METRIC2[metric2.name] ?? `raise the measured ${metric2.label.toLowerCase()}`
10328
+ )
10329
+ )
10330
+ ).join("; ");
10331
+ return `${criterion.id} dimension band is ${band} at ${score}/4.0. Lowest contributing measurements: ${measurements}. To improve: ${actions}.`;
10332
+ }
10333
+ function selectLowestDrivers2(metrics) {
10334
+ return [...metrics].filter((metric2) => metric2.max !== void 0).sort((left, right) => {
10335
+ const leftRatio = normalizedMetricValue2(left);
10336
+ const rightRatio = normalizedMetricValue2(right);
10337
+ if (leftRatio !== rightRatio) return leftRatio - rightRatio;
10338
+ if (left.weight !== right.weight) return right.weight - left.weight;
10339
+ return left.name.localeCompare(right.name);
10340
+ }).slice(0, 2);
10341
+ }
10342
+ function normalizedMetricValue2(metric2) {
10343
+ if (metric2.max === void 0) return 1;
10344
+ return Math.min(metric2.value / metric2.max, 1);
10345
+ }
10346
+ function formatMetricMember2(metric2) {
10347
+ const unit = metric2.unit ? ` ${metric2.unit}` : "";
10348
+ const maximum = metric2.max;
10349
+ if (maximum === void 0) return `${metric2.label} ${formatMetricValue3(metric2.value)}${unit}`;
10350
+ if (metric2.kind === "saturating_count" && metric2.value > maximum) {
10351
+ return `${metric2.label} ${formatMetricValue3(maximum)}${unit} capped (${formatMetricValue3(metric2.value)} raw)`;
10352
+ }
10353
+ return `${metric2.label} ${formatMetricValue3(metric2.value)}/${formatMetricValue3(maximum)}${unit}`;
10354
+ }
10355
+ function formatMetricValue3(value) {
10356
+ return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
10357
+ }
10358
+
9563
10359
  // src/summary.ts
9564
10360
  var SEVERITY_RANK2 = {
9565
10361
  critical: 0,
@@ -9569,11 +10365,16 @@ var SEVERITY_RANK2 = {
9569
10365
  var TOP_FINDINGS_LIMIT = 5;
9570
10366
  function buildWitanCliSummary(report) {
9571
10367
  const allFindings = report.criteria.flatMap(
9572
- (criterion) => criterion.findings.map((finding) => ({
9573
- criterionId: criterion.id,
9574
- severity: finding.severity,
9575
- summary: finding.summary
9576
- }))
10368
+ (criterion) => criterion.findings.map((finding) => {
10369
+ const displaySummary = renderFindingSummary2(criterion, finding);
10370
+ return {
10371
+ criterionId: criterion.id,
10372
+ severity: finding.severity,
10373
+ dimensionBand: criterion.status,
10374
+ summary: finding.summary,
10375
+ ...displaySummary !== finding.summary ? { displaySummary } : {}
10376
+ };
10377
+ })
9577
10378
  );
9578
10379
  const sorted = [...allFindings].sort(
9579
10380
  (a, b) => SEVERITY_RANK2[a.severity] - SEVERITY_RANK2[b.severity]
@@ -9600,7 +10401,7 @@ function buildWitanCliSummary(report) {
9600
10401
  overallScore: null,
9601
10402
  codeTrustScore: null,
9602
10403
  processTrustScore: null,
9603
- verdict: "Insufficient source",
10404
+ verdict: renderReportVerdict(report),
9604
10405
  insufficientSourceReason: report.insufficientSourceReason
9605
10406
  };
9606
10407
  }
@@ -9618,7 +10419,7 @@ function runCejelScan(options) {
9618
10419
  const identity = deriveProductIdentity(options.repoPath);
9619
10420
  const report = scoreRepoWithPublicCejel({
9620
10421
  productSlug: identity.productSlug,
9621
- productDisplayName: identity.productDisplayName,
10422
+ productDisplayName: options.productDisplayName ?? identity.productDisplayName,
9622
10423
  repoPath: options.repoPath,
9623
10424
  ingestPatterns: options.ingestPatterns,
9624
10425
  warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch