@intentius/chant 0.124.17 → 0.124.18

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 (115) hide show
  1. package/dist/__golden__/wave-loop/fan-out-applied.json +511 -0
  2. package/dist/__golden__/wave-loop/fan-out-apply-fails.json +406 -0
  3. package/dist/__golden__/wave-loop/fan-out-gated.json +707 -0
  4. package/dist/__golden__/wave-loop/fan-out-no-members.json +898 -0
  5. package/dist/__golden__/wave-loop/fan-out-only.json +514 -0
  6. package/dist/__golden__/wave-loop/fan-out-plan-fails.json +359 -0
  7. package/dist/__golden__/wave-loop/fan-out-same-root.json +311 -0
  8. package/dist/__golden__/wave-loop/op-by-apply-after-fails.json +401 -0
  9. package/dist/__golden__/wave-loop/op-by-apply-hold.json +756 -0
  10. package/dist/__golden__/wave-loop/op-by-apply-ignored.json +340 -0
  11. package/dist/__golden__/wave-loop/op-by-apply-pass.json +400 -0
  12. package/dist/__golden__/wave-loop/op-by-apply-skip.json +404 -0
  13. package/dist/__golden__/wave-loop/op-cost-member-clash.json +62 -0
  14. package/dist/__golden__/wave-loop/op-cost-member.json +191 -0
  15. package/dist/__golden__/wave-loop/op-denied-source.json +407 -0
  16. package/dist/__golden__/wave-loop/op-denied.json +399 -0
  17. package/dist/__golden__/wave-loop/op-discover-rest-short.json +92 -0
  18. package/dist/__golden__/wave-loop/op-discover-rest-stops.json +340 -0
  19. package/dist/__golden__/wave-loop/op-discover-rest.json +659 -0
  20. package/dist/__golden__/wave-loop/op-discover-wide.json +290 -0
  21. package/dist/__golden__/wave-loop/op-held-on-again.json +176 -0
  22. package/dist/__golden__/wave-loop/op-held-on-before-apply.json +689 -0
  23. package/dist/__golden__/wave-loop/op-held-on.json +1455 -0
  24. package/dist/__golden__/wave-loop/op-hooks-apply-fails.json +673 -0
  25. package/dist/__golden__/wave-loop/op-hooks-fail-after-apply.json +737 -0
  26. package/dist/__golden__/wave-loop/op-hooks-fail-before-apply.json +450 -0
  27. package/dist/__golden__/wave-loop/op-hooks-fail-before-plan.json +97 -0
  28. package/dist/__golden__/wave-loop/op-hooks-held.json +1059 -0
  29. package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +693 -0
  30. package/dist/__golden__/wave-loop/op-hooks-pass.json +735 -0
  31. package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +148 -0
  32. package/dist/__golden__/wave-loop/op-hooks-share.json +1225 -0
  33. package/dist/__golden__/wave-loop/op-outcome-held.json +202 -0
  34. package/dist/__golden__/wave-loop/op-outcome-replan-approved.json +286 -0
  35. package/dist/__golden__/wave-loop/op-outcome-replan-forever.json +823 -0
  36. package/dist/__golden__/wave-loop/op-outcome-replan.json +259 -0
  37. package/dist/__golden__/wave-loop/op-outcome-stood-down.json +196 -0
  38. package/dist/__golden__/wave-loop/op-outcome-unreadable.json +193 -0
  39. package/dist/__golden__/wave-loop/op-overridden.json +508 -0
  40. package/dist/__golden__/wave-loop/op-per-run.json +518 -0
  41. package/dist/__golden__/wave-loop/op-point.json +495 -0
  42. package/dist/__golden__/wave-loop/op-pr-review.json +397 -0
  43. package/dist/__golden__/wave-loop/op-sealed.json +125 -0
  44. package/dist/__golden__/wave-loop/op-spend-change-only.json +168 -0
  45. package/dist/__golden__/wave-loop/op-spend.json +328 -0
  46. package/dist/__golden__/wave-loop/op-wave-commands.json +1808 -0
  47. package/dist/__golden__/wave-loop/op-wide-refused.json +291 -0
  48. package/dist/__golden__/wave-loop/op-wide-waiting.json +231 -0
  49. package/dist/__golden__/wave-loop/op-wide.json +440 -0
  50. package/dist/components/fan-out-run.d.ts +4 -3
  51. package/dist/components/fan-out-run.d.ts.map +1 -1
  52. package/dist/components/fan-out-run.js +138 -123
  53. package/dist/op/op-waves-run.d.ts +7 -1
  54. package/dist/op/op-waves-run.d.ts.map +1 -1
  55. package/dist/op/op-waves-run.js +129 -72
  56. package/dist/wave-engine.d.ts +74 -5
  57. package/dist/wave-engine.d.ts.map +1 -1
  58. package/dist/wave-engine.js +47 -5
  59. package/dist/wave-runtime.mjs +202 -202
  60. package/package.json +1 -1
  61. package/src/__golden__/wave-loop/fan-out-applied.json +511 -0
  62. package/src/__golden__/wave-loop/fan-out-apply-fails.json +406 -0
  63. package/src/__golden__/wave-loop/fan-out-gated.json +707 -0
  64. package/src/__golden__/wave-loop/fan-out-no-members.json +898 -0
  65. package/src/__golden__/wave-loop/fan-out-only.json +514 -0
  66. package/src/__golden__/wave-loop/fan-out-plan-fails.json +359 -0
  67. package/src/__golden__/wave-loop/fan-out-same-root.json +311 -0
  68. package/src/__golden__/wave-loop/op-by-apply-after-fails.json +401 -0
  69. package/src/__golden__/wave-loop/op-by-apply-hold.json +756 -0
  70. package/src/__golden__/wave-loop/op-by-apply-ignored.json +340 -0
  71. package/src/__golden__/wave-loop/op-by-apply-pass.json +400 -0
  72. package/src/__golden__/wave-loop/op-by-apply-skip.json +404 -0
  73. package/src/__golden__/wave-loop/op-cost-member-clash.json +62 -0
  74. package/src/__golden__/wave-loop/op-cost-member.json +191 -0
  75. package/src/__golden__/wave-loop/op-denied-source.json +407 -0
  76. package/src/__golden__/wave-loop/op-denied.json +399 -0
  77. package/src/__golden__/wave-loop/op-discover-rest-short.json +92 -0
  78. package/src/__golden__/wave-loop/op-discover-rest-stops.json +340 -0
  79. package/src/__golden__/wave-loop/op-discover-rest.json +659 -0
  80. package/src/__golden__/wave-loop/op-discover-wide.json +290 -0
  81. package/src/__golden__/wave-loop/op-held-on-again.json +176 -0
  82. package/src/__golden__/wave-loop/op-held-on-before-apply.json +689 -0
  83. package/src/__golden__/wave-loop/op-held-on.json +1455 -0
  84. package/src/__golden__/wave-loop/op-hooks-apply-fails.json +673 -0
  85. package/src/__golden__/wave-loop/op-hooks-fail-after-apply.json +737 -0
  86. package/src/__golden__/wave-loop/op-hooks-fail-before-apply.json +450 -0
  87. package/src/__golden__/wave-loop/op-hooks-fail-before-plan.json +97 -0
  88. package/src/__golden__/wave-loop/op-hooks-held.json +1059 -0
  89. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +693 -0
  90. package/src/__golden__/wave-loop/op-hooks-pass.json +735 -0
  91. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +148 -0
  92. package/src/__golden__/wave-loop/op-hooks-share.json +1225 -0
  93. package/src/__golden__/wave-loop/op-outcome-held.json +202 -0
  94. package/src/__golden__/wave-loop/op-outcome-replan-approved.json +286 -0
  95. package/src/__golden__/wave-loop/op-outcome-replan-forever.json +823 -0
  96. package/src/__golden__/wave-loop/op-outcome-replan.json +259 -0
  97. package/src/__golden__/wave-loop/op-outcome-stood-down.json +196 -0
  98. package/src/__golden__/wave-loop/op-outcome-unreadable.json +193 -0
  99. package/src/__golden__/wave-loop/op-overridden.json +508 -0
  100. package/src/__golden__/wave-loop/op-per-run.json +518 -0
  101. package/src/__golden__/wave-loop/op-point.json +495 -0
  102. package/src/__golden__/wave-loop/op-pr-review.json +397 -0
  103. package/src/__golden__/wave-loop/op-sealed.json +125 -0
  104. package/src/__golden__/wave-loop/op-spend-change-only.json +168 -0
  105. package/src/__golden__/wave-loop/op-spend.json +328 -0
  106. package/src/__golden__/wave-loop/op-wave-commands.json +1808 -0
  107. package/src/__golden__/wave-loop/op-wide-refused.json +291 -0
  108. package/src/__golden__/wave-loop/op-wide-waiting.json +231 -0
  109. package/src/__golden__/wave-loop/op-wide.json +440 -0
  110. package/src/components/fan-out-run.ts +156 -128
  111. package/src/meta/peer-deps.test.ts +39 -0
  112. package/src/op/op-waves-run.ts +184 -68
  113. package/src/wave-engine.test.ts +38 -1
  114. package/src/wave-engine.ts +101 -5
  115. package/src/wave-loop-golden.test.ts +627 -0
@@ -50,7 +50,7 @@
50
50
  import { execFileSync, spawnSync } from "node:child_process";
51
51
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
52
52
  import { dirname, isAbsolute, join, posix } from "node:path";
53
- import { decideWaveGate, waveDigestMembers, waveSetDigest, writeWaveRecordFile } from "../wave-engine.js";
53
+ import { decideWaveGate, runWaveLoop, waveDigestMembers, waveSetDigest, writeWaveRecordFile, } from "../wave-engine.js";
54
54
  import { approveCommand, evaluateGate, gitGateLedgerPort } from "./gate.js";
55
55
  import { isApprovalSpent } from "../lifecycle/gate-applied.js";
56
56
  import { OP_WAVE_DEFAULT_APPLY, OP_WAVE_GATE_POLICIES, OP_WAVE_HOOK_MOMENTS, GATE_APPROVAL_SOURCES, assertOpWaveHooks, assertOpWavesSpec, opWaveApplyHooksRunBy, opWaveGateName, opWaveHooksAt, opWavePointAt, opWavePointProblems, opWaveRecordPath, opWaveShare, } from "./op-waves.js";
@@ -710,7 +710,13 @@ function noteHooksAtBase(ctx, rules) {
710
710
  ctx.log(`wave ${ctx.k} (${ctx.wave.name}): its hooks are read at ${base.slice(0, 12)}, and this change's differ; the ones at base run`);
711
711
  }
712
712
  }
713
- /** Plan, decide and apply one wave, or one part of a wide wave. */
713
+ /**
714
+ * Plan, decide and apply one wave, or one part of a wide wave: the wave
715
+ * engine's loop (`runWaveLoop`, #3766) over this module's steps. An apply
716
+ * that comes back `replan` plans the wave again (#3821), at most
717
+ * {@link OP_WAVE_MAX_REPLANS} times; a `rest` wave goes on through the waves
718
+ * its plan command found.
719
+ */
714
720
  export async function runOpWave(options) {
715
721
  const { spec } = options;
716
722
  assertOpWavesSpec(spec);
@@ -718,37 +724,47 @@ export async function runOpWave(options) {
718
724
  const last = spec.waves[spec.waves.length - 1];
719
725
  const applied = [];
720
726
  const rest = [];
721
- for (let k = options.wave;; k++) {
722
- let result;
723
- let prior;
724
- // #3821: an apply that came back "replan" plans the wave again, a bounded number of times.
725
- for (let pass = 0;; pass++) {
726
- result = await runOneWave(options, k, prior, pass);
727
- if (!result.replan)
728
- break;
729
- if (pass + 1 >= OP_WAVE_MAX_REPLANS) {
730
- log(`wave ${k}: its apply asked to plan again ${OP_WAVE_MAX_REPLANS} times, so the wave stops here`);
731
- result.exitCode = 1;
732
- break;
733
- }
734
- log(`wave ${k}: the apply applied nothing and asks to plan again${result.replan.reason ? ` (${result.replan.reason})` : ""}; planning again`);
735
- prior = result.decision;
736
- }
737
- applied.push(...result.applied);
738
- // #3821: a `rest` wave goes on through the waves its plan command found.
739
- const found = result.decision?.discovered?.waves;
740
- const onward = k >= spec.waves.length && last.rest === true && options.share === undefined && !options.decide && result.exitCode === 0 && found !== undefined && k < found;
741
- if (rest.length > 0 || onward)
742
- rest.push({ wave: k, exitCode: result.exitCode, ...(result.decision ? { digest: result.decision.digest } : {}) });
743
- if (!onward)
744
- return { ...result, applied, ...(rest.length > 0 ? { rest } : {}) };
745
- log(`wave ${k} applied; the plan found ${found} waves, so this job goes on to wave ${k + 1}`);
746
- }
727
+ const result = await runWaveLoop({
728
+ first: options.wave,
729
+ plan: (at) => planOpWave(options, at),
730
+ // A share job applies under the digest its deciding job recorded; it does not take the set digest again.
731
+ digest: (_at, planned) => (planned.kind === "share" ? planned.decision.digest : opWaveSetDigest(spec, planned.members, planned.extras)),
732
+ decide: (at, planned, digest) => (planned.kind === "share" ? shareUnmoved(planned) : decideOpWave(options, at, planned, digest)),
733
+ apply: (_at, planned, decided) => applyOpWave(options, planned, decided),
734
+ replan: (_at, outcome) => outcome.replan !== undefined,
735
+ maxPasses: OP_WAVE_MAX_REPLANS,
736
+ passesSpent({ k }, outcome) {
737
+ log(`wave ${k}: its apply asked to plan again ${OP_WAVE_MAX_REPLANS} times, so the wave stops here`);
738
+ outcome.exitCode = 1;
739
+ return outcome;
740
+ },
741
+ replanning({ k }, outcome) {
742
+ log(`wave ${k}: the apply applied nothing and asks to plan again${outcome.replan.reason ? ` (${outcome.replan.reason})` : ""}; planning again`);
743
+ },
744
+ onward(k, outcome) {
745
+ applied.push(...outcome.applied);
746
+ // #3821: a `rest` wave goes on through the waves its plan command found.
747
+ const found = outcome.decision?.discovered?.waves;
748
+ const onward = k >= spec.waves.length && last.rest === true && options.share === undefined && !options.decide && outcome.exitCode === 0 && found !== undefined && k < found;
749
+ if (rest.length > 0 || onward)
750
+ rest.push({ wave: k, exitCode: outcome.exitCode, ...(outcome.decision ? { digest: outcome.decision.digest } : {}) });
751
+ if (onward)
752
+ log(`wave ${k} applied; the plan found ${found} waves, so this job goes on to wave ${k + 1}`);
753
+ return onward;
754
+ },
755
+ });
756
+ return { ...result, applied, ...(rest.length > 0 ? { rest } : {}) };
747
757
  }
748
758
  /** How many times one wave job plans again when its apply asks to (#3821). */
749
759
  export const OP_WAVE_MAX_REPLANS = 10;
750
- /** Plan, decide and apply wave `k`, or one part of a wide wave. `prior`: the decision of the pass whose apply asked to plan again. */
751
- async function runOneWave(options, k, prior, pass) {
760
+ /**
761
+ * Plan wave `k`: read its rules at base and run the before-plan hooks
762
+ * around planning its runs, or for a share job, read the deciding job's
763
+ * decision and plan the share's slice between its hooks. Ends the wave when
764
+ * a hook fails or holds it on another gate, or a share's decision waits or
765
+ * was refused.
766
+ */
767
+ async function planOpWave(options, { k, pass }) {
752
768
  const { spec } = options;
753
769
  const cwd = options.cwd ?? process.cwd();
754
770
  const exec = options.exec ?? spawnExec;
@@ -779,7 +795,7 @@ async function runOneWave(options, k, prior, pass) {
779
795
  ctx.gateRule = { approval: decision.approval, base: decision.base, neverOverMcp: decision.neverOverMcp === true };
780
796
  if (decision.status === "waiting" || decision.status === "refused") {
781
797
  log(`wave ${k} (${wave.name}) is ${decision.status === "refused" ? "refused by its policy" : `waiting on ${decision.heldOn?.gate ?? decision.gate}`}; nothing applied. ${decision.approve ?? ""}`.trim());
782
- return { ...result, exitCode: decision.status === "refused" ? refusedExit(decision) : 3 };
798
+ return { end: { ...result, exitCode: decision.status === "refused" ? refusedExit(decision) : 3 } };
783
799
  }
784
800
  // The share runs the hooks its deciding job read at base, around its own slice.
785
801
  if (wave.discover) {
@@ -790,32 +806,17 @@ async function runOneWave(options, k, prior, pass) {
790
806
  const slice = opWaveShare(wave.runs, options.share, shares);
791
807
  const before = await runHooks(ctx, "before-plan", decision.hooks?.["before-plan"], slice);
792
808
  if (before.failed)
793
- return failed(ctx, `a hook before the plan failed, so nothing in share ${options.share} of wave ${k} was planned or applied`);
809
+ return { end: failed(ctx, `a hook before the plan failed, so nothing in share ${options.share} of wave ${k} was planned or applied`) };
794
810
  if (before.waitingOn)
795
- return waitOnOther(ctx, decision, before.waitingOn);
811
+ return { end: await waitOnOther(ctx, decision, before.waitingOn) };
796
812
  const { members } = planRuns(ctx, slice);
797
813
  const planned = new Map(members.map((m) => [m.member, m]));
798
814
  const after = await runHooks(ctx, "after-plan", decision.hooks?.["after-plan"], slice, planned);
799
815
  if (after.failed)
800
- return failed(ctx, `a hook after the plan failed, so nothing in share ${options.share} of wave ${k} was applied`);
816
+ return { end: failed(ctx, `a hook after the plan failed, so nothing in share ${options.share} of wave ${k} was applied`) };
801
817
  if (after.waitingOn)
802
- return waitOnOther(ctx, decision, after.waitingOn);
803
- const decided = new Map(decision.members.map((m) => [m.member, m.planDigest]));
804
- const unmoved = [];
805
- for (const run of slice) {
806
- const now = planned.get(run.target);
807
- if (decided.get(run.target) !== now.planDigest) {
808
- log(`${run.target}: its plan moved since wave ${k} was decided, so it was not applied. ` +
809
- `decided: ${decided.get(run.target) ?? "(not in the decision)"}; planned now: ${now.planDigest}`);
810
- result.moved.push(run.target);
811
- continue;
812
- }
813
- unmoved.push(run);
814
- }
815
- await applyStage(options, ctx, decision, unmoved, planned, [...before.holds, ...after.holds]);
816
- if (result.exitCode === 0 && result.moved.length > 0)
817
- result.exitCode = 4;
818
- return result;
818
+ return { end: await waitOnOther(ctx, decision, after.waitingOn) };
819
+ return { go: { kind: "share", ctx, decision, slice, planned, holds: [...before.holds, ...after.holds] } };
819
820
  }
820
821
  // The policy, the approval mode and the hooks, read at base before anything runs.
821
822
  const rules = readOpWavePolicy(options.specFile, wave.name, options.base ?? spec.base ?? "HEAD^1", cwd, options.show);
@@ -825,7 +826,7 @@ async function runOneWave(options, k, prior, pass) {
825
826
  noteHooksAtBase(ctx, rules);
826
827
  // #3836: a point at base that cannot be asked fails the wave; the ledger never stands in for it.
827
828
  if (rules.pointRefused)
828
- return failed(ctx, `wave ${k} (${wave.name}): ${rules.pointRefused}, so nothing in it was planned or applied`);
829
+ return { end: failed(ctx, `wave ${k} (${wave.name}): ${rules.pointRefused}, so nothing in it was planned or applied`) };
829
830
  const own = opWavePointAt(spec, wave.name);
830
831
  if (JSON.stringify(own) !== JSON.stringify(rules.point)) {
831
832
  log(`wave ${k} (${wave.name}): its gate's decision point is read at ${base.slice(0, 12)} (${rules.point?.name ?? "none"}), and this change's differs (${own?.name ?? "none"}); the one at base holds`);
@@ -858,9 +859,9 @@ async function runOneWave(options, k, prior, pass) {
858
859
  };
859
860
  const before = await runHooks(ctx, "before-plan", rules.hooks["before-plan"], wave.runs);
860
861
  if (before.failed)
861
- return failed(ctx, `a hook before the plan failed, so nothing in wave ${k} was planned or applied`);
862
+ return { end: failed(ctx, `a hook before the plan failed, so nothing in wave ${k} was planned or applied`) };
862
863
  if (before.waitingOn)
863
- return decided(ctx, decision, recordPath, before.waitingOn);
864
+ return { end: await decided(ctx, decision, recordPath, before.waitingOn) };
864
865
  const { members, pending, overriders, overridersSource, extras, waves: found } = planRuns(ctx, wave.runs, wave.discover === true);
865
866
  if (wave.discover) {
866
867
  wave = { ...wave, runs: members.map((m) => ({ target: m.member, ...(m.op !== spec.op ? { op: m.op } : {}) })) };
@@ -869,47 +870,96 @@ async function runOneWave(options, k, prior, pass) {
869
870
  log(`wave ${k} (${wave.name}): the plan found ${members.length} run${members.length === 1 ? "" : "s"}${found !== undefined ? ` of ${found} waves` : ""}${members.length ? `: ${members.map((m) => m.member).join(", ")}` : ""}`);
870
871
  }
871
872
  const planned = new Map(members.map((m) => [m.member, m]));
872
- const digest = opWaveSetDigest(spec, members, extras);
873
+ return {
874
+ go: {
875
+ kind: "decide",
876
+ ctx,
877
+ recordPath,
878
+ rules,
879
+ decision,
880
+ holds,
881
+ before: before.holds,
882
+ members,
883
+ planned,
884
+ ...(pending ? { pending } : {}),
885
+ ...(overriders ? { overriders } : {}),
886
+ ...(overridersSource ? { overridersSource } : {}),
887
+ ...(extras ? { extras } : {}),
888
+ },
889
+ };
890
+ }
891
+ /**
892
+ * A share job's decision is its deciding job's: it applies the runs of its
893
+ * slice whose plan has not moved since, and names each one that has.
894
+ */
895
+ async function shareUnmoved(planned) {
896
+ const { ctx, decision, slice, holds } = planned;
897
+ const decidedDigests = new Map(decision.members.map((m) => [m.member, m.planDigest]));
898
+ const unmoved = [];
899
+ for (const run of slice) {
900
+ const now = planned.planned.get(run.target);
901
+ if (decidedDigests.get(run.target) !== now.planDigest) {
902
+ ctx.log(`${run.target}: its plan moved since wave ${ctx.k} was decided, so it was not applied. ` +
903
+ `decided: ${decidedDigests.get(run.target) ?? "(not in the decision)"}; planned now: ${now.planDigest}`);
904
+ ctx.result.moved.push(run.target);
905
+ continue;
906
+ }
907
+ unmoved.push(run);
908
+ }
909
+ return { go: { runs: unmoved, earlierHolds: holds } };
910
+ }
911
+ /**
912
+ * Decide wave `k` against its set digest: run the after-plan hooks, settle
913
+ * the product's denials and the gate under the rules read at base, write the
914
+ * decision record and run the after-gate hooks. Ends the wave when it waits,
915
+ * is refused or failed, and for a deciding job (`--decide`), which spends the
916
+ * approval and overrides its shares apply under and applies nothing.
917
+ */
918
+ async function decideOpWave(options, { prior }, planned, digest) {
919
+ const { ctx, recordPath, decision, holds, members, pending, overriders, overridersSource, extras } = planned;
920
+ const { k, wave, log, result } = ctx;
921
+ const { policy, source, base } = planned.rules;
873
922
  decision.digest = digest;
874
923
  decision.members = members;
875
924
  if (extras)
876
925
  decision.extras = extras;
877
926
  if (pending)
878
927
  decision.pending = pending;
879
- const after = await runHooks(ctx, "after-plan", rules.hooks["after-plan"], wave.runs, planned, digest);
928
+ const after = await runHooks(ctx, "after-plan", planned.rules.hooks["after-plan"], wave.runs, planned.planned, digest);
880
929
  if (after.failed)
881
- return failed(ctx, `a hook after the plan failed, so nothing in wave ${k} was applied`);
882
- holds.push(...before.holds, ...after.holds);
930
+ return { end: failed(ctx, `a hook after the plan failed, so nothing in wave ${k} was applied`) };
931
+ holds.push(...planned.before, ...after.holds);
883
932
  if (holds.length > 0)
884
933
  decision.heldBy = holds;
885
934
  if (after.waitingOn)
886
- return decided(ctx, decision, recordPath, after.waitingOn);
935
+ return { end: await decided(ctx, decision, recordPath, after.waitingOn) };
887
936
  // #3821: a run the product's policy denied applies only under an override; with none the wave is refused, and no approval is waited for.
888
- const kept = prior !== undefined && (prior.status === "approved" || prior.status === "not-required") && prior.digest === digest;
937
+ const was = prior?.decision;
938
+ const kept = was !== undefined && (was.status === "approved" || was.status === "not-required") && was.digest === digest;
889
939
  if (kept) {
890
940
  // The overrides this job applied under, and spent, still let the same plans through.
891
- const byRun = new Map(prior.members.map((m) => [m.member, m.override]));
941
+ const byRun = new Map(was.members.map((m) => [m.member, m.override]));
892
942
  for (const m of members)
893
943
  if (m.denied && byRun.get(m.member))
894
944
  m.override = byRun.get(m.member);
895
- if (prior.overridesSpentAt)
896
- decision.overridesSpentAt = prior.overridesSpentAt;
945
+ if (was.overridesSpentAt)
946
+ decision.overridesSpentAt = was.overridesSpentAt;
897
947
  }
898
948
  if (members.some((m) => m.denied && !m.override))
899
949
  await settleDenials(ctx, decision, overriders, overridersSource);
900
- if (kept && prior.status === "approved" && decision.status !== "refused") {
950
+ if (kept && was.status === "approved" && decision.status !== "refused") {
901
951
  // #3821: planned again after the apply asked to, to the same digest: the approval this job found still holds, and is not spent twice.
902
952
  for (const key of ["via", "approvedBy", "approvedAt", "review", "spentAt"])
903
- if (prior[key] !== undefined)
904
- decision[key] = prior[key];
953
+ if (was[key] !== undefined)
954
+ decision[key] = was[key];
905
955
  decision.status = "approved";
906
- log(`wave ${k} (${wave.name}): planned again to the same digest, so the approval by ${prior.approvedBy} still holds`);
956
+ log(`wave ${k} (${wave.name}): planned again to the same digest, so the approval by ${was.approvedBy} still holds`);
907
957
  }
908
958
  else if (decision.status !== "refused" && (opWaveNeedsApproval(policy, members) || holds.length > 0))
909
959
  await settleGate(options, ctx, decision);
910
960
  const code = await decided(ctx, decision, recordPath);
911
961
  if (code.exitCode !== 0)
912
- return code;
962
+ return { end: code };
913
963
  log(`wave ${k} (${wave.name}): gate ${policy} (read at ${base.slice(0, 12)}${source === "absent-at-base" ? ", where the wave is absent" : ""}), ` +
914
964
  `digest ${digest}: ${decision.status}${decision.approvedBy ? ` by ${decision.approvedBy}` : ""}` +
915
965
  (decision.via === "pr-review" ? ` (review of #${decision.review?.pr})` : "") +
@@ -917,13 +967,20 @@ async function runOneWave(options, k, prior, pass) {
917
967
  // A wide wave's shares apply under the deciding job's approval and overrides, so they are spent here, once.
918
968
  if (options.decide) {
919
969
  if (!(await spendApproval(options, ctx, decision)) || !(await spendOverrides(ctx, decision)))
920
- return ctx.result;
970
+ return { end: ctx.result };
921
971
  if (decision.spentAt || decision.overridesSpentAt)
922
972
  writeRecord(recordPath, decision, result);
923
- return result;
973
+ return { end: result };
924
974
  }
925
- await applyStage(options, ctx, decision, wave.runs, planned, [], recordPath);
926
- return result;
975
+ return { go: { runs: wave.runs, earlierHolds: [], recordPath } };
976
+ }
977
+ /** Apply a decided wave, or a share's unmoved runs (exit 4 when any moved). */
978
+ async function applyOpWave(options, planned, decidedWave) {
979
+ const { ctx, decision } = planned;
980
+ await applyStage(options, ctx, decision, decidedWave.runs, planned.planned, decidedWave.earlierHolds, decidedWave.recordPath);
981
+ if (planned.kind === "share" && ctx.result.exitCode === 0 && ctx.result.moved.length > 0)
982
+ ctx.result.exitCode = 4;
983
+ return ctx.result;
927
984
  }
928
985
  /**
929
986
  * The gate has decided (#3821): write the decision record, run the
@@ -8,6 +8,9 @@
8
8
  * - The share split of a wide wave: {@link waveShare}.
9
9
  * - The gate decision call: {@link decideWaveGate}.
10
10
  * - The record file a wave job hands the next one: {@link writeWaveRecordFile}.
11
+ * - The loop itself: {@link runWaveLoop} plans a wave, takes its set digest,
12
+ * decides its gate and applies it, then goes on to the next wave or plans
13
+ * the same one again, as the entry point's steps say.
11
14
  *
12
15
  * The parts that need neither the gate ledger nor the filesystem live in
13
16
  * `./gated-waves.ts`, which terragucci bundles on its own (#3421), and
@@ -15,11 +18,12 @@
15
18
  * use it without the change-set code; this module re-exports both so a
16
19
  * runner imports the engine from one place.
17
20
  *
18
- * What stays with each entry point: how a wave is planned (Op runs' plan
19
- * commands, or each component's capabilities), the gate policy, approval
20
- * mode, decision point, hooks and spend rule (Op waves only), and the shape
21
- * of the record each writes. Those shapes are read by jobs and approvals
22
- * already in the wild, so the engine writes what it is handed.
21
+ * What stays with each entry point, as the steps it hands {@link runWaveLoop}:
22
+ * how a wave is planned (Op runs' plan commands, or each component's
23
+ * capabilities), which members its digest covers, the gate policy, approval
24
+ * mode, decision point, hooks and spend rule (Op waves only), how it applies,
25
+ * and the shape of the record each writes. Those shapes are read by jobs and
26
+ * approvals already in the wild, so the engine writes what it is handed.
23
27
  */
24
28
  import { type GateCheck, type GateLedgerPort } from "./op/gate.js";
25
29
  import type { AppliedGateRecord } from "./lifecycle/gate-applied.js";
@@ -59,4 +63,69 @@ export declare function decideWaveGate(port: GateLedgerPort, request: WaveGateRe
59
63
  * through it.
60
64
  */
61
65
  export declare function writeWaveRecordFile(path: string, value: unknown): void;
66
+ /**
67
+ * What a step hands {@link runWaveLoop}: `go` carries a value into the
68
+ * wave's next step; `end` ends the wave with its outcome, and nothing after
69
+ * that step runs for it.
70
+ */
71
+ export type WaveStep<T, R> = {
72
+ go: T;
73
+ } | {
74
+ end: R;
75
+ };
76
+ /** Where the loop is: wave `k` (1-based), its `pass`, and the outcome of the pass before when the wave is planned again. */
77
+ export interface WaveAttempt<R> {
78
+ k: number;
79
+ /** 0 on a wave's first plan. */
80
+ pass: number;
81
+ /** The outcome of the pass that asked to plan again. */
82
+ prior?: R;
83
+ }
84
+ /**
85
+ * The steps an entry point hands {@link runWaveLoop}, typed by what planning
86
+ * gives (`P`), what deciding gives (`D`) and a wave's outcome (`R`).
87
+ *
88
+ * The two entry points differ in each step, on purpose:
89
+ *
90
+ * - `chant run wave` (Op waves) plans with argv commands and hooks, takes
91
+ * the digest under `digest.over` with the plans file's extras, decides
92
+ * with the approval mode, decision point, spend rule, denials and pending
93
+ * fields read at base, and runs one wave per job (a `rest` wave walks on,
94
+ * an apply may ask to plan again; a share job applies under its deciding
95
+ * job's decision).
96
+ * - `chant components fan-out --wave-gate` plans each component's
97
+ * capabilities, takes the digest over every member (a wave where nothing
98
+ * planned has the empty digest, and one where nothing is left to run skips
99
+ * its gate), asks the plain ledger, and walks every wave in one job.
100
+ */
101
+ export interface WaveLoopSteps<P, D, R> {
102
+ /** The first wave's number. */
103
+ first: number;
104
+ /** Plan the wave, or end it before it has a set (nothing to run, a hook failed or waits). */
105
+ plan(at: WaveAttempt<R>): Promise<WaveStep<P, R>>;
106
+ /** The wave's set digest over what was planned. */
107
+ digest(at: WaveAttempt<R>, planned: P): string;
108
+ /** Decide the wave's gate against its digest: what the apply goes ahead under, or the outcome when the wave stops here. */
109
+ decide(at: WaveAttempt<R>, planned: P, digest: string): Promise<WaveStep<D, R>>;
110
+ /** Apply the decided wave. */
111
+ apply(at: WaveAttempt<R>, planned: P, decided: D): Promise<R>;
112
+ /** Whether the outcome asks for the same wave to be planned again in this job. Absent: never. */
113
+ replan?(at: WaveAttempt<R>, outcome: R): boolean;
114
+ /** How many passes one wave gets before a replan is refused. Default 1. */
115
+ maxPasses?: number;
116
+ /** The wave asked to plan again after its last pass: the outcome it ends with. */
117
+ passesSpent?(at: WaveAttempt<R>, outcome: R): R;
118
+ /** Called before another pass at the same wave. */
119
+ replanning?(at: WaveAttempt<R>, outcome: R): void;
120
+ /** The wave ended with `outcome`: whether the job goes on to wave `k + 1`. */
121
+ onward(k: number, outcome: R): boolean;
122
+ }
123
+ /**
124
+ * The wave loop both entry points run (#3766): for each wave, plan it, take
125
+ * its set digest, decide its gate and apply it; plan it again while its
126
+ * outcome asks to, up to `maxPasses`; then go on to the next wave while
127
+ * `onward` says so. Returns the outcome of the last wave it ran. A step that
128
+ * throws ends the loop with that error.
129
+ */
130
+ export declare function runWaveLoop<P, D, R>(steps: WaveLoopSteps<P, D, R>): Promise<R>;
62
131
  //# sourceMappingURL=wave-engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wave-engine.d.ts","sourceRoot":"","sources":["../src/wave-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,EAAgB,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAC9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,EACL,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,eAAe,EACf,cAAc,EACd,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,UAAU,GAChB,MAAM,eAAe,CAAC;AAEvB,iCAAiC;AACjC,MAAM,WAAW,eAAe;IAC9B,wFAAwF;IACxF,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,KAAK,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACrC,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,uDAAuD;IACvD,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAYjG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAKtE"}
1
+ {"version":3,"file":"wave-engine.d.ts","sourceRoot":"","sources":["../src/wave-engine.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,9 @@
8
8
  * - The share split of a wide wave: {@link waveShare}.
9
9
  * - The gate decision call: {@link decideWaveGate}.
10
10
  * - The record file a wave job hands the next one: {@link writeWaveRecordFile}.
11
+ * - The loop itself: {@link runWaveLoop} plans a wave, takes its set digest,
12
+ * decides its gate and applies it, then goes on to the next wave or plans
13
+ * the same one again, as the entry point's steps say.
11
14
  *
12
15
  * The parts that need neither the gate ledger nor the filesystem live in
13
16
  * `./gated-waves.ts`, which terragucci bundles on its own (#3421), and
@@ -15,11 +18,12 @@
15
18
  * use it without the change-set code; this module re-exports both so a
16
19
  * runner imports the engine from one place.
17
20
  *
18
- * What stays with each entry point: how a wave is planned (Op runs' plan
19
- * commands, or each component's capabilities), the gate policy, approval
20
- * mode, decision point, hooks and spend rule (Op waves only), and the shape
21
- * of the record each writes. Those shapes are read by jobs and approvals
22
- * already in the wild, so the engine writes what it is handed.
21
+ * What stays with each entry point, as the steps it hands {@link runWaveLoop}:
22
+ * how a wave is planned (Op runs' plan commands, or each component's
23
+ * capabilities), which members its digest covers, the gate policy, approval
24
+ * mode, decision point, hooks and spend rule (Op waves only), how it applies,
25
+ * and the shape of the record each writes. Those shapes are read by jobs and
26
+ * approvals already in the wild, so the engine writes what it is handed.
23
27
  */
24
28
  import { dirname } from "node:path";
25
29
  import { mkdirSync, renameSync, writeFileSync } from "node:fs";
@@ -57,3 +61,41 @@ export function writeWaveRecordFile(path, value) {
57
61
  writeFileSync(temporary, JSON.stringify(value, null, 2) + "\n");
58
62
  renameSync(temporary, path);
59
63
  }
64
+ /**
65
+ * The wave loop both entry points run (#3766): for each wave, plan it, take
66
+ * its set digest, decide its gate and apply it; plan it again while its
67
+ * outcome asks to, up to `maxPasses`; then go on to the next wave while
68
+ * `onward` says so. Returns the outcome of the last wave it ran. A step that
69
+ * throws ends the loop with that error.
70
+ */
71
+ export async function runWaveLoop(steps) {
72
+ for (let k = steps.first;; k++) {
73
+ let outcome;
74
+ let prior;
75
+ for (let pass = 0;; pass++) {
76
+ const at = { k, pass, ...(prior !== undefined ? { prior } : {}) };
77
+ outcome = await runWave(steps, at);
78
+ if (!steps.replan?.(at, outcome))
79
+ break;
80
+ if (pass + 1 >= (steps.maxPasses ?? 1)) {
81
+ outcome = steps.passesSpent ? steps.passesSpent(at, outcome) : outcome;
82
+ break;
83
+ }
84
+ steps.replanning?.(at, outcome);
85
+ prior = outcome;
86
+ }
87
+ if (!steps.onward(k, outcome))
88
+ return outcome;
89
+ }
90
+ }
91
+ /** One pass at one wave: plan, digest, decide, apply. */
92
+ async function runWave(steps, at) {
93
+ const planned = await steps.plan(at);
94
+ if ("end" in planned)
95
+ return planned.end;
96
+ const digest = steps.digest(at, planned.go);
97
+ const decided = await steps.decide(at, planned.go, digest);
98
+ if ("end" in decided)
99
+ return decided.end;
100
+ return steps.apply(at, planned.go, decided.go);
101
+ }