@patronage/software-factory 0.30.0 → 1.0.0-alpha.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/index.js CHANGED
@@ -14,10 +14,10 @@ import { setImmediate } from "node:timers";
14
14
  import { setImmediate as setImmediate$1, setTimeout as setTimeout$1 } from "node:timers/promises";
15
15
  import { Worker } from "node:worker_threads";
16
16
  import picomatch from "picomatch";
17
- import { promisify } from "node:util";
18
17
  import { parse } from "yaml";
18
+ import { promisify } from "node:util";
19
19
  //#region package.json
20
- var version = "0.30.0";
20
+ var version = "1.0.0-alpha.0";
21
21
  //#endregion
22
22
  //#region src/review-rungs.ts
23
23
  const EVIDENCE_REVIEW_RUNGS$1 = [
@@ -944,6 +944,7 @@ const globSchema = z.string().min(1).refine(isValidGlob, "must be a valid glob")
944
944
  const commandSchema = z.object({
945
945
  command: z.string().min(1),
946
946
  description: z.string().min(1),
947
+ impactTarget: z.string().min(1).optional(),
947
948
  name: z.string().min(1),
948
949
  requiredCheck: z.string().min(1).optional(),
949
950
  scope: z.enum([
@@ -969,10 +970,22 @@ const requiredCheckSchema = z.object({
969
970
  name: z.string().min(1),
970
971
  scope: requiredCheckScopeSchema$1.optional()
971
972
  }).strict();
973
+ const impactTargetSchema = z.object({
974
+ importers: z.array(z.string().min(1)).min(1),
975
+ name: z.string().min(1)
976
+ }).strict();
977
+ const impactConfigSchema = z.object({ targets: z.array(impactTargetSchema).min(1).superRefine((targets, context) => {
978
+ const names = targets.map((target) => target.name);
979
+ if (new Set(names).size !== names.length) context.addIssue({
980
+ code: "custom",
981
+ message: "impact.targets entries must have distinct names."
982
+ });
983
+ }) }).strict();
972
984
  const factoryProjectProfileSchema = z.object({
973
985
  $schema: z.string().min(1).optional(),
974
986
  extensions: z.record(z.string(), z.unknown()).optional(),
975
987
  hq: hqIngestConfigSchema.optional(),
988
+ impact: impactConfigSchema.optional(),
976
989
  proof: z.object({ classificationPolicy: z.object({
977
990
  docsOnly: z.array(globSchema).default([]),
978
991
  trivial: z.array(globSchema).default([])
@@ -1006,6 +1019,30 @@ const factoryProjectProfileSchema = z.object({
1006
1019
  });
1007
1020
  commandNames.add(command.name);
1008
1021
  }
1022
+ const impactTargetNames = new Set((profile.impact?.targets ?? []).map((target) => target.name));
1023
+ for (const [commandIndex, command] of profile.verification.commands.entries()) {
1024
+ if (command.impactTarget === void 0) continue;
1025
+ if (!impactTargetNames.has(command.impactTarget)) context.addIssue({
1026
+ code: "custom",
1027
+ message: `Verification command impactTarget "${command.impactTarget}" does not name a declared impact.targets entry.`,
1028
+ path: [
1029
+ "verification",
1030
+ "commands",
1031
+ commandIndex,
1032
+ "impactTarget"
1033
+ ]
1034
+ });
1035
+ if (command.requiredCheck !== void 0 && command.requiredCheck !== command.impactTarget) context.addIssue({
1036
+ code: "custom",
1037
+ message: `Verification command "${command.name}" maps requiredCheck "${command.requiredCheck}" but scopes on impactTarget "${command.impactTarget}"; a scoped command's evidence demand and impact target must be the same name.`,
1038
+ path: [
1039
+ "verification",
1040
+ "commands",
1041
+ commandIndex,
1042
+ "impactTarget"
1043
+ ]
1044
+ });
1045
+ }
1009
1046
  const requiredChecks = new Map((profile.requiredChecks ?? []).map((check) => [check.name, check]));
1010
1047
  const mappedChecks = /* @__PURE__ */ new Set();
1011
1048
  for (const [commandIndex, command] of profile.verification.commands.entries()) {
@@ -1036,7 +1073,7 @@ const factoryProjectProfileSchema = z.object({
1036
1073
  mappedChecks.add(command.requiredCheck);
1037
1074
  }
1038
1075
  }).meta({ description: PROFILE_JSON_SCHEMA_DESCRIPTION });
1039
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1076
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1040
1077
  const stringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1041
1078
  const quoteList = (values) => JSON.stringify(values);
1042
1079
  const namesExistingFile = (root, entry) => {
@@ -1085,13 +1122,13 @@ const migratedPolicyLines = (policy, root) => {
1085
1122
  */
1086
1123
  function v2MigrationLines(input, root) {
1087
1124
  const lines = [];
1088
- const proof = isRecord(input.proof) ? input.proof : void 0;
1089
- if (proof && isRecord(proof.classificationPolicy)) lines.push(...migratedPolicyLines(proof.classificationPolicy, root));
1090
- const commands = isRecord(input.verification) ? input.verification.commands : void 0;
1125
+ const proof = isRecord$1(input.proof) ? input.proof : void 0;
1126
+ if (proof && isRecord$1(proof.classificationPolicy)) lines.push(...migratedPolicyLines(proof.classificationPolicy, root));
1127
+ const commands = isRecord$1(input.verification) ? input.verification.commands : void 0;
1091
1128
  if (Array.isArray(commands)) {
1092
- for (const [index, command] of commands.entries()) if (isRecord(command) && command.scope === "always") lines.push(`verification.commands[${index}].scope "always" -> "docs-only"`);
1129
+ for (const [index, command] of commands.entries()) if (isRecord$1(command) && command.scope === "always") lines.push(`verification.commands[${index}].scope "always" -> "docs-only"`);
1093
1130
  }
1094
- if (isRecord(input.hq) && typeof input.hq.endpoint === "string") try {
1131
+ if (isRecord$1(input.hq) && typeof input.hq.endpoint === "string") try {
1095
1132
  const url = new URL(input.hq.endpoint);
1096
1133
  if (url.href !== `${url.origin}/` && url.href !== url.origin) lines.push(`hq.endpoint ${JSON.stringify(input.hq.endpoint)} -> ${JSON.stringify(url.origin)}`);
1097
1134
  } catch {}
@@ -1103,25 +1140,25 @@ function hasCorrectnessOnlyReview(review) {
1103
1140
  }
1104
1141
  const v4MigrationLines = (input) => {
1105
1142
  const lines = [];
1106
- if (isRecord(input.project) && "key" in input.project) lines.push("project.key -> removed; current proof writers derive projectKey from repository.name");
1107
- if (isRecord(input.repository) && "defaultBranch" in input.repository) lines.push("repository.defaultBranch -> removed; commands resolve the live base from Git/GitHub or an explicit --base");
1108
- if (isRecord(input.env) && "required" in input.env) lines.push("env.required -> removed; the command that needs an environment value owns its fail-closed diagnostic");
1109
- if (isRecord(input.review)) {
1143
+ if (isRecord$1(input.project) && "key" in input.project) lines.push("project.key -> removed; current proof writers derive projectKey from repository.name");
1144
+ if (isRecord$1(input.repository) && "defaultBranch" in input.repository) lines.push("repository.defaultBranch -> removed; commands resolve the live base from Git/GitHub or an explicit --base");
1145
+ if (isRecord$1(input.env) && "required" in input.env) lines.push("env.required -> removed; the command that needs an environment value owns its fail-closed diagnostic");
1146
+ if (isRecord$1(input.review)) {
1110
1147
  const isCorrectnessOnlyReview = hasCorrectnessOnlyReview(input.review);
1111
1148
  if ("defaultMaxCycles" in input.review) lines.push("review.defaultMaxCycles -> removed; the factory review cap is 5");
1112
1149
  if ("modes" in input.review) lines.push("review.modes -> removed; correctness is always required");
1113
1150
  if (isCorrectnessOnlyReview) lines.push("review -> removed; a correctness-only review has no v5 configuration, so delete the object");
1114
1151
  if (Array.isArray(input.review.conditional)) {
1115
- for (const [index, conditional] of input.review.conditional.entries()) if (isRecord(conditional) && "modes" in conditional) lines.push(`review.conditional[${index}].modes -> removed; each retained conditional path list demands security review`);
1152
+ for (const [index, conditional] of input.review.conditional.entries()) if (isRecord$1(conditional) && "modes" in conditional) lines.push(`review.conditional[${index}].modes -> removed; each retained conditional path list demands security review`);
1116
1153
  }
1117
1154
  }
1118
1155
  if (Array.isArray(input.requiredChecks)) {
1119
- for (const [index, check] of input.requiredChecks.entries()) if (isRecord(check) && "checkType" in check) lines.push(`requiredChecks[${index}].checkType -> removed; external required checks are verify-type`);
1156
+ for (const [index, check] of input.requiredChecks.entries()) if (isRecord$1(check) && "checkType" in check) lines.push(`requiredChecks[${index}].checkType -> removed; external required checks are verify-type`);
1120
1157
  }
1121
1158
  return lines;
1122
1159
  };
1123
1160
  function legacyProfileMigration(input, root) {
1124
- if (!(isRecord(input) && typeof input.schemaVersion === "number" && LEGACY_PROFILE_SCHEMA_VERSIONS.includes(input.schemaVersion))) return;
1161
+ if (!(isRecord$1(input) && typeof input.schemaVersion === "number" && LEGACY_PROFILE_SCHEMA_VERSIONS.includes(input.schemaVersion))) return;
1125
1162
  const foundVersion = input.schemaVersion;
1126
1163
  const lines = [`schemaVersion ${foundVersion} -> 5`];
1127
1164
  if (foundVersion === 2) lines.push(...v2MigrationLines(input, root));
@@ -3792,6 +3829,623 @@ const authorizeDemandWaiver = ({ authenticatedLogin, session }) => {
3792
3829
  ...normalizedSession && normalizedSession !== "unknown" ? { session: normalizedSession } : {}
3793
3830
  } };
3794
3831
  };
3832
+ //#endregion
3833
+ //#region src/impact-stamp.ts
3834
+ const LOCKFILE_PATH = "pnpm-lock.yaml";
3835
+ const SUPPORTED_LOCKFILE_VERSION = "9.0";
3836
+ const impactStampTargetSchema = z.object({
3837
+ basis: z.string().min(1),
3838
+ impact: z.enum(["affected", "not-affected"]),
3839
+ name: z.string().min(1)
3840
+ });
3841
+ /**
3842
+ * The recorded stamp. `targets` satisfies the completeness invariant: every
3843
+ * target the profile declares is present, each with its exact basis — never
3844
+ * silently absent. `basis: "conservative"` means an unmodelled or unprovable
3845
+ * input widened every target to full impact.
3846
+ */
3847
+ const impactStampSchema = z.object({
3848
+ basis: z.enum(["lockfile-scoped", "conservative"]),
3849
+ reasons: z.array(z.string()),
3850
+ stampVersion: z.literal(1),
3851
+ targets: z.array(impactStampTargetSchema).superRefine((targets, context) => {
3852
+ const names = targets.map((target) => target.name);
3853
+ if (new Set(names).size !== names.length) context.addIssue({
3854
+ code: "custom",
3855
+ message: "impact stamp targets must have distinct names."
3856
+ });
3857
+ })
3858
+ });
3859
+ /**
3860
+ * A configuration contradiction between the profile's impact declarations and
3861
+ * the repository's actual lockfile — e.g. a declared importer root that
3862
+ * matches no importer on either side of the delta. Controls fail loudly (ADR
3863
+ * 0024): this error must ABORT the run, never degrade to a stamp, because a
3864
+ * profile typo would otherwise silently mis-scope every future candidate.
3865
+ * Data/runtime failures (unreadable, unparseable, malformed lockfiles, reader
3866
+ * throws) are the opposite case and stay on the conservative-stamp path.
3867
+ */
3868
+ var ImpactStampConfigError = class extends Error {
3869
+ name = "ImpactStampConfigError";
3870
+ };
3871
+ const CONSERVATIVE_TARGET_BASIS = "conservative: impact not provable; full impact assumed";
3872
+ /**
3873
+ * The surfaces that consume the stamp. One classification, consumed
3874
+ * everywhere (#430): every surface routes through
3875
+ * {@link impactStampScopeDecision}, so no surface can be more trusting than
3876
+ * another, and no surface re-derives path rules of its own. The value only
3877
+ * selects the wording of the conservative floor a refusal falls back to.
3878
+ */
3879
+ const IMPACT_SCOPE_SURFACES = {
3880
+ demand: "full demand floor",
3881
+ "preview-lifecycle": "full preview-lifecycle floor",
3882
+ "verification-battery": "full verification-battery floor"
3883
+ };
3884
+ /**
3885
+ * Whether a trusted stamp RELEASES one target-scoped unit of work — an
3886
+ * external proof demand (#542 wave 2), a verification-battery command, or a
3887
+ * preview (Alchemy) lifecycle stack (#430 wave 3).
3888
+ *
3889
+ * The only release path is a `lockfile-scoped` stamp of exactly this build's
3890
+ * stamp version whose target entry for exactly this name is provably
3891
+ * `not-affected`. Every other input — no trusted stamp, an unknown stamp
3892
+ * version, a conservative stamp, a name the stamp does not classify, or an
3893
+ * `affected` verdict — keeps today's full-work floor. Work only ever gets
3894
+ * released, never added or satisfied: this function can withdraw a demand, it
3895
+ * can never certify one.
3896
+ *
3897
+ * Identity binding is the CALLER's precondition: pass only a stamp computed
3898
+ * for, or read from a pr:verify proof bound to, the current candidate (see
3899
+ * `trustedImpactStamp` in `pr-readiness/verification-proof.ts`); pass
3900
+ * `undefined` otherwise.
3901
+ */
3902
+ const impactStampScopeDecision = ({ stamp, surface, targetName }) => {
3903
+ const floor = IMPACT_SCOPE_SURFACES[surface];
3904
+ if (stamp === void 0) return {
3905
+ reason: `no trusted identity-bound impact stamp covers this candidate; ${floor}`,
3906
+ scoped: false
3907
+ };
3908
+ if (stamp.stampVersion !== 1) return {
3909
+ reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 1; ${floor}`,
3910
+ scoped: false
3911
+ };
3912
+ if (stamp.basis !== "lockfile-scoped") return {
3913
+ reason: `impact stamp is conservative (full impact assumed); ${floor}`,
3914
+ scoped: false
3915
+ };
3916
+ const matches = stamp.targets.filter((candidate) => candidate.name === targetName);
3917
+ if (matches.length > 1) return {
3918
+ reason: `impact stamp is self-contradictory: ${matches.length} rows classify a target named "${targetName}"; ${floor}`,
3919
+ scoped: false
3920
+ };
3921
+ const [target] = matches;
3922
+ if (target === void 0) return {
3923
+ reason: `impact stamp does not classify a target named "${targetName}"; ${floor}`,
3924
+ scoped: false
3925
+ };
3926
+ if (target.impact !== "not-affected") return {
3927
+ reason: `impact stamp records target "${targetName}" as affected (${target.basis})`,
3928
+ scoped: false
3929
+ };
3930
+ return {
3931
+ reason: `impact stamp released: target "${targetName}" is provably not affected by this candidate's delta (${target.basis})`,
3932
+ scoped: true
3933
+ };
3934
+ };
3935
+ const impactStampDemandRelease = ({ checkName, stamp }) => {
3936
+ const decision = impactStampScopeDecision({
3937
+ stamp,
3938
+ surface: "demand",
3939
+ targetName: checkName
3940
+ });
3941
+ return {
3942
+ reason: decision.reason,
3943
+ released: decision.scoped
3944
+ };
3945
+ };
3946
+ /**
3947
+ * The full-impact stamp: every declared target `affected`. This is the floor
3948
+ * every doubt path lands on, and the stamp a producer must fall back to if
3949
+ * stamp computation itself fails for any reason.
3950
+ */
3951
+ const conservativeImpactStamp = (targets, reasons) => ({
3952
+ basis: "conservative",
3953
+ reasons,
3954
+ stampVersion: 1,
3955
+ targets: targets.map((target) => ({
3956
+ basis: CONSERVATIVE_TARGET_BASIS,
3957
+ impact: "affected",
3958
+ name: target.name
3959
+ }))
3960
+ });
3961
+ const conservativeStamp = conservativeImpactStamp;
3962
+ const DEPENDENCY_FIELDS = [
3963
+ "dependencies",
3964
+ "devDependencies",
3965
+ "optionalDependencies"
3966
+ ];
3967
+ const importerDependencySchema = z.looseObject({
3968
+ specifier: z.string().optional(),
3969
+ version: z.string()
3970
+ });
3971
+ const importerSectionSchema = z.looseObject({
3972
+ dependencies: z.record(z.string(), importerDependencySchema).optional(),
3973
+ devDependencies: z.record(z.string(), importerDependencySchema).optional(),
3974
+ optionalDependencies: z.record(z.string(), importerDependencySchema).optional()
3975
+ });
3976
+ const snapshotSectionSchema = z.looseObject({
3977
+ dependencies: z.record(z.string(), z.string()).optional(),
3978
+ devDependencies: z.record(z.string(), z.string()).optional(),
3979
+ optionalDependencies: z.record(z.string(), z.string()).optional()
3980
+ });
3981
+ const pnpmLockfileSchema = z.looseObject({
3982
+ importers: z.record(z.string(), importerSectionSchema),
3983
+ lockfileVersion: z.unknown().optional(),
3984
+ packages: z.record(z.string(), z.unknown()).optional(),
3985
+ snapshots: z.record(z.string(), snapshotSectionSchema).optional()
3986
+ });
3987
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3988
+ const canonical = (value) => {
3989
+ if (Array.isArray(value)) return value.map(canonical);
3990
+ if (isRecord(value)) return Object.fromEntries(Object.keys(value).toSorted().map((key) => [key, canonical(value[key])]));
3991
+ return value;
3992
+ };
3993
+ const stableStringify$1 = (value) => JSON.stringify(canonical(value));
3994
+ const parseLockfile = (content) => {
3995
+ try {
3996
+ const parsed = parse(content);
3997
+ if (!isRecord(parsed)) return;
3998
+ const validated = pnpmLockfileSchema.safeParse(parsed);
3999
+ return validated.success ? validated.data : void 0;
4000
+ } catch {
4001
+ return;
4002
+ }
4003
+ };
4004
+ const nonGraphSections = ({ importers: _importers, packages: _packages, snapshots: _snapshots, ...rest }) => rest;
4005
+ const changedSectionKeys = (before, after) => {
4006
+ return [...new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})])].filter((key) => stableStringify$1(before?.[key]) !== stableStringify$1(after?.[key]));
4007
+ };
4008
+ const linkTarget = (importerPath, version) => {
4009
+ if (!version.startsWith("link:")) return;
4010
+ const raw = version.slice(5);
4011
+ const joined = importerPath === "." ? raw : `${importerPath}/${raw}`;
4012
+ const resolved = [];
4013
+ for (const segment of joined.split("/")) {
4014
+ if (segment === "" || segment === ".") continue;
4015
+ if (segment === "..") {
4016
+ resolved.pop();
4017
+ continue;
4018
+ }
4019
+ resolved.push(segment);
4020
+ }
4021
+ return resolved.length === 0 ? "." : resolved.join("/");
4022
+ };
4023
+ const importerMatchesTarget = (importerPath, importerRoots) => importerRoots.some((root) => importerPath === root || importerPath.startsWith(`${root}/`));
4024
+ const workspaceClosure = (lockfile, importerRoots) => {
4025
+ const closure = new Set(["."]);
4026
+ const queue = Object.keys(lockfile.importers ?? {}).filter((importerPath) => importerMatchesTarget(importerPath, importerRoots));
4027
+ while (queue.length > 0) {
4028
+ const importerPath = queue.pop();
4029
+ if (importerPath === void 0 || closure.has(importerPath)) continue;
4030
+ closure.add(importerPath);
4031
+ const section = lockfile.importers?.[importerPath];
4032
+ for (const field of DEPENDENCY_FIELDS) for (const dependency of Object.values(section?.[field] ?? {})) {
4033
+ const target = dependency.version === void 0 ? void 0 : linkTarget(importerPath, dependency.version);
4034
+ if (target !== void 0 && lockfile.importers?.[target]) queue.push(target);
4035
+ }
4036
+ }
4037
+ return closure;
4038
+ };
4039
+ const pushReference = (lockfile, queue, name, value) => {
4040
+ if (value.startsWith("link:")) return;
4041
+ queue.push(`${name}@${value}`);
4042
+ if (lockfile.snapshots?.[value]) queue.push(value);
4043
+ };
4044
+ const snapshotClosure = (lockfile, importerPaths) => {
4045
+ const keys = /* @__PURE__ */ new Set();
4046
+ const queue = [];
4047
+ for (const importerPath of importerPaths) {
4048
+ const section = lockfile.importers?.[importerPath];
4049
+ for (const field of DEPENDENCY_FIELDS) for (const [name, dependency] of Object.entries(section?.[field] ?? {})) if (dependency.version !== void 0) pushReference(lockfile, queue, name, dependency.version);
4050
+ }
4051
+ while (queue.length > 0) {
4052
+ const key = queue.pop();
4053
+ if (key === void 0 || keys.has(key)) continue;
4054
+ keys.add(key);
4055
+ const snapshot = lockfile.snapshots?.[key];
4056
+ for (const field of DEPENDENCY_FIELDS) for (const [name, version] of Object.entries(snapshot?.[field] ?? {})) pushReference(lockfile, queue, name, version);
4057
+ }
4058
+ return keys;
4059
+ };
4060
+ const withoutPeerSuffix = (key) => key.split("(")[0] ?? key;
4061
+ const supportedLockfileVersion = (lockfile) => String(lockfile.lockfileVersion) === SUPPORTED_LOCKFILE_VERSION;
4062
+ const targetImpactForLockfileDelta = (target, delta) => {
4063
+ const closures = delta.sides.map((lockfile) => ({
4064
+ importers: workspaceClosure(lockfile, target.importers),
4065
+ lockfile
4066
+ }));
4067
+ const changedImporter = delta.changedImporters.find((importerPath) => closures.some((side) => side.importers.has(importerPath)));
4068
+ if (changedImporter !== void 0) return {
4069
+ basis: `changed importer "${changedImporter}" is in this target's workspace closure`,
4070
+ impact: "affected",
4071
+ name: target.name
4072
+ };
4073
+ const resolved = new Set(closures.flatMap((side) => [...snapshotClosure(side.lockfile, side.importers)]));
4074
+ const resolvedBare = new Set([...resolved].map(withoutPeerSuffix));
4075
+ const changedKey = delta.changedSnapshots.find((key) => resolved.has(key)) ?? delta.changedPackages.find((key) => resolved.has(key) || resolvedBare.has(key));
4076
+ if (changedKey !== void 0) return {
4077
+ basis: `changed graph key "${changedKey}" is resolved by this target's importer closure`,
4078
+ impact: "affected",
4079
+ name: target.name
4080
+ };
4081
+ return {
4082
+ basis: `no changed importer or resolved graph key reaches importers [${target.importers.join(", ")}]`,
4083
+ impact: "not-affected",
4084
+ name: target.name
4085
+ };
4086
+ };
4087
+ /**
4088
+ * Compute the impact stamp for one candidate: every declared target recorded
4089
+ * as affected/not-affected with its exact basis. `not-affected` is only ever
4090
+ * returned for a provably-scoped pnpm-lock.yaml (v9) delta; every other input
4091
+ * — non-lockfile changed files, an empty diff, a missing, unparseable, or
4092
+ * structurally malformed lockfile side, an unsupported lockfile version, or a
4093
+ * delta outside the importers/packages/snapshots graph sections — widens to
4094
+ * full impact.
4095
+ *
4096
+ * Total by contract: this function never throws. Any unexpected failure in
4097
+ * reading, parsing, or traversal is itself a doubt path and returns the
4098
+ * conservative stamp, so a producer can never be aborted by stamp
4099
+ * computation.
4100
+ */
4101
+ const computeImpactStamp = (input) => {
4102
+ const targets = input.profile.impact?.targets ?? [];
4103
+ try {
4104
+ return computeImpactStampOrThrow(input, targets);
4105
+ } catch (error) {
4106
+ if (error instanceof ImpactStampConfigError) throw error;
4107
+ return conservativeStamp(targets, [`impact stamp computation failed (${error instanceof Error ? error.message : String(error)}); fail closed to full impact`]);
4108
+ }
4109
+ };
4110
+ const readLockfileSides = (readLockfile) => {
4111
+ let base;
4112
+ let head;
4113
+ try {
4114
+ base = readLockfile("base");
4115
+ head = readLockfile("head");
4116
+ } catch (error) {
4117
+ return {
4118
+ error,
4119
+ kind: "threw"
4120
+ };
4121
+ }
4122
+ if (base === void 0 || head === void 0) return {
4123
+ kind: "unreadable",
4124
+ side: base === void 0 ? "base" : "head"
4125
+ };
4126
+ const baseLock = parseLockfile(base);
4127
+ const headLock = parseLockfile(head);
4128
+ if (!(baseLock && headLock)) return {
4129
+ kind: "unparseable",
4130
+ side: baseLock ? "head" : "base"
4131
+ };
4132
+ if (![baseLock, headLock].every(supportedLockfileVersion)) return { kind: "unsupported-version" };
4133
+ return {
4134
+ kind: "ok",
4135
+ parsed: [baseLock, headLock]
4136
+ };
4137
+ };
4138
+ /**
4139
+ * Every declared importer root must exist somewhere in the delta: a root
4140
+ * matching no importer on EITHER side is a profile typo, and scoping from it
4141
+ * would leave only the implicit root importer in that target's closure —
4142
+ * silently classifying real changes `not-affected` forever. That is a
4143
+ * configuration contradiction, so it fails loudly instead of degrading. A root
4144
+ * present on ONE side only is legitimate (this very delta adds or removes the
4145
+ * importer) and scopes normally.
4146
+ *
4147
+ * Checked on EVERY computation whose lockfile sides are readable, not only on
4148
+ * the lockfile-only deltas that can actually scope. The check answers a
4149
+ * question about the PROFILE, which does not depend on what this candidate
4150
+ * changed; running it only where scoping happens would leave a typo latent
4151
+ * through every code PR and first surface it on the cheap lockfile-only PR
4152
+ * this machinery exists to speed up.
4153
+ */
4154
+ const assertDeclaredImporterRoots = (targets, sides) => {
4155
+ for (const target of targets) for (const root of target.importers) if (!sides.some((lockfile) => Object.keys(lockfile.importers).some((importerPath) => importerMatchesTarget(importerPath, [root])))) throw new ImpactStampConfigError(`impact target "${target.name}" declares importer root "${root}", but no importer under that root exists in pnpm-lock.yaml on either side of the delta; fix the profile's impact.targets declaration`);
4156
+ };
4157
+ const computeImpactStampOrThrow = ({ changedFiles, readLockfile }, targets) => {
4158
+ if (targets.length === 0) return {
4159
+ basis: "conservative",
4160
+ reasons: ["no impact targets declared in the profile; every surface keeps full demand"],
4161
+ stampVersion: 1,
4162
+ targets: []
4163
+ };
4164
+ const sides = readLockfileSides(readLockfile);
4165
+ if (sides.kind === "ok") assertDeclaredImporterRoots(targets, sides.parsed);
4166
+ if (changedFiles.length === 0) return conservativeStamp(targets, ["no changed files detected; full impact assumed"]);
4167
+ const unmodelledFiles = changedFiles.filter((file) => file !== LOCKFILE_PATH);
4168
+ if (unmodelledFiles.length > 0) return conservativeStamp(targets, [`changed files outside the lockfile classifier's model (${unmodelledFiles.toSorted().join(", ")}); full impact assumed`]);
4169
+ if (sides.kind === "threw") throw sides.error;
4170
+ if (sides.kind === "unreadable") return conservativeStamp(targets, [`pnpm-lock.yaml is unreadable at the ${sides.side} side; fail closed to full impact`]);
4171
+ if (sides.kind === "unparseable") return conservativeStamp(targets, [`pnpm-lock.yaml is unparseable or structurally malformed at the ${sides.side} side; fail closed to full impact`]);
4172
+ if (sides.kind === "unsupported-version") return conservativeStamp(targets, [`pnpm-lock.yaml is not lockfileVersion ${SUPPORTED_LOCKFILE_VERSION}; only pnpm v9 lockfiles are modelled; fail closed to full impact`]);
4173
+ const [baseLock, headLock] = sides.parsed;
4174
+ if (stableStringify$1(nonGraphSections(baseLock)) !== stableStringify$1(nonGraphSections(headLock))) return conservativeStamp(targets, ["pnpm-lock.yaml delta touches sections outside importers/packages/snapshots (e.g. settings, overrides, patchedDependencies); fail closed to full impact"]);
4175
+ const delta = {
4176
+ changedImporters: changedSectionKeys(baseLock.importers, headLock.importers),
4177
+ changedPackages: changedSectionKeys(baseLock.packages, headLock.packages),
4178
+ changedSnapshots: changedSectionKeys(baseLock.snapshots, headLock.snapshots),
4179
+ sides: [baseLock, headLock]
4180
+ };
4181
+ return {
4182
+ basis: "lockfile-scoped",
4183
+ reasons: ["pnpm-lock.yaml is the only changed file; impact scoped by importer/snapshot graph analysis"],
4184
+ stampVersion: 1,
4185
+ targets: targets.map((target) => targetImpactForLockfileDelta(target, delta))
4186
+ };
4187
+ };
4188
+ //#endregion
4189
+ //#region src/verification-battery.ts
4190
+ const UNCONDITIONAL_BASIS = "unconditional: the profile declares no impactTarget for this command";
4191
+ /**
4192
+ * The `vetoedTargets` contract, enforced at RUNTIME as well as at the type
4193
+ * level. TypeScript makes the parameter required for callers it compiles;
4194
+ * `new Set(undefined)` is a perfectly good empty set, so an untyped JavaScript
4195
+ * caller that omits it would otherwise scope exactly as if it had inspected
4196
+ * the envelopes and found nothing — the silent state requiring the parameter
4197
+ * exists to eliminate.
4198
+ *
4199
+ * A malformed CALL is API misuse, not data doubt: it fails loudly (ADR 0024)
4200
+ * rather than degrading to the conservative floor, because a producer that
4201
+ * never resolved the veto set has a bug its author must see.
4202
+ */
4203
+ const assertVetoedTargets = (vetoedTargets, caller) => {
4204
+ if (!Array.isArray(vetoedTargets)) throw new TypeError(`${caller} requires vetoedTargets: the target names with a current, candidate-bound FAILING envelope. Resolve it with boundFailingCheckNames and pass the result; pass [] to mean "inspected the envelopes and found no bound failure". It has no default because omitting it would silently scope work whose standing demand nothing on this head could meet.`);
4205
+ };
4206
+ /**
4207
+ * Plan one verification battery against the candidate's impact stamp.
4208
+ *
4209
+ * Pure and total: it makes a decision, it never reads a proof, a profile file,
4210
+ * or the filesystem, and no INPUT can make it throw — every doubt path is a
4211
+ * decision, not an exception. A malformed CALL is the one exception to that,
4212
+ * and deliberately so: omitting `vetoedTargets` is API misuse rather than data
4213
+ * doubt, and it throws (see {@link assertVetoedTargets}).
4214
+ *
4215
+ * `stamp` must already be trusted by
4216
+ * the caller — inside `pr:verify` that is the stamp just computed for this
4217
+ * candidate's own identity triple; anywhere else it is `trustedImpactStamp`'s
4218
+ * output or `undefined`.
4219
+ */
4220
+ const planVerificationBattery = ({ commands, stamp, vetoedTargets }) => {
4221
+ assertVetoedTargets(vetoedTargets, "planVerificationBattery");
4222
+ const vetoed = new Set(vetoedTargets);
4223
+ const dispositions = [];
4224
+ const execute = [];
4225
+ const notRequired = [];
4226
+ for (const command of commands) {
4227
+ const { impactTarget, name } = command;
4228
+ if (impactTarget === void 0) {
4229
+ dispositions.push({
4230
+ basis: UNCONDITIONAL_BASIS,
4231
+ disposition: "executed",
4232
+ name
4233
+ });
4234
+ execute.push(command);
4235
+ continue;
4236
+ }
4237
+ const decision = impactStampScopeDecision({
4238
+ stamp,
4239
+ surface: "verification-battery",
4240
+ targetName: impactTarget
4241
+ });
4242
+ const vetoedHere = decision.scoped && vetoed.has(impactTarget);
4243
+ const basis = vetoedHere ? `veto: a current envelope bound to this candidate records target "${impactTarget}" as FAILING, so the stamp's release is withheld (${decision.reason})` : decision.reason;
4244
+ const scoped = decision.scoped && !vetoedHere;
4245
+ dispositions.push({
4246
+ basis,
4247
+ disposition: scoped ? "not-required" : "executed",
4248
+ impactTarget,
4249
+ name
4250
+ });
4251
+ if (scoped) notRequired.push({
4252
+ basis,
4253
+ impactTarget,
4254
+ name
4255
+ });
4256
+ else execute.push(command);
4257
+ }
4258
+ return {
4259
+ dispositions,
4260
+ execute,
4261
+ notRequired
4262
+ };
4263
+ };
4264
+ /**
4265
+ * The completeness invariant as an enforceable control (ADR 0024: controls
4266
+ * fail loudly), shared by both pr:verify proof schemas so they cannot drift.
4267
+ *
4268
+ * Four rules:
4269
+ * 1. A withheld command names one the resolved mode actually selected.
4270
+ * 2. A command is never recorded as both executed and not-required.
4271
+ * 3. Withheld commands are distinct — a duplicated disposition would let one
4272
+ * name carry two different bases.
4273
+ * 4. On a PASSED proof, every selected command has a disposition: silence is
4274
+ * not a disposition, and "absent from both lists" is exactly the silent
4275
+ * skip this epic forbids.
4276
+ *
4277
+ * Rule 4 is scoped to `outcome: "passed"` because an ABORTED proof legitimately
4278
+ * records partial execution — the run stopped at the failing command. It is
4279
+ * also scoped to schemaVersion >= 4: `executedCommands` postdates v1–v3, so a
4280
+ * legacy proof that never recorded it is not making a false completeness claim.
4281
+ *
4282
+ * The check is one-directional on purpose. Every SELECTED command must be
4283
+ * accounted for, but `executedCommands` may legitimately carry MORE than the
4284
+ * profile selected — full mode appends the `workspace:install-resolves` probe,
4285
+ * which is real work no profile declares.
4286
+ */
4287
+ const assertBatteryCompleteness = (proof, context) => {
4288
+ const selected = proof.verificationCommands.map((command) => command.name);
4289
+ const selectedSet = new Set(selected);
4290
+ const executed = new Set((proof.executedCommands ?? []).map((command) => command.name));
4291
+ const notRequired = proof.notRequiredCommands ?? [];
4292
+ const seen = /* @__PURE__ */ new Set();
4293
+ for (const [index, entry] of notRequired.entries()) {
4294
+ if (!selectedSet.has(entry.name)) context.addIssue({
4295
+ code: "custom",
4296
+ message: `notRequiredCommands entry "${entry.name}" is not one of this proof's verificationCommands.`,
4297
+ path: [
4298
+ "notRequiredCommands",
4299
+ index,
4300
+ "name"
4301
+ ]
4302
+ });
4303
+ if (executed.has(entry.name)) context.addIssue({
4304
+ code: "custom",
4305
+ message: `Command "${entry.name}" is recorded both as executed and as not-required.`,
4306
+ path: [
4307
+ "notRequiredCommands",
4308
+ index,
4309
+ "name"
4310
+ ]
4311
+ });
4312
+ if (seen.has(entry.name)) context.addIssue({
4313
+ code: "custom",
4314
+ message: `notRequiredCommands records "${entry.name}" more than once.`,
4315
+ path: [
4316
+ "notRequiredCommands",
4317
+ index,
4318
+ "name"
4319
+ ]
4320
+ });
4321
+ seen.add(entry.name);
4322
+ }
4323
+ if (proof.outcome !== "passed" || proof.schemaVersion < 4) return;
4324
+ const disposed = new Set([...executed, ...seen]);
4325
+ const undisposed = selected.filter((name) => !disposed.has(name));
4326
+ if (undisposed.length > 0) context.addIssue({
4327
+ code: "custom",
4328
+ message: `A passed pr:verify proof must give every selected verification command a disposition; [${undisposed.join(", ")}] appear in verificationCommands but in neither executedCommands nor notRequiredCommands.`,
4329
+ path: ["executedCommands"]
4330
+ });
4331
+ };
4332
+ /**
4333
+ * The stamp-authorization control: a passed v4 proof may only claim a command
4334
+ * was NOT REQUIRED if its own recorded stamp says so.
4335
+ *
4336
+ * `assertBatteryCompleteness` proves the two lists partition the selected
4337
+ * commands — that no command is silently absent. It does not prove the
4338
+ * withholding was EARNED. Without this rule a proof could name every expensive
4339
+ * command in `notRequiredCommands`, carry no stamp at all (or a conservative
4340
+ * one), and still read as full verification: the omission would be recorded,
4341
+ * accounted for, and completely unauthorized.
4342
+ *
4343
+ * Two things have to hold, and the first is what keeps the second honest:
4344
+ *
4345
+ * 1. COHERENCE. The entry's `impactTarget` must equal the `impactTarget` the
4346
+ * proof records for that same command in `verificationCommands`. The entry
4347
+ * does not get to nominate its own target; the proof's command→target
4348
+ * mapping (written by `pr:verify` from the loaded profile) does. Without
4349
+ * this, authorization validates a self-reported field and a crafted proof
4350
+ * can withhold an affected command while pointing at an unrelated released
4351
+ * target.
4352
+ * 2. AUTHORIZATION. That recorded target must be one the proof's own stamp
4353
+ * provably released.
4354
+ *
4355
+ * The proof is one artifact and a determined forger controls all of it; this
4356
+ * is internal-coherence belt-and-braces in the same trust domain as the
4357
+ * version guards, and write-time truth stays `pr:verify`'s job.
4358
+ *
4359
+ * The authorizing evidence is the proof's OWN `impactStamp` — the same stamp
4360
+ * `pr:verify` computed for this candidate's identity triple and recorded here,
4361
+ * so authorization is bound to the same identities the proof binds. The
4362
+ * predicate is the shared {@link impactStampScopeDecision}, not a second
4363
+ * reading of the stamp: an entry is authorized exactly when the stamp would
4364
+ * have released that target's command in the first place (lockfile-scoped
4365
+ * basis, this build's stamp version, exactly one row for the target,
4366
+ * `not-affected`). A conservative stamp, an unknown version, a
4367
+ * self-contradictory stamp, an unclassified name, or an `affected` verdict all
4368
+ * fail the same way scoping itself would.
4369
+ *
4370
+ * Scoped to `outcome: "passed"` and schemaVersion >= 4 for the same reasons as
4371
+ * the completeness rule: an aborted run records partial execution, and a
4372
+ * legacy proof predates both fields, so neither is making a false completeness
4373
+ * claim.
4374
+ */
4375
+ const assertNotRequiredStampAuthorization = (proof, context) => {
4376
+ const notRequired = proof.notRequiredCommands ?? [];
4377
+ if (proof.outcome !== "passed" || proof.schemaVersion < 4 || notRequired.length === 0) return;
4378
+ const { impactStamp } = proof;
4379
+ if (impactStamp === void 0) {
4380
+ context.addIssue({
4381
+ code: "custom",
4382
+ message: `A passed pr:verify proof that withholds verification commands must record the impact stamp that authorized the withholding; [${notRequired.map((entry) => entry.name).join(", ")}] are recorded not-required by a proof carrying no impactStamp.`,
4383
+ path: ["impactStamp"]
4384
+ });
4385
+ return;
4386
+ }
4387
+ const recordedTargets = new Map(proof.verificationCommands.map((command) => [command.name, command.impactTarget]));
4388
+ for (const [index, entry] of notRequired.entries()) {
4389
+ const recorded = recordedTargets.get(entry.name);
4390
+ if (recorded !== entry.impactTarget) {
4391
+ context.addIssue({
4392
+ code: "custom",
4393
+ message: recorded === void 0 ? `notRequiredCommands entry "${entry.name}" claims impactTarget "${entry.impactTarget}", but this proof records no impactTarget for that command; only a command the profile scopes can be withheld.` : `notRequiredCommands entry "${entry.name}" claims impactTarget "${entry.impactTarget}", but this proof records impactTarget "${recorded}" for that command.`,
4394
+ path: [
4395
+ "notRequiredCommands",
4396
+ index,
4397
+ "impactTarget"
4398
+ ]
4399
+ });
4400
+ continue;
4401
+ }
4402
+ const decision = impactStampScopeDecision({
4403
+ stamp: impactStamp,
4404
+ surface: "verification-battery",
4405
+ targetName: entry.impactTarget
4406
+ });
4407
+ if (!decision.scoped) context.addIssue({
4408
+ code: "custom",
4409
+ message: `notRequiredCommands entry "${entry.name}" is not authorized by this proof's impactStamp: ${decision.reason}.`,
4410
+ path: [
4411
+ "notRequiredCommands",
4412
+ index,
4413
+ "impactTarget"
4414
+ ]
4415
+ });
4416
+ }
4417
+ };
4418
+ /** Console lines naming every withheld command and why. Never silent. */
4419
+ const batteryScopeSummaryLines = (plan) => {
4420
+ if (plan.notRequired.length === 0) return [];
4421
+ return ["Verification battery scoped by the impact stamp (not-required):", ...plan.notRequired.map((entry) => `- ${entry.name} (${entry.impactTarget}): ${entry.basis}`)];
4422
+ };
4423
+ //#endregion
4424
+ //#region src/pr-verify-proof-rules.ts
4425
+ const assertPrVerifyProofRules = (proof, context) => {
4426
+ if (proof.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
4427
+ code: "custom",
4428
+ message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
4429
+ path: ["authoringSession"]
4430
+ });
4431
+ if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
4432
+ code: "custom",
4433
+ message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
4434
+ path: ["authoringSession"]
4435
+ });
4436
+ if (proof.schemaVersion < 4 && proof.impactStamp !== void 0) context.addIssue({
4437
+ code: "custom",
4438
+ message: "impactStamp is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
4439
+ path: ["impactStamp"]
4440
+ });
4441
+ if (proof.schemaVersion < 4 && proof.notRequiredCommands !== void 0) context.addIssue({
4442
+ code: "custom",
4443
+ message: "notRequiredCommands is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
4444
+ path: ["notRequiredCommands"]
4445
+ });
4446
+ assertBatteryCompleteness(proof, context);
4447
+ assertNotRequiredStampAuthorization(proof, context);
4448
+ };
3795
4449
  const LEGACY_CLOSEOUT_SCHEMA_VERSION = 3;
3796
4450
  const MAX_CLOSEOUT_ROWS = 1e3;
3797
4451
  const IdentifierSchema = z.string().min(1).max(500);
@@ -4297,6 +4951,11 @@ const executedCommandSchema$1 = z.object({
4297
4951
  "full"
4298
4952
  ])
4299
4953
  });
4954
+ const notRequiredCommandSchema$1 = z.object({
4955
+ basis: z.string().min(1),
4956
+ impactTarget: z.string().min(1),
4957
+ name: z.string().min(1)
4958
+ });
4300
4959
  const prVerifySchemaVersionSchema$1 = z.number().refine((value) => SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS$1.includes(value), { message: `schemaVersion must be one of: ${SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS$1.join(", ")}` });
4301
4960
  z.object({
4302
4961
  authoringSession: z.string().trim().min(1).optional(),
@@ -4317,12 +4976,14 @@ z.object({
4317
4976
  endedAt: z.iso.datetime(),
4318
4977
  executedCommands: z.array(executedCommandSchema$1).optional(),
4319
4978
  headSha: z.string().regex(/^[0-9a-f]{40}$/u),
4979
+ impactStamp: impactStampSchema.optional(),
4320
4980
  mergeBaseSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
4321
4981
  mode: z.enum([
4322
4982
  "docs-only",
4323
4983
  "trivial",
4324
4984
  "full"
4325
4985
  ]),
4986
+ notRequiredCommands: z.array(notRequiredCommandSchema$1).min(1).optional(),
4326
4987
  outcome: z.enum(["aborted", "passed"]).optional(),
4327
4988
  patchId: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
4328
4989
  profilePath: z.string().min(1),
@@ -4333,6 +4994,7 @@ z.object({
4333
4994
  verificationCommands: z.array(z.object({
4334
4995
  command: z.string().min(1),
4335
4996
  description: z.string().min(1),
4997
+ impactTarget: z.string().min(1).optional(),
4336
4998
  name: z.string().min(1),
4337
4999
  scope: z.enum([
4338
5000
  "always",
@@ -4341,18 +5003,7 @@ z.object({
4341
5003
  "full"
4342
5004
  ])
4343
5005
  }))
4344
- }).superRefine((proof, context) => {
4345
- if (proof.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
4346
- code: "custom",
4347
- message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
4348
- path: ["authoringSession"]
4349
- });
4350
- if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
4351
- code: "custom",
4352
- message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
4353
- path: ["authoringSession"]
4354
- });
4355
- });
5006
+ }).superRefine(assertPrVerifyProofRules);
4356
5007
  const PR_REVIEW_FINDING_PROVENANCE_VERSION$1 = 1;
4357
5008
  const nonBlankString$3 = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
4358
5009
  const parseableDateString$1 = z.string().refine((value) => Number.isFinite(Date.parse(value)), { message: "must be a parseable date string" });
@@ -9403,6 +10054,7 @@ var external_evidence_exports = /* @__PURE__ */ __exportAll({
9403
10054
  EVIDENCE_DIR: () => EVIDENCE_DIR,
9404
10055
  EVIDENCE_ENVELOPE_SCHEMA_VERSION: () => 1,
9405
10056
  EVIDENCE_REVIEW_RUNGS: () => EVIDENCE_REVIEW_RUNGS$1,
10057
+ boundFailingCheckNames: () => boundFailingCheckNames,
9406
10058
  evaluateRequiredChecks: () => evaluateRequiredChecks,
9407
10059
  evidenceBindingFresh: () => evidenceBindingFresh,
9408
10060
  evidenceEnvelopeSchema: () => evidenceEnvelopeSchema,
@@ -9491,6 +10143,41 @@ const evidenceReviewIndependence = ({ authoringSessionIds, envelope }) => {
9491
10143
  };
9492
10144
  return { independent: true };
9493
10145
  };
10146
+ /**
10147
+ * The VETO predicate: does a current envelope, bound to exactly this
10148
+ * candidate, record a FAILING outcome for this check?
10149
+ *
10150
+ * An inferred graph verdict never silences a direct observation (ADR 0024:
10151
+ * controls fail loudly). Wave 2 uses this to withhold a demand release; wave 3
10152
+ * (#430) uses the same predicate — via {@link boundFailingCheckNames} — to
10153
+ * withhold battery and preview-lifecycle scoping. It is ONE matcher on
10154
+ * purpose: two vetoes that matched differently could withhold a demand while
10155
+ * releasing the work that would satisfy it, leaving a demand nothing on this
10156
+ * head can ever meet.
10157
+ */
10158
+ const envelopeRecordsBoundFailure = ({ candidate, check, envelope }) => envelope.check === check.name && envelope.checkType === check.checkType && envelope.outcome === "fail" && evidenceBindingFresh({
10159
+ candidate,
10160
+ envelope
10161
+ });
10162
+ /**
10163
+ * Every check name with a current, candidate-bound FAILING envelope on disk.
10164
+ *
10165
+ * The wave-3 scoping veto input. Callers pass the resolved set to
10166
+ * `planVerificationBattery` / `planPreviewLifecycle`, which stay pure — the
10167
+ * planners decide, they never read the filesystem.
10168
+ *
10169
+ * `checkType` is the caller's, not a guess: schema-v5 requiredChecks are
10170
+ * verify-only (`resolveProfileRequiredChecks`), so a scoping caller passes
10171
+ * `"verify"` and matches exactly the pair wave 2 evaluates.
10172
+ */
10173
+ const boundFailingCheckNames = ({ candidate, checkType, envelopes }) => [...new Set(envelopes.flatMap((loaded) => loaded.ok && envelopeRecordsBoundFailure({
10174
+ candidate,
10175
+ check: {
10176
+ checkType,
10177
+ name: loaded.envelope.check
10178
+ },
10179
+ envelope: loaded.envelope
10180
+ }) ? [loaded.envelope.check] : []))];
9494
10181
  const bindingRefusalReason = (name) => `Required check "${name}" has no passing, current envelope bound to the candidate delta (patchId).`;
9495
10182
  /**
9496
10183
  * The `evidence:emit` invocation that would satisfy a specific demanded check.
@@ -9618,6 +10305,23 @@ const evaluateRequiredChecks = ({ authoringSessionIds, candidate, envelopes, req
9618
10305
  scopeReason: scopeDecision.reason,
9619
10306
  status: "out-of-scope"
9620
10307
  };
10308
+ const release = impactStampDemandRelease({
10309
+ checkName: check.name,
10310
+ stamp: scopeContext.impactStamp
10311
+ });
10312
+ const boundFailureObserved = parsed.some((loaded) => envelopeRecordsBoundFailure({
10313
+ candidate,
10314
+ check,
10315
+ envelope: loaded.envelope
10316
+ }));
10317
+ if (release.released && !boundFailureObserved) return {
10318
+ checkType: check.checkType,
10319
+ inScope: false,
10320
+ name: check.name,
10321
+ ...check.scope ? { scope: check.scope } : {},
10322
+ scopeReason: release.reason,
10323
+ status: "out-of-scope"
10324
+ };
9621
10325
  return {
9622
10326
  ...evaluateInScopeCheck({
9623
10327
  authoringSessionIds,
@@ -9790,6 +10494,7 @@ var verification_proof_exports = /* @__PURE__ */ __exportAll({
9790
10494
  prVerifyProofHeadShas: () => prVerifyProofHeadShas,
9791
10495
  readPrVerifyProof: () => readPrVerifyProof,
9792
10496
  resolveVerifyProofApplicability: () => resolveVerifyProofApplicability,
10497
+ trustedImpactStamp: () => trustedImpactStamp,
9793
10498
  validatePrVerifyProof: () => validatePrVerifyProof,
9794
10499
  verificationProofApplicabilityFor: () => verificationProofApplicabilityFor,
9795
10500
  verificationProofBlockingReason: () => verificationProofBlockingReason,
@@ -9820,6 +10525,11 @@ const executedCommandSchema = z.object({
9820
10525
  "full"
9821
10526
  ])
9822
10527
  });
10528
+ const notRequiredCommandSchema = z.object({
10529
+ basis: z.string().min(1),
10530
+ impactTarget: z.string().min(1),
10531
+ name: z.string().min(1)
10532
+ });
9823
10533
  const prVerifySchemaVersionSchema = z.number().refine((value) => SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS.includes(value), { message: `schemaVersion must be one of: ${SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS.join(", ")}` });
9824
10534
  const prVerifyProofSchema = z.object({
9825
10535
  authoringSession: z.string().trim().min(1).optional(),
@@ -9844,12 +10554,14 @@ const prVerifyProofSchema = z.object({
9844
10554
  endedAt: z.iso.datetime(),
9845
10555
  executedCommands: z.array(executedCommandSchema).optional(),
9846
10556
  headSha: z.string().regex(/^[0-9a-f]{40}$/u),
10557
+ impactStamp: impactStampSchema.optional(),
9847
10558
  mergeBaseSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
9848
10559
  mode: z.enum([
9849
10560
  "docs-only",
9850
10561
  "trivial",
9851
10562
  "full"
9852
10563
  ]),
10564
+ notRequiredCommands: z.array(notRequiredCommandSchema).min(1).optional(),
9853
10565
  outcome: z.enum(["aborted", "passed"]).optional(),
9854
10566
  patchId: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
9855
10567
  profilePath: z.string().min(1),
@@ -9860,6 +10572,7 @@ const prVerifyProofSchema = z.object({
9860
10572
  verificationCommands: z.array(z.object({
9861
10573
  command: z.string().min(1),
9862
10574
  description: z.string().min(1),
10575
+ impactTarget: z.string().min(1).optional(),
9863
10576
  name: z.string().min(1),
9864
10577
  scope: z.enum([
9865
10578
  "always",
@@ -9868,18 +10581,7 @@ const prVerifyProofSchema = z.object({
9868
10581
  "full"
9869
10582
  ])
9870
10583
  }))
9871
- }).superRefine((proof, context) => {
9872
- if (proof.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
9873
- code: "custom",
9874
- message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
9875
- path: ["authoringSession"]
9876
- });
9877
- if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
9878
- code: "custom",
9879
- message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
9880
- path: ["authoringSession"]
9881
- });
9882
- });
10584
+ }).superRefine(assertPrVerifyProofRules);
9883
10585
  function validatePrVerifyProof(value) {
9884
10586
  return prVerifyProofSchema.parse(value);
9885
10587
  }
@@ -9920,6 +10622,29 @@ const verifyProofIdentityMatches = ({ currentIdentity, proof }) => Boolean(proof
9920
10622
  recorded: { patchId: proof.patchId },
9921
10623
  rule: "patch-id"
9922
10624
  }));
10625
+ /**
10626
+ * Wave-2 consumption (#542): the recorded impact stamp, released for demand
10627
+ * resolution ONLY when it is identity-bound to the current candidate — the
10628
+ * proof passed, and its recorded headSha AND patchId both name exactly this
10629
+ * candidate (the same identities the proof already binds). Everything else —
10630
+ * no proof, an aborted run, a stale head, a moved patch id, a pre-stamp proof
10631
+ * — returns `undefined`, and the consumer keeps today's full-demand floor.
10632
+ *
10633
+ * Deliberately stricter than verify-once applicability (which tolerates a
10634
+ * head-only or patch-id-only match): a stamp narrows external demands, so it
10635
+ * is trusted only on an exact double binding, fail closed.
10636
+ */
10637
+ const trustedImpactStamp = ({ candidate, proof }) => {
10638
+ if (!proof || !verifyProofPassed(proof)) return;
10639
+ if (proof.schemaVersion < 4) return;
10640
+ const headBound = sameHeadSha(candidate.headSha, proof.headSha);
10641
+ const patchBound = evidenceFresh({
10642
+ candidate: { patchId: candidate.patchId },
10643
+ recorded: { patchId: proof.patchId },
10644
+ rule: "patch-id"
10645
+ });
10646
+ return headBound && patchBound ? proof.impactStamp : void 0;
10647
+ };
9923
10648
  const prVerifyFullHeadShasForDiff = ({ docsOnlyDeltaAcceptedSince, files, proof }) => {
9924
10649
  if (!verifyProofPassed(proof)) return [];
9925
10650
  if (proof.mode === "full") return sameFileList(proof.changedFiles, files) ? [proof.headSha] : [];
@@ -10287,6 +11012,7 @@ function defaultPrVerifyGit() {
10287
11012
  exitCode: result.exitCode
10288
11013
  };
10289
11014
  },
11015
+ showFileAtRef,
10290
11016
  stablePatchId: runStablePatchId,
10291
11017
  statusPorcelain
10292
11018
  };
@@ -10316,7 +11042,7 @@ function baselineFullProofsForDocsOnly({ previousProof, profilePath, projectKey,
10316
11042
  seedFilter: matchesProfile
10317
11043
  });
10318
11044
  }
10319
- function buildPrVerifyProof({ authoringSession, base, baselineFullProofs, classification, commands, endedAt, executedCommands, files, headSha, mode, mergeBaseSha: proofMergeBaseSha, outcome, patchId, profilePath, projectKey, repository, startedAt }) {
11045
+ function buildPrVerifyProof({ authoringSession, base, baselineFullProofs, classification, commands, endedAt, executedCommands, files, headSha, impactStamp, mode, mergeBaseSha: proofMergeBaseSha, notRequiredCommands, outcome, patchId, profilePath, projectKey, repository, startedAt }) {
10320
11046
  return {
10321
11047
  authoringSession,
10322
11048
  base,
@@ -10329,8 +11055,10 @@ function buildPrVerifyProof({ authoringSession, base, baselineFullProofs, classi
10329
11055
  endedAt: endedAt.toISOString(),
10330
11056
  executedCommands,
10331
11057
  headSha,
11058
+ impactStamp,
10332
11059
  mergeBaseSha: proofMergeBaseSha,
10333
11060
  mode,
11061
+ ...notRequiredCommands.length > 0 ? { notRequiredCommands } : {},
10334
11062
  outcome,
10335
11063
  patchId,
10336
11064
  profilePath,
@@ -10375,9 +11103,10 @@ function resolveMode({ classification, mode }) {
10375
11103
  return mode;
10376
11104
  }
10377
11105
  function commandsForMode(profile, mode) {
10378
- return profile.verification.commands.filter((command) => commandAppliesToMode(command, mode)).map(({ command, description, name, scope }) => ({
11106
+ return profile.verification.commands.filter((command) => commandAppliesToMode(command, mode)).map(({ command, description, impactTarget, name, scope }) => ({
10379
11107
  command,
10380
11108
  description,
11109
+ ...impactTarget === void 0 ? {} : { impactTarget },
10381
11110
  name,
10382
11111
  scope
10383
11112
  }));
@@ -10432,6 +11161,35 @@ function runPrVerify(args, dependencies = {}) {
10432
11161
  cwd,
10433
11162
  git
10434
11163
  });
11164
+ const readLockfileAtRef = git.showFileAtRef ?? showFileAtRef;
11165
+ let impactStamp;
11166
+ try {
11167
+ impactStamp = computeImpactStamp({
11168
+ changedFiles: files,
11169
+ profile,
11170
+ readLockfile: (side) => readLockfileAtRef(cwd, side === "base" ? proofMergeBaseSha : headSha, path.resolve(cwd, LOCKFILE_PATH))
11171
+ });
11172
+ } catch (error) {
11173
+ if (error instanceof ImpactStampConfigError) throw error;
11174
+ impactStamp = conservativeImpactStamp(profile.impact?.targets ?? [], [`impact stamp computation failed (${error instanceof Error ? error.message : String(error)}); fail closed to full impact`]);
11175
+ }
11176
+ const vetoedTargets = boundFailingCheckNames({
11177
+ candidate: {
11178
+ headSha,
11179
+ mergeBaseSha: proofMergeBaseSha,
11180
+ patchId
11181
+ },
11182
+ checkType: "verify",
11183
+ envelopes: loadEvidenceEnvelopes(cwd)
11184
+ });
11185
+ const battery = planVerificationBattery({
11186
+ commands: selectedProfileCommands,
11187
+ stamp: impactStamp,
11188
+ vetoedTargets
11189
+ });
11190
+ const notRequiredCommands = battery.notRequired;
11191
+ const scopedOutNames = new Set(notRequiredCommands.map(({ name }) => name));
11192
+ for (const line of batteryScopeSummaryLines(battery)) console.log(line);
10435
11193
  const repository = `${profile.repository.owner}/${profile.repository.name}`;
10436
11194
  let gateState = "failure";
10437
11195
  const { env: verificationEnv, cleanup } = context.buildEnv();
@@ -10455,8 +11213,10 @@ function runPrVerify(args, dependencies = {}) {
10455
11213
  commands,
10456
11214
  files,
10457
11215
  headSha,
11216
+ impactStamp,
10458
11217
  mergeBaseSha: proofMergeBaseSha,
10459
11218
  mode: resolvedMode,
11219
+ notRequiredCommands,
10460
11220
  patchId,
10461
11221
  profilePath,
10462
11222
  projectKey: profile.repository.name,
@@ -10488,6 +11248,7 @@ function runPrVerify(args, dependencies = {}) {
10488
11248
  };
10489
11249
  try {
10490
11250
  for (const command of commands) {
11251
+ if (scopedOutNames.has(command.name)) continue;
10491
11252
  console.log(`\n> ${command.name}: ${command.command}`);
10492
11253
  const result = git.runVerificationCommand(command.command, cwd, verificationEnv);
10493
11254
  const executedCommand = {
@@ -10554,6 +11315,7 @@ function runPrVerify(args, dependencies = {}) {
10554
11315
  });
10555
11316
  for (const command of selectedProfileCommands) {
10556
11317
  if (!command.requiredCheck) continue;
11318
+ if (scopedOutNames.has(command.name)) continue;
10557
11319
  runEvidenceEmit({
10558
11320
  base: args.base,
10559
11321
  check: command.requiredCheck,
@@ -11235,6 +11997,13 @@ const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyPr
11235
11997
  name: "admission:required-checks",
11236
11998
  status: "warning"
11237
11999
  }];
12000
+ const impactStamp = trustedImpactStamp({
12001
+ candidate: {
12002
+ headSha: candidate.headSha,
12003
+ patchId: candidate.patchId
12004
+ },
12005
+ proof: verifyProof
12006
+ });
11238
12007
  const { outcomes } = evaluateRequiredChecks({
11239
12008
  authoringSessionIds: resolveAuthoringSessionIds({ recorded: verifyProof?.authoringSession }),
11240
12009
  candidate: {
@@ -11244,7 +12013,10 @@ const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyPr
11244
12013
  },
11245
12014
  envelopes: loadEvidenceEnvelopes(cwd),
11246
12015
  requiredChecks,
11247
- scopeContext: { classification: candidate.classification }
12016
+ scopeContext: {
12017
+ classification: candidate.classification,
12018
+ ...impactStamp ? { impactStamp } : {}
12019
+ }
11248
12020
  });
11249
12021
  return outcomes.map((outcome) => ({
11250
12022
  message: requiredCheckMessage(outcome),
@@ -12638,8 +13410,8 @@ const inactiveMergeFreezeStateSchema = z.object({
12638
13410
  });
12639
13411
  const mergeFreezeStateSchema = z.discriminatedUnion("active", [activeMergeFreezeStateSchema, inactiveMergeFreezeStateSchema]);
12640
13412
  /**
12641
- * The write side of this contract lives in the generated main-push verify
12642
- * workflow (#356, ADR 0016 as amended): it is the ONLY producer of
13413
+ * The write side of this contract lives in the generated merge-target push
13414
+ * Verify workflow (#356, ADR 0016 as amended; #429): it is the ONLY producer of
12643
13415
  * `patronage-factory/merge-freeze` generations. Its emitted `output.text`
12644
13416
  * payload must parse under this exact reader schema, which is what the
12645
13417
  * workflow's own tests assert through this export.
@@ -12732,7 +13504,7 @@ function selectListedMergeFreezeGeneration(listed, expectedHeadSha) {
12732
13504
  function unconfiguredMergeFreeze(input, detail) {
12733
13505
  return {
12734
13506
  kind: "unreadable",
12735
- reason: `The ${MERGE_FREEZE_CHECK_NAME} writer is unconfigured or its settle window cannot be confirmed for base ${input.headSha}: ${detail} Generate the main-push verify workflow, configure FACTORY_GITHUB_APP_PRIVATE_KEY, and rerun pr:ready after the first App-owned freeze generation appears.`
13507
+ reason: `The ${MERGE_FREEZE_CHECK_NAME} writer is unconfigured or its settle window cannot be confirmed for base ${input.headSha}: ${detail} Generate the merge-target push Verify workflow, configure FACTORY_GITHUB_APP_PRIVATE_KEY, and rerun pr:ready after the first App-owned freeze generation appears on this base branch.`
12736
13508
  };
12737
13509
  }
12738
13510
  function selectMissingMergeFreezeGenerationUnchecked(api, input) {
@@ -12839,8 +13611,38 @@ function createGitHubCheckRunMergeFreezeApi(dependencies = {}) {
12839
13611
  };
12840
13612
  }
12841
13613
  const githubMergeFreezeStore = createGitHubCheckRunMergeFreezeStore(createGitHubCheckRunMergeFreezeApi());
12842
- /** The one refusal sentence, naming both exits, wherever the freeze refuses. */
12843
- const activeMergeFreezeReason = (reason) => `Merge freeze is active (${reason}). It clears automatically when the main-push verify workflow completes green after a fix-forward or revert; an operator may waive it for one candidate with patronage-factory demand:waive --pr <pr> --demand merge-freeze --rationale <why>.`;
13614
+ /**
13615
+ * The one refusal sentence, naming both exits, wherever the freeze refuses.
13616
+ *
13617
+ * Deliberately base-tip-scoped rather than main-scoped (#429). Since the writer
13618
+ * completes a generation on every factory merge target, the freeze this reader
13619
+ * found lives on the tip it was asked about — `main` for a main-targeting
13620
+ * candidate, the epic branch for an epic-targeting one — and only a later green
13621
+ * Verify push to THAT branch writes a newer inactive generation. The writer's
13622
+ * own recovery text, quoted verbatim in `reason`, names the branch; this
13623
+ * wrapper must not contradict it by naming a different one.
13624
+ */
13625
+ const activeMergeFreezeReason = (reason) => `Merge freeze is active (${reason}). The generation is the one on this candidate's own base tip, so it clears automatically when a later Verify push to that base branch completes green after a fix-forward or revert; an operator may waive it for one candidate with patronage-factory demand:waive --pr <pr> --demand merge-freeze --rationale <why>.`;
13626
+ /**
13627
+ * The refusal for unreadable authority, which is NOT the active-freeze
13628
+ * sentence (#429 review cycles 2 and 3).
13629
+ *
13630
+ * Both refusals are equally fail-closed, but their recovery is not the same
13631
+ * act, and — the cycle-3 correction — "unreadable" is not one situation either.
13632
+ * It covers a repository with no writer at all, an unreachable Checks API, and
13633
+ * a defective *selected* generation: ambiguous newest runs, a malformed
13634
+ * payload, state inconsistent with its own conclusion. The first two are exited
13635
+ * by making the authority readable; the last is exited by a newer valid
13636
+ * App-owned generation winning the `started_at` selection, which a later Verify
13637
+ * push to the base branch normally produces.
13638
+ *
13639
+ * So this message states both paths without asserting which one applies. The
13640
+ * detail string it quotes is what distinguishes them, and the operator can read
13641
+ * it. Two categorical claims have already been wrong here — "clears when the
13642
+ * main-push verify completes green" (cycle 1) and "no Verify push clears this"
13643
+ * (cycle 2) — both from a refusal that spoke for states it could not see.
13644
+ */
13645
+ const unreadableMergeFreezeReason = (headSha, detail) => `Merge freeze refuses this candidate: the authoritative ${MERGE_FREEZE_CHECK_NAME} state for its base tip ${headSha} could not be read (${detail}). Which recovery applies is what that detail says: restore readability where the authority is missing or unreachable — the merge-target push Verify workflow generated, its writer App credentials configured, GitHub's Checks API reachable with read access — and/or let a fresh valid App-owned generation supersede a defective one, which a later Verify push to this base branch normally produces. Then rerun pr:ready. An operator may waive it for one candidate with patronage-factory demand:waive --pr <pr> --demand merge-freeze --rationale <why>.`;
12844
13646
  /**
12845
13647
  * The freeze read during readiness, before any authorized arming (#477,
12846
13648
  * ADR 0016 as amended 2026-07-31/2026-08-01).
@@ -12852,17 +13654,17 @@ const activeMergeFreezeReason = (reason) => `Merge freeze is active (${reason}).
12852
13654
  * notice only when the writer is observable: either its generation is running,
12853
13655
  * or the prior tip has a settled generation while this tip's source-pinned
12854
13656
  * hosted verify is running (#521). Epic #473 decision 5 deliberately trades
12855
- * the settle-window wait against the measured rarity of a red main. A
13657
+ * the settle-window wait against the measured rarity of a red merge target. A
12856
13658
  * writerless or unprovable repository fails closed.
12857
13659
  */
12858
13660
  function armingMergeFreezeOutcome(input, authority) {
12859
13661
  const generation = authority.readGeneration(input);
12860
13662
  if (generation.kind === "settling") return {
12861
13663
  blockingReasons: [],
12862
- notices: [`Merge freeze generation for base ${input.headSha} is still settling (${generation.reason}); readiness proceeds (ADR 0016, 2026-07-31/2026-08-01 amendments). If this wave authorizes machine merge, arming follows the admission decision. The main-push verify for that tip still refuses every later readiness decision if it goes red.`]
13664
+ notices: [`Merge freeze generation for base ${input.headSha} is still settling (${generation.reason}); readiness proceeds (ADR 0016, 2026-07-31/2026-08-01 amendments). If this wave authorizes machine merge, arming follows the admission decision. The Verify run for that tip still refuses every later readiness decision against this base branch if it goes red.`]
12863
13665
  };
12864
13666
  if (generation.kind === "unreadable") return {
12865
- blockingReasons: [activeMergeFreezeReason(`Authoritative GitHub merge freeze check-run state is unavailable or invalid: ${generation.reason}`)],
13667
+ blockingReasons: [unreadableMergeFreezeReason(input.headSha, `Authoritative GitHub merge freeze check-run state is unavailable or invalid: ${generation.reason}`)],
12866
13668
  notices: []
12867
13669
  };
12868
13670
  return {
@@ -13354,12 +14156,23 @@ const requiredChecksEvaluation = (input) => {
13354
14156
  mergeBaseSha: input.mergeBaseSha ?? input.baseSha,
13355
14157
  patchId: input.patchId
13356
14158
  };
14159
+ const { verificationProof } = input;
14160
+ const impactStamp = trustedImpactStamp({
14161
+ candidate: {
14162
+ headSha: input.headSha,
14163
+ patchId: input.patchId
14164
+ },
14165
+ proof: verificationProof && "proof" in verificationProof ? verificationProof.proof : void 0
14166
+ });
13357
14167
  const { blockingReasons, outcomes } = evaluateRequiredChecks({
13358
14168
  authoringSessionIds: input.authoringSessionIds ?? [],
13359
14169
  candidate,
13360
14170
  envelopes: input.evidenceEnvelopes ?? [],
13361
14171
  requiredChecks,
13362
- scopeContext: { classification: input.classification }
14172
+ scopeContext: {
14173
+ classification: input.classification,
14174
+ ...impactStamp ? { impactStamp } : {}
14175
+ }
13363
14176
  });
13364
14177
  const blockers = outcomes.flatMap((outcome) => outcome.reason ? [{
13365
14178
  demand: requiredCheckDemand(outcome.name),
@@ -15490,6 +16303,60 @@ const assembleReviewPrompt = ({ base, changedFiles, headSha, issueFocus, kind, p
15490
16303
  };
15491
16304
  };
15492
16305
  //#endregion
16306
+ //#region src/preview-lifecycle.ts
16307
+ /**
16308
+ * The declared preview stacks: impact targets that are ALSO declared required
16309
+ * checks — i.e. targets whose proof is produced externally, which is exactly
16310
+ * what a preview producer is.
16311
+ *
16312
+ * Deriving the list from the two existing declarations rather than adding a
16313
+ * third is what makes lifecycle and demand unable to disagree: the stacks this
16314
+ * plan can withhold are precisely the demands wave 2 can release, so a stack
16315
+ * can never be skipped while its proof stays demanded.
16316
+ */
16317
+ const previewLifecycleTargets = (profile) => {
16318
+ const demanded = new Set((profile.requiredChecks ?? []).map(({ name }) => name));
16319
+ return (profile.impact?.targets ?? []).map(({ name }) => name).filter((name) => demanded.has(name));
16320
+ };
16321
+ /**
16322
+ * Plan the preview lifecycle for one candidate.
16323
+ *
16324
+ * Trust is resolved here through the SAME identity gate demand release uses
16325
+ * (`trustedImpactStamp`): the proof must have passed and must bind both the
16326
+ * candidate's headSha and its patchId. A stale, aborted, absent, or
16327
+ * differently-bound proof yields no stamp, and every declared stack is planned
16328
+ * for deploy. Total over its INPUTS — no proof, stamp, or profile can make it
16329
+ * throw. A malformed CALL is the deliberate exception: omitting
16330
+ * `vetoedTargets` is API misuse, not data doubt, and throws.
16331
+ */
16332
+ const planPreviewLifecycle = ({ candidate, profile, proof, vetoedTargets }) => {
16333
+ assertVetoedTargets(vetoedTargets, "planPreviewLifecycle");
16334
+ const stamp = trustedImpactStamp({
16335
+ candidate,
16336
+ proof
16337
+ });
16338
+ const vetoed = new Set(vetoedTargets);
16339
+ const targets = previewLifecycleTargets(profile).map((name) => {
16340
+ const decision = impactStampScopeDecision({
16341
+ stamp,
16342
+ surface: "preview-lifecycle",
16343
+ targetName: name
16344
+ });
16345
+ const vetoedHere = decision.scoped && vetoed.has(name);
16346
+ return {
16347
+ basis: vetoedHere ? `veto: a current envelope bound to this candidate records stack "${name}" as FAILING, so the stamp's release is withheld (${decision.reason})` : decision.reason,
16348
+ disposition: decision.scoped && !vetoedHere ? "not-required" : "deploy",
16349
+ name
16350
+ };
16351
+ });
16352
+ return {
16353
+ deploy: targets.filter((target) => target.disposition === "deploy").map((target) => target.name),
16354
+ notRequired: targets.filter((target) => target.disposition === "not-required"),
16355
+ stampTrusted: stamp !== void 0,
16356
+ targets
16357
+ };
16358
+ };
16359
+ //#endregion
15493
16360
  //#region src/index.ts
15494
16361
  function createProgram(options = {}) {
15495
16362
  const factoryCliInvocation = options.factoryCliInvocation ? FactoryCliInvocationSchema.parse(options.factoryCliInvocation) : defaultFactoryCliInvocation(import.meta.filename);
@@ -15537,4 +16404,4 @@ if (isDirectCliExecution()) try {
15537
16404
  process.exit(1);
15538
16405
  }
15539
16406
  //#endregion
15540
- export { DEFAULT_DEMAND_WAIVER_PATH, DEFAULT_FACTORY_REPOSITORY, DemandWaiveRefusalError, EPIC_STRUCTURE_NODE_STATUSES, EPIC_STRUCTURE_SCHEMA_VERSION, EpicStructureValidationError, FactoryCliInvocationSchema, FollowUpActionSchema, MERGE_FREEZE_APP_SLUG, MERGE_FREEZE_CHECK_NAME, PrPublishFollowUpError, RETRO_ENVELOPE_SCHEMA_VERSION, RETRO_ENVELOPE_VALIDATORS, REVIEW_FOCUS_SECTION, SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS, WorkerCheckoutGuardError, appendReviewLadderStageTraceEvent, appendWithTraceSinks, applyDemandWaiver, assembleReviewPrompt, assertWorkerCheckoutAllowed, authorizeDemandWaiver, blockingLadderFindings, boundary_manifest_exports as boundaryManifest, boundary_review_proof_exports as boundaryReviewProof, buildEpicStructureEvent, buildEpicStructurePayload, buildEvidenceEnvelope, buildRetroEnvelope, buildReviewGateNotRequiredTraceEvent, buildReviewLadderStageTraceEvent, comment_provenance_exports as commentProvenance, createLocalJsonlTraceSink, createProgram, doctorProjectProfile, epicStructureEventId, evaluateReviewLadder, evidenceEnvelopeFilename, findingKey, followUpFromArgv, inferFixedInThreadDispositions, isProductionHqUrl, isRetroEnvelopeWireComplete, loadProjectProfile, normalizeIssueComments, openLadderFindings, parseRetroEnvelope, planPublishFollowUp, readiness_evaluation_exports as prReadinessEvaluation, external_evidence_exports as prReadinessExternalEvidence, pr_body_renderer_exports as prReadinessPrBodyRenderer, proof_identity_exports as prReadinessProofIdentity, review_proof_exports as prReadinessReviewProof, status_check_rollup_exports as prReadinessStatusChecks, verification_proof_exports as prReadinessVerificationProof, publishEpicStructure, readDemandWaivers, readPrReadyProof, readPrReviewProof, resolveFactoryRepository, resolveFindingBlocking, resolveReviewFindingCategory, resolveReviewFindingSeverity, resolveReviewLadderPolicy, resolveTraceWriteSinks, retroEnvelopeSchemaVersionOf, retroEnvelopeV1Schema, retroEpicReference, reviewCycleStateFor, reviewFocusFromIssueBody, reviewPromptSectionSchema, reviewPromptSectionsSchema, run, runBoundaryCheck, runDemandWaive, runEvidenceEmit, runPrPublish, runPrReady, runPrReview, runPrVerify, scanFactoryTraceDiagnostics, scanFactoryTraceEvents, selectWaiversForCandidate, staleRepeatLadderFindings, toFactoryTraceEnvelope, tryAppendReviewGateNotRequiredTraceEvent, tryAppendReviewLadderStageTraceEvent, validateBoundaryCheckProof, validateDagDocument, validateDemandWaiverStore, validateFactoryTraceEvent, validateMergeFreezeState, validatePrReadyProof, validatePrReviewProof, validatePrVerifyProof, waivedDemandNotice, waivedDemandSchema, worktree_scratch_files_exports as worktreeScratchFiles };
16407
+ export { DEFAULT_DEMAND_WAIVER_PATH, DEFAULT_FACTORY_REPOSITORY, DemandWaiveRefusalError, EPIC_STRUCTURE_NODE_STATUSES, EPIC_STRUCTURE_SCHEMA_VERSION, EpicStructureValidationError, FactoryCliInvocationSchema, FollowUpActionSchema, MERGE_FREEZE_APP_SLUG, MERGE_FREEZE_CHECK_NAME, PrPublishFollowUpError, RETRO_ENVELOPE_SCHEMA_VERSION, RETRO_ENVELOPE_VALIDATORS, REVIEW_FOCUS_SECTION, SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS, WorkerCheckoutGuardError, appendReviewLadderStageTraceEvent, appendWithTraceSinks, applyDemandWaiver, assembleReviewPrompt, assertWorkerCheckoutAllowed, authorizeDemandWaiver, batteryScopeSummaryLines, blockingLadderFindings, boundary_manifest_exports as boundaryManifest, boundary_review_proof_exports as boundaryReviewProof, buildEpicStructureEvent, buildEpicStructurePayload, buildEvidenceEnvelope, buildRetroEnvelope, buildReviewGateNotRequiredTraceEvent, buildReviewLadderStageTraceEvent, comment_provenance_exports as commentProvenance, createLocalJsonlTraceSink, createProgram, doctorProjectProfile, epicStructureEventId, evaluateReviewLadder, evidenceEnvelopeFilename, findingKey, followUpFromArgv, impactStampScopeDecision, inferFixedInThreadDispositions, isProductionHqUrl, isRetroEnvelopeWireComplete, loadProjectProfile, normalizeIssueComments, openLadderFindings, parseRetroEnvelope, planPreviewLifecycle, planPublishFollowUp, planVerificationBattery, readiness_evaluation_exports as prReadinessEvaluation, external_evidence_exports as prReadinessExternalEvidence, pr_body_renderer_exports as prReadinessPrBodyRenderer, proof_identity_exports as prReadinessProofIdentity, review_proof_exports as prReadinessReviewProof, status_check_rollup_exports as prReadinessStatusChecks, verification_proof_exports as prReadinessVerificationProof, previewLifecycleTargets, publishEpicStructure, readDemandWaivers, readPrReadyProof, readPrReviewProof, resolveFactoryRepository, resolveFindingBlocking, resolveReviewFindingCategory, resolveReviewFindingSeverity, resolveReviewLadderPolicy, resolveTraceWriteSinks, retroEnvelopeSchemaVersionOf, retroEnvelopeV1Schema, retroEpicReference, reviewCycleStateFor, reviewFocusFromIssueBody, reviewPromptSectionSchema, reviewPromptSectionsSchema, run, runBoundaryCheck, runDemandWaive, runEvidenceEmit, runPrPublish, runPrReady, runPrReview, runPrVerify, scanFactoryTraceDiagnostics, scanFactoryTraceEvents, selectWaiversForCandidate, staleRepeatLadderFindings, toFactoryTraceEnvelope, tryAppendReviewGateNotRequiredTraceEvent, tryAppendReviewLadderStageTraceEvent, validateBoundaryCheckProof, validateDagDocument, validateDemandWaiverStore, validateFactoryTraceEvent, validateMergeFreezeState, validatePrReadyProof, validatePrReviewProof, validatePrVerifyProof, waivedDemandNotice, waivedDemandSchema, worktree_scratch_files_exports as worktreeScratchFiles };