@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/CONTEXT.md +1 -1
- package/dist/index.d.ts +88 -45
- package/dist/index.js +275 -151
- package/dist/schemas.d.ts +17 -0
- package/dist/schemas.js +16 -0
- package/package.json +2 -2
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.
|
|
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:
|
|
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
|
|
4052
|
-
*
|
|
4053
|
-
*
|
|
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
|
|
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
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
]
|
|
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
|
-
] : [
|
|
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
|
|
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
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
if (
|
|
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:
|
|
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 "
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
9624
|
-
* stamp
|
|
9625
|
-
*
|
|
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 =
|
|
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 =
|
|
9644
|
-
const
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
};
|
|
9650
|
-
return
|
|
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("--
|
|
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 };
|