@patronage/software-factory 1.0.0-alpha.16 → 1.0.0-alpha.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { appendFileSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync,
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { Command, InvalidArgumentError } from "commander";
6
6
  import path from "node:path";
7
- import crypto, { createHash, randomUUID } from "node:crypto";
7
+ import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
8
8
  import { z } from "zod";
9
9
  import { execFileSync, spawnSync } from "node:child_process";
10
10
  import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "node:fs/promises";
@@ -17,7 +17,7 @@ import { parse } from "yaml";
17
17
  import { FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
18
18
  import { promisify } from "node:util";
19
19
  //#region package.json
20
- var version = "1.0.0-alpha.16";
20
+ var version = "1.0.0-alpha.18";
21
21
  //#endregion
22
22
  //#region src/review-rungs.ts
23
23
  const EVIDENCE_REVIEW_RUNGS$1 = [
@@ -1178,9 +1178,10 @@ const hqIngestConfigSchema = z.object({
1178
1178
  }
1179
1179
  }, "Must be an HTTPS origin without a path, query, fragment, or embedded credentials").describe("Repository-declared HQ origin (scheme and host only; the factory composes the ingest path). Credential delivery additionally requires exact-origin authorization in the operator user config hqAllowedOrigins list.")
1180
1180
  }).strict();
1181
+ const profileRequiredCheckScopeSchema = requiredCheckScopeSchema$1.extend({ classifications: z.array(z.enum(["docs/process-only", "non-trivial"])).min(1) }).strict();
1181
1182
  const requiredCheckSchema = z.object({
1182
1183
  name: z.string().min(1),
1183
- scope: requiredCheckScopeSchema$1.optional()
1184
+ scope: profileRequiredCheckScopeSchema.optional()
1184
1185
  }).strict();
1185
1186
  const impactTargetSchema = z.object({
1186
1187
  importers: z.array(z.string().min(1)).min(1),
@@ -1197,11 +1198,16 @@ const impactConfigSchema = z.object({
1197
1198
  });
1198
1199
  })
1199
1200
  }).strict();
1201
+ const lifecycleContractSchema = z.object({
1202
+ command: z.string().min(1).describe("Package-script invocation hosted CI runs for the non-credentialed lifecycle-contract lane (for example `pnpm test:factory`). Never a direct Vitest invocation."),
1203
+ jobName: z.string().min(1).optional().describe("Display name for the generated GitHub Actions job. Defaults to Factory / Alchemy Contracts when omitted.")
1204
+ }).strict().describe("Opt-in hosted lifecycle-contract lane. Presence includes the non-credentialed job in generated Verify; absence omits it. factory-ci does not load this object; generators pass it as typed options (ADR 0021).");
1200
1205
  const factoryProjectProfileSchema = z.object({
1201
1206
  $schema: z.string().min(1).optional(),
1202
1207
  extensions: z.record(z.string(), z.unknown()).optional(),
1203
1208
  hq: hqIngestConfigSchema.optional(),
1204
1209
  impact: impactConfigSchema.optional(),
1210
+ lifecycleContract: lifecycleContractSchema.optional(),
1205
1211
  repository: z.object({
1206
1212
  name: z.string().min(1),
1207
1213
  owner: z.string().min(1)
@@ -1212,15 +1218,6 @@ const factoryProjectProfileSchema = z.object({
1212
1218
  code: "custom",
1213
1219
  message: "requiredChecks entries must have distinct names."
1214
1220
  });
1215
- for (const [index, check] of checks.entries()) if (check.scope?.classifications.includes("trivial")) context.addIssue({
1216
- code: "custom",
1217
- message: `requiredChecks entry "${check.name}" scopes on classification "trivial", which the classifier no longer produces (#742); scope on "docs/process-only" or "non-trivial".`,
1218
- path: [
1219
- index,
1220
- "scope",
1221
- "classifications"
1222
- ]
1223
- });
1224
1221
  }).optional(),
1225
1222
  review: z.object({ conditional: z.array(conditionalReviewSchema).min(1) }).strict().optional(),
1226
1223
  schemaVersion: z.literal(6),
@@ -3797,6 +3794,7 @@ const impactStampTargetSchema = z.object({
3797
3794
  */
3798
3795
  const impactStampSchema = z.object({
3799
3796
  basis: z.enum(["target-scoped", "conservative"]),
3797
+ inertPaths: z.array(z.string()).default([]),
3800
3798
  reasons: z.array(z.string()),
3801
3799
  stampVersion: z.literal(3),
3802
3800
  targets: z.array(impactStampTargetSchema).superRefine((targets, context) => {
@@ -3813,6 +3811,11 @@ const unsubscribedPathsLine = (paths, reason = "legacy or malformed stamp") => {
3813
3811
  if (!Array.isArray(paths) || paths.some((one) => typeof one !== "string")) return `Unsubscribed changed paths: unavailable (${reason}).`;
3814
3812
  return paths.length === 0 ? "Unsubscribed changed paths: none." : `Unsubscribed changed paths (demand no target): ${paths.join(", ")}.`;
3815
3813
  };
3814
+ /** The one inert-owned path line (#761): candidate-impact console and summary. */
3815
+ const inertPathsLine = (paths, reason = "legacy or malformed stamp") => {
3816
+ if (!Array.isArray(paths) || paths.some((one) => typeof one !== "string")) return `Inert-owned changed paths: unavailable (${reason}).`;
3817
+ return paths.length === 0 ? "Inert-owned changed paths: none." : `Inert-owned changed paths: ${paths.join(", ")}.`;
3818
+ };
3816
3819
  /** Human-readable lines for every impact decision, including quiet releases. */
3817
3820
  const impactStampSummaryLines = (stamp) => [`Impact stamp: ${stamp.basis}; ${stamp.targets.filter(({ impact }) => impact === "affected").length}/${stamp.targets.length} target(s) affected.`, unsubscribedPathsLine(stamp.unsubscribedPaths)];
3818
3821
  /**
@@ -3907,8 +3910,9 @@ const impactStampDemandRelease = ({ checkName, stamp }) => {
3907
3910
  * every doubt path lands on, and the stamp a producer must fall back to if
3908
3911
  * stamp computation itself fails for any reason.
3909
3912
  */
3910
- const conservativeImpactStamp = (targets, reasons, unsubscribedPaths = []) => ({
3913
+ const conservativeImpactStamp = (targets, reasons, unsubscribedPaths = [], inertPaths = []) => ({
3911
3914
  basis: "conservative",
3915
+ inertPaths,
3912
3916
  reasons,
3913
3917
  stampVersion: 3,
3914
3918
  targets: targets.map((target) => ({
@@ -4048,9 +4052,13 @@ const targetImpactForLockfileDelta = (target, delta) => {
4048
4052
  * as affected/not-affected with its exact basis. `not-affected` is only ever
4049
4053
  * returned only when every changed input is a valid source path or an
4050
4054
  * analyzable pnpm-lock.yaml (v9) delta. Source and lockfile reachability are
4051
- * unioned per target. A valid source path matched by no target is recorded in
4052
- * `unsubscribedPaths` and affects none. Invalid, unreadable, unsupported, or
4053
- * otherwise doubtful input widens the whole stamp to full impact.
4055
+ * unioned per target. A valid source path matched by a target subscription is
4056
+ * product-owned and appears in neither path list. A valid source path matching
4057
+ * `impact.inert` and no target is recorded in `inertPaths`. A valid source
4058
+ * path with no declared owner is recorded in `unsubscribedPaths`.
4059
+ * `pnpm-lock.yaml` is a graph input and is never inert. Invalid, unreadable,
4060
+ * unsupported, or otherwise doubtful input widens the whole stamp to full
4061
+ * impact.
4054
4062
  *
4055
4063
  * Total by contract: this function never throws. Any unexpected failure in
4056
4064
  * reading, parsing, or traversal is itself a doubt path and returns the
@@ -4116,7 +4124,27 @@ const assertDeclaredImporterRoots = (targets, sides) => {
4116
4124
  const assertDeclaredPathGlobs = (targets) => {
4117
4125
  for (const target of targets) for (const pattern of target.paths ?? []) if (!isValidRepoRelativeGlob(pattern)) throw new ImpactStampConfigError(`impact target "${target.name}" declares invalid repo-relative path glob "${pattern}"; fix the profile's impact.targets declaration`);
4118
4126
  };
4119
- const analyzeSourcePaths = (sourceFiles, targets, profilePath) => {
4127
+ const assertDeclaredInertGlobs = (inert) => {
4128
+ for (const pattern of inert) if (!isValidRepoRelativeGlob(pattern)) throw new ImpactStampConfigError(`impact.inert declares invalid repo-relative path glob "${pattern}"; fix the profile's impact.inert declaration`);
4129
+ };
4130
+ const matchesTargetSubscription = (targets, file) => targets.some((target) => matchesAnyGlob(target.paths ?? [], file));
4131
+ const partitionUnownedSourcePaths = (sourceFiles, targets, inertGlobs) => {
4132
+ const inertPaths = [];
4133
+ const unsubscribedPaths = [];
4134
+ for (const file of sourceFiles) {
4135
+ if (file === "pnpm-lock.yaml" || matchesTargetSubscription(targets, file)) continue;
4136
+ if (matchesAnyGlob(inertGlobs, file)) {
4137
+ inertPaths.push(file);
4138
+ continue;
4139
+ }
4140
+ unsubscribedPaths.push(file);
4141
+ }
4142
+ return {
4143
+ inertPaths,
4144
+ unsubscribedPaths
4145
+ };
4146
+ };
4147
+ const analyzeSourcePaths = (sourceFiles, targets, profilePath, inertGlobs) => {
4120
4148
  for (const file of sourceFiles) {
4121
4149
  if (!isRepoRelativePath(file)) return {
4122
4150
  kind: "conservative",
@@ -4129,6 +4157,7 @@ const analyzeSourcePaths = (sourceFiles, targets, profilePath) => {
4129
4157
  }
4130
4158
  return {
4131
4159
  kind: "ok",
4160
+ ...partitionUnownedSourcePaths(sourceFiles, targets, inertGlobs),
4132
4161
  targets: targets.map((target) => {
4133
4162
  const matched = sourceFiles.find((file) => matchesAnyGlob(target.paths ?? [], file));
4134
4163
  return matched === void 0 ? {
@@ -4140,8 +4169,7 @@ const analyzeSourcePaths = (sourceFiles, targets, profilePath) => {
4140
4169
  impact: "affected",
4141
4170
  name: target.name
4142
4171
  };
4143
- }),
4144
- unsubscribedPaths: sourceFiles.filter((file) => !targets.some((target) => matchesAnyGlob(target.paths ?? [], file)))
4172
+ })
4145
4173
  };
4146
4174
  };
4147
4175
  const unionTargetImpact = (source, lockfile) => {
@@ -4153,34 +4181,41 @@ const unionTargetImpact = (source, lockfile) => {
4153
4181
  name: source.name
4154
4182
  };
4155
4183
  };
4156
- const computeImpactStampOrThrow = ({ changedFiles, profilePath = "software-factory.profile.json", readLockfile }, targets) => {
4157
- if (targets.length === 0) return {
4158
- basis: "conservative",
4159
- reasons: ["no impact targets declared in the profile; every surface keeps full demand"],
4160
- stampVersion: 3,
4161
- targets: [],
4162
- unsubscribedPaths: changedFiles.filter((file) => file !== "pnpm-lock.yaml" && isRepoRelativePath(file))
4163
- };
4184
+ const computeImpactStampOrThrow = ({ changedFiles, profile, profilePath = "software-factory.profile.json", readLockfile }, targets) => {
4185
+ const inertGlobs = profile.impact?.inert ?? [];
4186
+ assertDeclaredInertGlobs(inertGlobs);
4187
+ if (targets.length === 0) {
4188
+ const { inertPaths, unsubscribedPaths } = partitionUnownedSourcePaths(changedFiles.filter((file) => file !== "pnpm-lock.yaml" && isRepoRelativePath(file)), [], inertGlobs);
4189
+ return {
4190
+ basis: "conservative",
4191
+ inertPaths,
4192
+ reasons: ["no impact targets declared in the profile; every surface keeps full demand"],
4193
+ stampVersion: 3,
4194
+ targets: [],
4195
+ unsubscribedPaths
4196
+ };
4197
+ }
4164
4198
  assertDeclaredPathGlobs(targets);
4165
4199
  const sides = readLockfileSides(readLockfile);
4166
4200
  if (sides.kind === "ok") assertDeclaredImporterRoots(targets, sides.parsed);
4167
4201
  if (changedFiles.length === 0) return conservativeImpactStamp(targets, ["no changed files detected; full impact assumed"]);
4168
4202
  const sourceFiles = changedFiles.filter((file) => file !== LOCKFILE_PATH);
4169
- const source = analyzeSourcePaths(sourceFiles, targets, profilePath);
4203
+ const source = analyzeSourcePaths(sourceFiles, targets, profilePath, inertGlobs);
4170
4204
  if (source.kind === "conservative") return conservativeImpactStamp(targets, [source.reason]);
4171
- if (sides.kind === "threw") return conservativeImpactStamp(targets, [`impact stamp computation failed (${sides.error instanceof Error ? sides.error.message : String(sides.error)}); fail closed to full impact`], source.unsubscribedPaths);
4172
- if (sides.kind === "unreadable") return conservativeImpactStamp(targets, [`pnpm-lock.yaml is unreadable at the ${sides.side} side; fail closed to full impact`], source.unsubscribedPaths);
4173
- if (sides.kind === "unparseable") return conservativeImpactStamp(targets, [`pnpm-lock.yaml is unparseable or structurally malformed at the ${sides.side} side; fail closed to full impact`], source.unsubscribedPaths);
4174
- if (sides.kind === "unsupported-version") return conservativeImpactStamp(targets, [`pnpm-lock.yaml is not lockfileVersion ${SUPPORTED_LOCKFILE_VERSION}; only pnpm v9 lockfiles are modelled; fail closed to full impact`], source.unsubscribedPaths);
4205
+ if (sides.kind === "threw") return conservativeImpactStamp(targets, [`impact stamp computation failed (${sides.error instanceof Error ? sides.error.message : String(sides.error)}); fail closed to full impact`], source.unsubscribedPaths, source.inertPaths);
4206
+ if (sides.kind === "unreadable") return conservativeImpactStamp(targets, [`pnpm-lock.yaml is unreadable at the ${sides.side} side; fail closed to full impact`], source.unsubscribedPaths, source.inertPaths);
4207
+ if (sides.kind === "unparseable") return conservativeImpactStamp(targets, [`pnpm-lock.yaml is unparseable or structurally malformed at the ${sides.side} side; fail closed to full impact`], source.unsubscribedPaths, source.inertPaths);
4208
+ if (sides.kind === "unsupported-version") return conservativeImpactStamp(targets, [`pnpm-lock.yaml is not lockfileVersion ${SUPPORTED_LOCKFILE_VERSION}; only pnpm v9 lockfiles are modelled; fail closed to full impact`], source.unsubscribedPaths, source.inertPaths);
4175
4209
  if (!changedFiles.includes("pnpm-lock.yaml")) return {
4176
4210
  basis: "target-scoped",
4177
- reasons: [source.unsubscribedPaths.length === 0 ? "all changed source paths match at least one declared target subscription; impact scoped by repo-relative path globs" : `${source.unsubscribedPaths.length} changed source path(s) match no declared target subscription and affect no target`],
4211
+ inertPaths: source.inertPaths,
4212
+ reasons: [source.unsubscribedPaths.length === 0 ? "all changed source paths have a declared owner (target subscription or impact.inert); impact scoped by repo-relative path globs" : `${source.unsubscribedPaths.length} changed source path(s) match no declared target subscription and affect no target`],
4178
4213
  stampVersion: 3,
4179
4214
  targets: source.targets,
4180
4215
  unsubscribedPaths: source.unsubscribedPaths
4181
4216
  };
4182
4217
  const [baseLock, headLock] = sides.parsed;
4183
- if (stableStringify$1(nonGraphSections(baseLock)) !== stableStringify$1(nonGraphSections(headLock))) return conservativeImpactStamp(targets, ["pnpm-lock.yaml delta touches sections outside importers/packages/snapshots (e.g. settings, overrides, patchedDependencies); fail closed to full impact"], source.unsubscribedPaths);
4218
+ if (stableStringify$1(nonGraphSections(baseLock)) !== stableStringify$1(nonGraphSections(headLock))) return conservativeImpactStamp(targets, ["pnpm-lock.yaml delta touches sections outside importers/packages/snapshots (e.g. settings, overrides, patchedDependencies); fail closed to full impact"], source.unsubscribedPaths, source.inertPaths);
4184
4219
  const delta = {
4185
4220
  changedImporters: changedSectionKeys(baseLock.importers, headLock.importers),
4186
4221
  changedPackages: changedSectionKeys(baseLock.packages, headLock.packages),
@@ -4191,7 +4226,8 @@ const computeImpactStampOrThrow = ({ changedFiles, profilePath = "software-facto
4191
4226
  const mixed = sourceFiles.length > 0;
4192
4227
  return {
4193
4228
  basis: "target-scoped",
4194
- reasons: mixed ? [source.unsubscribedPaths.length === 0 ? "all changed source paths match at least one declared target subscription and pnpm-lock.yaml is analyzable; impact is the union of path subscriptions and importer/snapshot graph analysis" : `${source.unsubscribedPaths.length} changed source path(s) match no declared target subscription; subscribed path impact is unioned with importer/snapshot graph analysis`] : ["pnpm-lock.yaml is the only changed file; impact scoped by importer/snapshot graph analysis"],
4229
+ inertPaths: source.inertPaths,
4230
+ reasons: mixed ? [source.unsubscribedPaths.length === 0 ? "all changed source paths have a declared owner (target subscription or impact.inert) and pnpm-lock.yaml is analyzable; impact is the union of path subscriptions and importer/snapshot graph analysis" : `${source.unsubscribedPaths.length} changed source path(s) match no declared target subscription; subscribed path impact is unioned with importer/snapshot graph analysis`] : ["pnpm-lock.yaml is the only changed file; impact scoped by importer/snapshot graph analysis"],
4195
4231
  stampVersion: 3,
4196
4232
  targets: mixed ? source.targets.map((sourceTarget, index) => unionTargetImpact(sourceTarget, lockfileTargets[index])) : lockfileTargets,
4197
4233
  unsubscribedPaths: source.unsubscribedPaths
@@ -4455,6 +4491,16 @@ const assertPrVerifyProofRules = (proof, context) => {
4455
4491
  message: "notRequiredCommands is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
4456
4492
  path: ["notRequiredCommands"]
4457
4493
  });
4494
+ if (proof.schemaVersion < 4 && proof.notDemandedRecords !== void 0) context.addIssue({
4495
+ code: "custom",
4496
+ message: "notDemandedRecords is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
4497
+ path: ["notDemandedRecords"]
4498
+ });
4499
+ if (proof.notDemandedRecords !== void 0 && proof.impactStamp === void 0) context.addIssue({
4500
+ code: "custom",
4501
+ message: "notDemandedRecords requires impactStamp; package-level not-demanded records are bound to the stamp identity.",
4502
+ path: ["notDemandedRecords"]
4503
+ });
4458
4504
  assertBatteryCompleteness(proof, context);
4459
4505
  assertNotRequiredStampAuthorization(proof, context);
4460
4506
  };
@@ -4487,6 +4533,10 @@ const notRequiredCommandSchema = z.object({
4487
4533
  impactTarget: z.string().min(1),
4488
4534
  name: z.string().min(1)
4489
4535
  });
4536
+ const notDemandedRecordSchema = z.object({
4537
+ basis: z.string().min(1),
4538
+ name: z.string().min(1)
4539
+ });
4490
4540
  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(", ")}` });
4491
4541
  const prVerifyProofSchema = z.object({
4492
4542
  authoringSession: z.string().trim().min(1).optional(),
@@ -4518,6 +4568,7 @@ const prVerifyProofSchema = z.object({
4518
4568
  "trivial",
4519
4569
  "full"
4520
4570
  ]),
4571
+ notDemandedRecords: z.array(notDemandedRecordSchema).min(1).optional(),
4521
4572
  notRequiredCommands: z.array(notRequiredCommandSchema).min(1).optional(),
4522
4573
  outcome: z.enum(["aborted", "passed"]).optional(),
4523
4574
  patchId: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
@@ -6004,6 +6055,84 @@ function createBoundaryCheckCommand(output, action) {
6004
6055
  if (record.status !== "ready") throw new Error(`boundary:check refused: ${record.blockingReasons.join("; ")}`);
6005
6056
  })));
6006
6057
  }
6058
+ //#endregion
6059
+ //#region src/verification-run-context.ts
6060
+ const FACTORY_BASE_REF_ENV = "FACTORY_BASE_REF";
6061
+ const FACTORY_CHANGED_FILES_ENV = "FACTORY_CHANGED_FILES";
6062
+ const FACTORY_CHANGED_FILES_FILE_ENV = "FACTORY_CHANGED_FILES_FILE";
6063
+ const DEFAULT_CHANGED_FILES_INLINE_LIMIT_BYTES = 1e5;
6064
+ const EMPTY_TREE_OBJECT_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
6065
+ const DEFAULT_ROOT_SHARED_GLOBS = [
6066
+ "pnpm-lock.yaml",
6067
+ "package-lock.json",
6068
+ "yarn.lock",
6069
+ "bun.lockb",
6070
+ "pnpm-workspace.yaml",
6071
+ "package.json",
6072
+ "tsconfig*.json",
6073
+ "*.config.*",
6074
+ ".npmrc",
6075
+ ".nvmrc"
6076
+ ];
6077
+ function fullSuiteReasonForFile(file) {
6078
+ return `${file}: root/shared change forces full suite`;
6079
+ }
6080
+ function noopCleanup() {}
6081
+ function createVerificationRunContext(input) {
6082
+ const rootSharedGlobs = input.rootSharedGlobs ?? DEFAULT_ROOT_SHARED_GLOBS;
6083
+ const fullSuiteBaseRef = input.fullSuiteBaseRef ?? "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
6084
+ const inlineLimitBytes = input.inlineLimitBytes ?? DEFAULT_CHANGED_FILES_INLINE_LIMIT_BYTES;
6085
+ const tmpDir = input.tmpDir ?? os.tmpdir();
6086
+ const matchingFiles = input.changedFiles.filter((file) => matchesAnyGlob(rootSharedGlobs, file));
6087
+ const forcesFullSuite = matchingFiles.length > 0 || input.changedFiles.length === 0;
6088
+ const fullSuiteReasons = input.changedFiles.length === 0 ? ["no changed files detected; forcing full suite"] : matchingFiles.map(fullSuiteReasonForFile);
6089
+ return {
6090
+ baseRef: input.baseRef,
6091
+ buildEnv: () => {
6092
+ const payload = input.changedFiles.join("\n");
6093
+ const env = {
6094
+ [FACTORY_BASE_REF_ENV]: forcesFullSuite ? fullSuiteBaseRef : input.baseRef,
6095
+ [FACTORY_CHANGED_FILES_ENV]: "",
6096
+ [FACTORY_CHANGED_FILES_FILE_ENV]: ""
6097
+ };
6098
+ if (Buffer.byteLength(payload, "utf-8") > inlineLimitBytes) {
6099
+ const dir = mkdtempSync(path.join(tmpDir, "factory-changed-files-"));
6100
+ const filePath = path.join(dir, "changed-files.txt");
6101
+ writeFileSync(filePath, `${payload}\n`, "utf-8");
6102
+ env[FACTORY_CHANGED_FILES_FILE_ENV] = filePath;
6103
+ return {
6104
+ cleanup: () => {
6105
+ rmSync(dir, {
6106
+ force: true,
6107
+ recursive: true
6108
+ });
6109
+ },
6110
+ env
6111
+ };
6112
+ }
6113
+ env[FACTORY_CHANGED_FILES_ENV] = payload;
6114
+ return {
6115
+ cleanup: noopCleanup,
6116
+ env
6117
+ };
6118
+ },
6119
+ changedFiles: input.changedFiles,
6120
+ forcesFullSuite,
6121
+ fullSuiteReasons,
6122
+ runSummaryLines: () => {
6123
+ const lines = [];
6124
+ if (input.changedFiles.length > 0) {
6125
+ lines.push("Changed files considered by pr:verify:");
6126
+ for (const file of input.changedFiles) lines.push(`- ${file}`);
6127
+ } else lines.push("No changed files detected.");
6128
+ if (forcesFullSuite) {
6129
+ lines.push("Forcing full verification suite:");
6130
+ for (const reason of fullSuiteReasons) lines.push(`- ${reason}`);
6131
+ }
6132
+ return lines;
6133
+ }
6134
+ };
6135
+ }
6007
6136
  const COMMIT_SHA_PATTERN$1 = /^[0-9a-f]{40}$/iu;
6008
6137
  const ZERO_SHA_PATTERN$1 = /^0{40}$/u;
6009
6138
  const demandedTargets$1 = (profile, reason) => (profile?.impact?.targets ?? []).map(({ name }) => ({
@@ -6020,6 +6149,7 @@ const refusal$1 = ({ base, head, profile, reason }) => ({
6020
6149
  },
6021
6150
  decision: "refused",
6022
6151
  ...head ? { headSha: head } : {},
6152
+ inertPaths: [],
6023
6153
  schemaVersion: 1,
6024
6154
  targets: demandedTargets$1(profile, reason),
6025
6155
  unsubscribedPaths: []
@@ -6056,13 +6186,23 @@ const assertReachableCommit$1 = (cwd, sha, name) => {
6056
6186
  throw new Error(`${name} commit ${sha} is not reachable in this checkout`);
6057
6187
  }
6058
6188
  };
6189
+ const githubOutputAssignment = (name, value) => {
6190
+ if (value.includes("\n") || value.includes("\r")) {
6191
+ const delimiter = `ghadelim_${randomBytes(8).toString("hex")}`;
6192
+ return `${name}<<${delimiter}\n${value}\n${delimiter}`;
6193
+ }
6194
+ return `${name}=${value}`;
6195
+ };
6059
6196
  const writeGithubOutputs$1 = (outputPath, decision) => {
6060
- appendFileSync(outputPath, `${[
6197
+ const lines = [
6061
6198
  `decision=${decision.decision}`,
6062
6199
  `basis=${JSON.stringify(decision.basis)}`,
6200
+ `inert_paths=${JSON.stringify(decision.inertPaths)}`,
6063
6201
  `unsubscribed_paths=${JSON.stringify(decision.unsubscribedPaths)}`,
6064
6202
  ...decision.targets.map((target) => `${productionImpactTargetOutput(target.name)}=${target.state}`)
6065
- ].join("\n")}\n`, "utf-8");
6203
+ ];
6204
+ if (decision.runContext) lines.push(githubOutputAssignment("factory_base_ref", decision.runContext.FACTORY_BASE_REF), githubOutputAssignment("factory_changed_files", decision.runContext.FACTORY_CHANGED_FILES), githubOutputAssignment("factory_changed_files_file", decision.runContext.FACTORY_CHANGED_FILES_FILE), githubOutputAssignment("merge_base_sha", decision.runContext.mergeBaseSha ?? ""));
6205
+ appendFileSync(outputPath, `${lines.join("\n")}\n`, "utf-8");
6066
6206
  };
6067
6207
  /**
6068
6208
  * Classify candidate impact from one exact base/head identity. The delta is
@@ -6076,6 +6216,7 @@ const runCandidateImpact = (args) => {
6076
6216
  const cwd = path.resolve(args.cwd ?? process.cwd());
6077
6217
  let profile;
6078
6218
  let profilePath;
6219
+ let relativeProfile;
6079
6220
  let decision;
6080
6221
  try {
6081
6222
  const { path: loadedPath, profile: loadedProfile } = loadProjectProfile({
@@ -6084,6 +6225,7 @@ const runCandidateImpact = (args) => {
6084
6225
  });
6085
6226
  profile = loadedProfile;
6086
6227
  profilePath = loadedPath;
6228
+ relativeProfile = path.relative(cwd, profilePath).split(path.sep).join("/");
6087
6229
  const base = assertCommitIdentity$1(args.base, "base");
6088
6230
  const head = assertCommitIdentity$1(args.head, "head");
6089
6231
  assertReachableCommit$1(cwd, base, "base");
@@ -6123,6 +6265,7 @@ const runCandidateImpact = (args) => {
6123
6265
  },
6124
6266
  decision: usable ? "usable" : "refused",
6125
6267
  headSha: head,
6268
+ inertPaths: stamp.inertPaths,
6126
6269
  mergeBaseSha: mergeBase,
6127
6270
  schemaVersion: 1,
6128
6271
  targets: stamp.targets.map((target) => ({
@@ -6140,6 +6283,29 @@ const runCandidateImpact = (args) => {
6140
6283
  reason: `candidate impact classification refused: ${error instanceof Error ? error.message : String(error)}`
6141
6284
  });
6142
6285
  }
6286
+ if (args.emitRunContext) {
6287
+ const fullSuiteBaseRef = emptyTreeHash(cwd);
6288
+ const context = createVerificationRunContext({
6289
+ baseRef: decision.mergeBaseSha ?? fullSuiteBaseRef,
6290
+ changedFiles: decision.basis.changedFiles,
6291
+ fullSuiteBaseRef,
6292
+ rootSharedGlobs: relativeProfile ? [...DEFAULT_ROOT_SHARED_GLOBS, relativeProfile] : [...DEFAULT_ROOT_SHARED_GLOBS]
6293
+ });
6294
+ const { env } = context.buildEnv();
6295
+ decision = {
6296
+ ...decision,
6297
+ runContext: {
6298
+ FACTORY_BASE_REF: env["FACTORY_BASE_REF"] ?? "",
6299
+ FACTORY_CHANGED_FILES: env["FACTORY_CHANGED_FILES"] ?? "",
6300
+ FACTORY_CHANGED_FILES_FILE: env["FACTORY_CHANGED_FILES_FILE"] ?? "",
6301
+ baseRef: context.baseRef,
6302
+ changedFiles: context.changedFiles,
6303
+ forcesFullSuite: context.forcesFullSuite,
6304
+ fullSuiteReasons: context.fullSuiteReasons,
6305
+ ...decision.mergeBaseSha ? { mergeBaseSha: decision.mergeBaseSha } : {}
6306
+ }
6307
+ };
6308
+ }
6143
6309
  if (args.githubOutput) writeGithubOutputs$1(path.resolve(cwd, args.githubOutput), decision);
6144
6310
  if (args.githubSummary) appendFileSync(path.resolve(cwd, args.githubSummary), `### Candidate impact\n\n${candidateImpactSummaryLines(decision).join("\n")}\n`, "utf-8");
6145
6311
  return decision;
@@ -6147,22 +6313,36 @@ const runCandidateImpact = (args) => {
6147
6313
  const candidateImpactSummaryLines = (decision) => decision.basis.classification === "refused" ? [
6148
6314
  "Candidate impact: refused; all routed work remains demanded.",
6149
6315
  ...decision.basis.reasons,
6316
+ inertPathsLine(void 0, "classification was refused"),
6150
6317
  unsubscribedPathsLine(void 0, "classification was refused")
6151
- ] : [`Candidate impact: ${decision.basis.classification}; ${decision.targets.filter(({ state }) => state === "demanded").length}/${decision.targets.length} target(s) demanded.`, unsubscribedPathsLine(decision.unsubscribedPaths)];
6318
+ ] : [
6319
+ `Candidate impact: ${decision.basis.classification}; ${decision.targets.filter(({ state }) => state === "demanded").length}/${decision.targets.length} target(s) demanded.`,
6320
+ inertPathsLine(decision.inertPaths),
6321
+ unsubscribedPathsLine(decision.unsubscribedPaths)
6322
+ ];
6152
6323
  //#endregion
6153
6324
  //#region src/commands/candidate-impact.ts
6154
6325
  function createCandidateImpactCommand(output) {
6155
- return markCwdOptionDefault(new Command("candidate:impact").description("Classify candidate targets from an exact pull-request base/head identity").option("--base <sha>", "candidate base commit SHA").option("--head <sha>", "candidate head commit SHA").option("--cwd <path>", "repository working directory", collectCwdOption).option("--profile <path>", "path to the project profile JSON file").option("--github-output <path>", "append stable decision outputs to a GitHub Actions output file").option("--github-summary <path>", "append a human-readable decision to a GitHub Actions summary file").option("--json", "print the complete decision as JSON").action((options) => {
6326
+ return markCwdOptionDefault(new Command("candidate:impact").description("Classify candidate targets from an exact pull-request base/head identity").option("--base <sha>", "candidate base commit SHA").option("--head <sha>", "candidate head commit SHA").option("--cwd <path>", "repository working directory", collectCwdOption).option("--profile <path>", "path to the project profile JSON file").option("--github-output <path>", "append stable decision outputs to a GitHub Actions output file").option("--github-summary <path>", "append a human-readable decision to a GitHub Actions summary file").option("--json", "print the complete decision as JSON").option("--emit-run-context", "emit the FACTORY_BASE_REF / FACTORY_CHANGED_FILES run-context contract").action((options) => {
6156
6327
  const decision = runCandidateImpact({
6157
6328
  base: options.base,
6158
6329
  cwd: resolveCwdOption(options.cwd),
6330
+ emitRunContext: options.emitRunContext,
6159
6331
  githubOutput: options.githubOutput,
6160
6332
  githubSummary: options.githubSummary,
6161
6333
  head: options.head,
6162
6334
  profilePath: options.profile
6163
6335
  });
6164
6336
  if (options.json) output.stdout.write(`${JSON.stringify(decision, null, 2)}\n`);
6165
- else output.stdout.write(`${candidateImpactSummaryLines(decision).join("\n")}\n`);
6337
+ else if (options.emitRunContext) {
6338
+ const context = decision.runContext;
6339
+ if (context === void 0) throw new Error("--emit-run-context produced no run context; refusing to print an empty contract");
6340
+ output.stdout.write(`${[
6341
+ `${FACTORY_BASE_REF_ENV}=${context.FACTORY_BASE_REF}`,
6342
+ `${FACTORY_CHANGED_FILES_ENV}=${context.FACTORY_CHANGED_FILES}`,
6343
+ `${FACTORY_CHANGED_FILES_FILE_ENV}=${context.FACTORY_CHANGED_FILES_FILE}`
6344
+ ].join("\n")}\n`);
6345
+ } else output.stdout.write(`${candidateImpactSummaryLines(decision).join("\n")}\n`);
6166
6346
  }));
6167
6347
  }
6168
6348
  /** `tool` discriminator every emitted report carries. */
@@ -9573,25 +9753,49 @@ function createFactoryCheckPublisher(output, dependencies = {}) {
9573
9753
  }
9574
9754
  //#endregion
9575
9755
  //#region src/pr-classification.ts
9576
- function ownershipForFile(profile, file) {
9577
- if (file === "pnpm-lock.yaml") return { kind: "graph-input" };
9578
- const subscribed = (profile.impact?.targets ?? []).find((target) => matchesAnyGlob(target.paths ?? [], file));
9579
- if (subscribed) return {
9756
+ const MISSING_STAMP_REASON = "impact stamp is missing; fail closed to non-trivial";
9757
+ function ownershipFromStamp(stamp, file) {
9758
+ if (stamp.inertPaths.includes(file)) return { kind: "inert" };
9759
+ if (stamp.unsubscribedPaths.includes(file)) return { kind: "unowned" };
9760
+ const named = stamp.targets.find((target) => target.impact === "affected" && target.basis.includes(`"${file}"`));
9761
+ if (named) return {
9762
+ kind: "target",
9763
+ target: named.name
9764
+ };
9765
+ const affected = stamp.targets.find((target) => target.impact === "affected");
9766
+ if (affected) return {
9580
9767
  kind: "target",
9581
- target: subscribed.name
9768
+ target: affected.name
9582
9769
  };
9583
- if (matchesAnyGlob(profile.impact?.inert ?? [], file)) return { kind: "inert" };
9584
9770
  return { kind: "unowned" };
9585
9771
  }
9586
9772
  function reasonForOwnership(file, ownership) {
9587
9773
  switch (ownership.kind) {
9588
- case "graph-input": return `${file}: dependency graph input; never inert`;
9589
- case "inert": return `${file}: repository-inert ownership (impact.inert)`;
9774
+ case "inert": return `${file}: repository-inert ownership (stamp.inertPaths)`;
9590
9775
  case "target": return `${file}: affects product target "${ownership.target}"`;
9591
9776
  default: return `${file}: no declared owner; fail closed to non-trivial`;
9592
9777
  }
9593
9778
  }
9594
- function classifyDiff(profile, files) {
9779
+ function relativeProfilePath(options) {
9780
+ return path.relative(options.cwd, options.profilePath).split(path.sep).join("/");
9781
+ }
9782
+ function applyProfileEditGuard(files, relativeProfile, profilePath, base) {
9783
+ if (!files.some((file) => file === relativeProfile || file === profilePath)) return base;
9784
+ return {
9785
+ classification: "non-trivial",
9786
+ reasons: [...base.reasons, `${relativeProfile}: edits the ownership declarations; forced non-trivial`]
9787
+ };
9788
+ }
9789
+ /**
9790
+ * Path ownership as the stamp recorded it. A missing stamp fails closed.
9791
+ * Does not glob, and does not refuse the docs rung on a conservative basis —
9792
+ * that gate belongs to {@link classifyDiffForRunWithStamp}.
9793
+ */
9794
+ function classifyDiff(files, stamp) {
9795
+ if (stamp === void 0) return {
9796
+ classification: "non-trivial",
9797
+ reasons: [MISSING_STAMP_REASON]
9798
+ };
9595
9799
  const sortedFiles = [...files].toSorted();
9596
9800
  if (sortedFiles.length === 0) return {
9597
9801
  classification: "non-trivial",
@@ -9599,7 +9803,7 @@ function classifyDiff(profile, files) {
9599
9803
  };
9600
9804
  const ownerships = sortedFiles.map((file) => ({
9601
9805
  file,
9602
- ownership: ownershipForFile(profile, file)
9806
+ ownership: ownershipFromStamp(stamp, file)
9603
9807
  }));
9604
9808
  const reasons = ownerships.map(({ file, ownership }) => reasonForOwnership(file, ownership));
9605
9809
  if (ownerships.every(({ ownership }) => ownership.kind === "inert")) return {
@@ -9620,14 +9824,13 @@ function classifyDiff(profile, files) {
9620
9824
  * fail-closed stamp can never coexist with a reduced verification battery
9621
9825
  * (#742 review cycle 3).
9622
9826
  *
9623
- * `classifyDiffForRun` below stays the pure path predicate. Its remaining
9624
- * stamp-less consumer is pr-ready's docs-only DELTA check, where the delta
9625
- * rides on an already fully verified baseline and a lockfile-bearing delta
9626
- * already refuses through the graph-input rule.
9827
+ * Ownership and reasons come from `stamp.targets`, `stamp.inertPaths`, and
9828
+ * `stamp.unsubscribedPaths`. A missing stamp fails closed; this function
9829
+ * never evaluates profile globs.
9627
9830
  */
9628
9831
  function classifyDiffForRunWithStamp(profile, files, stamp, options) {
9629
- const base = classifyDiffForRun(profile, files, options);
9630
- if (base.classification === "non-trivial") return base;
9832
+ const base = applyProfileEditGuard(files, relativeProfilePath(options), options.profilePath, classifyDiff(files, stamp));
9833
+ if (stamp === void 0 || base.classification === "non-trivial") return base;
9631
9834
  if (stamp.basis !== "target-scoped") return {
9632
9835
  classification: "non-trivial",
9633
9836
  reasons: [...base.reasons, "impact stamp is conservative; the docs/process-only rung needs a target-scoped stamp with zero affected targets"]
@@ -9639,15 +9842,20 @@ function classifyDiffForRunWithStamp(profile, files, stamp, options) {
9639
9842
  };
9640
9843
  return base;
9641
9844
  }
9845
+ /**
9846
+ * Path predicate for consumers that do not already hold a stamp (pr-ready's
9847
+ * docs-only delta). Computes one stamp through {@link computeImpactStamp} —
9848
+ * the only glob-evaluation site — then reads ownership from it.
9849
+ */
9642
9850
  function classifyDiffForRun(profile, files, options) {
9643
- const relativeProfile = path.relative(options.cwd, options.profilePath).split(path.sep).join("/");
9644
- const base = classifyDiff(profile, files);
9645
- if (base.classification === "non-trivial") return base;
9646
- if (files.some((file) => file === relativeProfile || file === options.profilePath)) return {
9647
- classification: "non-trivial",
9648
- reasons: [...base.reasons, `${relativeProfile}: edits the ownership declarations; forced non-trivial`]
9649
- };
9650
- return base;
9851
+ const relativeProfile = relativeProfilePath(options);
9852
+ const stamp = computeImpactStamp({
9853
+ changedFiles: files,
9854
+ profile,
9855
+ profilePath: relativeProfile,
9856
+ readLockfile: () => void 0
9857
+ });
9858
+ return applyProfileEditGuard(files, relativeProfile, options.profilePath, classifyDiff(files, stamp));
9651
9859
  }
9652
9860
  //#endregion
9653
9861
  //#region src/pr-readiness/verification-proof.ts
@@ -9855,83 +10063,6 @@ const verificationProofBlockingReason = (state) => {
9855
10063
  }
9856
10064
  };
9857
10065
  //#endregion
9858
- //#region src/verification-run-context.ts
9859
- const FACTORY_BASE_REF_ENV = "FACTORY_BASE_REF";
9860
- const FACTORY_CHANGED_FILES_ENV = "FACTORY_CHANGED_FILES";
9861
- const FACTORY_CHANGED_FILES_FILE_ENV = "FACTORY_CHANGED_FILES_FILE";
9862
- const DEFAULT_CHANGED_FILES_INLINE_LIMIT_BYTES = 1e5;
9863
- const DEFAULT_ROOT_SHARED_GLOBS = [
9864
- "pnpm-lock.yaml",
9865
- "package-lock.json",
9866
- "yarn.lock",
9867
- "bun.lockb",
9868
- "pnpm-workspace.yaml",
9869
- "package.json",
9870
- "tsconfig*.json",
9871
- "*.config.*",
9872
- ".npmrc",
9873
- ".nvmrc"
9874
- ];
9875
- function fullSuiteReasonForFile(file) {
9876
- return `${file}: root/shared change forces full suite`;
9877
- }
9878
- function noopCleanup() {}
9879
- function createVerificationRunContext(input) {
9880
- const rootSharedGlobs = input.rootSharedGlobs ?? DEFAULT_ROOT_SHARED_GLOBS;
9881
- const fullSuiteBaseRef = input.fullSuiteBaseRef ?? "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
9882
- const inlineLimitBytes = input.inlineLimitBytes ?? DEFAULT_CHANGED_FILES_INLINE_LIMIT_BYTES;
9883
- const tmpDir = input.tmpDir ?? os.tmpdir();
9884
- const matchingFiles = input.changedFiles.filter((file) => matchesAnyGlob(rootSharedGlobs, file));
9885
- const forcesFullSuite = matchingFiles.length > 0 || input.changedFiles.length === 0;
9886
- const fullSuiteReasons = input.changedFiles.length === 0 ? ["no changed files detected; forcing full suite"] : matchingFiles.map(fullSuiteReasonForFile);
9887
- return {
9888
- baseRef: input.baseRef,
9889
- buildEnv: () => {
9890
- const payload = input.changedFiles.join("\n");
9891
- const env = {
9892
- [FACTORY_BASE_REF_ENV]: forcesFullSuite ? fullSuiteBaseRef : input.baseRef,
9893
- [FACTORY_CHANGED_FILES_ENV]: "",
9894
- [FACTORY_CHANGED_FILES_FILE_ENV]: ""
9895
- };
9896
- if (Buffer.byteLength(payload, "utf-8") > inlineLimitBytes) {
9897
- const dir = mkdtempSync(path.join(tmpDir, "factory-changed-files-"));
9898
- const filePath = path.join(dir, "changed-files.txt");
9899
- writeFileSync(filePath, `${payload}\n`, "utf-8");
9900
- env[FACTORY_CHANGED_FILES_FILE_ENV] = filePath;
9901
- return {
9902
- cleanup: () => {
9903
- rmSync(dir, {
9904
- force: true,
9905
- recursive: true
9906
- });
9907
- },
9908
- env
9909
- };
9910
- }
9911
- env[FACTORY_CHANGED_FILES_ENV] = payload;
9912
- return {
9913
- cleanup: noopCleanup,
9914
- env
9915
- };
9916
- },
9917
- changedFiles: input.changedFiles,
9918
- forcesFullSuite,
9919
- fullSuiteReasons,
9920
- runSummaryLines: () => {
9921
- const lines = [];
9922
- if (input.changedFiles.length > 0) {
9923
- lines.push("Changed files considered by pr:verify:");
9924
- for (const file of input.changedFiles) lines.push(`- ${file}`);
9925
- } else lines.push("No changed files detected.");
9926
- if (forcesFullSuite) {
9927
- lines.push("Forcing full verification suite:");
9928
- for (const reason of fullSuiteReasons) lines.push(`- ${reason}`);
9929
- }
9930
- return lines;
9931
- }
9932
- };
9933
- }
9934
- //#endregion
9935
10066
  //#region src/workspace-install-resolution.ts
9936
10067
  const readJson = (filePath, read) => {
9937
10068
  if (!existsSync(filePath)) return null;
@@ -10178,7 +10309,6 @@ function formatVerifyTimingSummary(proof) {
10178
10309
  function resolveMode({ classification, mode }) {
10179
10310
  if (mode === "auto") {
10180
10311
  if (classification === "docs/process-only") return "docs-only";
10181
- if (classification === "trivial") return "trivial";
10182
10312
  return "full";
10183
10313
  }
10184
10314
  return mode;
@@ -12938,7 +13068,6 @@ var pr_body_renderer_exports = /* @__PURE__ */ __exportAll({
12938
13068
  });
12939
13069
  const commandFor = (proof) => {
12940
13070
  if (proof.mode === "docs-only") return "patronage-factory pr:verify --docs-only";
12941
- if (proof.mode === "trivial") return "patronage-factory pr:verify --trivial";
12942
13071
  return "patronage-factory pr:verify";
12943
13072
  };
12944
13073
  const renderPrBodySectionParts = ({ reviewProof, verifyProof }) => {
@@ -15921,17 +16050,13 @@ function createPrReviewCommand(output, action) {
15921
16050
  //#endregion
15922
16051
  //#region src/commands/pr-verify.ts
15923
16052
  function createPrVerifyCommand(output, action = runPrVerify) {
15924
- return markCwdOptionDefault(new Command("pr:verify").description("Run project-profile verification commands and write typed proof").option("--base <ref>", "base branch or ref", "origin/main").option("--cwd <path>", "working directory to verify", collectCwdOption).option("--docs-only", "run the docs/process verification gate").option("--trivial", "run the trivial verification gate").option("--full", "run the full verification gate").option("--authoring-session <id>", "explicit authoring session identity (required for review evidence)").option("--json", "print the proof as JSON after verification").option("--output <path>", "write proof JSON to a file").option("--profile <path>", "path to the project profile JSON file").option("--no-status", "skip posting the patronage-factory/pr-verify commit status").action(withGateTiming({
16053
+ return markCwdOptionDefault(new Command("pr:verify").description("Run project-profile verification commands and write typed proof").option("--base <ref>", "base branch or ref", "origin/main").option("--cwd <path>", "working directory to verify", collectCwdOption).option("--docs-only", "run the docs/process verification gate").option("--full", "run the full verification gate").option("--authoring-session <id>", "explicit authoring session identity (required for review evidence)").option("--json", "print the proof as JSON after verification").option("--output <path>", "write proof JSON to a file").option("--profile <path>", "path to the project profile JSON file").option("--no-status", "skip posting the patronage-factory/pr-verify commit status").action(withGateTiming({
15925
16054
  gate: "pr:verify",
15926
16055
  resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
15927
16056
  stderr: output.stderr
15928
16057
  }, (options) => {
15929
16058
  assertForegroundGate({ gate: "pr:verify" });
15930
- if ([
15931
- options.docsOnly,
15932
- options.trivial,
15933
- options.full
15934
- ].filter(Boolean).length > 1) throw new Error("Use at most one of --docs-only, --trivial, or --full");
16059
+ if ([options.docsOnly, options.full].filter(Boolean).length > 1) throw new Error("Use at most one of --docs-only or --full");
15935
16060
  const dependencies = options.status === false ? {} : { publishCheckRun: createFactoryCheckPublisher(output) };
15936
16061
  const proof = action({
15937
16062
  authoringSession: options.authoringSession,
@@ -15947,7 +16072,6 @@ function createPrVerifyCommand(output, action = runPrVerify) {
15947
16072
  }
15948
16073
  function modeFor(options) {
15949
16074
  if (options.docsOnly) return "docs-only";
15950
- if (options.trivial) return "trivial";
15951
16075
  if (options.full) return "full";
15952
16076
  return "auto";
15953
16077
  }
@@ -16412,4 +16536,4 @@ if (isDirectCliExecution()) try {
16412
16536
  process.exit(1);
16413
16537
  }
16414
16538
  //#endregion
16415
- 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, REVIEW_FOCUS_SECTION, WorkerCheckoutGuardError, appendReviewLadderStageTraceEvent, appendWithTraceSinks, applyDemandWaiver, assembleReviewPrompt, assertWorkerCheckoutAllowed, authorizeDemandWaiver, batteryScopeSummaryLines, blockingLadderFindings, boundary_manifest_exports as boundaryManifest, boundary_review_proof_exports as boundaryReviewProof, buildEpicStructureEvent, buildEpicStructurePayload, buildEvidenceEnvelope, buildReviewGateNotRequiredTraceEvent, buildReviewLadderStageTraceEvent, comment_provenance_exports as commentProvenance, createLocalJsonlTraceSink, createProgram, doctorProjectProfile, epicStructureEventId, evaluateReviewLadder, evidenceEnvelopeFilename, findingKey, followUpFromArgv, impactStampScopeDecision, inferFixedInThreadDispositions, isProductionHqUrl, loadProjectProfile, normalizeIssueComments, openLadderFindings, 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, 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 };
16539
+ export { DEFAULT_DEMAND_WAIVER_PATH, DEFAULT_FACTORY_REPOSITORY, DEFAULT_ROOT_SHARED_GLOBS, DemandWaiveRefusalError, EMPTY_TREE_OBJECT_HASH, EPIC_STRUCTURE_NODE_STATUSES, EPIC_STRUCTURE_SCHEMA_VERSION, EpicStructureValidationError, FACTORY_BASE_REF_ENV, FACTORY_CHANGED_FILES_ENV, FACTORY_CHANGED_FILES_FILE_ENV, FactoryCliInvocationSchema, FollowUpActionSchema, MERGE_FREEZE_APP_SLUG, MERGE_FREEZE_CHECK_NAME, PrPublishFollowUpError, REVIEW_FOCUS_SECTION, WorkerCheckoutGuardError, appendReviewLadderStageTraceEvent, appendWithTraceSinks, applyDemandWaiver, assembleReviewPrompt, assertWorkerCheckoutAllowed, authorizeDemandWaiver, batteryScopeSummaryLines, blockingLadderFindings, boundary_manifest_exports as boundaryManifest, boundary_review_proof_exports as boundaryReviewProof, buildEpicStructureEvent, buildEpicStructurePayload, buildEvidenceEnvelope, buildReviewGateNotRequiredTraceEvent, buildReviewLadderStageTraceEvent, comment_provenance_exports as commentProvenance, createLocalJsonlTraceSink, createProgram, createVerificationRunContext, doctorProjectProfile, epicStructureEventId, evaluateReviewLadder, evidenceEnvelopeFilename, findingKey, followUpFromArgv, impactStampScopeDecision, inferFixedInThreadDispositions, isProductionHqUrl, loadProjectProfile, normalizeIssueComments, openLadderFindings, 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, 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 };