@jevitate/cli 0.19.0 → 0.21.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;
@@ -53868,6 +53947,7 @@ function relaxedMutationReplay(journey, m, opts = {}) {
53868
53947
  return { journey: copy };
53869
53948
  }
53870
53949
  var MAX_TARGETED_RELAXATIONS = 4;
53950
+ var MAX_DECLARED_RELAXATIONS = 8;
53871
53951
  function failedSiteKey(f2) {
53872
53952
  if (f2.where === "step")
53873
53953
  return `step:${f2.step}`;
@@ -53980,6 +54060,60 @@ async function runTargetedProof(journey, plan2, m, target, initial, replay, opts
53980
54060
  }
53981
54061
  return { site, relaxedSites, outcome: run.outcome, failedSites: run.failedSites };
53982
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
+ }
53983
54117
  function needsRelaxedReplay(plan2, m, result) {
53984
54118
  if (result.outcome !== "failed")
53985
54119
  return false;
@@ -54025,6 +54159,19 @@ function classifyMutationPlain(site, m, result) {
54025
54159
  return fs.some((f2) => f2.where === "end-state" && f2.index === site.index) ? "sensitive" : "cascade";
54026
54160
  }
54027
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
+ }
54028
54175
  function judgeAssertions(journey, plan2, results) {
54029
54176
  const proofFor = (m, key2) => results.get(m.id)?.targeted?.find((t) => t.site === key2);
54030
54177
  return journeyAssertions(journey).map((site) => {
@@ -54033,14 +54180,25 @@ function judgeAssertions(journey, plan2, results) {
54033
54180
  if (paired.length === 0)
54034
54181
  return { site: key2, verdict: "unpaired" };
54035
54182
  const verdicts = paired.map((m) => ({ m, v: classifyMutation(site, m, results.get(m.id) ?? { outcome: "error", failedSites: [] }) }));
54183
+ let unsound;
54036
54184
  for (const v of PRECEDENCE) {
54037
54185
  const first = verdicts.find((x) => x.v === v);
54038
- if (first === void 0)
54186
+ if (first === void 0 && !(v === "cascade" && unsound !== void 0))
54039
54187
  continue;
54040
54188
  if (v === "sensitive") {
54041
- const proof = proofFor(first.m, key2);
54042
- return proof !== void 0 && proof.refused === void 0 ? { site: key2, verdict: v, provedBy: first.m.id, relaxedSites: proof.relaxedSites } : { site: key2, verdict: v, provedBy: first.m.id };
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;
54043
54199
  }
54200
+ if (v === "cascade" && unsound !== void 0)
54201
+ return { site: key2, verdict: v, targetedRefusal: unsound };
54044
54202
  if (v === "cascade") {
54045
54203
  const refusal = paired.map((m) => proofFor(m, key2)).find((t) => t !== void 0 && t.refused !== void 0);
54046
54204
  if (refusal !== void 0)
@@ -54283,7 +54441,8 @@ var HANDOFF_FIELD_TYPES = ["id", "text", "path", "url", "timestamp"];
54283
54441
  var HANDOFF_CONTRACT_RE = /^[a-z0-9][a-z0-9.-]{0,63}@[1-9][0-9]{0,3}$/;
54284
54442
  var HandoffFieldSchema = external_exports.object({
54285
54443
  type: external_exports.enum(HANDOFF_FIELD_TYPES, { message: `E_HANDOFF_SCHEMA: a field type is one of ${HANDOFF_FIELD_TYPES.join("|")}` }),
54286
- required: external_exports.boolean().optional()
54444
+ required: external_exports.boolean().optional(),
54445
+ origins: external_exports.array(external_exports.string()).optional()
54287
54446
  }).strict();
54288
54447
  var HandoffFieldsSchema = external_exports.record(external_exports.string().min(1), HandoffFieldSchema);
54289
54448
  var JourneyOutputsSchema = external_exports.object({
@@ -54391,7 +54550,8 @@ function fieldErrors(fields, bag, ctx) {
54391
54550
  errors.push({ field: name, code: "missing" });
54392
54551
  continue;
54393
54552
  }
54394
- const err = validateFieldValue(decl.type, value, ctx);
54553
+ const fieldCtx = decl.type === "url" && decl.origins !== void 0 ? { allowedOrigins: decl.origins } : ctx;
54554
+ const err = validateFieldValue(decl.type, value, fieldCtx);
54395
54555
  if (err !== null)
54396
54556
  errors.push({ field: name, code: err.code });
54397
54557
  }
@@ -54496,6 +54656,39 @@ function forEachNames(journey) {
54496
54656
  names.add(step.as);
54497
54657
  return names;
54498
54658
  }
54659
+ var LOOPBACK_ORIGIN_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
54660
+ function originEntryError(entry) {
54661
+ let url2;
54662
+ try {
54663
+ url2 = new URL(entry);
54664
+ } catch {
54665
+ return "is not an origin";
54666
+ }
54667
+ if (url2.origin !== entry || url2.hostname.includes("*"))
54668
+ return "is not an exact origin";
54669
+ if (url2.protocol === "https:")
54670
+ return null;
54671
+ if (url2.protocol === "http:" && LOOPBACK_ORIGIN_HOSTS.has(url2.hostname))
54672
+ return null;
54673
+ return "must be https, or http only for a loopback host";
54674
+ }
54675
+ function originsFieldIssues(path2, fields) {
54676
+ const issues = [];
54677
+ for (const [name, decl] of Object.entries(fields)) {
54678
+ if (decl.origins === void 0)
54679
+ continue;
54680
+ if (decl.type !== "url") {
54681
+ issues.push({ path: [...path2, name], message: `E_HANDOFF_SCHEMA: origins is only allowed on url fields ("${name}" is ${decl.type})` });
54682
+ continue;
54683
+ }
54684
+ decl.origins.forEach((entry, i) => {
54685
+ const reason = originEntryError(entry);
54686
+ if (reason !== null)
54687
+ issues.push({ path: [...path2, name, i], message: `E_HANDOFF_SCHEMA: origins entry "${entry}" ${reason}` });
54688
+ });
54689
+ }
54690
+ return issues;
54691
+ }
54499
54692
  function handoffSchemaIssues(journey) {
54500
54693
  const issues = [];
54501
54694
  const outputs = journey.metadata.outputs;
@@ -54508,6 +54701,7 @@ function handoffSchemaIssues(journey) {
54508
54701
  if (outputs.collection !== void 0 && !forEachNames(journey).has(outputs.collection)) {
54509
54702
  issues.push({ path: ["metadata", "outputs", "collection"], message: `E_HANDOFF_SCHEMA: outputs.collection "${outputs.collection}" names no forEach as` });
54510
54703
  }
54704
+ issues.push(...originsFieldIssues(["metadata", "outputs", "fields"], outputs.fields));
54511
54705
  if (outputs.key !== void 0) {
54512
54706
  if (new Set(outputs.key).size !== outputs.key.length)
54513
54707
  issues.push({ path: ["metadata", "outputs", "key"], message: "E_HANDOFF_SCHEMA: outputs.key: duplicate field" });
@@ -54546,6 +54740,7 @@ function handoffSchemaIssues(journey) {
54546
54740
  if (!(param in inputs.bindings))
54547
54741
  issues.push({ path: ["metadata", "inputs", "bindings"], message: `E_HANDOFF_SCHEMA: required param "${param}" has no binding` });
54548
54742
  }
54743
+ issues.push(...originsFieldIssues(["metadata", "inputs", "request", "fields"], inputs.request.fields));
54549
54744
  inputs.request.key.forEach((field3, i) => {
54550
54745
  const decl = inputs.request.fields[field3];
54551
54746
  if (decl === void 0)
@@ -54634,6 +54829,12 @@ var JourneySchema = external_exports.object({
54634
54829
  endState: external_exports.array(OutcomeCheckSchema).max(50).optional(),
54635
54830
  acceptedWeak: external_exports.object({ reason: external_exports.string().min(1), rules: external_exports.array(external_exports.string()) }).strict().optional(),
54636
54831
  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(),
54832
+ // #616: a declared targeted-proof plan; the relax list is bounded tighter than the automatic proof.
54833
+ targetedProofs: external_exports.array(external_exports.object({
54834
+ check: external_exports.string().min(1).max(100),
54835
+ mustFailWhen: external_exports.string().min(1).max(200),
54836
+ relax: external_exports.array(external_exports.string().min(1).max(60)).min(1).max(MAX_DECLARED_RELAXATIONS)
54837
+ }).strict()).max(20).optional(),
54637
54838
  approval: external_exports.object({
54638
54839
  contentHash: external_exports.string().regex(/^[0-9a-f]{64}$/, "approval.contentHash: a sha256 hex digest"),
54639
54840
  at: external_exports.string().min(1),
@@ -54659,6 +54860,9 @@ var JourneySchema = external_exports.object({
54659
54860
  for (const issue3 of mutationPairIssues(j)) {
54660
54861
  ctx.addIssue({ code: "custom", path: ["metadata", "mutationPairs", issue3.index, issue3.field], message: issue3.message });
54661
54862
  }
54863
+ for (const issue3 of targetedProofPlanIssues(j)) {
54864
+ ctx.addIssue({ code: "custom", path: ["metadata", "targetedProofs", issue3.index, issue3.field], message: issue3.message });
54865
+ }
54662
54866
  const declared = /* @__PURE__ */ new Set([...j.metadata.params, ...(j.metadata.parameters ?? []).map((p) => p.name)]);
54663
54867
  j.recording.pages.forEach((page, pi) => page.steps.forEach((rs, si) => {
54664
54868
  if (rs.step.kind !== "navigate")
@@ -55498,7 +55702,9 @@ var ReviewVerifySchema = external_exports.discriminatedUnion("status", [
55498
55702
  /** True when the Journey changed after this proof ran (the verdict is for other content). */
55499
55703
  stale: external_exports.boolean(),
55500
55704
  reason: external_exports.string().optional(),
55501
- summary: external_exports.record(external_exports.string(), external_exports.number())
55705
+ summary: external_exports.record(external_exports.string(), external_exports.number()),
55706
+ /** #616: a reviewable declared targeted-proof plan the automatic bound refused (names only). */
55707
+ suggestedTargetedProofs: external_exports.array(external_exports.object({ check: external_exports.string(), mustFailWhen: external_exports.string(), relax: external_exports.array(external_exports.string()) }).strict()).optional()
55502
55708
  }).strict()
55503
55709
  ]);
55504
55710
  var DiffSchema = external_exports.object({ added: external_exports.array(external_exports.string()), removed: external_exports.array(external_exports.string()) }).strict();
@@ -55518,7 +55724,9 @@ var ReviewChangeSchema = external_exports.discriminatedUnion("kind", [
55518
55724
  stepIdOnly: external_exports.boolean().optional(),
55519
55725
  steps: DiffSchema,
55520
55726
  assertions: DiffSchema,
55521
- sideEffects: DiffSchema
55727
+ sideEffects: DiffSchema,
55728
+ /** #616: proof-plan changes (`metadata.mutationPairs` / `metadata.targetedProofs`); omitted when neither side declares any. */
55729
+ proofPlans: DiffSchema.optional()
55522
55730
  }).strict()
55523
55731
  ]);
55524
55732
  var ReviewAttentionSchema = external_exports.object({
@@ -55563,6 +55771,8 @@ var JourneyReviewSchema = external_exports.object({
55563
55771
  endStateText: external_exports.array(external_exports.string()).optional(),
55564
55772
  lint: external_exports.object({ errors: external_exports.number().int(), warnings: external_exports.number().int(), findings: external_exports.array(ReviewLintFindingSchema) }).strict(),
55565
55773
  acceptedWeak: external_exports.object({ reason: external_exports.string(), rules: external_exports.array(external_exports.string()) }).strict().optional(),
55774
+ /** #616: the declared targeted-proof plans (`metadata.targetedProofs`) a reviewer approves with the Journey. Additive. */
55775
+ targetedProofs: external_exports.array(external_exports.object({ check: external_exports.string(), mustFailWhen: external_exports.string(), relax: external_exports.array(external_exports.string()) }).strict()).optional(),
55566
55776
  verify: ReviewVerifySchema
55567
55777
  }).strict(),
55568
55778
  changeSinceApproval: ReviewChangeSchema,
@@ -62534,9 +62744,9 @@ function hostStarvedFailure(failure3, health) {
62534
62744
  }
62535
62745
 
62536
62746
  // src/build-info.generated.ts
62537
- var GENERATED_COMMIT = "0a3d2f5";
62538
- var GENERATED_TREE = "963f59f";
62539
- var GENERATED_BUILT_AT = "2026-10-19T17:41:04.191Z";
62747
+ var GENERATED_COMMIT = "f37c33b";
62748
+ var GENERATED_TREE = "cd26ad0";
62749
+ var GENERATED_BUILT_AT = "2026-10-22T22:17:55.712Z";
62540
62750
 
62541
62751
  // src/version.ts
62542
62752
  import { readFileSync as readFileSync5 } from "node:fs";
@@ -62558,13 +62768,13 @@ function readCliVersion() {
62558
62768
 
62559
62769
  // src/engine.ts
62560
62770
  function bundledCommit() {
62561
- return true ? "0a3d2f5" : void 0;
62771
+ return true ? "f37c33b" : void 0;
62562
62772
  }
62563
62773
  function bundledBuiltAt() {
62564
- return true ? "2026-10-19T17:41:04.191Z" : void 0;
62774
+ return true ? "2026-10-22T22:17:55.712Z" : void 0;
62565
62775
  }
62566
62776
  function bundledTree() {
62567
- return true ? "963f59f" : void 0;
62777
+ return true ? "cd26ad0" : void 0;
62568
62778
  }
62569
62779
  function currentBuildTree() {
62570
62780
  return bundledTree() ?? GENERATED_TREE;
@@ -79986,9 +80196,11 @@ function renderSteps(r, w, opts) {
79986
80196
  function renderProvesAtTheEnd(r, w, opts) {
79987
80197
  void opts;
79988
80198
  const endState = r.proof.endStateText ?? r.proof.endState;
79989
- if (endState.length > 0) {
80199
+ const plans = r.proof.targetedProofs ?? [];
80200
+ if (endState.length > 0 || plans.length > 0) {
79990
80201
  w.head("PROVES AT THE END", "Proves at the end");
79991
80202
  for (const e of endState) w.put(`${w.bullet}\u2713 `, e, w.bullet.length + 2);
80203
+ 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);
79992
80204
  w.gap();
79993
80205
  }
79994
80206
  }
@@ -80079,7 +80291,13 @@ function renderFullDetail(r, w, opts) {
80079
80291
  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"}`);
80080
80292
  if (c.stepIdOnly === true) w.put(w.bullet, STEP_ID_ONLY_LABEL);
80081
80293
  if (c.kind === "diff") {
80082
- for (const [label2, d] of [["Steps", c.steps], ["Assertions", c.assertions], ["Side effects", c.sideEffects]]) {
80294
+ const blocks = [
80295
+ ["Steps", c.steps],
80296
+ ["Assertions", c.assertions],
80297
+ ["Side effects", c.sideEffects]
80298
+ ];
80299
+ if (c.proofPlans !== void 0) blocks.push(["Proof plans", c.proofPlans]);
80300
+ for (const [label2, d] of blocks) {
80083
80301
  if (d.added.length === 0 && d.removed.length === 0) continue;
80084
80302
  w.put(w.bullet, `${label2}:`);
80085
80303
  for (const l2 of d.removed) w.put(`${w.bullet} `, `\u2212 ${l2}`, w.bullet.length + 4);
@@ -80360,6 +80578,8 @@ function buildChange(journey, hash3, snapshot2, stepIdOnly) {
80360
80578
  }
80361
80579
  const approvedHash = journeyReviewHash(snapshot2);
80362
80580
  const approvedAt = approval?.at ?? snapshot2.metadata.approval?.at;
80581
+ const beforePlans = proofPlanLines(snapshot2);
80582
+ const afterPlans = proofPlanLines(journey);
80363
80583
  return {
80364
80584
  kind: "diff",
80365
80585
  approvedHash,
@@ -80368,7 +80588,9 @@ function buildChange(journey, hash3, snapshot2, stepIdOnly) {
80368
80588
  ...idOnly,
80369
80589
  steps: multisetDiff(stepLines(snapshot2), stepLines(journey)),
80370
80590
  assertions: multisetDiff(assertionLines(snapshot2), assertionLines(journey)),
80371
- sideEffects: multisetDiff(sideEffectLines(snapshot2), sideEffectLines(journey))
80591
+ sideEffects: multisetDiff(sideEffectLines(snapshot2), sideEffectLines(journey)),
80592
+ // #616: only present when a side declares a proof plan, so an unrelated diff stays as before.
80593
+ ...beforePlans.length === 0 && afterPlans.length === 0 ? {} : { proofPlans: multisetDiff(beforePlans, afterPlans) }
80372
80594
  };
80373
80595
  }
80374
80596
  function stepLines(j) {
@@ -80380,6 +80602,12 @@ function assertionLines(j) {
80380
80602
  ...journeyAssertions(j).filter((s3) => s3.where === "step-request").map((s3) => `step request: ${describeCheck(s3.check)}`)
80381
80603
  ];
80382
80604
  }
80605
+ function proofPlanLines(j) {
80606
+ return [
80607
+ ...(j.metadata.mutationPairs ?? []).map((p) => `mutation pair: ${p.check} must fail when ${p.mustFailWhen}`),
80608
+ ...(j.metadata.targetedProofs ?? []).map((p) => `targeted proof: ${p.check} against ${p.mustFailWhen} may relax ${p.relax.join(", ")}`)
80609
+ ];
80610
+ }
80383
80611
  function sideEffectLines(j) {
80384
80612
  return [
80385
80613
  ...[...new Set(buildWriteRequests(j).map((w) => `write ${w.method} ${w.endpoint}`))],
@@ -80417,7 +80645,8 @@ function buildVerify(journey, hash3, rec) {
80417
80645
  // #523: an executable-content change stales the proof; an old record without it compares the full hash.
80418
80646
  stale: rec.proofHash === void 0 ? rec.contentHash !== hash3 : rec.proofHash !== journeyProofHash(journey),
80419
80647
  ...rec.reason === void 0 ? {} : { reason: rec.reason },
80420
- summary: { ...rec.summary }
80648
+ summary: { ...rec.summary },
80649
+ ...rec.suggestedTargetedProofs === void 0 ? {} : { suggestedTargetedProofs: rec.suggestedTargetedProofs.map((p) => ({ ...p, relax: [...p.relax] })) }
80421
80650
  };
80422
80651
  }
80423
80652
  function buildJourneyReview(journey, ctx = {}) {
@@ -80495,6 +80724,8 @@ function buildJourneyReview(journey, ctx = {}) {
80495
80724
  findings: findings.map((f2) => ({ rule: f2.rule, level: f2.level, ...f2.step === void 0 ? {} : { step: f2.step }, message: f2.message }))
80496
80725
  },
80497
80726
  ...m.acceptedWeak === void 0 ? {} : { acceptedWeak: { reason: m.acceptedWeak.reason, rules: [...m.acceptedWeak.rules] } },
80727
+ // #616: the declared targeted-proof plans, copied out of the metadata for review.
80728
+ ...m.targetedProofs === void 0 || m.targetedProofs.length === 0 ? {} : { targetedProofs: m.targetedProofs.map((p) => ({ check: p.check, mustFailWhen: p.mustFailWhen, relax: [...p.relax] })) },
80498
80729
  verify: buildVerify(journey, hash3, ctx.lastVerify)
80499
80730
  },
80500
80731
  changeSinceApproval: buildChange(journey, hash3, ctx.approvedSnapshot, ctx.stepIdOnly === true),
@@ -94738,6 +94969,7 @@ var JourneyOutcomeChecks = class {
94738
94969
  async #judge(s3, fallback) {
94739
94970
  const window2 = s3.capture ?? fallback;
94740
94971
  if (window2 === void 0) return [];
94972
+ const ownCapture = s3.capture !== void 0;
94741
94973
  const since = s3.startedAt;
94742
94974
  const until = s3.until;
94743
94975
  const out = [];
@@ -94746,7 +94978,7 @@ var JourneyOutcomeChecks = class {
94746
94978
  await evaluateNetworkCheckAwaiting(
94747
94979
  c,
94748
94980
  () => ({
94749
- 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)),
94981
+ 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)),
94750
94982
  truncated: window2.truncated
94751
94983
  }),
94752
94984
  s3.responseTimeoutMs ?? this.#responseTimeoutMs
@@ -108612,7 +108844,7 @@ function redactProducerParams(reason, params) {
108612
108844
  return params === void 0 ? reason : redactReason(reason, params, Object.keys(params));
108613
108845
  }
108614
108846
  function chainSucceeded(result) {
108615
- return result.unmatched.length === 0 && result.runs.every((run) => run.outcome === "ok");
108847
+ return result.unmatched.length === 0 && (result.rejected?.length ?? 0) === 0 && result.runs.every((run) => run.outcome === "ok");
108616
108848
  }
108617
108849
  function sharedRunOptions(opts) {
108618
108850
  return {
@@ -108707,7 +108939,8 @@ async function runHandoffChain(inputs, outputs, fromJourney, fromOutput, request
108707
108939
  runs.push({ ...identity, outcome: "error", ...stamp, reason: redactProducerParams(redactReason(err instanceof Error ? err.message : String(err), params, redactors), opts.params) });
108708
108940
  }
108709
108941
  }
108710
- return { from: { journey: opts.from, outcome: "ok", records: records.length }, runs, unmatched, rejected };
108942
+ const total = requestValidation.valid.length + requestValidation.rejected.length;
108943
+ return { from: { journey: opts.from, outcome: "ok", records: records.length }, runs, unmatched, requests: { total, ran: runs.length }, rejected };
108711
108944
  }
108712
108945
  async function runJourneyChain(opts, deps = {}) {
108713
108946
  const runner = deps.runJourney ?? runJourneyProgrammatically;
@@ -108758,6 +108991,7 @@ async function runJourneyChain(opts, deps = {}) {
108758
108991
  }
108759
108992
  const runs = [];
108760
108993
  const unmatched = [];
108994
+ let ran = 0;
108761
108995
  for (const request of requests ?? []) {
108762
108996
  const record2 = byKey.get(recordKey2(request.commentId, request.account));
108763
108997
  if (record2 === void 0) {
@@ -108769,6 +109003,7 @@ async function runJourneyChain(opts, deps = {}) {
108769
109003
  runs.push({ commentId: request.commentId, account: request.account, outcome: E_CHAIN_FIELD, reason: mapped.detail });
108770
109004
  continue;
108771
109005
  }
109006
+ ran += 1;
108772
109007
  try {
108773
109008
  const result = await runner({ ...shared3, id: opts.to, params: mapped.params });
108774
109009
  runs.push({
@@ -108781,7 +109016,7 @@ async function runJourneyChain(opts, deps = {}) {
108781
109016
  runs.push({ commentId: request.commentId, account: request.account, outcome: "error", reason: redactProducerParams(redactReason(err instanceof Error ? err.message : String(err), mapped.params), opts.params) });
108782
109017
  }
108783
109018
  }
108784
- return { from: { journey: opts.from, outcome: "ok", records: count }, runs, unmatched };
109019
+ return { from: { journey: opts.from, outcome: "ok", records: count }, runs, unmatched, requests: { total: (requests ?? []).length, ran } };
108785
109020
  }
108786
109021
 
108787
109022
  // src/journey-cli/chain.ts
@@ -109237,6 +109472,8 @@ async function verifyJourneyMutations(opts, run = runJourneyProgrammatically) {
109237
109472
  const checkOf = new Map(sites.map((s3) => [assertionSiteKey(s3), describeCheck(s3.check)]));
109238
109473
  const plan2 = planJourneyMutations(journey, { ...lintOpts, params: opts.params });
109239
109474
  const mutations = [];
109475
+ const suggested = [];
109476
+ const seenSuggestions = /* @__PURE__ */ new Set();
109240
109477
  if (base.outcome === "ok") {
109241
109478
  const replayOpts2 = { ...lintOpts, params: opts.params };
109242
109479
  const runReplay = async (m, describe6) => {
@@ -109283,7 +109520,18 @@ async function verifyJourneyMutations(opts, run = runJourneyProgrammatically) {
109283
109520
  if (pair.mutation !== m.id) continue;
109284
109521
  const site = siteByKey.get(pair.site);
109285
109522
  if (site === void 0 || classifyMutation(site, m, initial) !== "cascade") continue;
109286
- targeted.push(await runTargetedProof(journey, plan2, m, site, initial, (describe6) => runReplay(m, describe6), replayOpts2));
109523
+ const declared = declaredTargetedProofFor(journey, m, pair.site);
109524
+ targeted.push(
109525
+ 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)
109526
+ );
109527
+ const suggestion = suggestTargetedProofPlan(targeted[targeted.length - 1], m);
109528
+ if (suggestion !== null) {
109529
+ const key2 = `${suggestion.check}\0${suggestion.mustFailWhen}\0${suggestion.relax.join(",")}`;
109530
+ if (!seenSuggestions.has(key2)) {
109531
+ seenSuggestions.add(key2);
109532
+ suggested.push(suggestion);
109533
+ }
109534
+ }
109287
109535
  }
109288
109536
  mutations.push({
109289
109537
  kind: m.kind,
@@ -109310,8 +109558,12 @@ async function verifyJourneyMutations(opts, run = runJourneyProgrammatically) {
109310
109558
  verdict: a.verdict,
109311
109559
  ...a.provedBy === void 0 ? {} : { provedBy: a.provedBy },
109312
109560
  ...a.relaxedSites === void 0 ? {} : { relaxedSites: a.relaxedSites },
109313
- ...a.targetedRefusal === void 0 ? {} : { targetedRefusal: a.targetedRefusal }
109561
+ ...a.targetedRefusal === void 0 ? {} : { targetedRefusal: a.targetedRefusal },
109562
+ // #616: the proving proof was a declared plan, not the automatic relaxation search.
109563
+ ...a.provedBy !== void 0 && results.get(a.provedBy)?.targeted?.some((t) => t.site === a.site && t.declared === true) === true ? { declaredProof: true } : {}
109314
109564
  }));
109565
+ const order = new Map(journeyAssertions(journey).map((s3, i) => [assertionSiteKey(s3), i]));
109566
+ const siteOrder = (key2) => order.get(key2) ?? Number.MAX_SAFE_INTEGER;
109315
109567
  const { verdict, reason } = mutationProofVerdict({ basePassed: base.outcome === "ok", mutations, assertions: judged });
109316
109568
  const count = (v) => assertions.filter((a) => a.verdict === v).length;
109317
109569
  const report2 = {
@@ -109331,7 +109583,9 @@ async function verifyJourneyMutations(opts, run = runJourneyProgrammatically) {
109331
109583
  unpaired: count("unpaired"),
109332
109584
  // #609: assertions proven sensitive through a targeted proof that relaxed earlier sites.
109333
109585
  targeted: assertions.filter((a) => (a.relaxedSites?.length ?? 0) > 0).length
109334
- }
109586
+ },
109587
+ // #616: in assertion order, so the first suggestion is the one for the refusal the verdict quotes.
109588
+ ...suggested.length === 0 ? {} : { suggestedTargetedProofs: [...suggested].sort((a, b2) => siteOrder(a.check) - siteOrder(b2.check)) }
109335
109589
  };
109336
109590
  const redacted = redactSecretParams(report2, journey, opts.params);
109337
109591
  await writeVerifyRecord(opts.dir, {
@@ -109341,12 +109595,13 @@ async function verifyJourneyMutations(opts, run = runJourneyProgrammatically) {
109341
109595
  verdict: redacted.verdict,
109342
109596
  ...redacted.reason === void 0 ? {} : { reason: redacted.reason },
109343
109597
  summary: { ...redacted.summary },
109598
+ ...redacted.suggestedTargetedProofs === void 0 ? {} : { suggestedTargetedProofs: redacted.suggestedTargetedProofs },
109344
109599
  at: clock.nowIso()
109345
109600
  });
109346
109601
  return redacted;
109347
109602
  }
109348
109603
  function verifyAssertionLine(a) {
109349
- const relaxed2 = a.relaxedSites === void 0 || a.relaxedSites.length === 0 ? "" : `, relaxed ${a.relaxedSites.join(", ")}`;
109604
+ const relaxed2 = a.relaxedSites === void 0 || a.relaxedSites.length === 0 ? "" : a.declaredProof === true ? `, declared plan relaxed ${a.relaxedSites.join(", ")}` : `, relaxed ${a.relaxedSites.join(", ")}`;
109350
109605
  const proved = a.provedBy === void 0 ? "" : ` (proved by ${a.provedBy}${relaxed2})`;
109351
109606
  const refused = a.targetedRefusal === void 0 ? "" : ` (targeted proof refused: ${a.targetedRefusal})`;
109352
109607
  return `${a.verdict.padEnd(11)} ${a.site.padEnd(18)} ${a.check}${proved}${refused}`;
@@ -109507,13 +109762,14 @@ function proofCheck(ctx, journeysDir, cwd) {
109507
109762
  if (nothingToMutate(ctx.journey, verify2.summary)) {
109508
109763
  return { check: { check: "proof", journey: ctx.id, state: "pass", detail: "read-only with no inputs: nothing a mutation could break", evidence } };
109509
109764
  }
109765
+ const suggested = verify2.status === "recorded" ? verify2.suggestedTargetedProofs : void 0;
109510
109766
  return {
109511
109767
  check: { check: "proof", journey: ctx.id, state: "inconclusive", detail: verify2.reason ?? `proof verdict ${verify2.verdict}`, evidence },
109512
- blocker: { check: "proof", journey: ctx.id, why: inconclusiveProofGuidance(verify2.reason ?? ""), next: `jevitate journey verify ${ctx.id} --mutate` }
109768
+ blocker: { check: "proof", journey: ctx.id, why: inconclusiveProofGuidance(verify2.reason ?? "", suggested), next: `jevitate journey verify ${ctx.id} --mutate` }
109513
109769
  };
109514
109770
  }
109515
109771
  var TARGETED_REFUSAL_MARKER = "a targeted proof was refused: ";
109516
- function inconclusiveProofGuidance(reason) {
109772
+ function inconclusiveProofGuidance(reason, suggested) {
109517
109773
  const at3 = reason.indexOf(TARGETED_REFUSAL_MARKER);
109518
109774
  if (at3 < 0) return "inspect the cascade assertions in the verify report";
109519
109775
  const refusal = reason.slice(at3 + TARGETED_REFUSAL_MARKER.length).trim();
@@ -109522,7 +109778,15 @@ function inconclusiveProofGuidance(reason) {
109522
109778
  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`;
109523
109779
  }
109524
109780
  const bound2 = /relaxation bound (\d+)/.exec(refusal);
109525
- if (bound2 !== null) return `reduce the layered checks between the mutation and the target (relaxation bound ${bound2[1]})`;
109781
+ if (bound2 !== null) {
109782
+ const first = suggested?.[0];
109783
+ 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)`;
109784
+ return `${plan2}, then re-run verify (relaxation bound ${bound2[1]})`;
109785
+ }
109786
+ const blocked = /^(\S+) blocked the declared proof \(not in relax\)$/.exec(refusal);
109787
+ if (blocked !== null) return `add ${blocked[1]} to that plan's relax list (metadata.targetedProofs), then re-run verify`;
109788
+ const unsound = /^relaxed site (\S+) is not itself proven sensitive to (\S+)$/.exec(refusal);
109789
+ if (unsound !== null) return `pair ${unsound[1]} with ${unsound[2]} (metadata.mutationPairs) or make it a real defense, then re-run verify`;
109526
109790
  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";
109527
109791
  return "inspect the cascade assertions in the verify report";
109528
109792
  }
@@ -109570,10 +109834,18 @@ function oneJourneyReplay(result) {
109570
109834
  if (result.outcome === "inconclusive") return { state: "inconclusive", detail: result.reason ?? "the replay had no verdict" };
109571
109835
  return { state: "fail", detail: `the replay ended ${result.outcome}` };
109572
109836
  }
109837
+ function firstRejection(result) {
109838
+ const rejection = result.rejected?.[0];
109839
+ if (rejection === void 0) return void 0;
109840
+ const error62 = rejection.errors[0];
109841
+ return error62 === void 0 ? rejection.key : `${rejection.key}: ${error62.field} ${error62.code}`;
109842
+ }
109573
109843
  function pairReplay(result) {
109574
109844
  if (chainSucceeded(result)) return { state: "pass", detail: "the chain held for every request" };
109575
109845
  if (result.runs.some((r) => r.outcome === "inconclusive")) return { state: "inconclusive", detail: "a chained run had no verdict" };
109576
- return { state: "fail", detail: `${result.unmatched.length} unmatched request(s), ${result.runs.filter((r) => r.outcome !== "ok").length} failed run(s)` };
109846
+ const counts2 = `${result.unmatched.length} unmatched request(s), ${result.rejected?.length ?? 0} rejected, ${result.runs.filter((r) => r.outcome !== "ok").length} failed run(s)`;
109847
+ const first = firstRejection(result);
109848
+ return { state: "fail", detail: first === void 0 ? counts2 : `${counts2}; first rejection ${first}` };
109577
109849
  }
109578
109850
  function verdictOf2(checks, mode) {
109579
109851
  if (checks.some((c) => c.state === "fail")) return "blocked";
@@ -109614,13 +109886,14 @@ async function runJourneyReadiness(opts, deps = {}) {
109614
109886
  let environment;
109615
109887
  let replay;
109616
109888
  let boundConsumerParams;
109889
+ let chained;
109617
109890
  if (mode !== "report") {
109618
109891
  const producerSchema = journeyParamSchema(contexts[0].journey).required;
109619
109892
  environment = resolveTarget2(opts, deps, requiredParams(contexts[0].journey, pair, opts.requests !== void 0), producerSchema);
109620
109893
  const params = { ...opts.params ?? {} };
109621
109894
  try {
109622
109895
  if (pair) {
109623
- const chained = await chain(
109896
+ chained = await chain(
109624
109897
  {
109625
109898
  from: opts.id,
109626
109899
  to: opts.with,
@@ -109666,11 +109939,13 @@ async function runJourneyReadiness(opts, deps = {}) {
109666
109939
  for (const ctx of contexts) {
109667
109940
  const consumer = pair && ctx.id === opts.with;
109668
109941
  if (consumer && boundConsumerParams === void 0) {
109942
+ const rejection = chained === void 0 ? void 0 : firstRejection(chained);
109943
+ const hint2 = rejection === void 0 ? `the chain bound no request for '${ctx.id}' \u2014 supply a --requests line that matches a discovered record` : `the chain rejected the consumer's matching record (${rejection})`;
109669
109944
  ctx.review = {
109670
109945
  ...ctx.review,
109671
109946
  proof: {
109672
109947
  ...ctx.review.proof,
109673
- verify: { status: "not-verified", hint: `the chain bound no request for '${ctx.id}' \u2014 supply a --requests line that matches a discovered record` }
109948
+ verify: { status: "not-verified", hint: hint2 }
109674
109949
  }
109675
109950
  };
109676
109951
  continue;