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

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
  deriveProductIdentity,
6
6
  runCejelScan,
7
7
  serializeWitanReport
8
- } from "../chunk-G6AN4EDL.js";
8
+ } from "../chunk-73XAL26O.js";
9
9
  import {
10
10
  listCejelLlmPackFiles
11
11
  } from "../chunk-RRPQHEBU.js";
@@ -5274,6 +5274,8 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAss
5274
5274
  const rootPublishingDetails = /* @__PURE__ */ new Map();
5275
5275
  const checkedPublishingDetails = /* @__PURE__ */ new Map();
5276
5276
  const rootCheckedPublishingDetails = /* @__PURE__ */ new Map();
5277
+ const uncreditedLines = /* @__PURE__ */ new Map();
5278
+ const rootUncreditedLines = /* @__PURE__ */ new Map();
5277
5279
  const uncreditedWorkflows = useV27FalseAssuranceClosure ? withContentReadSignals("B3", ["ci_script_depth", "default_branch_ci_depth"], () => {
5278
5280
  const inventory = ownPublishOnlyInventory(repoFiles);
5279
5281
  return publishingWorkflowReasons(
@@ -5284,7 +5286,8 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAss
5284
5286
  checkedPublishingDetails,
5285
5287
  // Eighth pass (#659): v17's test-file list, the one A1 counts, read only when a runner is
5286
5288
  // given a positional word (testPathReader reads it once).
5287
- checkReading(repoPath, inventory, () => findConcreteTestFiles(repoPath, repoFiles, WITAN_RUBRIC_VERSION_V17))
5289
+ checkReading(repoPath, inventory, () => findConcreteTestFiles(repoPath, repoFiles, WITAN_RUBRIC_VERSION_V17)),
5290
+ uncreditedLines
5288
5291
  );
5289
5292
  }) : /* @__PURE__ */ new Map();
5290
5293
  const uncreditedRootWorkflows = useV27FalseAssuranceClosure && mono ? withContentReadSignals(
@@ -5299,7 +5302,8 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAss
5299
5302
  // The monorepo root's package.json and package-manager configuration are outside this
5300
5303
  // scan's inventory, so none is read: the npm-family test script is not a check there
5301
5304
  // (item E), and no npm, pnpm, yarn or bun command is a check or a setup step (third pass).
5302
- UNREAD_CHECK_READING
5305
+ UNREAD_CHECK_READING,
5306
+ rootUncreditedLines
5303
5307
  )
5304
5308
  ) : /* @__PURE__ */ new Map();
5305
5309
  const countsAsCi = (uncredited) => (file) => !uncredited.has(file);
@@ -5317,7 +5321,7 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAss
5317
5321
  ...checkedPublishingDetails,
5318
5322
  ...[...rootCheckedPublishingDetails].map(([file, detail]) => [`${file} at the monorepo root`, detail])
5319
5323
  ]);
5320
- const b3Note = b3NoteWithCheckedPublishing(
5324
+ const b3NoteBeforeUnreadCi = b3NoteWithCheckedPublishing(
5321
5325
  b3Uncredited.size > 0 ? (budget) => b3UncreditedWorkflowsNote(b3Uncredited, b3PublishingDetails, budget) : void 0,
5322
5326
  b3CheckedPublishing
5323
5327
  );
@@ -5342,6 +5346,17 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAss
5342
5346
  (file) => `${file} at the monorepo root`
5343
5347
  ) : { printOnly: [] }
5344
5348
  );
5349
+ const unreadCiConfigurations = useV27FalseAssuranceClosure && defaultBranchCiCount === 0 ? repoFiles.filter(isUnreadCiConfiguration).sort() : [];
5350
+ const publishingRuleEmptiedCount = unreadCiConfigurations.length > 0 && withContentReadSignal(
5351
+ "B3",
5352
+ "default_branch_ci_depth",
5353
+ () => [...uncreditedWorkflows].some(
5354
+ ([file, reason]) => reason === "publishes" && workflowTargetsDefaultBranch(repoPath, file)
5355
+ ) || (mono ? [...uncreditedRootWorkflows].some(
5356
+ ([file, reason]) => reason === "publishes" && workflowTargetsDefaultBranch(mono.root, file)
5357
+ ) : false)
5358
+ );
5359
+ const b3Note = publishingRuleEmptiedCount ? b3NoteWithUnreadCiConfigurations(b3NoteBeforeUnreadCi, unreadCiConfigurations) : b3NoteBeforeUnreadCi;
5345
5360
  if (packageJson && scripts.has("test")) {
5346
5361
  evidence.push(evidenceForRelative(repoPath, packageJson, "test_run", "Test script"));
5347
5362
  }
@@ -5400,6 +5415,22 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAss
5400
5415
  )
5401
5416
  );
5402
5417
  }
5418
+ if (useV27FalseAssuranceClosure) {
5419
+ withContentReadSignals("B3", ["ci_script_depth", "default_branch_ci_depth"], () => {
5420
+ for (const [file, reason] of uncreditedWorkflows) {
5421
+ evidence.push(
5422
+ evidenceForRelativeAtLine(repoPath, file, "ci_run", b3UncreditedWorkflowLabel(reason, false), uncreditedLines.get(file) ?? null)
5423
+ );
5424
+ }
5425
+ if (mono) {
5426
+ for (const [file, reason] of uncreditedRootWorkflows) {
5427
+ evidence.push(
5428
+ evidenceForRelativeAtLine(mono.root, file, "ci_run", b3UncreditedWorkflowLabel(reason, true), rootUncreditedLines.get(file) ?? null)
5429
+ );
5430
+ }
5431
+ }
5432
+ });
5433
+ }
5403
5434
  if (evidence.length === 0) return null;
5404
5435
  const CI_BUILD_COMMAND_PATTERN = /\b(cargo\s+build|cmake|ninja|make\s+(build|all))\b/i;
5405
5436
  const CI_COMMAND_CATEGORIES = [
@@ -5484,14 +5515,21 @@ function b3NoteWithCheckedPublishing(uncreditedNote, checked) {
5484
5515
  const entries = [...checked];
5485
5516
  const lintClause = (lint) => lint ? LINT_CONFIGURATION_CLAUSE : "";
5486
5517
  const lintFrom = (index) => entries.slice(index).some(([, { lint }]) => lint);
5487
- const countSentence = (rest, more, lint) => `${rest}${more ? " more" : ""} publishing workflow${rest === 1 ? " is" : "s are"} counted as CI verification because a check runs before ${rest === 1 ? "it publishes" : "they publish"}${lint ? rest === 1 ? LINT_CONFIGURATION_CLAUSE : LINT_CONFIGURATION_SOME_CLAUSE : ""}.`;
5488
- let note = uncreditedNote === void 0 ? "" : uncreditedNote(entries.length === 0 ? void 0 : CRITERION_NOTES_MAX_LENGTH - 1 - countSentence(entries.length, true, lintFrom(0)).length);
5518
+ const gatedFrom = (index) => entries.slice(index).filter(([, { gated }]) => gated === true).length;
5519
+ const countSentence = (rest, more, lint, gated = 0) => {
5520
+ const workflows = `${rest}${more ? " more" : ""} publishing workflow${rest === 1 ? " is" : "s are"} counted as CI verification because`;
5521
+ const lintClause2 = lint ? rest - gated === 1 ? LINT_CONFIGURATION_CLAUSE : LINT_CONFIGURATION_SOME_CLAUSE : "";
5522
+ if (gated === 0) return `${workflows} a check runs before ${rest === 1 ? "it publishes" : "they publish"}${lintClause2}.`;
5523
+ if (gated === rest) return `${workflows} ${rest === 1 ? GATED_PUBLISHING_REASON : GATED_PUBLISHING_PLURAL_REASON}.`;
5524
+ return `${workflows}, in each, a check runs before it publishes${lintClause2}, or ${GATED_PUBLISHING_REASON}.`;
5525
+ };
5526
+ let note = uncreditedNote === void 0 ? "" : uncreditedNote(entries.length === 0 ? void 0 : CRITERION_NOTES_MAX_LENGTH - 1 - countSentence(entries.length, true, lintFrom(0), gatedFrom(0)).length);
5489
5527
  const joined = (base, sentence) => base === "" ? sentence : `${base} ${sentence}`;
5490
5528
  const fits = (text) => text.length <= CRITERION_NOTES_MAX_LENGTH;
5491
- for (const [index, [name, { detail, lint }]] of entries.entries()) {
5529
+ for (const [index, [name, { detail, lint, gated }]] of entries.entries()) {
5492
5530
  const rest = entries.length - index - 1;
5493
- const leavesRoom = (text) => fits(text) && (rest === 0 || fits(joined(text, countSentence(rest, true, lintFrom(index + 1)))));
5494
- const counted = `${name} is counted as CI verification because a check runs before it publishes${lintClause(lint)}`;
5531
+ const leavesRoom = (text) => fits(text) && (rest === 0 || fits(joined(text, countSentence(rest, true, lintFrom(index + 1), gatedFrom(index + 1)))));
5532
+ const counted = gated === true ? `${name} is counted as CI verification because ${GATED_PUBLISHING_REASON}` : `${name} is counted as CI verification because a check runs before it publishes${lintClause(lint)}`;
5495
5533
  const full = joined(note, `${counted}; it ${detail}`);
5496
5534
  if (leavesRoom(full)) {
5497
5535
  note = full;
@@ -5502,11 +5540,34 @@ function b3NoteWithCheckedPublishing(uncreditedNote, checked) {
5502
5540
  note = short;
5503
5541
  continue;
5504
5542
  }
5505
- note = joined(note, countSentence(rest + 1, index > 0, lintFrom(index)));
5543
+ note = joined(note, countSentence(rest + 1, index > 0, lintFrom(index), gatedFrom(index)));
5506
5544
  break;
5507
5545
  }
5508
5546
  return note === "" ? void 0 : note;
5509
5547
  }
5548
+ function isUnreadCiConfiguration(file) {
5549
+ return /^(?:\.travis\.yml|\.drone\.yml|bitbucket-pipelines\.yml|appveyor\.yml|\.buildkite\/.+)$/u.test(file);
5550
+ }
5551
+ var UNREAD_CI_CONFIGURATION_LEAD = "With the publishing workflows not counted, no workflow B3 counts targets pull requests or the default branch;";
5552
+ function b3NoteWithUnreadCiConfigurations(note, files) {
5553
+ const plural2 = files.length !== 1;
5554
+ const sentence = (shown) => {
5555
+ const names = shown === 0 ? `${files.length} file${plural2 ? "s" : ""}` : shown === files.length ? shown === 1 ? files[0] : `${files.slice(0, shown - 1).join(", ")} and ${files[shown - 1]}` : `${files.slice(0, shown).join(", ")} and ${files.length - shown} more`;
5556
+ return `${UNREAD_CI_CONFIGURATION_LEAD} ${names} ${plural2 ? "are each" : "is"} a CI configuration Cejel does not read, matched by path alone with ${plural2 ? "their" : "its"} contents not read, so whether ${plural2 ? "they check" : "it checks"} pull requests or the default branch is not known.`;
5557
+ };
5558
+ const joined = (text) => note === void 0 || note === "" ? text : `${note} ${text}`;
5559
+ for (let shown = Math.min(files.length, CRITERION_NOTES_MAX_LENGTH); shown >= 0; shown -= 1) {
5560
+ const candidate = joined(sentence(shown));
5561
+ if (candidate.length <= CRITERION_NOTES_MAX_LENGTH) return candidate;
5562
+ }
5563
+ return note;
5564
+ }
5565
+ function b3UncreditedWorkflowLabel(reason, monorepoRoot) {
5566
+ const subject = monorepoRoot ? "Monorepo-root CI workflow not counted" : "CI workflow not counted";
5567
+ return reason === "publishes" ? `${subject}: publishes, releases or deploys with no pull-request trigger (cited at the publishing step, or the job calling the workflow that holds it)` : `${subject}: not shown to run a check, it ${UNCREDITED_CI_WORKFLOW_REASON_TEXT[reason]} (cited at its on: trigger)`;
5568
+ }
5569
+ var GATED_PUBLISHING_REASON = "every job that publishes, releases or deploys on every run declares an environment, a deployment gate whose protection rules Cejel cannot read offline";
5570
+ var GATED_PUBLISHING_PLURAL_REASON = "every job in each that publishes, releases or deploys on every run declares an environment, a deployment gate whose protection rules Cejel cannot read offline";
5510
5571
  var LINT_CONFIGURATION_CLAUSE = " (a lint or format check, whose configuration Cejel does not read)";
5511
5572
  var LINT_CONFIGURATION_SOME_CLAUSE = " (some on a lint or format check, whose configuration Cejel does not read)";
5512
5573
  function b3Disclosures(unshown, ...uncredited) {
@@ -5599,12 +5660,13 @@ function isPublishingCommand(input) {
5599
5660
  return false;
5600
5661
  }
5601
5662
  }
5602
- function publishingWorkflowReasons(root, workflows, uncredited, details, checkedDetails, reading) {
5663
+ function publishingWorkflowReasons(root, workflows, uncredited, details, checkedDetails, reading, lines) {
5603
5664
  const reasons = /* @__PURE__ */ new Map();
5604
5665
  for (const file of workflows) {
5605
5666
  const known = uncredited.get(file);
5606
5667
  if (known) {
5607
5668
  reasons.set(file, known);
5669
+ lines.set(file, workflowTriggerLine(root, file));
5608
5670
  continue;
5609
5671
  }
5610
5672
  const read = publishingWorkflowDetail(root, file, reading);
@@ -5613,11 +5675,24 @@ function publishingWorkflowReasons(root, workflows, uncredited, details, checked
5613
5675
  checkedDetails.set(file, { detail: read.detail, lint: read.lint });
5614
5676
  continue;
5615
5677
  }
5678
+ if (read.gated) {
5679
+ checkedDetails.set(file, { detail: read.detail, lint: false, gated: true });
5680
+ continue;
5681
+ }
5616
5682
  reasons.set(file, "publishes");
5617
5683
  details.set(file, read.detail);
5684
+ lines.set(file, read.line > 0 ? read.line : workflowTriggerLine(root, file));
5618
5685
  }
5619
5686
  return reasons;
5620
5687
  }
5688
+ function workflowTriggerLine(root, file) {
5689
+ if (!isGitHubActionsWorkflow(file)) return null;
5690
+ const fullPath = join2(root, file);
5691
+ if (!isRegularFile(fullPath)) return null;
5692
+ const lines = readRepoText(fullPath, "utf8").split(/\r?\n/);
5693
+ const at = lines.flatMap((line, index) => WORKFLOW_ON_KEY_PATTERN.test(line) ? [index + 1] : []);
5694
+ return at.length === 1 ? at[0] ?? null : null;
5695
+ }
5621
5696
  var RUNS_ON_KEY_PATTERN = /^[ \t]*runs-on:[ \t]*(.*)$/;
5622
5697
  var SHELL_KEY_PATTERN = /^[ \t]*(?:-[ \t]+)?shell:[ \t]*(.*)$/;
5623
5698
  var ONE_LABEL_LIST_PATTERN = /^\[[ \t]*([^,[\]]+?)[ \t]*\]$/;
@@ -5856,66 +5931,114 @@ function workflowJobs(contents, isGuardLine) {
5856
5931
  return jobs;
5857
5932
  }
5858
5933
  function publishingJobsOnEveryRun(root, contents, context, depth, reading) {
5859
- const none = { environments: [], checkedBeforePublish: false, notCredited: [], lintCredited: false };
5934
+ const none = { environments: [], checkedBeforePublish: false, notCredited: [], lintCredited: false, line: 0 };
5860
5935
  const notCredited = [];
5861
5936
  const credit = { lint: false };
5862
5937
  if (YAML_LINE_BREAK_NOT_SPLIT_PATTERN.test(contents)) return none;
5938
+ const { isGuardLine, jobs, publishingStepsKeys, publishes, firstPublishLine } = publishingStepsOnEveryRun(contents, context);
5939
+ if (publishingStepsKeys.size > 0) {
5940
+ return {
5941
+ environments: publishingJobEnvironments(contents, jobs, publishingStepsKeys),
5942
+ checkedBeforePublish: depth === 0 && checksBeforeEveryPublish(contents, jobs, publishes, isGuardLine, reading, notCredited, credit),
5943
+ notCredited,
5944
+ lintCredited: credit.lint,
5945
+ line: firstPublishLine
5946
+ };
5947
+ }
5948
+ if (depth >= REUSABLE_WORKFLOW_MAX_DEPTH) return none;
5949
+ for (const job of jobs) {
5950
+ const { target, conditional } = job;
5951
+ if (target === void 0 || conditional) continue;
5952
+ const local = LOCAL_REUSABLE_WORKFLOW_PATTERN.exec(target);
5953
+ if (!local) continue;
5954
+ const calledPath = join2(root, local[1] ?? "");
5955
+ if (!isRegularFile(calledPath)) continue;
5956
+ const called = publishingJobsOnEveryRun(root, readRepoText(calledPath, "utf8"), context, depth + 1, reading);
5957
+ if (called.environments.length > 0) {
5958
+ return {
5959
+ environments: called.environments,
5960
+ checkedBeforePublish: depth === 0 && checksBeforeEveryPublish(contents, jobs, publishes, isGuardLine, reading, notCredited, credit),
5961
+ notCredited,
5962
+ lintCredited: credit.lint,
5963
+ // Item B: the calling job's `uses:` line in this file.
5964
+ line: jobUsesLine(contents, job)
5965
+ };
5966
+ }
5967
+ }
5968
+ return none;
5969
+ }
5970
+ function publishingStepsOnEveryRun(contents, context) {
5863
5971
  const isGuardLine = (line) => isStepOrJobGuard(line) && !isAlwaysTrueIfLine(line, context);
5864
5972
  const jobs = workflowJobs(contents, isGuardLine);
5865
5973
  const mayRun = (step) => !step.conditional && step.job !== -1;
5866
5974
  const readsRunLines = runsOnlyOnHostedBashRunners(contents);
5867
5975
  const publishingStepsKeys = /* @__PURE__ */ new Set();
5868
5976
  const publishes = [];
5977
+ let firstPublishLine = 0;
5978
+ const onEveryRun = (stepsKey, line) => {
5979
+ publishingStepsKeys.add(stepsKey);
5980
+ if (line > 0 && (firstPublishLine === 0 || line < firstPublishLine)) firstPublishLine = line;
5981
+ };
5869
5982
  for (const step of githubActionsUsesSteps(contents, isGuardLine)) {
5870
5983
  const value = unquoteYamlScalar(beforeShellComment(step.lines[0]?.text ?? ""));
5871
5984
  const at = value.indexOf("@");
5872
5985
  if (!(at > 0 && PUBLISHING_ACTIONS.has(value.slice(0, at).toLowerCase()))) continue;
5873
5986
  if (step.job !== -1) publishes.push({ job: step.job, line: step.lines[0]?.line ?? 0, command: 0 });
5874
- if (mayRun(step)) publishingStepsKeys.add(step.job);
5987
+ if (mayRun(step)) onEveryRun(step.job, step.lines[0]?.line ?? 0);
5875
5988
  }
5876
5989
  for (const step of githubActionsRunSteps(contents, isGuardLine)) {
5877
5990
  const commands = joinShellContinuationLines(step.lines.map(({ text }) => text)).flatMap(
5878
5991
  (text) => shellCommands(text).map((words) => ({ words }))
5879
5992
  );
5880
5993
  if (!commands.some(({ words }) => isPublishingCommand(words))) continue;
5994
+ const line = step.lines.find(({ text }) => shellCommands(text).some(isPublishingCommand)) ?? step.lines[0];
5881
5995
  if (step.job !== -1) {
5882
- const line = step.lines.find(({ text }) => shellCommands(text).some(isPublishingCommand)) ?? step.lines[0];
5883
5996
  publishes.push({ job: step.job, line: line?.line ?? 0, command: Math.max(0, shellCommands(line?.text ?? "").findIndex(isPublishingCommand)) });
5884
5997
  }
5885
- if (readsRunLines && mayRun(step) && !stepShellIsConditional(step, commands)) publishingStepsKeys.add(step.job);
5998
+ if (readsRunLines && mayRun(step) && !stepShellIsConditional(step, commands)) onEveryRun(step.job, line?.line ?? 0);
5886
5999
  }
5887
- if (publishingStepsKeys.size > 0) {
5888
- const lines = contents.split(/\r?\n/);
5889
- const indents = lines.map(lineIndent);
5890
- const inBlockScalar = blockScalarBodyLines(lines, indents);
5891
- return {
5892
- environments: [...publishingStepsKeys].sort((left, right) => left - right).map((stepsKey) => {
5893
- const job = jobs.find(({ start, end }) => start < stepsKey && stepsKey < end);
5894
- return job ? jobEnvironment(lines, indents, inBlockScalar, job) : { kind: "unread" };
5895
- }),
5896
- checkedBeforePublish: depth === 0 && checksBeforeEveryPublish(contents, jobs, publishes, isGuardLine, reading, notCredited, credit),
5897
- notCredited,
5898
- lintCredited: credit.lint
5899
- };
6000
+ return { isGuardLine, jobs, publishingStepsKeys, publishes, firstPublishLine };
6001
+ }
6002
+ function publishingJobEnvironments(contents, jobs, publishingStepsKeys) {
6003
+ const lines = contents.split(/\r?\n/);
6004
+ const indents = lines.map(lineIndent);
6005
+ const inBlockScalar = blockScalarBodyLines(lines, indents);
6006
+ return [...publishingStepsKeys].sort((left, right) => left - right).map((stepsKey) => {
6007
+ const job = jobs.find(({ start, end }) => start < stepsKey && stepsKey < end);
6008
+ return job ? jobEnvironment(lines, indents, inBlockScalar, job) : { kind: "unread" };
6009
+ });
6010
+ }
6011
+ function jobUsesLine(contents, job) {
6012
+ const lines = contents.split(/\r?\n/);
6013
+ for (let index = job.start + 1; index < job.end; index += 1) {
6014
+ if (JOB_USES_KEY_PATTERN.test(stripYamlComment(lines[index] ?? ""))) return index + 1;
5900
6015
  }
5901
- if (depth >= REUSABLE_WORKFLOW_MAX_DEPTH) return none;
6016
+ return job.start + 1;
6017
+ }
6018
+ function everyPublishingJobEnvironment(root, contents, context, depth) {
6019
+ if (YAML_LINE_BREAK_NOT_SPLIT_PATTERN.test(contents)) return { environments: [], complete: false };
6020
+ const { jobs, publishingStepsKeys } = publishingStepsOnEveryRun(contents, context);
6021
+ const environments = publishingJobEnvironments(contents, jobs, publishingStepsKeys);
6022
+ let complete = true;
5902
6023
  for (const { target, conditional } of jobs) {
5903
6024
  if (target === void 0 || conditional) continue;
5904
6025
  const local = LOCAL_REUSABLE_WORKFLOW_PATTERN.exec(target);
5905
- if (!local) continue;
5906
- const calledPath = join2(root, local[1] ?? "");
5907
- if (!isRegularFile(calledPath)) continue;
5908
- const called = publishingJobsOnEveryRun(root, readRepoText(calledPath, "utf8"), context, depth + 1, reading);
5909
- if (called.environments.length > 0) {
5910
- return {
5911
- environments: called.environments,
5912
- checkedBeforePublish: depth === 0 && checksBeforeEveryPublish(contents, jobs, publishes, isGuardLine, reading, notCredited, credit),
5913
- notCredited,
5914
- lintCredited: credit.lint
5915
- };
6026
+ const calledPath = local ? join2(root, local[1] ?? "") : "";
6027
+ if (!local || depth >= REUSABLE_WORKFLOW_MAX_DEPTH || !isRegularFile(calledPath)) {
6028
+ complete = false;
6029
+ continue;
5916
6030
  }
6031
+ const called = everyPublishingJobEnvironment(root, readRepoText(calledPath, "utf8"), context, depth + 1);
6032
+ environments.push(...called.environments);
6033
+ complete &&= called.complete;
5917
6034
  }
5918
- return none;
6035
+ return { environments, complete };
6036
+ }
6037
+ function publishingGate(reading) {
6038
+ const { environments, complete } = reading;
6039
+ if (environments.some(({ kind }) => kind === "none")) return "ungated";
6040
+ const declared = (entry) => entry.kind === "named" || entry.kind === "unnamed" && entry.expression !== true;
6041
+ return complete && environments.length > 0 && environments.every(declared) ? "gated" : "unknown";
5919
6042
  }
5920
6043
  var STATUS_FUNCTION_NOT_SUCCESS_PATTERN = /\b(?:always|cancelled|failure)[ \t]*\(/i;
5921
6044
  var SUCCESS_CALL_PATTERN = /\bsuccess[ \t]*\(/gi;
@@ -7058,7 +7181,7 @@ function publishingWorkflowDetail(root, file, reading) {
7058
7181
  if (!isRegularFile(fullPath)) return void 0;
7059
7182
  const contents = readRepoText(fullPath, "utf8");
7060
7183
  if (YAML_LINE_BREAK_NOT_SPLIT_PATTERN.test(contents)) return void 0;
7061
- if (contents.split(/\r?\n/).filter((line) => WORKFLOW_ON_KEY_PATTERN.test(line)).length !== 1) return void 0;
7184
+ if (contents.split(/\r?\n/).filter((line2) => WORKFLOW_ON_KEY_PATTERN.test(line2)).length !== 1) return void 0;
7062
7185
  const triggers = githubWorkflowTriggers(contents);
7063
7186
  if (triggers === null || triggers.size === 0 || triggers.has("workflow_call")) return void 0;
7064
7187
  if ([...triggers].some((name) => PULL_REQUEST_TRIGGER_NAMES.has(name))) return void 0;
@@ -7066,15 +7189,31 @@ function publishingWorkflowDetail(root, file, reading) {
7066
7189
  triggers,
7067
7190
  pushBranches: triggers.has("push") ? githubPushBranchFilter(contents) : null
7068
7191
  };
7069
- const { environments, checkedBeforePublish, notCredited, lintCredited } = publishingJobsOnEveryRun(root, contents, context, 0, reading);
7192
+ const { environments, checkedBeforePublish, notCredited, lintCredited, line } = publishingJobsOnEveryRun(root, contents, context, 0, reading);
7070
7193
  if (environments.length === 0) return void 0;
7071
- const detail = `is triggered by ${publishingTriggerText(contents, triggers)}, and ${publishingEnvironmentText(environments)}.`;
7194
+ const triggerText = publishingTriggerText(contents, triggers);
7195
+ const detail = `is triggered by ${triggerText}, and ${publishingEnvironmentText(environments)}.`;
7196
+ if (checkedBeforePublish) return { detail, checked: true, lint: lintCredited, gated: false, line };
7197
+ const everyJob = everyPublishingJobEnvironment(root, contents, context, 0);
7198
+ const gate = publishingGate(everyJob);
7199
+ if (gate === "gated") {
7200
+ return {
7201
+ detail: `is triggered by ${triggerText}, and ${publishingEnvironmentText(everyJob.environments)}.`,
7202
+ checked: false,
7203
+ lint: false,
7204
+ gated: true,
7205
+ line
7206
+ };
7207
+ }
7072
7208
  return {
7073
- detail: checkedBeforePublish ? detail : `${detail}${notCreditedSentence(notCredited)}`,
7074
- checked: checkedBeforePublish,
7075
- lint: checkedBeforePublish && lintCredited
7209
+ detail: `${detail}${gate === "unknown" ? PUBLISHING_GATE_NOT_READ_SENTENCE : ""}${notCreditedSentence(notCredited)}`,
7210
+ checked: false,
7211
+ lint: false,
7212
+ gated: false,
7213
+ line
7076
7214
  };
7077
7215
  }
7216
+ var PUBLISHING_GATE_NOT_READ_SENTENCE = " Cejel could not read whether every job that publishes on every run declares an environment, so no deployment gate is credited.";
7078
7217
  var NOT_CREDITED_MAX_REASONS = 3;
7079
7218
  function notCreditedSentence(reasons) {
7080
7219
  const distinct = [...new Set(reasons)];
@@ -7096,7 +7235,8 @@ function joinWords(items, last) {
7096
7235
  function environmentNamed(written) {
7097
7236
  const name = unquoteYamlScalar(written);
7098
7237
  if (name === "") return { kind: "unread" };
7099
- return PUBLISHING_NOTE_NAME_PATTERN.test(name) ? { kind: "named", name } : { kind: "unnamed" };
7238
+ if (PUBLISHING_NOTE_NAME_PATTERN.test(name)) return { kind: "named", name };
7239
+ return name.includes("${{") ? { kind: "unnamed", expression: true } : { kind: "unnamed" };
7100
7240
  }
7101
7241
  function jobEnvironment(lines, indents, inBlockScalar, job) {
7102
7242
  const structural = (index) => {
@@ -20679,7 +20819,7 @@ var CERTIFICATE_GLOSSARY = [
20679
20819
  metricNames: ["protected_path_review_gate"]
20680
20820
  }
20681
20821
  ];
20682
- var V27_PR_GATE_CI_WORKFLOW_COUNT_DEFINITION = "The number of detected CI workflow files configured for pull requests or the main or master branch, including a covering monorepo-root workflow, capped at four for scoring. Under the prospective witan-rubric-v27, a workflow file is not counted when Cejel reads every step as only checking out, printing text or doing nothing, or, for a workflow with no pull-request trigger, as only uploading an already-built artifact. Cejel makes either reading only when the runs-on value of every job names a documented GitHub-hosted Ubuntu or macOS runner label. Such a file was not shown to run a check, which is not a finding that it runs none. Under the same rubric, a workflow file with no pull-request trigger is also not counted, by this metric or by CI verification depth, when a step publishes, releases or deploys (a registry publish or upload, a release, an image push or a deploy command or action) on every run its triggers start, including through a job that calls a reusable workflow in the same repository, unless a check (a test, lint, type-check or verifying-build command whose failure stops the job) runs before every publish step, in the publishing job or in a job it needs. Cejel reads that only when it can read every step, the file holds no YAML escape and writes every other key and value plainly, nothing but setup runs before the check, no if condition uses a status-check function other than a plain success(), and no job needs itself or is part of a needs cycle. It also requires that the check command is not written as a path (such as ./ruff), that a test script, make target, justfile recipe, tox environment, or pdm, hatch or pipenv script the check runs reaches a test runner from a fixed list (no other task runner, such as nox, pre-commit, turbo, gradle, mvn, bazel or ctest, is read) with no project installed first by tox or hatch, that the runner or check carries only flags, and flag values, from a fixed list known to keep it failing, and no argument file, that a runner run as a Python module (python -m) has no module of its name, or of a module it or the standard library imports, in the repository where Python would import it first, that a Makefile it runs assigns no variable, that a tsc build before it writes only into a new directory outside node_modules, that a lint or format command is written in a form that fails on a finding, that every environment key it reads on the check's path (in env: mappings, in an assignment on a command's line, or written to GITHUB_ENV) is one from a fixed list of keys that do not choose what runs, that no earlier step writes to GITHUB_PATH, and that the job runs in no container and has no service container that sets env:. That is what Cejel reads, not a proof that the check can fail: it does not read a test runner's or linter's own configuration. An if condition is a gate only when it can be false under the workflow's own triggers in the repository itself (a check of the repository's own name or owner guards forks, not runs), and a continue-on-error setting keeps the file counted. Cejel reads a publishing action on any runner, and a publish command only on the same runner labels, with no shell setting other than bash.";
20822
+ var V27_PR_GATE_CI_WORKFLOW_COUNT_DEFINITION = "The number of detected CI workflow files configured for pull requests or the main or master branch, including a covering monorepo-root workflow, capped at four for scoring. Under the prospective witan-rubric-v27, a workflow file is not counted when Cejel reads every step as only checking out, printing text or doing nothing, or, for a workflow with no pull-request trigger, as only uploading an already-built artifact. Cejel makes either reading only when the runs-on value of every job names a documented GitHub-hosted Ubuntu or macOS runner label. Such a file was not shown to run a check, which is not a finding that it runs none. Under the same rubric, a workflow file with no pull-request trigger is also not counted, by this metric or by CI verification depth, when a step publishes, releases or deploys (a registry publish or upload, a release, an image push or a deploy command or action) on every run its triggers start, including through a job that calls a reusable workflow in the same repository, unless a check (a test, lint, type-check or verifying-build command whose failure stops the job) runs before every publish step, in the publishing job or in a job it needs. It is counted too when every job that publishes on every run, in the workflow or in a reusable workflow in the same repository that it calls, declares a job-level environment whose name is not an expression: Cejel credits that declaration as a deployment gate, and cannot read offline the protection rules the environment sets. Each workflow file not counted is cited in the criterion's evidence at the line that decided it. Cejel reads a check before a publish only when it can read every step, the file holds no YAML escape and writes every other key and value plainly, nothing but setup runs before the check, no if condition uses a status-check function other than a plain success(), and no job needs itself or is part of a needs cycle. It also requires that the check command is not written as a path (such as ./ruff), that a test script, make target, justfile recipe, tox environment, or pdm, hatch or pipenv script the check runs reaches a test runner from a fixed list (no other task runner, such as nox, pre-commit, turbo, gradle, mvn, bazel or ctest, is read) with no project installed first by tox or hatch, that the runner or check carries only flags, and flag values, from a fixed list known to keep it failing, and no argument file, that a runner run as a Python module (python -m) has no module of its name, or of a module it or the standard library imports, in the repository where Python would import it first, that a Makefile it runs assigns no variable, that a tsc build before it writes only into a new directory outside node_modules, that a lint or format command is written in a form that fails on a finding, that every environment key it reads on the check's path (in env: mappings, in an assignment on a command's line, or written to GITHUB_ENV) is one from a fixed list of keys that do not choose what runs, that no earlier step writes to GITHUB_PATH, and that the job runs in no container and has no service container that sets env:. That is what Cejel reads, not a proof that the check can fail: it does not read a test runner's or linter's own configuration. An if condition is a gate only when it can be false under the workflow's own triggers in the repository itself (a check of the repository's own name or owner guards forks, not runs), and a continue-on-error setting keeps the file counted. Cejel reads a publishing action on any runner, and a publish command only on the same runner labels, with no shell setting other than bash.";
20683
20823
  var V27_NON_HOLLOW_TEST_SHARE_DEFINITION = `test files that actually assert something rather than being empty or skipped. Under the prospective witan-rubric-v27, A1's note names each file a v27 rule rates hollow, and each pytest collection hook it did not read (as "hook not read", with the reason and the conftest.py's path: it could prove neither that the hook leaves no test to run nor that it puts the collected tests back, so no file was rated hollow for it), and each pytest run whose arguments or directory it could not read from the repository (as "pytest run not read", with the reason and the file: any option may be in force for it, so no file it may collect is rated hollow). A collection hook that removes or skips every test is applied to every test file a pytest run that loads it collects, inside its directory and outside it, since pytest calls it with every collected test. A path that holds a comma or semicolon followed by a space, a space followed by an opening parenthesis, a backtick or a double quote is written in double quotes, with each backslash and double quote in it preceded by a backslash; any other path is written as it is. The evidence cites the first eight files rated hollow, and the first eight hooks, pytest settings and runs the note names where a rule was not applied (each at the line that holds it), and the note says how many it names beyond those carry no evidence pointer.`;
20684
20824
  function glossaryEntryForRubric(entry, rubricVersion) {
20685
20825
  if (entry.key !== "pr-gate-ci-workflow-count" && entry.key !== "non-hollow" || rubricVersion === void 0) return entry;
@@ -5,7 +5,7 @@ import {
5
5
  renderWitanBadgeSvg,
6
6
  renderWitanHtmlReport,
7
7
  runCejelScan
8
- } from "./chunk-G6AN4EDL.js";
8
+ } from "./chunk-73XAL26O.js";
9
9
  import {
10
10
  ZodFirstPartyTypeKind,
11
11
  ZodOptional,
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire as __cejelCreateRequire } from 'node:module';
3
3
  const require = __cejelCreateRequire(import.meta.url);
4
4
  import {
5
5
  serveCejelMcpOverStdio
6
- } from "./chunk-IJYGVKYU.js";
6
+ } from "./chunk-QIBLNTZB.js";
7
7
  import {
8
8
  CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE,
9
9
  CERTIFICATE_GLOSSARY,
@@ -45,7 +45,7 @@ import {
45
45
  verifyRecordedInputDigests,
46
46
  verifyWitanAttestationBinding,
47
47
  witanHistoryAbstentionInstruction
48
- } from "./chunk-G6AN4EDL.js";
48
+ } from "./chunk-73XAL26O.js";
49
49
  import "./chunk-2UMHJ23V.js";
50
50
  import {
51
51
  external_exports
package/dist/mcp/index.js CHANGED
@@ -3,8 +3,8 @@ import { createRequire as __cejelCreateRequire } from 'node:module';
3
3
  const require = __cejelCreateRequire(import.meta.url);
4
4
  import {
5
5
  serveCejelMcpOverStdio
6
- } from "../chunk-IJYGVKYU.js";
7
- import "../chunk-G6AN4EDL.js";
6
+ } from "../chunk-QIBLNTZB.js";
7
+ import "../chunk-73XAL26O.js";
8
8
  import "../chunk-2UMHJ23V.js";
9
9
  import "../chunk-AMVP7L26.js";
10
10
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cejel/cejel",
3
- "version": "0.7.0-rc.2",
3
+ "version": "0.7.0-rc.3",
4
4
  "mcpName": "io.github.BargLabs/cejel",
5
5
  "private": false,
6
6
  "publishConfig": {