@cejel/cejel 0.7.0-rc.1 → 0.7.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import {
5
5
  renderWitanBadgeSvg,
6
6
  renderWitanHtmlReport,
7
7
  runCejelScan
8
- } from "./chunk-SNRQAKKE.js";
8
+ } from "./chunk-G6AN4EDL.js";
9
9
  import {
10
10
  ZodFirstPartyTypeKind,
11
11
  ZodOptional,
@@ -5,7 +5,7 @@ import {
5
5
  describeGitFailure,
6
6
  execGit,
7
7
  isExpectedGitAbsence
8
- } from "./chunk-D7MWEPPX.js";
8
+ } from "./chunk-2UMHJ23V.js";
9
9
  import {
10
10
  init_esm_shims
11
11
  } from "./chunk-AWLKQUI4.js";
package/dist/index.js CHANGED
@@ -3,15 +3,17 @@ import { createRequire as __cejelCreateRequire } from 'node:module';
3
3
  const require = __cejelCreateRequire(import.meta.url);
4
4
  import {
5
5
  serveCejelMcpOverStdio
6
- } from "./chunk-LBJFDK4R.js";
6
+ } from "./chunk-IJYGVKYU.js";
7
7
  import {
8
8
  CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE,
9
9
  CERTIFICATE_GLOSSARY,
10
10
  EXTERNAL_FINDINGS_DISPLAY_LIMIT,
11
+ METRIC_DERIVED_FINDING_PATTERN,
11
12
  PROSPECTIVE_RUBRIC_NOTICE,
12
13
  WITAN_RUBRIC_VERSION_V22,
13
14
  WitanReportSchema,
14
15
  assertSelectableRubricVersion,
16
+ buildPlainSummary,
15
17
  buildRelyingPartySummary,
16
18
  buildVerdictExplanation,
17
19
  computeApplicableMeasuredCoverage,
@@ -26,10 +28,13 @@ import {
26
28
  formatExternalSourceLine,
27
29
  formatLowConfidenceSentence,
28
30
  formatVerdictCapSentence,
31
+ glossaryEntryForRubric,
29
32
  importedAdjustmentSentence,
30
33
  inheritedFindingNote,
31
34
  inputBindingHeadline,
32
35
  isProspectiveRubricVersion,
36
+ isWitanAbstentionLabel,
37
+ renderWitanAbstentionHeadline,
33
38
  renderWitanBadgeEndpoint,
34
39
  renderWitanBadgeSvg,
35
40
  renderWitanHtmlReport,
@@ -38,9 +43,10 @@ import {
38
43
  serializeWitanReport,
39
44
  stripTerminalControls,
40
45
  verifyRecordedInputDigests,
41
- verifyWitanAttestationBinding
42
- } from "./chunk-SNRQAKKE.js";
43
- import "./chunk-D7MWEPPX.js";
46
+ verifyWitanAttestationBinding,
47
+ witanHistoryAbstentionInstruction
48
+ } from "./chunk-G6AN4EDL.js";
49
+ import "./chunk-2UMHJ23V.js";
44
50
  import {
45
51
  external_exports
46
52
  } from "./chunk-AMVP7L26.js";
@@ -62,7 +68,7 @@ init_esm_shims();
62
68
  init_esm_shims();
63
69
  var WITAN_ISSUANCE_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
64
70
  var WITAN_ISSUANCE_PREDICATE_TYPE = "https://cejel.dev/attestations/issuance/v1";
65
- var WITAN_ISSUANCE_FORMAT_VERSION = "1.0";
71
+ var WITAN_ISSUANCE_FORMAT_VERSION = "1.1";
66
72
  var WITAN_ISSUANCE_SIGNATURE_NAMESPACE = "cejel-issuance";
67
73
  var WITAN_ISSUANCE_PRINCIPAL = "issuance@barglabs.ai";
68
74
  var WITAN_ISSUER_NAME = "Barg Labs Inc.";
@@ -90,6 +96,9 @@ var WitanIssuanceStatementSchema = external_exports.object({
90
96
  toolVersion: external_exports.string().min(1).max(80),
91
97
  rubricVersion: external_exports.string().min(1).max(120),
92
98
  rubricBehaviourFingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
99
+ // The scan time the reproduction replayed: the attested scan's predicate.generatedAt
100
+ // (issuance format 1.1 and later). Optional on read, so a 1.0 issuance still verifies.
101
+ generatedAt: external_exports.string().datetime({ offset: true }).optional(),
93
102
  reportByteIdentical: external_exports.literal(true)
94
103
  }).strict(),
95
104
  issuedAt: external_exports.string().datetime({ offset: true }),
@@ -154,6 +163,7 @@ function createWitanIssuanceStatement(report, options) {
154
163
  toolVersion: options.toolVersion,
155
164
  rubricVersion: report.rubricVersion,
156
165
  ...report.rubricBehaviourFingerprint ? { rubricBehaviourFingerprint: report.rubricBehaviourFingerprint } : {},
166
+ ...options.reproductionGeneratedAt ? { generatedAt: options.reproductionGeneratedAt } : {},
157
167
  reportByteIdentical: true
158
168
  },
159
169
  issuedAt: options.issuedAt,
@@ -162,6 +172,40 @@ function createWitanIssuanceStatement(report, options) {
162
172
  }
163
173
  });
164
174
  }
175
+ var ISO_8601_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
176
+ function readReproductionGeneratedAt(attestation, now) {
177
+ const predicate = typeof attestation === "object" && attestation !== null ? attestation.predicate : void 0;
178
+ const value = typeof predicate === "object" && predicate !== null ? predicate.generatedAt : void 0;
179
+ if (value === void 0 || value === null) {
180
+ return {
181
+ ok: false,
182
+ reason: "the attestation's predicate.generatedAt is missing, so the reproduction has no scan time to replay"
183
+ };
184
+ }
185
+ const instant = typeof value === "string" ? Date.parse(value) : Number.NaN;
186
+ if (typeof value !== "string" || !ISO_8601_INSTANT.test(value) || !Number.isFinite(instant)) {
187
+ return {
188
+ ok: false,
189
+ reason: `the attestation's predicate.generatedAt is not a valid ISO 8601 time: ${JSON.stringify(value)}`
190
+ };
191
+ }
192
+ if (instant > now.getTime()) {
193
+ return {
194
+ ok: false,
195
+ reason: `the attestation's predicate.generatedAt (${value}) is later than this issuer's clock (${now.toISOString()})`
196
+ };
197
+ }
198
+ return { ok: true, generatedAt: value };
199
+ }
200
+ var NEW_YEAR_WINDOW_MS = 24 * 60 * 60 * 1e3;
201
+ function isWithinDayOfNewYearUtc(generatedAt) {
202
+ const instant = Date.parse(generatedAt);
203
+ if (!Number.isFinite(instant)) return false;
204
+ const year = new Date(instant).getUTCFullYear();
205
+ const sinceNewYear = instant - Date.UTC(year, 0, 1);
206
+ const untilNewYear = Date.UTC(year + 1, 0, 1) - instant;
207
+ return sinceNewYear <= NEW_YEAR_WINDOW_MS || untilNewYear <= NEW_YEAR_WINDOW_MS;
208
+ }
165
209
  function verifyWitanIssuanceBinding(statement, options) {
166
210
  const parsed = WitanIssuanceStatementSchema.safeParse(statement);
167
211
  if (!parsed.success) {
@@ -501,8 +545,15 @@ function normalizeRepoRelativePath(raw) {
501
545
  if (path.split("/").some((segment) => segment === ".." || segment === "")) return void 0;
502
546
  return path;
503
547
  }
504
- function fingerprint(rule, path, line, identity) {
505
- return createHash2("sha256").update(JSON.stringify([rule, path, line, identity])).digest("hex");
548
+ var METRIC_DERIVED_IDENTITY = "metric-derived";
549
+ var NUMBER_PLACEHOLDER = "<n>";
550
+ var NUMBER_PATTERN = /\d+(?:\.\d+)?/g;
551
+ function findingIdentity(summary) {
552
+ if (METRIC_DERIVED_FINDING_PATTERN.test(summary)) return METRIC_DERIVED_IDENTITY;
553
+ return summary.replace(NUMBER_PATTERN, NUMBER_PLACEHOLDER);
554
+ }
555
+ function fingerprint(rule, path, line, identity, ordinal) {
556
+ return createHash2("sha256").update(JSON.stringify([rule, path, line, identity, ordinal])).digest("hex");
506
557
  }
507
558
  function exportGitLabCodeQuality(report) {
508
559
  const ingestedCount = (report.consumedSignals ?? []).reduce(
@@ -520,6 +571,7 @@ function exportGitLabCodeQuality(report) {
520
571
  const entries = [];
521
572
  let excludedUnlocated = 0;
522
573
  const seen = /* @__PURE__ */ new Set();
574
+ const groupSizes = /* @__PURE__ */ new Map();
523
575
  for (const criterion of report.criteria) {
524
576
  for (const finding of criterion.findings) {
525
577
  const path = finding.evidence.path === void 0 ? void 0 : normalizeRepoRelativePath(finding.evidence.path);
@@ -528,9 +580,14 @@ function exportGitLabCodeQuality(report) {
528
580
  excludedUnlocated += 1;
529
581
  continue;
530
582
  }
531
- const print = fingerprint(criterion.id, path, line, finding.summary);
532
- if (seen.has(print)) continue;
533
- seen.add(print);
583
+ const exact = JSON.stringify([criterion.id, path, line, finding.summary]);
584
+ if (seen.has(exact)) continue;
585
+ seen.add(exact);
586
+ const identity = findingIdentity(finding.summary);
587
+ const group = JSON.stringify([criterion.id, path, line, identity]);
588
+ const ordinal = groupSizes.get(group) ?? 0;
589
+ groupSizes.set(group, ordinal + 1);
590
+ const print = fingerprint(criterion.id, path, line, identity, ordinal);
534
591
  entries.push({
535
592
  description: finding.summary,
536
593
  check_name: criterion.id,
@@ -551,6 +608,64 @@ function renderGitLabCodeQualityFooter(result) {
551
608
  `;
552
609
  }
553
610
 
611
+ // src/export/gitlab-metrics.ts
612
+ init_esm_shims();
613
+ var UNMEASURED_STATUSES = /* @__PURE__ */ new Set([
614
+ "insufficient_data",
615
+ "not_applicable"
616
+ ]);
617
+ function formatGitLabMetricScore(score) {
618
+ return score.toFixed(1);
619
+ }
620
+ function gitLabCriterionMetricName(id) {
621
+ return `cejel_criterion_${id.toLowerCase().replaceAll("-", "_")}_score`;
622
+ }
623
+ function isMeasured(criterion) {
624
+ return !UNMEASURED_STATUSES.has(criterion.status);
625
+ }
626
+ function exportGitLabMetrics(report) {
627
+ if (report.verdict === "insufficient_source") {
628
+ return { lines: [], leftOut: [], abstainedReason: report.insufficientSourceReason };
629
+ }
630
+ const lines = [];
631
+ const leftOut = [];
632
+ const add = (name, score, measured) => {
633
+ if (score === null || !measured) {
634
+ leftOut.push(name);
635
+ return;
636
+ }
637
+ lines.push(`${name} ${formatGitLabMetricScore(score)}`);
638
+ };
639
+ const criteria = report.criteria;
640
+ add("cejel_overall_score", report.overallScore, criteria.some(isMeasured));
641
+ const trust = [
642
+ ["cejel_process_trust_score", report.processTrustScore, "process_trust"],
643
+ ["cejel_code_trust_score", report.codeTrustScore, "code_trust"]
644
+ ];
645
+ for (const [name, score, category] of trust) {
646
+ const inCategory = criteria.filter((criterion) => criterion.category === category);
647
+ if (inCategory.length === 0) continue;
648
+ add(name, score, inCategory.some(isMeasured));
649
+ }
650
+ for (const criterion of criteria) {
651
+ add(gitLabCriterionMetricName(criterion.id), criterion.score, isMeasured(criterion));
652
+ }
653
+ return { lines, leftOut };
654
+ }
655
+ function renderGitLabMetricsFile(result) {
656
+ return result.lines.length === 0 ? "" : `${result.lines.join("\n")}
657
+ `;
658
+ }
659
+ function renderGitLabMetricsFooter(result) {
660
+ if (result.abstainedReason !== void 0) {
661
+ return `Cejel: abstained report (insufficient_source): wrote 0 metrics. ${result.abstainedReason}
662
+ `;
663
+ }
664
+ const named = result.leftOut.length === 0 ? "" : ` (${result.leftOut.join(", ")})`;
665
+ return `Cejel: wrote ${result.lines.length} metric(s); ${result.leftOut.length} value(s) left out because Cejel did not measure them${named}.
666
+ `;
667
+ }
668
+
554
669
  // src/npx-version-guard.ts
555
670
  init_esm_shims();
556
671
  import { delimiter, resolve } from "path";
@@ -653,9 +768,14 @@ function writeOutputFiles(dir, files, what) {
653
768
  // src/terminal.ts
654
769
  init_esm_shims();
655
770
  function renderMinScoreAbstentionFailure(summary, minScore) {
656
- if (summary.verdict !== "Insufficient source" && summary.verdict !== "Insufficient evidence") {
771
+ if (!isWitanAbstentionLabel(summary.verdict)) {
657
772
  throw new Error("A minimum-score abstention message requires an abstained Cejel summary.");
658
773
  }
774
+ const instruction = witanHistoryAbstentionInstruction(summary.verdict);
775
+ if (instruction !== void 0) {
776
+ return `Cejel: cannot evaluate the required minimum ${minScore.toFixed(1)}/4.0 because the scan is ${summary.verdict.toLowerCase()}, with no composite score and no verdict. ${instruction}
777
+ `;
778
+ }
659
779
  return `Cejel: cannot evaluate the required minimum ${minScore.toFixed(1)}/4.0 because this repository has ${summary.verdict.toLowerCase()}.
660
780
  `;
661
781
  }
@@ -674,14 +794,14 @@ function renderMinScoreCoverageFailure(coverage, minScore) {
674
794
  `;
675
795
  }
676
796
  function renderTerminalCertificate(summary, report) {
677
- const abstained = summary.verdict === "Insufficient source" || summary.verdict === "Insufficient evidence";
797
+ const abstained = isWitanAbstentionLabel(summary.verdict);
678
798
  if (!abstained && (summary.overallScore === null || summary.codeTrustScore === null || summary.processTrustScore === null)) {
679
799
  throw new Error("A scored Cejel summary must carry numeric headline scores.");
680
800
  }
681
801
  const lines = abstained ? [
682
802
  `Cejel Trust Certificate \u2014 ${stripTerminalControls(summary.productDisplayName)}`,
683
803
  "",
684
- `${summary.verdict} to certify.`,
804
+ renderWitanAbstentionHeadline(summary.verdict),
685
805
  ` ${summary.insufficientSourceReason === void 0 ? "No ratable source or measurable evidence was found." : stripTerminalControls(summary.insufficientSourceReason)}`,
686
806
  ""
687
807
  ] : [
@@ -709,6 +829,7 @@ function renderTerminalCertificate(summary, report) {
709
829
  ""
710
830
  );
711
831
  }
832
+ if (report) lines.push(...renderPlainSummaryLines(buildPlainSummary(report), summary, abstained));
712
833
  if (summary.scanLimitations.length > 0) {
713
834
  lines.push(
714
835
  "LIMITED EVIDENCE \u2014 the score and verdict are qualified by scan limitations:",
@@ -718,7 +839,7 @@ function renderTerminalCertificate(summary, report) {
718
839
  }
719
840
  if (summary.historyLimitations?.length) {
720
841
  lines.push(
721
- "HISTORY LIMITATION \u2014 scored as usual, but some criteria read truncated history:",
842
+ abstained ? "HISTORY LIMITATION \u2014 some criteria read truncated history:" : "HISTORY LIMITATION \u2014 scored as usual, but some criteria read truncated history:",
722
843
  ...summary.historyLimitations.map((limitation) => ` - ${stripTerminalControls(limitation)}`),
723
844
  ""
724
845
  );
@@ -814,7 +935,7 @@ function renderTerminalCertificate(summary, report) {
814
935
  }
815
936
  }
816
937
  lines.push("", "Plain-language glossary:");
817
- for (const entry of CERTIFICATE_GLOSSARY) {
938
+ for (const entry of CERTIFICATE_GLOSSARY.map((curated) => glossaryEntryForRubric(curated, report?.rubricVersion))) {
818
939
  lines.push(` ${entry.term} \u2014 ${entry.definition}`);
819
940
  }
820
941
  if (report && isProspectiveRubricVersion(report.rubricVersion)) {
@@ -823,6 +944,43 @@ function renderTerminalCertificate(summary, report) {
823
944
  return `${lines.join("\n")}
824
945
  `;
825
946
  }
947
+ var PLAIN_TERMINAL_ITEM_LIMIT = 3;
948
+ function renderPlainSummaryLines(plain, summary, abstained) {
949
+ const section = (heading, leads, items, empty) => {
950
+ const shown = items.slice(0, PLAIN_TERMINAL_ITEM_LIMIT);
951
+ const remaining = items.length - shown.length;
952
+ return [
953
+ `${heading} (${items.length}):`,
954
+ ...leads.filter((lead) => lead !== void 0).map((lead) => ` ${lead}`),
955
+ ...items.length === 0 ? [` ${empty}`] : shown.map((item) => ` - ${stripTerminalControls(item)}`),
956
+ ...remaining > 0 ? [` ...and ${remaining} more in certificate.html.`] : []
957
+ ];
958
+ };
959
+ const itemLine = (item) => `${item.area} \u2014 ${item.text}${item.location !== void 0 ? ` (${item.location})` : ""}`;
960
+ const wrongFindings = plain.wrong.filter((item) => item.source.kind === "finding").length;
961
+ const otherFindings = summary.findingCount - wrongFindings;
962
+ const topFindingsNote = !abstained && otherFindings > 0 ? `Top findings below counts ${summary.findingCount} finding${summary.findingCount === 1 ? "" : "s"} in all, including ${otherFindings} informational finding${otherFindings === 1 ? "" : "s"} not listed here.` : void 0;
963
+ return [
964
+ "In plain words (the full lists, with links to the evidence, are in certificate.html):",
965
+ ...plain.prospectiveNotice !== void 0 ? [` ${plain.prospectiveNotice}`] : [],
966
+ ...section("What holds", [plain.areasLine, plain.holdsLead], plain.holds.map(itemLine), plain.holdsNote ?? ""),
967
+ ...section(
968
+ "What is wrong",
969
+ [plain.wrongNote, topFindingsNote],
970
+ plain.wrong.map((item) => `${item.level === "critical" ? "Critical" : "Warning"}: ${itemLine(item)}`),
971
+ "Cejel recorded no critical or warning-level problem in what it checked."
972
+ ),
973
+ ...section("What is not known", [], plain.notKnown.map(itemLine), "Cejel listed no gap here; anything it did not measure is still not established."),
974
+ ...section(
975
+ "What to check next",
976
+ [],
977
+ plain.checkNext.map((check) => `${check.area} \u2014 ${check.text}`),
978
+ "Nothing to check from these lists."
979
+ ),
980
+ plain.notCovered,
981
+ ""
982
+ ];
983
+ }
826
984
  function formatScore(score) {
827
985
  if (score === null) throw new Error("A scored Cejel summary must carry numeric scores.");
828
986
  return score.toFixed(1);
@@ -970,6 +1128,7 @@ Usage:
970
1128
  npx ${NPX_PACKAGE_NAME} scan [path] [options]
971
1129
  npx ${NPX_PACKAGE_NAME} verify <report.json> <attestation.json> [issuance.json issuance.json.sig]
972
1130
  npx ${NPX_PACKAGE_NAME} export gitlab-codequality <report.json> [-o gl-code-quality-report.json]
1131
+ npx ${NPX_PACKAGE_NAME} export gitlab-metrics <report.json> [-o metrics.txt]
973
1132
  npx ${NPX_PACKAGE_NAME} mcp
974
1133
  npx ${NPX_PACKAGE_NAME} issue [path] --report <report.json> --attestation <attestation.json> --key <key.pub> --engagement-ref <ref>
975
1134
 
@@ -977,7 +1136,8 @@ Commands:
977
1136
  scan score a repository (default when the command is omitted)
978
1137
  verify verify the report/attestation binding; with an issuance pair, also check the issuer
979
1138
  signature against docs/security/${ISSUER_SIGNERS_FILE} and docs/security/${ISSUER_REVOCATIONS_FILE}
980
- export write report.json's located findings as a GitLab Code Quality report (offline)
1139
+ export write report.json's located findings as a GitLab Code Quality report, or its scores
1140
+ as a GitLab metrics report (offline)
981
1141
  mcp serve the MCP server over stdio (the same server as the cejel-mcp bin)
982
1142
  issue re-run this version at the certificate's revision and, only if report.json reproduces
983
1143
  byte for byte, write an unsigned issuance.json for the issuer to sign through ssh-agent
@@ -1097,7 +1257,13 @@ async function runWitanCli(args, rubricVersion) {
1097
1257
  },
1098
1258
  { name: "badge.json", contents: JSON.stringify(renderWitanBadgeEndpoint(report), null, 2) },
1099
1259
  { name: "badge.svg", contents: renderWitanBadgeSvg(report) },
1100
- { name: "summary.json", contents: JSON.stringify(summary, null, 2) }
1260
+ // plainSummary (goal_cejel_four_question_summary_2026-11-02) is added here, at the one place
1261
+ // summary.json is written, so buildWitanCliSummary's shape (the MCP and HTTP responses) and
1262
+ // every key before it are unchanged.
1263
+ {
1264
+ name: "summary.json",
1265
+ contents: JSON.stringify({ ...summary, plainSummary: buildPlainSummary(report) }, null, 2)
1266
+ }
1101
1267
  ],
1102
1268
  "scan output"
1103
1269
  );
@@ -1186,12 +1352,22 @@ function parseCliInvocation(args) {
1186
1352
  }
1187
1353
  return { command: "scan", options: parseArgs(args) };
1188
1354
  }
1189
- var EXPORT_USAGE = () => `Usage: npx ${NPX_PACKAGE_NAME} export gitlab-codequality <report.json> [-o gl-code-quality-report.json]`;
1355
+ var EXPORT_DEFAULT_OUT = {
1356
+ "gitlab-codequality": "gl-code-quality-report.json",
1357
+ "gitlab-metrics": "metrics.txt"
1358
+ };
1359
+ function isExportFormat(value) {
1360
+ return value !== void 0 && Object.hasOwn(EXPORT_DEFAULT_OUT, value);
1361
+ }
1362
+ var EXPORT_USAGE = () => [
1363
+ `Usage: npx ${NPX_PACKAGE_NAME} export gitlab-codequality <report.json> [-o gl-code-quality-report.json]`,
1364
+ ` npx ${NPX_PACKAGE_NAME} export gitlab-metrics <report.json> [-o metrics.txt]`
1365
+ ].join("\n");
1190
1366
  function parseExportArgs(args) {
1191
1367
  const [format, ...rest] = args;
1192
- if (format !== "gitlab-codequality") throw new Error(EXPORT_USAGE());
1368
+ if (!isExportFormat(format)) throw new Error(EXPORT_USAGE());
1193
1369
  const positionals = [];
1194
- let outPath = "gl-code-quality-report.json";
1370
+ let outPath = EXPORT_DEFAULT_OUT[format];
1195
1371
  for (let index = 0; index < rest.length; index += 1) {
1196
1372
  const arg = rest[index];
1197
1373
  if (arg === void 0) continue;
@@ -1228,6 +1404,12 @@ ${members.map((member) => ` - ${member}`).join("\n")}
1228
1404
  );
1229
1405
  return 1;
1230
1406
  }
1407
+ if (invocation.format === "gitlab-metrics") {
1408
+ const metrics = exportGitLabMetrics(reportResult.data);
1409
+ writeOutputFile(invocation.outPath, renderGitLabMetricsFile(metrics), "export output");
1410
+ process.stderr.write(renderGitLabMetricsFooter(metrics));
1411
+ return 0;
1412
+ }
1231
1413
  const result = exportGitLabCodeQuality(reportResult.data);
1232
1414
  writeOutputFile(
1233
1415
  invocation.outPath,
@@ -1543,6 +1725,12 @@ ${attestationBinding.errors.map((error) => ` - ${error}`).join("\n")}
1543
1725
  );
1544
1726
  return 1;
1545
1727
  }
1728
+ const scanTime = readReproductionGeneratedAt(attestationArtifact.value, /* @__PURE__ */ new Date());
1729
+ if (!scanTime.ok) {
1730
+ process.stderr.write(`Cejel: refusing to issue \u2014 ${scanTime.reason}.
1731
+ `);
1732
+ return 1;
1733
+ }
1546
1734
  let keyContents;
1547
1735
  try {
1548
1736
  keyContents = readFileSync(options.keyPath, "utf8");
@@ -1565,7 +1753,8 @@ ${attestationBinding.errors.map((error) => ` - ${error}`).join("\n")}
1565
1753
  // comparison below, as it always has.
1566
1754
  ...suppliedReport.inputBinding?.policy === "strict" ? { strictIngest: true } : {},
1567
1755
  ...suppliedReport.inputBinding?.scannedSubject.repository ? { subjectRepository: suppliedReport.inputBinding.scannedSubject.repository } : {},
1568
- toolVersion: cliVersion()
1756
+ toolVersion: cliVersion(),
1757
+ generatedAt: scanTime.generatedAt
1569
1758
  });
1570
1759
  const reproducedBytes = serializeWitanReport(reproduction.report);
1571
1760
  const reproducedSha256 = createHash3("sha256").update(reproducedBytes, "utf8").digest("hex");
@@ -1579,6 +1768,12 @@ ${attestationBinding.errors.map((error) => ` - ${error}`).join("\n")}
1579
1768
  " Check that the checkout is at the certificate revision, that the @cejel/cejel version matches",
1580
1769
  " report.json's toolVersion, and that --product-name/--name/--rubric-pin/--ingest match the",
1581
1770
  " original invocation. An issuance that cannot reproduce is not issued.",
1771
+ // The reproduction replays the scan's instant, but v17's B4 reads that instant's year in
1772
+ // the local time zone. Near a new year two hosts can disagree on it; name the setting,
1773
+ // never guess a zone.
1774
+ ...isWithinDayOfNewYearUtc(scanTime.generatedAt) ? [
1775
+ ` The scan's generatedAt (${scanTime.generatedAt}) is within 24 hours of a new year in UTC, and v17's B4 freshness year is read in the scan host's local time: an issuer in another time zone may need to re-run with the TZ environment variable set to the scan host's.`
1776
+ ] : [],
1582
1777
  ""
1583
1778
  ].join("\n")
1584
1779
  );
@@ -1590,6 +1785,7 @@ ${attestationBinding.errors.map((error) => ` - ${error}`).join("\n")}
1590
1785
  issuerKeyFingerprint: issuerKey.fingerprint,
1591
1786
  toolVersion: cliVersion(),
1592
1787
  issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
1788
+ reproductionGeneratedAt: scanTime.generatedAt,
1593
1789
  engagementRef: options.engagementRef
1594
1790
  });
1595
1791
  const issuancePath = join2(options.outDir, ISSUANCE_OUTPUT_FILE);
package/dist/mcp/index.js CHANGED
@@ -3,9 +3,9 @@ import { createRequire as __cejelCreateRequire } from 'node:module';
3
3
  const require = __cejelCreateRequire(import.meta.url);
4
4
  import {
5
5
  serveCejelMcpOverStdio
6
- } from "../chunk-LBJFDK4R.js";
7
- import "../chunk-SNRQAKKE.js";
8
- import "../chunk-D7MWEPPX.js";
6
+ } from "../chunk-IJYGVKYU.js";
7
+ import "../chunk-G6AN4EDL.js";
8
+ import "../chunk-2UMHJ23V.js";
9
9
  import "../chunk-AMVP7L26.js";
10
10
  import {
11
11
  init_esm_shims
@@ -3,10 +3,10 @@ import { createRequire as __cejelCreateRequire } from 'node:module';
3
3
  const require = __cejelCreateRequire(import.meta.url);
4
4
  import {
5
5
  listCejelLlmPackFiles
6
- } from "../../chunk-UMGZWGOI.js";
6
+ } from "../../chunk-RRPQHEBU.js";
7
7
  import {
8
8
  execGit
9
- } from "../../chunk-D7MWEPPX.js";
9
+ } from "../../chunk-2UMHJ23V.js";
10
10
  import {
11
11
  require_typescript
12
12
  } from "../../chunk-6LVNNJTG.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cejel/cejel",
3
- "version": "0.7.0-rc.1",
3
+ "version": "0.7.0-rc.2",
4
4
  "mcpName": "io.github.BargLabs/cejel",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -31,7 +31,7 @@
31
31
  "type": "git",
32
32
  "url": "git+https://github.com/BargLabs/cejel.git"
33
33
  },
34
- "packageManager": "pnpm@9.0.0",
34
+ "packageManager": "pnpm@9.0.0+sha512.b4106707c7225b1748b61595953ccbebff97b54ad05d002aa3635f633b9c53cd666f7ce9b8bc44704f1fa048b9a49b55371ab2d9e9d667d1efe2ef1514bcd513",
35
35
  "type": "module",
36
36
  "bin": {
37
37
  "cejel": "dist/index.js",