@mutmutco/cli 3.93.0 → 3.94.0

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.
Files changed (2) hide show
  1. package/dist/main.cjs +68 -41
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -11706,6 +11706,29 @@ function parseAuthoritativeRuleset(raw, meta, repo) {
11706
11706
  function sameContexts(left, right) {
11707
11707
  return left.length === right.length && left.every((context, index) => context === right[index]);
11708
11708
  }
11709
+ function resolveProductRulesetReconcilePlan(input) {
11710
+ const liveNeedsContextConvergence = !sameContexts(input.liveContexts, input.authorityContexts);
11711
+ const liveIsActive = input.liveEnforcement === "active";
11712
+ if (liveIsActive && !liveNeedsContextConvergence) {
11713
+ return { shouldActivate: false, targetEnforcement: "active" };
11714
+ }
11715
+ if (input.unsafeContexts.length > 0) {
11716
+ const unsafe = [...input.unsafeContexts];
11717
+ return {
11718
+ shouldActivate: false,
11719
+ targetEnforcement: "disabled",
11720
+ holdReason: `product ruleset left non-enforcing \u2014 [${unsafe.join(", ")}] ${unsafe.length === 1 ? "is" : "are"} emitted ONLY by path-filtered workflow(s), so the context is not reported on a PR outside those paths and requiring it would block that PR forever (#3836). Give the gate a companion job that runs unconditionally, then re-run this command`
11721
+ };
11722
+ }
11723
+ if (!input.gateProvenGreen) {
11724
+ return {
11725
+ shouldActivate: false,
11726
+ targetEnforcement: "disabled",
11727
+ holdReason: "product ruleset left non-enforcing \u2014 the gate is not green on development; activating it would block every PR (#3694)"
11728
+ };
11729
+ }
11730
+ return { shouldActivate: true, targetEnforcement: "active" };
11731
+ }
11709
11732
  async function contentExists(deps, repo, branch, path2) {
11710
11733
  try {
11711
11734
  const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
@@ -12409,46 +12432,39 @@ async function applyCiReconcileRepo(repo, deps) {
12409
12432
  return finalizeCiReconcile(repo, deps, result, report);
12410
12433
  }
12411
12434
  const liveContexts = live == null ? [] : sortedUnique(rulesetRequiredContexts(live));
12412
- const liveNeedsConvergence = live == null || !sameContexts(liveContexts, authority.contexts);
12413
- const enforcement = live?.enforcement === "disabled" || live == null && authority.apiPayload.enforcement === "disabled" ? "disabled" : "active";
12414
- if (liveNeedsConvergence && enforcement === "active") {
12415
- const prWorkflows = await prTriggeredWorkflowsOnRef(deps, repo, baseBranch, "deployable") ?? [];
12416
- const gateFiles = gateWorkflowFiles(prWorkflows);
12417
- const allPrWorkflows = await listWorkflowPaths(deps, repo, baseBranch) ?? [];
12418
- const bodies = [];
12419
- for (const path2 of allPrWorkflows) {
12420
- const body = await fetchFileContent(deps, repo, baseBranch, path2);
12421
- if (body) bodies.push({ path: path2, body });
12422
- }
12423
- const filteredPaths = new Set(pathFilteredPullRequestWorkflows(bodies).filter((p) => !p.endsWith("/agent-pr.yml")));
12424
- const safeContexts = new Set(collectPullRequestWorkflowContexts(bodies.filter((b) => !filteredPaths.has(b.path))));
12425
- const unsafe = collectPullRequestWorkflowContexts(bodies.filter((b) => filteredPaths.has(b.path))).filter((c) => authority.contexts.includes(c) && !safeContexts.has(c));
12426
- if (unsafe.length) {
12427
- const reason = `product ruleset left non-enforcing \u2014 [${unsafe.join(", ")}] ${unsafe.length === 1 ? "is" : "are"} emitted ONLY by path-filtered workflow(s) (${[...filteredPaths].join(", ")}), so the context is not reported on a PR outside those paths and requiring it would block that PR forever (#3836). Give the gate a companion job that runs unconditionally, then re-run this command`;
12428
- result.skipped.push(reason);
12429
- return finalizeCiReconcile(repo, deps, result, report, reason);
12430
- }
12431
- if (!await gateIsProvenGreen(repo, deps.client, baseBranch, gateFiles)) {
12432
- const reason = "product ruleset left non-enforcing \u2014 the gate is not green on development; activating it would block every PR (#3694)";
12433
- result.skipped.push(reason);
12434
- return finalizeCiReconcile(repo, deps, result, report, reason);
12435
- }
12436
- }
12437
- if (liveNeedsConvergence) {
12438
- try {
12439
- const activation = await activateProductRuleset(repo, authority.apiPayload, deps.client, enforcement);
12440
- if (activation.action === "skipped") result.skipped.push(activation.detail ?? "product ruleset");
12441
- else result.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
12442
- } catch (e) {
12443
- result.errors.push(e.message);
12444
- return finalizeCiReconcile(repo, deps, result, report);
12435
+ const prWorkflows = await prTriggeredWorkflowsOnRef(deps, repo, baseBranch, "deployable") ?? [];
12436
+ const gateFiles = gateWorkflowFiles(prWorkflows);
12437
+ const allPrWorkflows = await listWorkflowPaths(deps, repo, baseBranch) ?? [];
12438
+ const bodies = [];
12439
+ for (const path2 of allPrWorkflows) {
12440
+ const body = await fetchFileContent(deps, repo, baseBranch, path2);
12441
+ if (body) bodies.push({ path: path2, body });
12442
+ }
12443
+ const filteredPaths = new Set(pathFilteredPullRequestWorkflows(bodies).filter((p) => !p.endsWith("/agent-pr.yml")));
12444
+ const safeContexts = new Set(collectPullRequestWorkflowContexts(bodies.filter((b) => !filteredPaths.has(b.path))));
12445
+ const unsafe = collectPullRequestWorkflowContexts(bodies.filter((b) => filteredPaths.has(b.path))).filter((c) => authority.contexts.includes(c) && !safeContexts.has(c));
12446
+ const plan = resolveProductRulesetReconcilePlan({
12447
+ liveEnforcement: live?.enforcement,
12448
+ liveContexts,
12449
+ authorityContexts: authority.contexts,
12450
+ gateProvenGreen: await gateIsProvenGreen(repo, deps.client, baseBranch, gateFiles),
12451
+ unsafeContexts: unsafe
12452
+ });
12453
+ if (!plan.shouldActivate) {
12454
+ if (plan.holdReason) {
12455
+ result.skipped.push(plan.holdReason);
12456
+ return finalizeCiReconcile(repo, deps, result, report, plan.holdReason);
12445
12457
  }
12446
- } else {
12447
12458
  result.skipped.push(`live ${PRODUCT_RULESET_NAME} contexts already match ${authority.source}`);
12459
+ return finalizeCiReconcile(repo, deps, result, report);
12448
12460
  }
12449
- if (enforcement === "disabled") {
12450
- const reason = `${PRODUCT_RULESET_NAME} remains parked (disabled); authoritative contexts were preserved without changing enforcement`;
12451
- return finalizeCiReconcile(repo, deps, result, report, reason);
12461
+ try {
12462
+ const activation = await activateProductRuleset(repo, authority.apiPayload, deps.client, plan.targetEnforcement);
12463
+ if (activation.action === "skipped") result.skipped.push(activation.detail ?? "product ruleset");
12464
+ else result.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
12465
+ } catch (e) {
12466
+ result.errors.push(e.message);
12467
+ return finalizeCiReconcile(repo, deps, result, report);
12452
12468
  }
12453
12469
  return finalizeCiReconcile(repo, deps, result, report);
12454
12470
  }
@@ -23445,6 +23461,12 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
23445
23461
  detail: optionDetail(missing)
23446
23462
  });
23447
23463
  } else if (repoClass === "deployable") {
23464
+ const productRuleset = rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
23465
+ checks.push({
23466
+ ok: productRuleset?.enforcement === "active",
23467
+ label: "product required-check ruleset enforcement active",
23468
+ detail: productRuleset?.enforcement !== "active" ? `${PRODUCT_RULESET_NAME} is ${productRuleset?.enforcement ?? "missing"} \u2014 run mmi-cli ci reconcile --apply --repo ${repo} once the gate is green` : void 0
23469
+ });
23448
23470
  const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
23449
23471
  const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
23450
23472
  checks.push({
@@ -28858,7 +28880,8 @@ function diagnoseSurface(evidence) {
28858
28880
  return { ...base, state: "repair-failed", repairDetail: evidence.repair.detail };
28859
28881
  }
28860
28882
  if (evidence.repair?.attempted && evidence.repair.ok) {
28861
- return { ...base, state: "clean", repairDetail: evidence.repair.detail };
28883
+ const behindReleased = evidence.installedVersion && evidence.releasedVersion && compareVersions(evidence.installedVersion, evidence.releasedVersion) < 0;
28884
+ return { ...base, state: behindReleased ? "pending-reload" : "clean", repairDetail: evidence.repair.detail };
28862
28885
  }
28863
28886
  if (!evidence.installRecordPresent) return { ...base, state: "missing" };
28864
28887
  if (!evidence.deliveryPresent || !evidence.payloadPresent || evidence.manifest === "missing") {
@@ -28876,7 +28899,7 @@ function reloadInstruction(descriptor) {
28876
28899
  return `${verb} ${descriptor.displayName}`;
28877
28900
  }
28878
28901
  function planSurfaceRepair(diagnosis) {
28879
- if (diagnosis.state === "clean" || diagnosis.state === "skipped" || diagnosis.state === "freshness-unknown") return null;
28902
+ if (diagnosis.state === "clean" || diagnosis.state === "pending-reload" || diagnosis.state === "skipped" || diagnosis.state === "freshness-unknown") return null;
28880
28903
  const descriptor = diagnosis.descriptor;
28881
28904
  if (descriptor.repairOwner !== "mmi-cli") {
28882
28905
  return {
@@ -28901,6 +28924,7 @@ function buildSurfaceDoctorCheck(diagnosis) {
28901
28924
  const detailByState = {
28902
28925
  skipped: "skipped \u2014 host not active",
28903
28926
  clean: diagnosis.repairDetail ? `clean \u2014 repaired and verified (${diagnosis.repairDetail})` : `clean${versions ? ` \u2014 ${versions}` : ""}`,
28927
+ "pending-reload": `pending ${descriptor.reload === "workspace" ? "reload" : "restart"} \u2014 repaired to ${diagnosis.releasedVersion}, but ${diagnosis.installedVersion} is still the version this host has loaded${diagnosis.repairDetail ? ` (${diagnosis.repairDetail})` : ""}`,
28904
28928
  "freshness-unknown": `${diagnosis.installedVersion ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
28905
28929
  stale: `stale${versions ? ` \u2014 ${versions}` : ""}`,
28906
28930
  missing: "missing \u2014 no install record",
@@ -28912,11 +28936,14 @@ function buildSurfaceDoctorCheck(diagnosis) {
28912
28936
  id: `${descriptor.token}-plugin`,
28913
28937
  surface: descriptor.token,
28914
28938
  state,
28915
- ok: state === "clean" || state === "skipped",
28939
+ ok: state === "clean" || state === "skipped" || state === "pending-reload",
28916
28940
  ...state === "freshness-unknown" ? { reportOnly: true } : {},
28941
+ // Nothing is broken, so this is not a ✗ — but it is the one OK row an operator must act on, and the
28942
+ // short default lane and the SessionStart banner both filter to failures unless a row says otherwise.
28943
+ ...state === "pending-reload" ? { warn: true } : {},
28917
28944
  label: `${descriptor.displayName} plugin`,
28918
28945
  detail: detailByState[state],
28919
- ...plan ? { fix: plan.instruction } : state === "freshness-unknown" ? { fix: "check `mmi-cli --version` against `npm view @mutmutco/cli version`, then rerun doctor" } : {},
28946
+ ...plan ? { fix: plan.instruction } : state === "pending-reload" ? { fix: reloadInstruction(descriptor) } : state === "freshness-unknown" ? { fix: "check `mmi-cli --version` against `npm view @mutmutco/cli version`, then rerun doctor" } : {},
28920
28947
  verbose: [
28921
28948
  // The three numbers the legacy builder used to print. They belong here now that this is the only
28922
28949
  // plugin row, and a report that names a state without naming the versions behind it is not evidence.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.93.0",
3
+ "version": "3.94.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",