@jevitate/cli 0.18.1 → 0.20.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.
package/dist/bin.js CHANGED
@@ -39690,15 +39690,23 @@ var INSTRUMENT = `(() => {
39690
39690
  })();`;
39691
39691
  var MAX_CAPTURED = 2e4;
39692
39692
  var RequestCapture = class {
39693
+ /**
39694
+ * #617: every request that finished during the capture (its whole completion history), each
39695
+ * tagged with whether THIS capture saw it begin. A request already in flight when the capture
39696
+ * started still finishes into it, but was not sent by the run the capture covers.
39697
+ */
39693
39698
  #requests = [];
39694
39699
  /** #283: requests sent since the capture started that have not finished (keyed by the request). */
39695
39700
  #inflight = /* @__PURE__ */ new Map();
39696
39701
  /** #283: requests sent that never reported an end (their document went away first). */
39697
39702
  #unfinished = [];
39698
39703
  #truncated = false;
39699
- /** @internal — fed by the page's monitor. */
39700
- add(r) {
39701
- this.#requests.push(r);
39704
+ /**
39705
+ * @internal — fed by the page's monitor. `sent` is false for a completion the capture never saw
39706
+ * begin (#617); it stays in the completion history but is not counted as sent by the capture.
39707
+ */
39708
+ add(r, sent = true) {
39709
+ this.#requests.push({ r, sent });
39702
39710
  if (this.#requests.length > MAX_CAPTURED) {
39703
39711
  this.#requests.shift();
39704
39712
  this.#truncated = true;
@@ -39716,22 +39724,27 @@ var RequestCapture = class {
39716
39724
  const sent = this.#inflight.get(key2);
39717
39725
  this.#inflight.delete(key2);
39718
39726
  if (finished !== null)
39719
- this.add(finished);
39727
+ this.add(finished, sent !== void 0);
39720
39728
  else if (sent !== void 0 && this.#unfinished.length < MAX_CAPTURED)
39721
39729
  this.#unfinished.push(sent);
39722
39730
  }
39723
- /** The requests captured so far, in the order they finished. */
39731
+ /** The requests captured so far, in the order they finished, including ones the capture never saw begin. */
39724
39732
  requests() {
39725
- return [...this.#requests];
39733
+ return this.#requests.map((e) => e.r);
39726
39734
  }
39727
39735
  /**
39728
- * Every request SENT since the capture started (#283): the finished ones (`requests()`), then the
39729
- * ones still in flight or abandoned without an end event (`pending: true`, `status: null`). A
39730
- * `requestMade` check is judged over these — a request counts once it was sent, whether or not
39731
- * its response has arrived.
39736
+ * Every request SENT since the capture started (#283): the finished ones this capture saw begin,
39737
+ * then the ones still in flight or abandoned without an end event (`pending: true`, `status: null`).
39738
+ * A `requestMade` check is judged over these — a request counts once it was sent, whether or not
39739
+ * its response has arrived. A request that began before the capture started is never among them
39740
+ * (#617): only its completion shows up, in `requests()`.
39732
39741
  */
39733
39742
  sent() {
39734
- return [...this.#requests, ...this.#unfinished, ...this.#inflight.values()];
39743
+ return [
39744
+ ...this.#requests.filter((e) => e.sent).map((e) => e.r),
39745
+ ...this.#unfinished,
39746
+ ...this.#inflight.values()
39747
+ ];
39735
39748
  }
39736
39749
  /** True when more requests finished than the capture keeps (the oldest were dropped). */
39737
39750
  get truncated() {
@@ -53640,6 +53653,72 @@ function mutationPairIssues(journey) {
53640
53653
  });
53641
53654
  return out;
53642
53655
  }
53656
+ function targetedProofPlanIssues(journey) {
53657
+ const plans = journey.metadata.targetedProofs ?? [];
53658
+ if (plans.length === 0)
53659
+ return [];
53660
+ const sites = new Set(journeyAssertions(journey).map(assertionSiteKey));
53661
+ const steps = flat(journey);
53662
+ const out = [];
53663
+ const seen = /* @__PURE__ */ new Set();
53664
+ plans.forEach((plan2, index) => {
53665
+ const key2 = `${plan2.check}\0${plan2.mustFailWhen}`;
53666
+ if (seen.has(key2))
53667
+ out.push({ index, field: "check", message: `targetedProofs: duplicate plan for ${plan2.check} and ${plan2.mustFailWhen}` });
53668
+ seen.add(key2);
53669
+ if (!SITE_RE.test(plan2.check)) {
53670
+ out.push({ index, field: "check", message: `targetedProofs: ${JSON.stringify(plan2.check)} is not an assertion site \u2014 expected step:<n>, step-request:<n>:<i> or end-state:<i>` });
53671
+ } else if (!sites.has(plan2.check)) {
53672
+ out.push({ index, field: "check", message: `targetedProofs: the Journey has no assertion at ${plan2.check}` });
53673
+ }
53674
+ let mutated;
53675
+ try {
53676
+ mutated = resolveMutationRef(journey, plan2.mustFailWhen);
53677
+ } catch (err) {
53678
+ out.push({ index, field: "mustFailWhen", message: `targetedProofs: ${err instanceof Error ? err.message : String(err)}` });
53679
+ }
53680
+ const check3 = /^step:([1-9][0-9]*)$/.exec(plan2.check);
53681
+ const targetStep = check3 === null ? null : Number(check3[1]);
53682
+ const relaxed2 = /* @__PURE__ */ new Set();
53683
+ for (const entry of plan2.relax) {
53684
+ if (!/^step:[1-9][0-9]*$/.test(entry)) {
53685
+ out.push({ index, field: "relax", message: `targetedProofs: ${JSON.stringify(entry)} is not a step assertion site \u2014 expected step:<n>` });
53686
+ continue;
53687
+ }
53688
+ if (relaxed2.has(entry)) {
53689
+ out.push({ index, field: "relax", message: `targetedProofs: ${entry} is relaxed twice` });
53690
+ continue;
53691
+ }
53692
+ relaxed2.add(entry);
53693
+ const n3 = Number(entry.slice("step:".length));
53694
+ const recorded = steps[n3 - 1];
53695
+ if (recorded === void 0) {
53696
+ out.push({ index, field: "relax", message: `targetedProofs: the Journey has no step ${n3}` });
53697
+ continue;
53698
+ }
53699
+ if (targetStep === n3) {
53700
+ out.push({ index, field: "relax", message: `targetedProofs: ${entry} is the target assertion \u2014 a declared proof never relaxes it` });
53701
+ continue;
53702
+ }
53703
+ if (targetStep !== null && n3 > targetStep) {
53704
+ out.push({ index, field: "relax", message: `targetedProofs: ${entry} is not before the target step:${targetStep}` });
53705
+ continue;
53706
+ }
53707
+ if (recorded.step.kind === "handback") {
53708
+ out.push({ index, field: "relax", message: `targetedProofs: ${entry} is a resume assertion \u2014 a declared proof never relaxes it` });
53709
+ continue;
53710
+ }
53711
+ if (recorded.step.kind !== "assert" && !("expect" in recorded.step)) {
53712
+ out.push({ index, field: "relax", message: `targetedProofs: ${entry} has no page postcondition to relax` });
53713
+ continue;
53714
+ }
53715
+ if (mutated !== void 0 && n3 < mutated.step) {
53716
+ out.push({ index, field: "relax", message: `targetedProofs: ${entry} is before the mutated step ${mutated.step} (${plan2.mustFailWhen})` });
53717
+ }
53718
+ }
53719
+ });
53720
+ return out;
53721
+ }
53643
53722
  function expectedText(a) {
53644
53723
  if (a.kind === "valueEquals")
53645
53724
  return typeof a.value === "string" ? { kind: a.kind, text: a.value } : null;
@@ -53836,13 +53915,14 @@ function relaxedMutationReplay(journey, m, opts = {}) {
53836
53915
  return { journey, skipIndex: index };
53837
53916
  if (m.kind === "swap-param" || m.kind === "truncate-param" || m.kind === "guard-param") {
53838
53917
  const how = paramReplay(journey, m, opts.params ?? {});
53839
- const target2 = flat(how.journey)[index];
53918
+ const copy2 = how.journey === journey ? structuredClone(journey) : how.journey;
53919
+ const target2 = flat(copy2)[index];
53840
53920
  if (target2 !== void 0 && "expect" in target2.step) {
53841
53921
  const step = { ...target2.step, expect: relaxedExpect(target2.step) };
53842
53922
  delete step.waitFor;
53843
53923
  target2.step = step;
53844
53924
  }
53845
- return how;
53925
+ return { ...how, journey: copy2 };
53846
53926
  }
53847
53927
  if (m.kind === "block-write") {
53848
53928
  const requests = journeyWriteSteps(journey, opts).find((w) => w.step === m.step)?.requests ?? [];
@@ -53866,6 +53946,174 @@ function relaxedMutationReplay(journey, m, opts = {}) {
53866
53946
  }
53867
53947
  return { journey: copy };
53868
53948
  }
53949
+ var MAX_TARGETED_RELAXATIONS = 4;
53950
+ var MAX_DECLARED_RELAXATIONS = 8;
53951
+ function failedSiteKey(f2) {
53952
+ if (f2.where === "step")
53953
+ return `step:${f2.step}`;
53954
+ if (f2.where === "step-request")
53955
+ return `step-request:${f2.step}:${f2.checkIndex}`;
53956
+ if (f2.where === "end-state")
53957
+ return `end-state:${f2.index}`;
53958
+ return "unevaluable";
53959
+ }
53960
+ function targetedMutationReplay(journey, m, targetSiteKey, relaxedSiteKeys, opts = {}) {
53961
+ if (relaxedSiteKeys.includes(targetSiteKey))
53962
+ throw new Error(`${targetSiteKey} is the target assertion \u2014 a targeted proof never relaxes it`);
53963
+ const base = relaxedMutationReplay(journey, m, opts);
53964
+ if (relaxedSiteKeys.length === 0)
53965
+ return base;
53966
+ return { ...base, journey: relaxStepSites(journey, base.journey, relaxedSiteKeys) };
53967
+ }
53968
+ function targetedControlReplay(journey, m, relaxedSiteKeys) {
53969
+ const own2 = flat(journey)[m.step - 1]?.step;
53970
+ const ownKey = own2 !== void 0 && (own2.kind === "assert" || "expect" in own2) ? [`step:${m.step}`] : [];
53971
+ return { journey: relaxStepSites(journey, journey, [.../* @__PURE__ */ new Set([...ownKey, ...relaxedSiteKeys])]) };
53972
+ }
53973
+ function relaxStepSites(journey, from, keys) {
53974
+ const byKey = new Map(journeyAssertions(journey).map((s3) => [assertionSiteKey(s3), s3]));
53975
+ const copy = structuredClone(from);
53976
+ const steps = flat(copy);
53977
+ for (const key2 of keys) {
53978
+ const match2 = /^step:([1-9][0-9]*)$/.exec(key2);
53979
+ if (match2 === null)
53980
+ throw new Error(`${key2} is not a step assertion site \u2014 a targeted proof only relaxes step:<n>`);
53981
+ const site = byKey.get(key2);
53982
+ if (site !== void 0 && site.where === "step" && site.field === "resume")
53983
+ throw new Error(`${key2} is a resume assertion \u2014 a targeted proof never relaxes it`);
53984
+ const recorded = steps[Number(match2[1]) - 1];
53985
+ if (recorded === void 0)
53986
+ throw new Error(`journey '${journey.metadata.id}' has no step ${match2[1]} to relax`);
53987
+ const step = { ...recorded.step };
53988
+ if (recorded.step.kind === "assert")
53989
+ step.check = { kind: "urlIncludes", text: "" };
53990
+ else if ("expect" in recorded.step)
53991
+ step.expect = relaxedExpect(recorded.step);
53992
+ else
53993
+ throw new Error(`${key2} has no page postcondition to relax`);
53994
+ delete step.waitFor;
53995
+ recorded.step = step;
53996
+ }
53997
+ return copy;
53998
+ }
53999
+ function nextTargetedRelaxation(journey, plan2, m, targetSiteKey, result, relaxedSoFar) {
54000
+ const target = journeyAssertions(journey).find((s3) => assertionSiteKey(s3) === targetSiteKey);
54001
+ if (target === void 0)
54002
+ return { refuse: `${targetSiteKey} is not an assertion site of this Journey` };
54003
+ if (classifyMutation(target, m, result) !== "cascade")
54004
+ return null;
54005
+ const first = result.failedSites[0];
54006
+ if (first === void 0)
54007
+ return { refuse: "no failed site to relax" };
54008
+ if (first.where === "unevaluable")
54009
+ return { refuse: "unevaluable end state" };
54010
+ if (first.where !== "step")
54011
+ return { refuse: `${failedSiteKey(first)} is not a step postcondition` };
54012
+ const key2 = `step:${first.step}`;
54013
+ if (!first.postcondition)
54014
+ return { refuse: `${key2} action failed (not a postcondition)` };
54015
+ if (first.step < m.step)
54016
+ return { refuse: "failure before the mutated step" };
54017
+ const step = flat(journey)[first.step - 1]?.step;
54018
+ if (step === void 0)
54019
+ return { refuse: `${key2} does not exist` };
54020
+ if (step.kind === "handback")
54021
+ return { refuse: `${key2} is a resume assertion (not a relaxable page postcondition)` };
54022
+ if (target.where === "step" && target.step <= first.step)
54023
+ return { refuse: `${key2} is not before the target ${targetSiteKey}` };
54024
+ if (!plan2.pairs.some((p) => p.site === key2 && p.mutation === m.id))
54025
+ return { refuse: `${key2} is not paired with ${m.id}` };
54026
+ if (relaxedSoFar.includes(key2))
54027
+ return { refuse: `${key2} is already relaxed` };
54028
+ if (relaxedSoFar.length >= MAX_TARGETED_RELAXATIONS)
54029
+ return { refuse: `relaxation bound ${MAX_TARGETED_RELAXATIONS} reached` };
54030
+ return { relax: key2 };
54031
+ }
54032
+ async function runTargetedProof(journey, plan2, m, target, initial, replay, opts = {}) {
54033
+ const site = assertionSiteKey(target);
54034
+ const relaxedSites = [];
54035
+ const relaxedFirst = initial.relaxed !== void 0 && initial.failedSites.some((f2) => f2.where === "step" && f2.step === m.step && f2.postcondition) && !(target.where === "step" && target.step === m.step);
54036
+ let run = relaxedFirst ? { outcome: initial.relaxed.outcome, failedSites: initial.relaxed.failedSites } : { outcome: initial.outcome, failedSites: initial.failedSites };
54037
+ for (; ; ) {
54038
+ const next = nextTargetedRelaxation(journey, plan2, m, site, run, relaxedSites);
54039
+ if (next === null)
54040
+ break;
54041
+ if ("refuse" in next)
54042
+ return { site, relaxedSites, outcome: run.outcome, failedSites: run.failedSites, refused: next.refuse };
54043
+ relaxedSites.push(next.relax);
54044
+ const relaxedNow = [...relaxedSites];
54045
+ run = await replay((j) => targetedMutationReplay(j, m, site, relaxedNow, opts), "targeted");
54046
+ }
54047
+ if (relaxedSites.length > 0 && classifyMutationPlain(target, m, run) === "sensitive") {
54048
+ const control = await replay((j) => targetedControlReplay(j, m, relaxedSites), "control");
54049
+ if (control.outcome !== "passed") {
54050
+ return {
54051
+ site,
54052
+ relaxedSites,
54053
+ outcome: run.outcome,
54054
+ failedSites: run.failedSites,
54055
+ control: control.outcome,
54056
+ refused: `the unmutated control replay with ${relaxedSites.join(", ")} relaxed did not pass (${control.outcome})`
54057
+ };
54058
+ }
54059
+ return { site, relaxedSites, outcome: run.outcome, failedSites: run.failedSites, control: control.outcome };
54060
+ }
54061
+ return { site, relaxedSites, outcome: run.outcome, failedSites: run.failedSites };
54062
+ }
54063
+ async function runDeclaredTargetedProof(journey, plan2, m, target, declared, replay, opts = {}) {
54064
+ const site = assertionSiteKey(target);
54065
+ const relax = declared.relax;
54066
+ const unpaired = relax.find((key2) => !plan2.pairs.some((p) => p.site === key2 && p.mutation === m.id));
54067
+ if (unpaired !== void 0) {
54068
+ return { site, relaxedSites: relax, outcome: "error", failedSites: [], declared: true, refused: `${unpaired} is not paired with ${m.id}` };
54069
+ }
54070
+ const relaxedNow = [...relax];
54071
+ const run = await replay((j) => targetedMutationReplay(j, m, site, relaxedNow, opts), "targeted");
54072
+ const first = run.failedSites[0];
54073
+ if (first !== void 0 && first.where === "step" && first.postcondition && !relax.includes(`step:${first.step}`) && (target.where !== "step" || first.step < target.step)) {
54074
+ const key2 = `step:${first.step}`;
54075
+ return { site, relaxedSites: relax, outcome: run.outcome, failedSites: run.failedSites, declared: true, refused: `${key2} blocked the declared proof (not in relax)` };
54076
+ }
54077
+ if (classifyMutationPlain(target, m, run) === "sensitive") {
54078
+ const control = await replay((j) => targetedControlReplay(j, m, relaxedNow), "control");
54079
+ if (control.outcome !== "passed") {
54080
+ return {
54081
+ site,
54082
+ relaxedSites: relax,
54083
+ outcome: run.outcome,
54084
+ failedSites: run.failedSites,
54085
+ declared: true,
54086
+ control: control.outcome,
54087
+ refused: `the unmutated control replay with ${relax.join(", ")} relaxed did not pass (${control.outcome})`
54088
+ };
54089
+ }
54090
+ return { site, relaxedSites: relax, outcome: run.outcome, failedSites: run.failedSites, declared: true, control: control.outcome };
54091
+ }
54092
+ return { site, relaxedSites: relax, outcome: run.outcome, failedSites: run.failedSites, declared: true };
54093
+ }
54094
+ function suggestTargetedProofPlan(proof, m) {
54095
+ if (proof.refused !== `relaxation bound ${MAX_TARGETED_RELAXATIONS} reached`)
54096
+ return null;
54097
+ const first = proof.failedSites[0];
54098
+ if (first === void 0 || first.where !== "step")
54099
+ return null;
54100
+ const relax = [...proof.relaxedSites, `step:${first.step}`];
54101
+ if (relax.length > MAX_DECLARED_RELAXATIONS)
54102
+ return null;
54103
+ return { check: proof.site, mustFailWhen: m.id, relax };
54104
+ }
54105
+ function declaredTargetedProofFor(journey, m, siteKey) {
54106
+ for (const plan2 of journey.metadata.targetedProofs ?? []) {
54107
+ if (plan2.check !== siteKey)
54108
+ continue;
54109
+ try {
54110
+ if (resolveMutationRef(journey, plan2.mustFailWhen).id === m.id)
54111
+ return plan2;
54112
+ } catch {
54113
+ }
54114
+ }
54115
+ return void 0;
54116
+ }
53869
54117
  function needsRelaxedReplay(plan2, m, result) {
53870
54118
  if (result.outcome !== "failed")
53871
54119
  return false;
@@ -53875,8 +54123,18 @@ function needsRelaxedReplay(plan2, m, result) {
53875
54123
  return plan2.pairs.some((p) => p.mutation === m.id && p.site !== own2);
53876
54124
  }
53877
54125
  function classifyMutation(site, m, result) {
54126
+ const verdict = classifyMutationPlain(site, m, result);
54127
+ if (verdict !== "cascade")
54128
+ return verdict;
54129
+ const proof = result.targeted?.find((t) => t.site === assertionSiteKey(site));
54130
+ if (proof === void 0 || proof.refused !== void 0)
54131
+ return "cascade";
54132
+ const targeted = classifyMutationPlain(site, m, { outcome: proof.outcome, failedSites: proof.failedSites });
54133
+ return targeted === "sensitive" || targeted === "insensitive" ? targeted : "cascade";
54134
+ }
54135
+ function classifyMutationPlain(site, m, result) {
53878
54136
  if (result.relaxed !== void 0 && result.failedSites.some((f2) => f2.where === "step" && f2.step === m.step && f2.postcondition) && !(site.where === "step" && site.step === m.step)) {
53879
- return classifyMutation(site, m, { outcome: result.relaxed.outcome, failedSites: result.relaxed.failedSites });
54137
+ return classifyMutationPlain(site, m, { outcome: result.relaxed.outcome, failedSites: result.relaxed.failedSites });
53880
54138
  }
53881
54139
  if (result.outcome === "passed")
53882
54140
  return "insensitive";
@@ -53901,18 +54159,52 @@ function classifyMutation(site, m, result) {
53901
54159
  return fs.some((f2) => f2.where === "end-state" && f2.index === site.index) ? "sensitive" : "cascade";
53902
54160
  }
53903
54161
  var PRECEDENCE = ["sensitive", "insensitive", "cascade", "not-applied", "error"];
54162
+ function provenSensitive(journey, m, results, key2, visiting) {
54163
+ if (visiting.has(key2))
54164
+ return false;
54165
+ const site = journeyAssertions(journey).find((s3) => assertionSiteKey(s3) === key2);
54166
+ const result = results.get(m.id) ?? { outcome: "error", failedSites: [] };
54167
+ if (site === void 0 || classifyMutation(site, m, result) !== "sensitive")
54168
+ return false;
54169
+ const proof = result.targeted?.find((t) => t.site === key2);
54170
+ if (proof === void 0 || proof.refused !== void 0)
54171
+ return true;
54172
+ const next = /* @__PURE__ */ new Set([...visiting, key2]);
54173
+ return proof.relaxedSites.every((relaxed2) => provenSensitive(journey, m, results, relaxed2, next));
54174
+ }
53904
54175
  function judgeAssertions(journey, plan2, results) {
54176
+ const proofFor = (m, key2) => results.get(m.id)?.targeted?.find((t) => t.site === key2);
53905
54177
  return journeyAssertions(journey).map((site) => {
53906
54178
  const key2 = assertionSiteKey(site);
53907
54179
  const paired = plan2.mutations.filter((m) => plan2.pairs.some((p) => p.site === key2 && p.mutation === m.id));
53908
54180
  if (paired.length === 0)
53909
54181
  return { site: key2, verdict: "unpaired" };
53910
54182
  const verdicts = paired.map((m) => ({ m, v: classifyMutation(site, m, results.get(m.id) ?? { outcome: "error", failedSites: [] }) }));
54183
+ let unsound;
53911
54184
  for (const v of PRECEDENCE) {
53912
54185
  const first = verdicts.find((x) => x.v === v);
53913
- if (first === void 0)
54186
+ if (first === void 0 && !(v === "cascade" && unsound !== void 0))
53914
54187
  continue;
53915
- return v === "sensitive" ? { site: key2, verdict: v, provedBy: first.m.id } : { site: key2, verdict: v };
54188
+ if (v === "sensitive") {
54189
+ for (const { m } of verdicts.filter((x) => x.v === "sensitive")) {
54190
+ const proof = proofFor(m, key2);
54191
+ if (proof === void 0 || proof.refused !== void 0)
54192
+ return { site: key2, verdict: v, provedBy: m.id };
54193
+ const bad = proof.relaxedSites.find((relaxed2) => !provenSensitive(journey, m, results, relaxed2, /* @__PURE__ */ new Set([key2])));
54194
+ if (bad === void 0)
54195
+ return { site: key2, verdict: v, provedBy: m.id, relaxedSites: proof.relaxedSites };
54196
+ unsound ??= `relaxed site ${bad} is not itself proven sensitive to ${m.id}`;
54197
+ }
54198
+ continue;
54199
+ }
54200
+ if (v === "cascade" && unsound !== void 0)
54201
+ return { site: key2, verdict: v, targetedRefusal: unsound };
54202
+ if (v === "cascade") {
54203
+ const refusal = paired.map((m) => proofFor(m, key2)).find((t) => t !== void 0 && t.refused !== void 0);
54204
+ if (refusal !== void 0)
54205
+ return { site: key2, verdict: v, targetedRefusal: refusal.refused };
54206
+ }
54207
+ return { site: key2, verdict: v };
53916
54208
  }
53917
54209
  return { site: key2, verdict: "error" };
53918
54210
  });
@@ -53930,6 +54222,10 @@ function mutationProofVerdict(input2) {
53930
54222
  return { verdict: "insensitive" };
53931
54223
  if (paired.every((a) => a.verdict === "sensitive"))
53932
54224
  return { verdict: "proven" };
54225
+ const refusals = input2.assertions.map((a) => a.targetedRefusal).filter((r) => r !== void 0);
54226
+ if (refusals.length > 0) {
54227
+ return { verdict: "inconclusive", reason: `some paired assertions were neither proven nor shown vacuous (cascade, not-applied or error); a targeted proof was refused: ${refusals[0]}` };
54228
+ }
53933
54229
  return { verdict: "inconclusive", reason: "some paired assertions were neither proven nor shown vacuous (cascade, not-applied or error)" };
53934
54230
  }
53935
54231
 
@@ -54496,6 +54792,12 @@ var JourneySchema = external_exports.object({
54496
54792
  endState: external_exports.array(OutcomeCheckSchema).max(50).optional(),
54497
54793
  acceptedWeak: external_exports.object({ reason: external_exports.string().min(1), rules: external_exports.array(external_exports.string()) }).strict().optional(),
54498
54794
  mutationPairs: external_exports.array(external_exports.object({ check: external_exports.string().min(1).max(100), mustFailWhen: external_exports.string().min(1).max(200) }).strict()).max(100).optional(),
54795
+ // #616: a declared targeted-proof plan; the relax list is bounded tighter than the automatic proof.
54796
+ targetedProofs: external_exports.array(external_exports.object({
54797
+ check: external_exports.string().min(1).max(100),
54798
+ mustFailWhen: external_exports.string().min(1).max(200),
54799
+ relax: external_exports.array(external_exports.string().min(1).max(60)).min(1).max(MAX_DECLARED_RELAXATIONS)
54800
+ }).strict()).max(20).optional(),
54499
54801
  approval: external_exports.object({
54500
54802
  contentHash: external_exports.string().regex(/^[0-9a-f]{64}$/, "approval.contentHash: a sha256 hex digest"),
54501
54803
  at: external_exports.string().min(1),
@@ -54521,6 +54823,9 @@ var JourneySchema = external_exports.object({
54521
54823
  for (const issue3 of mutationPairIssues(j)) {
54522
54824
  ctx.addIssue({ code: "custom", path: ["metadata", "mutationPairs", issue3.index, issue3.field], message: issue3.message });
54523
54825
  }
54826
+ for (const issue3 of targetedProofPlanIssues(j)) {
54827
+ ctx.addIssue({ code: "custom", path: ["metadata", "targetedProofs", issue3.index, issue3.field], message: issue3.message });
54828
+ }
54524
54829
  const declared = /* @__PURE__ */ new Set([...j.metadata.params, ...(j.metadata.parameters ?? []).map((p) => p.name)]);
54525
54830
  j.recording.pages.forEach((page, pi) => page.steps.forEach((rs, si) => {
54526
54831
  if (rs.step.kind !== "navigate")
@@ -55360,7 +55665,9 @@ var ReviewVerifySchema = external_exports.discriminatedUnion("status", [
55360
55665
  /** True when the Journey changed after this proof ran (the verdict is for other content). */
55361
55666
  stale: external_exports.boolean(),
55362
55667
  reason: external_exports.string().optional(),
55363
- summary: external_exports.record(external_exports.string(), external_exports.number())
55668
+ summary: external_exports.record(external_exports.string(), external_exports.number()),
55669
+ /** #616: a reviewable declared targeted-proof plan the automatic bound refused (names only). */
55670
+ suggestedTargetedProofs: external_exports.array(external_exports.object({ check: external_exports.string(), mustFailWhen: external_exports.string(), relax: external_exports.array(external_exports.string()) }).strict()).optional()
55364
55671
  }).strict()
55365
55672
  ]);
55366
55673
  var DiffSchema = external_exports.object({ added: external_exports.array(external_exports.string()), removed: external_exports.array(external_exports.string()) }).strict();
@@ -55380,10 +55687,18 @@ var ReviewChangeSchema = external_exports.discriminatedUnion("kind", [
55380
55687
  stepIdOnly: external_exports.boolean().optional(),
55381
55688
  steps: DiffSchema,
55382
55689
  assertions: DiffSchema,
55383
- sideEffects: DiffSchema
55690
+ sideEffects: DiffSchema,
55691
+ /** #616: proof-plan changes (`metadata.mutationPairs` / `metadata.targetedProofs`); omitted when neither side declares any. */
55692
+ proofPlans: DiffSchema.optional()
55384
55693
  }).strict()
55385
55694
  ]);
55386
- var ReviewAttentionSchema = external_exports.object({ severity: external_exports.enum(["warn", "info"]), text: external_exports.string(), hint: external_exports.string().optional() }).strict();
55695
+ var ReviewAttentionSchema = external_exports.object({
55696
+ severity: external_exports.enum(["warn", "info"]),
55697
+ text: external_exports.string(),
55698
+ hint: external_exports.string().optional(),
55699
+ /** #608: a person's catalog approval settles this item — readiness lists it as awaiting-human, never as a failure. */
55700
+ settledBy: external_exports.enum(["catalog-approval"]).optional()
55701
+ }).strict();
55387
55702
  var JourneyReviewSchema = external_exports.object({
55388
55703
  id: external_exports.string(),
55389
55704
  name: external_exports.string(),
@@ -55419,6 +55734,8 @@ var JourneyReviewSchema = external_exports.object({
55419
55734
  endStateText: external_exports.array(external_exports.string()).optional(),
55420
55735
  lint: external_exports.object({ errors: external_exports.number().int(), warnings: external_exports.number().int(), findings: external_exports.array(ReviewLintFindingSchema) }).strict(),
55421
55736
  acceptedWeak: external_exports.object({ reason: external_exports.string(), rules: external_exports.array(external_exports.string()) }).strict().optional(),
55737
+ /** #616: the declared targeted-proof plans (`metadata.targetedProofs`) a reviewer approves with the Journey. Additive. */
55738
+ targetedProofs: external_exports.array(external_exports.object({ check: external_exports.string(), mustFailWhen: external_exports.string(), relax: external_exports.array(external_exports.string()) }).strict()).optional(),
55422
55739
  verify: ReviewVerifySchema
55423
55740
  }).strict(),
55424
55741
  changeSinceApproval: ReviewChangeSchema,
@@ -62390,9 +62707,9 @@ function hostStarvedFailure(failure3, health) {
62390
62707
  }
62391
62708
 
62392
62709
  // src/build-info.generated.ts
62393
- var GENERATED_COMMIT = "ec540dd";
62394
- var GENERATED_TREE = "d9818ad";
62395
- var GENERATED_BUILT_AT = "2026-10-18T23:04:28.655Z";
62710
+ var GENERATED_COMMIT = "c9b33ab";
62711
+ var GENERATED_TREE = "35c694a";
62712
+ var GENERATED_BUILT_AT = "2026-10-19T23:00:10.969Z";
62396
62713
 
62397
62714
  // src/version.ts
62398
62715
  import { readFileSync as readFileSync5 } from "node:fs";
@@ -62414,13 +62731,13 @@ function readCliVersion() {
62414
62731
 
62415
62732
  // src/engine.ts
62416
62733
  function bundledCommit() {
62417
- return true ? "ec540dd" : void 0;
62734
+ return true ? "c9b33ab" : void 0;
62418
62735
  }
62419
62736
  function bundledBuiltAt() {
62420
- return true ? "2026-10-18T23:04:28.655Z" : void 0;
62737
+ return true ? "2026-10-19T23:00:10.969Z" : void 0;
62421
62738
  }
62422
62739
  function bundledTree() {
62423
- return true ? "d9818ad" : void 0;
62740
+ return true ? "35c694a" : void 0;
62424
62741
  }
62425
62742
  function currentBuildTree() {
62426
62743
  return bundledTree() ?? GENERATED_TREE;
@@ -79842,9 +80159,11 @@ function renderSteps(r, w, opts) {
79842
80159
  function renderProvesAtTheEnd(r, w, opts) {
79843
80160
  void opts;
79844
80161
  const endState = r.proof.endStateText ?? r.proof.endState;
79845
- if (endState.length > 0) {
80162
+ const plans = r.proof.targetedProofs ?? [];
80163
+ if (endState.length > 0 || plans.length > 0) {
79846
80164
  w.head("PROVES AT THE END", "Proves at the end");
79847
80165
  for (const e of endState) w.put(`${w.bullet}\u2713 `, e, w.bullet.length + 2);
80166
+ for (const p of plans) w.put(`${w.bullet}\u21BA `, `declared targeted proof: ${p.check} against ${p.mustFailWhen} may relax ${p.relax.join(", ")}`, w.bullet.length + 2);
79848
80167
  w.gap();
79849
80168
  }
79850
80169
  }
@@ -79935,7 +80254,13 @@ function renderFullDetail(r, w, opts) {
79935
80254
  w.put(w.bullet, `Approved${c.approvedAt === void 0 ? "" : ` ${humanTime(c.approvedAt)}`} as ${nb(w.code(c.approvedHash))}${c.kind === "snapshot-missing" ? "; its snapshot is missing" : ""} \u2014 ${c.changed ? "CHANGED since" : "unchanged since"}`);
79936
80255
  if (c.stepIdOnly === true) w.put(w.bullet, STEP_ID_ONLY_LABEL);
79937
80256
  if (c.kind === "diff") {
79938
- for (const [label2, d] of [["Steps", c.steps], ["Assertions", c.assertions], ["Side effects", c.sideEffects]]) {
80257
+ const blocks = [
80258
+ ["Steps", c.steps],
80259
+ ["Assertions", c.assertions],
80260
+ ["Side effects", c.sideEffects]
80261
+ ];
80262
+ if (c.proofPlans !== void 0) blocks.push(["Proof plans", c.proofPlans]);
80263
+ for (const [label2, d] of blocks) {
79939
80264
  if (d.added.length === 0 && d.removed.length === 0) continue;
79940
80265
  w.put(w.bullet, `${label2}:`);
79941
80266
  for (const l2 of d.removed) w.put(`${w.bullet} `, `\u2212 ${l2}`, w.bullet.length + 4);
@@ -80216,6 +80541,8 @@ function buildChange(journey, hash3, snapshot2, stepIdOnly) {
80216
80541
  }
80217
80542
  const approvedHash = journeyReviewHash(snapshot2);
80218
80543
  const approvedAt = approval?.at ?? snapshot2.metadata.approval?.at;
80544
+ const beforePlans = proofPlanLines(snapshot2);
80545
+ const afterPlans = proofPlanLines(journey);
80219
80546
  return {
80220
80547
  kind: "diff",
80221
80548
  approvedHash,
@@ -80224,7 +80551,9 @@ function buildChange(journey, hash3, snapshot2, stepIdOnly) {
80224
80551
  ...idOnly,
80225
80552
  steps: multisetDiff(stepLines(snapshot2), stepLines(journey)),
80226
80553
  assertions: multisetDiff(assertionLines(snapshot2), assertionLines(journey)),
80227
- sideEffects: multisetDiff(sideEffectLines(snapshot2), sideEffectLines(journey))
80554
+ sideEffects: multisetDiff(sideEffectLines(snapshot2), sideEffectLines(journey)),
80555
+ // #616: only present when a side declares a proof plan, so an unrelated diff stays as before.
80556
+ ...beforePlans.length === 0 && afterPlans.length === 0 ? {} : { proofPlans: multisetDiff(beforePlans, afterPlans) }
80228
80557
  };
80229
80558
  }
80230
80559
  function stepLines(j) {
@@ -80236,6 +80565,12 @@ function assertionLines(j) {
80236
80565
  ...journeyAssertions(j).filter((s3) => s3.where === "step-request").map((s3) => `step request: ${describeCheck(s3.check)}`)
80237
80566
  ];
80238
80567
  }
80568
+ function proofPlanLines(j) {
80569
+ return [
80570
+ ...(j.metadata.mutationPairs ?? []).map((p) => `mutation pair: ${p.check} must fail when ${p.mustFailWhen}`),
80571
+ ...(j.metadata.targetedProofs ?? []).map((p) => `targeted proof: ${p.check} against ${p.mustFailWhen} may relax ${p.relax.join(", ")}`)
80572
+ ];
80573
+ }
80239
80574
  function sideEffectLines(j) {
80240
80575
  return [
80241
80576
  ...[...new Set(buildWriteRequests(j).map((w) => `write ${w.method} ${w.endpoint}`))],
@@ -80273,7 +80608,8 @@ function buildVerify(journey, hash3, rec) {
80273
80608
  // #523: an executable-content change stales the proof; an old record without it compares the full hash.
80274
80609
  stale: rec.proofHash === void 0 ? rec.contentHash !== hash3 : rec.proofHash !== journeyProofHash(journey),
80275
80610
  ...rec.reason === void 0 ? {} : { reason: rec.reason },
80276
- summary: { ...rec.summary }
80611
+ summary: { ...rec.summary },
80612
+ ...rec.suggestedTargetedProofs === void 0 ? {} : { suggestedTargetedProofs: rec.suggestedTargetedProofs.map((p) => ({ ...p, relax: [...p.relax] })) }
80277
80613
  };
80278
80614
  }
80279
80615
  function buildJourneyReview(journey, ctx = {}) {
@@ -80351,6 +80687,8 @@ function buildJourneyReview(journey, ctx = {}) {
80351
80687
  findings: findings.map((f2) => ({ rule: f2.rule, level: f2.level, ...f2.step === void 0 ? {} : { step: f2.step }, message: f2.message }))
80352
80688
  },
80353
80689
  ...m.acceptedWeak === void 0 ? {} : { acceptedWeak: { reason: m.acceptedWeak.reason, rules: [...m.acceptedWeak.rules] } },
80690
+ // #616: the declared targeted-proof plans, copied out of the metadata for review.
80691
+ ...m.targetedProofs === void 0 || m.targetedProofs.length === 0 ? {} : { targetedProofs: m.targetedProofs.map((p) => ({ check: p.check, mustFailWhen: p.mustFailWhen, relax: [...p.relax] })) },
80354
80692
  verify: buildVerify(journey, hash3, ctx.lastVerify)
80355
80693
  },
80356
80694
  changeSinceApproval: buildChange(journey, hash3, ctx.approvedSnapshot, ctx.stepIdOnly === true),
@@ -80381,7 +80719,7 @@ function describeFile(step) {
80381
80719
  }
80382
80720
  function buildAttention(r, weakEndState, goal) {
80383
80721
  const out = [];
80384
- const warn = (text2, hint2) => void out.push({ severity: "warn", text: text2, ...hint2 === void 0 ? {} : { hint: hint2 } });
80722
+ const warn = (text2, hint2, settledBy) => void out.push({ severity: "warn", text: text2, ...hint2 === void 0 ? {} : { hint: hint2 }, ...settledBy === void 0 ? {} : { settledBy } });
80385
80723
  const info = (text2, hint2) => void out.push({ severity: "info", text: text2, ...hint2 === void 0 ? {} : { hint: hint2 } });
80386
80724
  const weakSteps = [...new Set(r.proof.stepAssertions.filter((a) => a.weak).map((a) => a.step))];
80387
80725
  if (weakSteps.length === 1) warn(`Step ${stepRanges(weakSteps)} proves nothing: its check passes even if the step fails`);
@@ -80402,12 +80740,12 @@ function buildAttention(r, weakEndState, goal) {
80402
80740
  else info(`${text2}, exempted by allowControl`);
80403
80741
  }
80404
80742
  if (r.catalog !== void 0) {
80405
- if (r.catalog.unvetted.length > 0) warn(`Unvetted catalog links: ${r.catalog.unvetted.join(", ")}`, `approve them, or promote with --accept-unvetted "<reason>"`);
80743
+ if (r.catalog.unvetted.length > 0) warn(`Unvetted catalog links: ${r.catalog.unvetted.join(", ")}`, `approve them, or promote with --accept-unvetted "<reason>"`, "catalog-approval");
80406
80744
  if (r.catalog.needsReReview.length > 0) warn(`Catalog links need re-review: ${r.catalog.needsReReview.join("; ")}`);
80407
80745
  }
80408
80746
  for (const f2 of r.findings ?? []) {
80409
80747
  if (f2.analyzer === "catalog-analysis") continue;
80410
- if (f2.severity !== "info") warn(f2.message, f2.fix);
80748
+ if (f2.severity !== "info") warn(f2.message, f2.fix, f2.analyzer === "catalog-links" && f2.code === "journey.unvetted" ? "catalog-approval" : void 0);
80411
80749
  else if (f2.analyzer === "readiness" || f2.analyzer === "readiness-jev") info(f2.message, f2.fix);
80412
80750
  }
80413
80751
  const c = r.changeSinceApproval;
@@ -94594,6 +94932,7 @@ var JourneyOutcomeChecks = class {
94594
94932
  async #judge(s3, fallback) {
94595
94933
  const window2 = s3.capture ?? fallback;
94596
94934
  if (window2 === void 0) return [];
94935
+ const ownCapture = s3.capture !== void 0;
94597
94936
  const since = s3.startedAt;
94598
94937
  const until = s3.until;
94599
94938
  const out = [];
@@ -94602,7 +94941,7 @@ var JourneyOutcomeChecks = class {
94602
94941
  await evaluateNetworkCheckAwaiting(
94603
94942
  c,
94604
94943
  () => ({
94605
- requests: window2.sent().filter((q) => (since === void 0 || q.startedAt !== void 0 && q.startedAt >= since) && (until === void 0 || q.startedAt !== void 0 && q.startedAt <= until)),
94944
+ requests: window2.sent().filter((q) => (ownCapture || since === void 0 || q.startedAt !== void 0 && q.startedAt >= since) && (until === void 0 || q.startedAt !== void 0 && q.startedAt <= until)),
94606
94945
  truncated: window2.truncated
94607
94946
  }),
94608
94947
  s3.responseTimeoutMs ?? this.#responseTimeoutMs
@@ -95099,7 +95438,8 @@ async function runJourneyProgrammatically(opts, mutation2 = {}) {
95099
95438
  let result;
95100
95439
  let runSecrets2 = secrets;
95101
95440
  try {
95102
- const raw = await outcomeChecks.run(actor, () => runner.run({ journey: replayed, params, policy }));
95441
+ const replayParams = replayed === journey ? params : prefixParams(journey, replayed, params);
95442
+ const raw = await outcomeChecks.run(actor, () => runner.run({ journey: replayed, params: replayParams, policy }));
95103
95443
  runSecrets2 = runSecretValues(secrets, raw);
95104
95444
  result = redactSecretValues(raw, runSecrets2);
95105
95445
  } finally {
@@ -108463,6 +108803,9 @@ function redactReason(reason, params, names = TEXT_PARAMS) {
108463
108803
  }
108464
108804
  return out;
108465
108805
  }
108806
+ function redactProducerParams(reason, params) {
108807
+ return params === void 0 ? reason : redactReason(reason, params, Object.keys(params));
108808
+ }
108466
108809
  function chainSucceeded(result) {
108467
108810
  return result.unmatched.length === 0 && result.runs.every((run) => run.outcome === "ok");
108468
108811
  }
@@ -108553,10 +108896,10 @@ async function runHandoffChain(inputs, outputs, fromJourney, fromOutput, request
108553
108896
  ...identity,
108554
108897
  outcome: result.outcome,
108555
108898
  ...stamp,
108556
- ..."reason" in result ? { reason: redactReason(result.reason, params, redactors) } : {}
108899
+ ..."reason" in result ? { reason: redactProducerParams(redactReason(result.reason, params, redactors), opts.params) } : {}
108557
108900
  });
108558
108901
  } catch (err) {
108559
- runs.push({ ...identity, outcome: "error", ...stamp, reason: redactReason(err instanceof Error ? err.message : String(err), params, redactors) });
108902
+ runs.push({ ...identity, outcome: "error", ...stamp, reason: redactProducerParams(redactReason(err instanceof Error ? err.message : String(err), params, redactors), opts.params) });
108560
108903
  }
108561
108904
  }
108562
108905
  return { from: { journey: opts.from, outcome: "ok", records: records.length }, runs, unmatched, rejected };
@@ -108568,9 +108911,15 @@ async function runJourneyChain(opts, deps = {}) {
108568
108911
  const toJourney = await registry3.get(opts.to);
108569
108912
  if (toJourney === null) throw new UnknownJourneyError(`unknown journey '${opts.to}'`);
108570
108913
  const inputs = toJourney.metadata.inputs;
108571
- const needsFrom = inputs !== void 0 || opts.requirePromoted === true;
108572
- const fromJourney = needsFrom ? await registry3.get(opts.from) : null;
108573
- if (needsFrom && fromJourney === null) throw new UnknownJourneyError(`unknown journey '${opts.from}'`);
108914
+ const fromJourney = await registry3.get(opts.from);
108915
+ if (fromJourney === null) throw new UnknownJourneyError(`unknown journey '${opts.from}'`);
108916
+ const producerParams = { ...opts.params ?? {} };
108917
+ try {
108918
+ validateParams(journeyParamSchema(fromJourney), producerParams);
108919
+ } catch (err) {
108920
+ if (err instanceof ParamValidationError) throw new JourneyChainError("E_CHAIN_PARAMS", err.message);
108921
+ throw err;
108922
+ }
108574
108923
  if (opts.requirePromoted === true) {
108575
108924
  for (const j of [fromJourney, toJourney]) {
108576
108925
  if (!j.metadata.promoted) throw new JourneyChainError("E_CHAIN_NOT_PROMOTED", `journey '${j.metadata.id}' is not promoted \u2014 a chain runs promoted Journeys (journey chain --allow-draft runs drafts on the CLI)`);
@@ -108585,9 +108934,9 @@ async function runJourneyChain(opts, deps = {}) {
108585
108934
  handoff2 = { inputs, outputs };
108586
108935
  }
108587
108936
  const requests = handoff2 === void 0 ? parseChainRequests(opts.requests) : void 0;
108588
- const fromResult = await runner({ ...shared3, id: opts.from, params: {} });
108937
+ const fromResult = await runner({ ...shared3, id: opts.from, params: producerParams });
108589
108938
  if (fromResult.outcome !== "ok") {
108590
- const reason = "reason" in fromResult ? fromResult.reason : void 0;
108939
+ const reason = "reason" in fromResult ? redactProducerParams(fromResult.reason, opts.params) : void 0;
108591
108940
  throw new JourneyChainError(
108592
108941
  "E_CHAIN_FROM_FAILED",
108593
108942
  `discover Journey '${opts.from}' did not run ok (${fromResult.outcome})${reason === void 0 ? "" : `: ${reason}`}`
@@ -108621,10 +108970,10 @@ async function runJourneyChain(opts, deps = {}) {
108621
108970
  commentId: request.commentId,
108622
108971
  account: request.account,
108623
108972
  outcome: result.outcome,
108624
- ..."reason" in result ? { reason: redactReason(result.reason, mapped.params) } : {}
108973
+ ..."reason" in result ? { reason: redactProducerParams(redactReason(result.reason, mapped.params), opts.params) } : {}
108625
108974
  });
108626
108975
  } catch (err) {
108627
- runs.push({ commentId: request.commentId, account: request.account, outcome: "error", reason: redactReason(err instanceof Error ? err.message : String(err), mapped.params) });
108976
+ runs.push({ commentId: request.commentId, account: request.account, outcome: "error", reason: redactProducerParams(redactReason(err instanceof Error ? err.message : String(err), mapped.params), opts.params) });
108628
108977
  }
108629
108978
  }
108630
108979
  return { from: { journey: opts.from, outcome: "ok", records: count }, runs, unmatched };
@@ -108632,12 +108981,12 @@ async function runJourneyChain(opts, deps = {}) {
108632
108981
 
108633
108982
  // src/journey-cli/chain.ts
108634
108983
  function registerJourneyChain(journey, program2, deps) {
108635
- withReplayAllowFlag(withEnvironmentFlags(withBrowserLaunchFlags(withEmulationFlags(withProjectFlags(journey.command("chain")))))).requiredOption("--from <id>", "the discover Journey, run once to produce the records").requiredOption("--to <id>", "the prepare-only reply Journey, run once per matched request").requiredOption("--requests <file>", "JSON Lines file of approved requests ({commentId, account, replyText})").option("--dir <path>", DEPRECATED_DIR_HELP).option("--storage-state <file>", "Playwright storageState JSON to start each run authenticated; must exist").option("--allow-draft", "also run Journeys that are not promoted (authoring); without it both must be promoted").option("--json", "emit a JSON envelope").action(async function() {
108984
+ withReplayAllowFlag(withEnvironmentFlags(withBrowserLaunchFlags(withEmulationFlags(withProjectFlags(journey.command("chain")))))).requiredOption("--from <id>", "the discover Journey, run once to produce the records").requiredOption("--to <id>", "the prepare-only reply Journey, run once per matched request").requiredOption("--requests <file>", "JSON Lines file of approved requests ({commentId, account, replyText})").option("--param <kv>", "param as key=value (repeatable)", collectParam, {}).option("--dir <path>", DEPRECATED_DIR_HELP).option("--storage-state <file>", "Playwright storageState JSON to start each run authenticated; must exist").option("--allow-draft", "also run Journeys that are not promoted (authoring); without it both must be promoted").option("--json", "emit a JSON envelope").action(async function() {
108636
108985
  const locationFlags = this.opts();
108637
108986
  const layout = journeyLayout(program2, deps, { dir: locationFlags.dir, project: locationFlags.project, journeysDir: locationFlags.journeysDir });
108638
108987
  if (layout === null) return;
108639
108988
  const chainJourneysDir = layout.journeysDir;
108640
- const { from, to, requests: requestsFile, storageState: storageStateFlag, json: json2, allowDraft } = this.opts();
108989
+ const { from, to, requests: requestsFile, storageState: storageStateFlag, json: json2, allowDraft, param } = this.opts();
108641
108990
  const { env: envName, baseUrl } = this.opts();
108642
108991
  let environment;
108643
108992
  let allow;
@@ -108685,6 +109034,7 @@ function registerJourneyChain(journey, program2, deps) {
108685
109034
  from,
108686
109035
  to,
108687
109036
  requests,
109037
+ ...Object.keys(param).length === 0 ? {} : { params: param },
108688
109038
  dir: chainJourneysDir,
108689
109039
  requirePromoted: allowDraft !== true,
108690
109040
  ...siteGate === void 0 ? {} : { siteGate },
@@ -109082,14 +109432,16 @@ async function verifyJourneyMutations(opts, run = runJourneyProgrammatically) {
109082
109432
  const checkOf = new Map(sites.map((s3) => [assertionSiteKey(s3), describeCheck(s3.check)]));
109083
109433
  const plan2 = planJourneyMutations(journey, { ...lintOpts, params: opts.params });
109084
109434
  const mutations = [];
109435
+ const suggested = [];
109436
+ const seenSuggestions = /* @__PURE__ */ new Set();
109085
109437
  if (base.outcome === "ok") {
109086
109438
  const replayOpts2 = { ...lintOpts, params: opts.params };
109087
- const replay = async (m, describe6) => {
109439
+ const runReplay = async (m, describe6) => {
109088
109440
  try {
109089
- const how = describe6(journey, m, replayOpts2);
109441
+ const how = describe6(journey);
109090
109442
  const r = await run(how.params === void 0 ? runOpts : { ...runOpts, params: { ...how.params } }, {
109091
109443
  structuredFailures: true,
109092
- ...how.journey === journey ? {} : { mutateJourney: (j) => describe6(j, m, replayOpts2).journey },
109444
+ ...how.journey === journey ? {} : { mutateJourney: (j) => describe6(j).journey },
109093
109445
  ...how.skipIndex === void 0 ? {} : { skipStep: (i) => i === how.skipIndex },
109094
109446
  ...how.swapSelectAtStep === void 0 ? {} : { selectAlternativeAtStep: how.swapSelectAtStep },
109095
109447
  ...how.blockIndex === void 0 ? {} : { blockWritesAtStep: how.blockIndex }
@@ -109117,9 +109469,30 @@ async function verifyJourneyMutations(opts, run = runJourneyProgrammatically) {
109117
109469
  return { outcome: "error", failedSites: [], reason: err instanceof Error ? err.message : String(err) };
109118
109470
  }
109119
109471
  };
109472
+ const replay = (m, describe6) => runReplay(m, (j) => describe6(j, m, replayOpts2));
109473
+ const siteByKey = new Map(sites.map((s3) => [assertionSiteKey(s3), s3]));
109120
109474
  for (const m of plan2.mutations) {
109121
109475
  const first = await replay(m, mutationReplay);
109122
109476
  const relaxed2 = needsRelaxedReplay(plan2, m, first) ? await replay(m, relaxedMutationReplay) : void 0;
109477
+ const initial = { outcome: first.outcome, failedSites: first.failedSites, ...relaxed2 === void 0 ? {} : { relaxed: relaxed2 } };
109478
+ const targeted = [];
109479
+ for (const pair of plan2.pairs) {
109480
+ if (pair.mutation !== m.id) continue;
109481
+ const site = siteByKey.get(pair.site);
109482
+ if (site === void 0 || classifyMutation(site, m, initial) !== "cascade") continue;
109483
+ const declared = declaredTargetedProofFor(journey, m, pair.site);
109484
+ targeted.push(
109485
+ declared === void 0 ? await runTargetedProof(journey, plan2, m, site, initial, (describe6) => runReplay(m, describe6), replayOpts2) : await runDeclaredTargetedProof(journey, plan2, m, site, declared, (describe6) => runReplay(m, describe6), replayOpts2)
109486
+ );
109487
+ const suggestion = suggestTargetedProofPlan(targeted[targeted.length - 1], m);
109488
+ if (suggestion !== null) {
109489
+ const key2 = `${suggestion.check}\0${suggestion.mustFailWhen}\0${suggestion.relax.join(",")}`;
109490
+ if (!seenSuggestions.has(key2)) {
109491
+ seenSuggestions.add(key2);
109492
+ suggested.push(suggestion);
109493
+ }
109494
+ }
109495
+ }
109123
109496
  mutations.push({
109124
109497
  kind: m.kind,
109125
109498
  step: m.step,
@@ -109127,15 +109500,30 @@ async function verifyJourneyMutations(opts, run = runJourneyProgrammatically) {
109127
109500
  ...m.param === void 0 ? {} : { param: m.param },
109128
109501
  ...m.source === void 0 ? {} : { source: m.source },
109129
109502
  ...first,
109130
- ...relaxed2 === void 0 ? {} : { relaxed: relaxed2 }
109503
+ ...relaxed2 === void 0 ? {} : { relaxed: relaxed2 },
109504
+ ...targeted.length === 0 ? {} : { targeted }
109131
109505
  });
109132
109506
  }
109133
109507
  }
109134
109508
  const results = new Map(
109135
- mutations.map((m) => [m.id, { outcome: m.outcome, failedSites: m.failedSites, ...m.relaxed === void 0 ? {} : { relaxed: m.relaxed } }])
109509
+ mutations.map((m) => [
109510
+ m.id,
109511
+ { outcome: m.outcome, failedSites: m.failedSites, ...m.relaxed === void 0 ? {} : { relaxed: m.relaxed }, ...m.targeted === void 0 ? {} : { targeted: m.targeted } }
109512
+ ])
109136
109513
  );
109137
109514
  const judged = judgeAssertions(journey, plan2, results);
109138
- const assertions = judged.map((a) => ({ site: a.site, check: checkOf.get(a.site) ?? a.site, verdict: a.verdict, ...a.provedBy === void 0 ? {} : { provedBy: a.provedBy } }));
109515
+ const assertions = judged.map((a) => ({
109516
+ site: a.site,
109517
+ check: checkOf.get(a.site) ?? a.site,
109518
+ verdict: a.verdict,
109519
+ ...a.provedBy === void 0 ? {} : { provedBy: a.provedBy },
109520
+ ...a.relaxedSites === void 0 ? {} : { relaxedSites: a.relaxedSites },
109521
+ ...a.targetedRefusal === void 0 ? {} : { targetedRefusal: a.targetedRefusal },
109522
+ // #616: the proving proof was a declared plan, not the automatic relaxation search.
109523
+ ...a.provedBy !== void 0 && results.get(a.provedBy)?.targeted?.some((t) => t.site === a.site && t.declared === true) === true ? { declaredProof: true } : {}
109524
+ }));
109525
+ const order = new Map(journeyAssertions(journey).map((s3, i) => [assertionSiteKey(s3), i]));
109526
+ const siteOrder = (key2) => order.get(key2) ?? Number.MAX_SAFE_INTEGER;
109139
109527
  const { verdict, reason } = mutationProofVerdict({ basePassed: base.outcome === "ok", mutations, assertions: judged });
109140
109528
  const count = (v) => assertions.filter((a) => a.verdict === v).length;
109141
109529
  const report2 = {
@@ -109152,8 +109540,12 @@ async function verifyJourneyMutations(opts, run = runJourneyProgrammatically) {
109152
109540
  cascade: count("cascade"),
109153
109541
  notApplied: count("not-applied"),
109154
109542
  error: count("error"),
109155
- unpaired: count("unpaired")
109156
- }
109543
+ unpaired: count("unpaired"),
109544
+ // #609: assertions proven sensitive through a targeted proof that relaxed earlier sites.
109545
+ targeted: assertions.filter((a) => (a.relaxedSites?.length ?? 0) > 0).length
109546
+ },
109547
+ // #616: in assertion order, so the first suggestion is the one for the refusal the verdict quotes.
109548
+ ...suggested.length === 0 ? {} : { suggestedTargetedProofs: [...suggested].sort((a, b2) => siteOrder(a.check) - siteOrder(b2.check)) }
109157
109549
  };
109158
109550
  const redacted = redactSecretParams(report2, journey, opts.params);
109159
109551
  await writeVerifyRecord(opts.dir, {
@@ -109163,12 +109555,16 @@ async function verifyJourneyMutations(opts, run = runJourneyProgrammatically) {
109163
109555
  verdict: redacted.verdict,
109164
109556
  ...redacted.reason === void 0 ? {} : { reason: redacted.reason },
109165
109557
  summary: { ...redacted.summary },
109558
+ ...redacted.suggestedTargetedProofs === void 0 ? {} : { suggestedTargetedProofs: redacted.suggestedTargetedProofs },
109166
109559
  at: clock.nowIso()
109167
109560
  });
109168
109561
  return redacted;
109169
109562
  }
109170
109563
  function verifyAssertionLine(a) {
109171
- return `${a.verdict.padEnd(11)} ${a.site.padEnd(18)} ${a.check}${a.provedBy === void 0 ? "" : ` (proved by ${a.provedBy})`}`;
109564
+ const relaxed2 = a.relaxedSites === void 0 || a.relaxedSites.length === 0 ? "" : a.declaredProof === true ? `, declared plan relaxed ${a.relaxedSites.join(", ")}` : `, relaxed ${a.relaxedSites.join(", ")}`;
109565
+ const proved = a.provedBy === void 0 ? "" : ` (proved by ${a.provedBy}${relaxed2})`;
109566
+ const refused = a.targetedRefusal === void 0 ? "" : ` (targeted proof refused: ${a.targetedRefusal})`;
109567
+ return `${a.verdict.padEnd(11)} ${a.site.padEnd(18)} ${a.check}${proved}${refused}`;
109172
109568
  }
109173
109569
 
109174
109570
  // src/journey-readiness.ts
@@ -109181,7 +109577,7 @@ var ReadinessError = class extends Error {
109181
109577
  }
109182
109578
  };
109183
109579
  var REQUIRED_PROOF_CHECK = "proof";
109184
- function resolveTarget2(opts, deps, required2) {
109580
+ function resolveTarget2(opts, deps, required2, declared) {
109185
109581
  let environment;
109186
109582
  if (opts.env !== void 0 || opts.baseUrl !== void 0) {
109187
109583
  environment = environmentFromFlags(
@@ -109199,6 +109595,10 @@ function resolveTarget2(opts, deps, required2) {
109199
109595
  if (missing.length > 0) {
109200
109596
  throw new ReadinessError("E_READINESS_PARAMS", `missing required param(s): ${missing.join(", ")}`);
109201
109597
  }
109598
+ const unknown2 = Object.keys(opts.params ?? {}).filter((name) => !declared.includes(name));
109599
+ if (unknown2.length > 0) {
109600
+ throw new ReadinessError("E_READINESS_PARAMS", `unknown param(s): ${unknown2.join(", ")}`);
109601
+ }
109202
109602
  return environment;
109203
109603
  }
109204
109604
  function requiredParams(journey, pair, hasRequests) {
@@ -109241,7 +109641,7 @@ function boundaryCheck(ctx) {
109241
109641
  return ctx.journey.metadata.boundary === void 0 ? { check: "boundary", journey: ctx.id, state: "not-run", detail: "no prepare-only boundary declared" } : { check: "boundary", journey: ctx.id, state: "pass", detail: "a prepare-only boundary is declared" };
109242
109642
  }
109243
109643
  function reviewStrictCheck(ctx) {
109244
- const warns = (ctx.review.attention ?? []).filter((a) => a.severity === "warn");
109644
+ const warns = (ctx.review.attention ?? []).filter((a) => a.severity === "warn" && !(a.settledBy === "catalog-approval" && ctx.review.catalog !== void 0));
109245
109645
  if (warns.length > 0) {
109246
109646
  return {
109247
109647
  check: { check: "review-strict", journey: ctx.id, state: "fail", detail: warns.map((a) => a.text).join("; ") },
@@ -109308,8 +109708,10 @@ function proofCheck(ctx, journeysDir, cwd) {
109308
109708
  };
109309
109709
  }
109310
109710
  if (verify2.verdict === "proven") {
109311
- const passed = Object.values(verify2.summary).reduce((a, b2) => a + b2, 0);
109312
- return { check: { check: "proof", journey: ctx.id, state: "pass", detail: `proven ${passed}/${passed}`, evidence } };
109711
+ const targeted = verify2.summary.targeted ?? 0;
109712
+ const passed = Object.entries(verify2.summary).filter(([k]) => k !== "targeted").reduce((a, [, b2]) => a + b2, 0);
109713
+ const detail = targeted === 0 ? `proven ${passed}/${passed}` : `proven ${passed}/${passed}, ${targeted} through a targeted proof (earlier defenses relaxed one at a time; see the verify report)`;
109714
+ return { check: { check: "proof", journey: ctx.id, state: "pass", detail, evidence } };
109313
109715
  }
109314
109716
  if (verify2.verdict === "insensitive") {
109315
109717
  return {
@@ -109320,10 +109722,36 @@ function proofCheck(ctx, journeysDir, cwd) {
109320
109722
  if (nothingToMutate(ctx.journey, verify2.summary)) {
109321
109723
  return { check: { check: "proof", journey: ctx.id, state: "pass", detail: "read-only with no inputs: nothing a mutation could break", evidence } };
109322
109724
  }
109323
- return { check: { check: "proof", journey: ctx.id, state: "inconclusive", detail: verify2.reason ?? `proof verdict ${verify2.verdict}`, evidence } };
109725
+ const suggested = verify2.status === "recorded" ? verify2.suggestedTargetedProofs : void 0;
109726
+ return {
109727
+ check: { check: "proof", journey: ctx.id, state: "inconclusive", detail: verify2.reason ?? `proof verdict ${verify2.verdict}`, evidence },
109728
+ blocker: { check: "proof", journey: ctx.id, why: inconclusiveProofGuidance(verify2.reason ?? "", suggested), next: `jevitate journey verify ${ctx.id} --mutate` }
109729
+ };
109730
+ }
109731
+ var TARGETED_REFUSAL_MARKER = "a targeted proof was refused: ";
109732
+ function inconclusiveProofGuidance(reason, suggested) {
109733
+ const at3 = reason.indexOf(TARGETED_REFUSAL_MARKER);
109734
+ if (at3 < 0) return "inspect the cascade assertions in the verify report";
109735
+ const refusal = reason.slice(at3 + TARGETED_REFUSAL_MARKER.length).trim();
109736
+ const unpaired = /^(\S+) is not paired with (\S+)$/.exec(refusal);
109737
+ if (unpaired !== null) {
109738
+ return `declare the earlier check ${unpaired[1]} as a defense of ${unpaired[2]} with a metadata.mutationPairs entry {check:'${unpaired[1]}', mustFailWhen:'${unpaired[2]}'}, then re-run verify`;
109739
+ }
109740
+ const bound2 = /relaxation bound (\d+)/.exec(refusal);
109741
+ if (bound2 !== null) {
109742
+ const first = suggested?.[0];
109743
+ const plan2 = first === void 0 ? `declare a metadata.targetedProofs plan (at most ${MAX_DECLARED_RELAXATIONS} earlier postconditions)` : `declare exactly this metadata.targetedProofs entry: ${JSON.stringify(first)} \u2014 a human reviews it like any other proof change (the review hash changes, so the Journey needs re-approval)`;
109744
+ return `${plan2}, then re-run verify (relaxation bound ${bound2[1]})`;
109745
+ }
109746
+ const blocked = /^(\S+) blocked the declared proof \(not in relax\)$/.exec(refusal);
109747
+ if (blocked !== null) return `add ${blocked[1]} to that plan's relax list (metadata.targetedProofs), then re-run verify`;
109748
+ const unsound = /^relaxed site (\S+) is not itself proven sensitive to (\S+)$/.exec(refusal);
109749
+ if (unsound !== null) return `pair ${unsound[1]} with ${unsound[2]} (metadata.mutationPairs) or make it a real defense, then re-run verify`;
109750
+ if (refusal.includes("action failed")) return "the mutated value makes an earlier action impossible, so that target cannot be proven independently \u2014 keep the check; it stays inconclusive by design";
109751
+ return "inspect the cascade assertions in the verify report";
109324
109752
  }
109325
109753
  function nothingToMutate(journey, summary2) {
109326
- const paired = Object.entries(summary2).filter(([k]) => k !== "unpaired").reduce((n3, [, v]) => n3 + v, 0);
109754
+ const paired = Object.entries(summary2).filter(([k]) => k !== "unpaired" && k !== "targeted").reduce((n3, [, v]) => n3 + v, 0);
109327
109755
  return (summary2.unpaired ?? 0) > 0 && paired === 0 && journeyWriteSteps(journey).length === 0 && journeyParamSchema(journey).required.length === 0;
109328
109756
  }
109329
109757
  function relativeEvidence(cwd, path2) {
@@ -109409,21 +109837,38 @@ async function runJourneyReadiness(opts, deps = {}) {
109409
109837
  const pair = opts.with !== void 0;
109410
109838
  let environment;
109411
109839
  let replay;
109840
+ let boundConsumerParams;
109412
109841
  if (mode !== "report") {
109413
- environment = resolveTarget2(opts, deps, requiredParams(contexts[0].journey, pair, opts.requests !== void 0));
109842
+ const producerSchema = journeyParamSchema(contexts[0].journey).required;
109843
+ environment = resolveTarget2(opts, deps, requiredParams(contexts[0].journey, pair, opts.requests !== void 0), producerSchema);
109414
109844
  const params = { ...opts.params ?? {} };
109415
109845
  try {
109416
109846
  if (pair) {
109417
- const chained = await chain({
109418
- from: opts.id,
109419
- to: opts.with,
109420
- requests: opts.requests ?? "",
109421
- dir: opts.journeysDir,
109422
- environment,
109423
- ...opts.storageState === void 0 ? {} : { storageState: opts.storageState },
109424
- ...opts.browser === void 0 ? {} : { browser: opts.browser },
109425
- ...opts.browserPortFactory === void 0 ? {} : { browserPortFactory: opts.browserPortFactory }
109426
- });
109847
+ const chained = await chain(
109848
+ {
109849
+ from: opts.id,
109850
+ to: opts.with,
109851
+ requests: opts.requests ?? "",
109852
+ params: { ...opts.params ?? {} },
109853
+ dir: opts.journeysDir,
109854
+ environment,
109855
+ ...opts.storageState === void 0 ? {} : { storageState: opts.storageState },
109856
+ ...opts.browser === void 0 ? {} : { browser: opts.browser },
109857
+ ...opts.browserPortFactory === void 0 ? {} : { browserPortFactory: opts.browserPortFactory }
109858
+ },
109859
+ {
109860
+ // #608: the consumer's proof must run with the params the chain actually bound for it
109861
+ // (record fields + request), captured from the first consumer run; names and outcomes
109862
+ // are all the chain itself returns.
109863
+ runJourney: async (chainOpts) => {
109864
+ const result = await run(chainOpts);
109865
+ if (chainOpts.id === opts.with && boundConsumerParams === void 0) {
109866
+ boundConsumerParams = { ...chainOpts.params };
109867
+ }
109868
+ return result;
109869
+ }
109870
+ }
109871
+ );
109427
109872
  replay = pairReplay(chained);
109428
109873
  } else {
109429
109874
  const result = await run({
@@ -109443,11 +109888,23 @@ async function runJourneyReadiness(opts, deps = {}) {
109443
109888
  }
109444
109889
  if (mode === "prove") {
109445
109890
  for (const ctx of contexts) {
109891
+ const consumer = pair && ctx.id === opts.with;
109892
+ if (consumer && boundConsumerParams === void 0) {
109893
+ ctx.review = {
109894
+ ...ctx.review,
109895
+ proof: {
109896
+ ...ctx.review.proof,
109897
+ verify: { status: "not-verified", hint: `the chain bound no request for '${ctx.id}' \u2014 supply a --requests line that matches a discovered record` }
109898
+ }
109899
+ };
109900
+ continue;
109901
+ }
109902
+ const proofParams = consumer ? boundConsumerParams : params;
109446
109903
  try {
109447
109904
  await verify2({
109448
109905
  dir: opts.journeysDir,
109449
109906
  id: ctx.id,
109450
- params,
109907
+ params: proofParams,
109451
109908
  environment,
109452
109909
  ...opts.storageState === void 0 ? {} : { storageState: opts.storageState },
109453
109910
  ...opts.browser === void 0 ? {} : { browser: opts.browser },
@@ -110800,8 +111257,9 @@ function registerJourneyVerify(journey, program2, deps) {
110800
111257
  for (const a of report2.assertions) out?.(`${verifyAssertionLine(a)}
110801
111258
  `);
110802
111259
  const s3 = report2.summary;
111260
+ const targeted = s3.targeted > 0 ? `, ${s3.targeted} targeted` : "";
110803
111261
  out?.(
110804
- `${report2.verdict} \u2014 journey '${report2.journeyId}': ${s3.sensitive} sensitive, ${s3.insensitive} insensitive, ${s3.cascade} cascade, ${s3.notApplied} not applied, ${s3.error} error, ${s3.unpaired} unpaired${report2.reason === void 0 ? "" : ` (${report2.reason})`}
111262
+ `${report2.verdict} \u2014 journey '${report2.journeyId}': ${s3.sensitive} sensitive, ${s3.insensitive} insensitive, ${s3.cascade} cascade, ${s3.notApplied} not applied, ${s3.error} error, ${s3.unpaired} unpaired${targeted}${report2.reason === void 0 ? "" : ` (${report2.reason})`}
110805
111263
  `
110806
111264
  );
110807
111265
  }
@@ -115046,13 +115504,14 @@ var CLI_TOOL_SPECS = [
115046
115504
  },
115047
115505
  {
115048
115506
  name: "chain_journeys",
115049
- description: "`jevitate journey chain --from <discover> --to <reply> --requests <file>` (#580): run a discover Journey once, then a prepare-only reply Journey once per approved request in the JSON Lines file ({commentId, account, replyText}) matched to a discovered record by commentId AND account. Both Journeys must be promoted. Unmatched requests never run; an empty discovery stops the chain (no target is guessed). The result names ids and outcomes only, never record or reply text. Exit 0 only when every request matched and its run was ok.",
115507
+ description: "`jevitate journey chain --from <discover> --to <reply> --requests <file>` (#580): run a discover Journey once, then a prepare-only reply Journey once per approved request in the JSON Lines file ({commentId, account, replyText}) matched to a discovered record by commentId AND account. Both Journeys must be promoted. #608: params supplies explicit producer (discover) params, validated against the producer's schema before anything runs. Unmatched requests never run; an empty discovery stops the chain (no target is guessed). The result names ids and outcomes only, never record or reply text. Exit 0 only when every request matched and its run was ok.",
115050
115508
  command: {
115051
115509
  path: "journey chain",
115052
115510
  params: {
115053
115511
  from: s2("--from", { required: true }),
115054
115512
  to: s2("--to", { required: true }),
115055
115513
  requests: path("--requests", { required: true }),
115514
+ params: { kind: "params", flag: "--param" },
115056
115515
  storageState: session("--storage-state"),
115057
115516
  ...EXTENSION,
115058
115517
  ...EMULATION,