@patronage/software-factory 1.0.0-alpha.26 → 1.0.0-alpha.28
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/cli.js +12 -0
- package/dist/index.d.ts +115 -55
- package/dist/index.js +685 -194
- package/dist/schemas.d.ts +17 -12
- package/dist/schemas.js +19 -53
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import { t as __exportAll } from "./chunk-pbuEa-1d.js";
|
|
3
|
-
import { appendFileSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
2
|
import { Command, InvalidArgumentError } from "commander";
|
|
6
3
|
import path from "node:path";
|
|
7
4
|
import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
8
5
|
import { z } from "zod";
|
|
9
6
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
7
|
+
import { appendFileSync, constants, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
10
8
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
11
9
|
import picomatch from "picomatch";
|
|
12
10
|
import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "node:fs/promises";
|
|
@@ -16,9 +14,27 @@ import { setImmediate as setImmediate$1, setTimeout as setTimeout$1 } from "node
|
|
|
16
14
|
import { Worker } from "node:worker_threads";
|
|
17
15
|
import { parse } from "yaml";
|
|
18
16
|
import { promisify } from "node:util";
|
|
19
|
-
import { FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, previewProofInventory, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
|
|
17
|
+
import { FACTORY_APP_BROKER_ORIGIN, FACTORY_APP_BROKER_PATH, FACTORY_CURSOR_OIDC_AUDIENCE, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, previewProofInventory, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
|
|
18
|
+
import http from "node:http";
|
|
19
|
+
import { pathToFileURL } from "node:url";
|
|
20
20
|
//#region package.json
|
|
21
|
-
var version = "1.0.0-alpha.
|
|
21
|
+
var version = "1.0.0-alpha.28";
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/cli-entry.ts
|
|
24
|
+
/**
|
|
25
|
+
* The path of the module that runs a factory command.
|
|
26
|
+
*
|
|
27
|
+
* The package publishes two dist entries (#386). `index.js` is the library
|
|
28
|
+
* export: consumers import it, so it runs nothing. `cli.js` is the bin, and it
|
|
29
|
+
* is the only entry that runs a command. A spawned follow-up that names
|
|
30
|
+
* `index.js` therefore exits 0 having done nothing.
|
|
31
|
+
*
|
|
32
|
+
* Every caller that needs the runnable entry resolves it here, so there is one
|
|
33
|
+
* place to get it wrong rather than one per call site. The entry is this
|
|
34
|
+
* module's sibling and carries this module's extension — `cli.ts` under tsx in
|
|
35
|
+
* development, `cli.js` in the built package.
|
|
36
|
+
*/
|
|
37
|
+
const factoryCliEntry = () => path.join(import.meta.dirname, `cli${path.extname(import.meta.filename)}`);
|
|
22
38
|
//#endregion
|
|
23
39
|
//#region src/review-rungs.ts
|
|
24
40
|
const EVIDENCE_REVIEW_RUNGS = [
|
|
@@ -1261,11 +1277,52 @@ function currentHeadSha(cwd) {
|
|
|
1261
1277
|
function repositoryRoot(cwd) {
|
|
1262
1278
|
return runCapture("git", ["rev-parse", "--show-toplevel"], cwd).stdout.trim();
|
|
1263
1279
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1280
|
+
/** A path handed to a git object read that is not repository-relative. */
|
|
1281
|
+
var RepositoryRelativePathError = class extends Error {
|
|
1282
|
+
name = "RepositoryRelativePathError";
|
|
1283
|
+
};
|
|
1284
|
+
/**
|
|
1285
|
+
* The repository-relative contract, enforced before a path reaches git.
|
|
1286
|
+
*
|
|
1287
|
+
* git resolves `<ref>:<path>` from the repository root ONLY for a bare name.
|
|
1288
|
+
* Measured against real git from a package subdirectory, `HEAD:./package.json`
|
|
1289
|
+
* and `HEAD:../software-factory/package.json` both resolve — the first against
|
|
1290
|
+
* the working directory, the second by traversal. So a caller that passes
|
|
1291
|
+
* either form silently addresses a different file depending on where the
|
|
1292
|
+
* command ran, which is the whole defect class this contract exists to close
|
|
1293
|
+
* (#922, #1004 review cycles 2 to 4).
|
|
1294
|
+
*
|
|
1295
|
+
* Every current call site already passes a bare name; this makes the promise
|
|
1296
|
+
* enforced rather than documented. It throws instead of returning a value,
|
|
1297
|
+
* because a control that cannot answer must fail loudly, and callers that
|
|
1298
|
+
* tolerate a missing file must not silently tolerate a mis-addressed one.
|
|
1299
|
+
*/
|
|
1300
|
+
function assertRepositoryRelativePath(repositoryRelativePath, caller) {
|
|
1301
|
+
const refuse = (reason) => {
|
|
1302
|
+
throw new RepositoryRelativePathError(`${caller} requires a repository-relative path, but received ${JSON.stringify(repositoryRelativePath)}: ${reason}. git resolves "<ref>:<path>" from the repository root only for a bare name, so a "./", "../", or absolute form addresses a different file depending on the working directory.`);
|
|
1303
|
+
};
|
|
1304
|
+
if (repositoryRelativePath.length === 0) return refuse("it is empty");
|
|
1305
|
+
if (repositoryRelativePath.startsWith("/") || path.isAbsolute(repositoryRelativePath)) return refuse("it is absolute");
|
|
1306
|
+
if (repositoryRelativePath.split("/").some((segment) => segment === "." || segment === "..")) return refuse("it contains a \".\" or \"..\" segment");
|
|
1307
|
+
return repositoryRelativePath;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* A file's content at a ref, addressed the way git addresses it (#922).
|
|
1311
|
+
*
|
|
1312
|
+
* `repositoryRelativePath` is repository-relative, NOT relative to `cwd`.
|
|
1313
|
+
* git resolves `<ref>:<path>` from the repository ROOT unless the path starts
|
|
1314
|
+
* with `./` or `../`, so a repository-relative name is correct from every
|
|
1315
|
+
* working directory inside the checkout, including one reached through a
|
|
1316
|
+
* symlink. `cwd` is still passed so git discovers the same repository.
|
|
1317
|
+
*
|
|
1318
|
+
* This used to relativize an absolute path against `cwd` and prefix `./`,
|
|
1319
|
+
* which made the address cwd-relative and therefore wrong from any
|
|
1320
|
+
* subdirectory (#1004 review cycles 2 and 3).
|
|
1321
|
+
*/
|
|
1322
|
+
function showFileAtRef(cwd, ref, repositoryRelativePath) {
|
|
1323
|
+
assertRepositoryRelativePath(repositoryRelativePath, "showFileAtRef");
|
|
1267
1324
|
try {
|
|
1268
|
-
return runCapture("git", ["show", `${ref}:${
|
|
1325
|
+
return runCapture("git", ["show", `${ref}:${repositoryRelativePath}`], cwd).stdout;
|
|
1269
1326
|
} catch {
|
|
1270
1327
|
return;
|
|
1271
1328
|
}
|
|
@@ -5483,7 +5540,7 @@ const analyzeSourcePaths = (sourceFiles, targets, profilePath, inertGlobs) => {
|
|
|
5483
5540
|
};
|
|
5484
5541
|
if (file === profilePath) return {
|
|
5485
5542
|
kind: "conservative",
|
|
5486
|
-
reason: `changed source path "${file}" is the impact profile being evaluated; fail closed to full impact
|
|
5543
|
+
reason: `changed source path "${file}" is the impact profile being evaluated; fail closed to full impact. A profile migration waives each retired target's required-check demand (see the admission skill).`
|
|
5487
5544
|
};
|
|
5488
5545
|
}
|
|
5489
5546
|
return {
|
|
@@ -6009,6 +6066,75 @@ function generationOrderMs(value) {
|
|
|
6009
6066
|
return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
|
|
6010
6067
|
}
|
|
6011
6068
|
//#endregion
|
|
6069
|
+
//#region src/cursor-oidc-mint.ts
|
|
6070
|
+
const DEFAULT_CURSOR_AGENT_SOCKET = "/run/cursor/api.sock";
|
|
6071
|
+
const DEFAULT_TIMEOUT_MS = 5e3;
|
|
6072
|
+
const cursorAgentSocketPath = (env = process.env) => {
|
|
6073
|
+
const fromEnv = env.CURSOR_AGENT_SOCKET;
|
|
6074
|
+
return fromEnv !== void 0 && fromEnv.length > 0 ? fromEnv : DEFAULT_CURSOR_AGENT_SOCKET;
|
|
6075
|
+
};
|
|
6076
|
+
const isCursorManagedVm = (env = process.env, exists = existsSync) => {
|
|
6077
|
+
if (env.CURSOR_AGENT_SOCKET !== void 0 && env.CURSOR_AGENT_SOCKET.length > 0) return true;
|
|
6078
|
+
return exists(DEFAULT_CURSOR_AGENT_SOCKET);
|
|
6079
|
+
};
|
|
6080
|
+
var CursorOidcMintError = class extends Error {
|
|
6081
|
+
constructor(message) {
|
|
6082
|
+
super(message);
|
|
6083
|
+
this.name = "CursorOidcMintError";
|
|
6084
|
+
}
|
|
6085
|
+
};
|
|
6086
|
+
/**
|
|
6087
|
+
* Mint a Cursor OIDC JWT from the local agent socket. No nonce: the broker
|
|
6088
|
+
* does not run a nonce protocol (#949).
|
|
6089
|
+
*/
|
|
6090
|
+
const mintCursorOidcToken = (options = {}) => new Promise((resolve, reject) => {
|
|
6091
|
+
const socketPath = options.socketPath ?? cursorAgentSocketPath();
|
|
6092
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
6093
|
+
const body = JSON.stringify({ aud: options.audience ?? FACTORY_CURSOR_OIDC_AUDIENCE });
|
|
6094
|
+
const req = http.request({
|
|
6095
|
+
headers: {
|
|
6096
|
+
"Content-Length": String(Buffer.byteLength(body)),
|
|
6097
|
+
"Content-Type": "application/json"
|
|
6098
|
+
},
|
|
6099
|
+
method: "POST",
|
|
6100
|
+
path: "/v1/tokens/oidc",
|
|
6101
|
+
socketPath
|
|
6102
|
+
}, (response) => {
|
|
6103
|
+
const chunks = [];
|
|
6104
|
+
response.on("data", (chunk) => {
|
|
6105
|
+
chunks.push(chunk);
|
|
6106
|
+
});
|
|
6107
|
+
response.on("end", () => {
|
|
6108
|
+
const text = Buffer.concat(chunks).toString("utf-8");
|
|
6109
|
+
if (response.statusCode !== 200) {
|
|
6110
|
+
reject(new CursorOidcMintError(`Cursor OIDC mint failed: ${String(response.statusCode)}`));
|
|
6111
|
+
return;
|
|
6112
|
+
}
|
|
6113
|
+
let parsed;
|
|
6114
|
+
try {
|
|
6115
|
+
parsed = JSON.parse(text);
|
|
6116
|
+
} catch {
|
|
6117
|
+
reject(new CursorOidcMintError("Cursor OIDC mint returned invalid JSON"));
|
|
6118
|
+
return;
|
|
6119
|
+
}
|
|
6120
|
+
if (parsed === null || typeof parsed !== "object" || typeof parsed.token !== "string") {
|
|
6121
|
+
reject(new CursorOidcMintError("Cursor OIDC mint omitted token"));
|
|
6122
|
+
return;
|
|
6123
|
+
}
|
|
6124
|
+
resolve(parsed.token);
|
|
6125
|
+
});
|
|
6126
|
+
});
|
|
6127
|
+
req.on("error", (error) => {
|
|
6128
|
+
reject(new CursorOidcMintError(`Cursor OIDC mint failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
6129
|
+
});
|
|
6130
|
+
req.setTimeout(timeoutMs, () => {
|
|
6131
|
+
req.destroy();
|
|
6132
|
+
reject(new CursorOidcMintError("Cursor OIDC mint timed out"));
|
|
6133
|
+
});
|
|
6134
|
+
req.write(body);
|
|
6135
|
+
req.end();
|
|
6136
|
+
});
|
|
6137
|
+
//#endregion
|
|
6012
6138
|
//#region src/pr-verify-status.ts
|
|
6013
6139
|
const PR_VERIFY_STATUS_CONTEXT = "patronage-factory/pr-verify";
|
|
6014
6140
|
const GITHUB_PUBLISH_TIMEOUT_MS = 5e3;
|
|
@@ -6067,6 +6193,143 @@ function createGhCommitStatusPoster(output, run = runGhStatus) {
|
|
|
6067
6193
|
};
|
|
6068
6194
|
}
|
|
6069
6195
|
//#endregion
|
|
6196
|
+
//#region src/factory-app-broker-client.ts
|
|
6197
|
+
const FACTORY_APP_BROKER_URL = `${FACTORY_APP_BROKER_ORIGIN}${FACTORY_APP_BROKER_PATH}`;
|
|
6198
|
+
var FactoryAppBrokerError = class extends Error {
|
|
6199
|
+
status;
|
|
6200
|
+
constructor(message, status) {
|
|
6201
|
+
super(message);
|
|
6202
|
+
this.name = "FactoryAppBrokerError";
|
|
6203
|
+
if (status !== void 0) this.status = status;
|
|
6204
|
+
}
|
|
6205
|
+
};
|
|
6206
|
+
const factoryAppBrokerUrl = (env = process.env) => {
|
|
6207
|
+
const override = env.FACTORY_APP_BROKER_URL;
|
|
6208
|
+
return override !== void 0 && override.length > 0 ? override : FACTORY_APP_BROKER_URL;
|
|
6209
|
+
};
|
|
6210
|
+
/**
|
|
6211
|
+
* Exchange a Cursor OIDC JWT for a repo-scoped Factory App installation token.
|
|
6212
|
+
* Missing or refused exchange fails closed — no token is invented.
|
|
6213
|
+
*/
|
|
6214
|
+
const exchangeFactoryAppInstallationToken = async (input) => {
|
|
6215
|
+
const request = input.fetch ?? fetch;
|
|
6216
|
+
const url = input.url ?? FACTORY_APP_BROKER_URL;
|
|
6217
|
+
let response;
|
|
6218
|
+
try {
|
|
6219
|
+
response = await request(url, {
|
|
6220
|
+
body: JSON.stringify({
|
|
6221
|
+
owner: input.owner,
|
|
6222
|
+
repo: input.repo
|
|
6223
|
+
}),
|
|
6224
|
+
headers: {
|
|
6225
|
+
Authorization: `Bearer ${input.oidc}`,
|
|
6226
|
+
"Content-Type": "application/json"
|
|
6227
|
+
},
|
|
6228
|
+
method: "POST",
|
|
6229
|
+
signal: AbortSignal.timeout(input.timeoutMs ?? 5e3)
|
|
6230
|
+
});
|
|
6231
|
+
} catch (error) {
|
|
6232
|
+
throw new FactoryAppBrokerError(`Factory App broker exchange failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6233
|
+
}
|
|
6234
|
+
if (!response.ok) throw new FactoryAppBrokerError(`Factory App broker exchange failed: ${String(response.status)}`, response.status);
|
|
6235
|
+
const body = await response.json();
|
|
6236
|
+
if (body === null || typeof body !== "object" || typeof body.token !== "string" || body.token.length === 0) throw new FactoryAppBrokerError("Factory App broker exchange omitted token");
|
|
6237
|
+
return body.token;
|
|
6238
|
+
};
|
|
6239
|
+
//#endregion
|
|
6240
|
+
//#region src/github-app-access.ts
|
|
6241
|
+
const NO_APP_SKIP_MESSAGE = "no factory GitHub App configured; skipping durable check-run republish.\n";
|
|
6242
|
+
const exchangeFromCursorVm = async (repository, dependencies, env, request, timeoutMs) => {
|
|
6243
|
+
const mint = dependencies.mintCursorOidc ?? mintCursorOidcToken;
|
|
6244
|
+
const exchange = dependencies.exchangeBroker ?? exchangeFactoryAppInstallationToken;
|
|
6245
|
+
let oidc;
|
|
6246
|
+
try {
|
|
6247
|
+
oidc = await mint({
|
|
6248
|
+
socketPath: cursorAgentSocketPath(env),
|
|
6249
|
+
timeoutMs
|
|
6250
|
+
});
|
|
6251
|
+
} catch (error) {
|
|
6252
|
+
return {
|
|
6253
|
+
failClosed: true,
|
|
6254
|
+
kind: "unavailable",
|
|
6255
|
+
message: `Cursor OIDC mint failed: ${error instanceof Error ? error.message : String(error)}\n`
|
|
6256
|
+
};
|
|
6257
|
+
}
|
|
6258
|
+
try {
|
|
6259
|
+
return {
|
|
6260
|
+
appId: FACTORY_PROOF_GATE_APP_ID,
|
|
6261
|
+
kind: "ok",
|
|
6262
|
+
token: await exchange({
|
|
6263
|
+
fetch: request,
|
|
6264
|
+
oidc,
|
|
6265
|
+
owner: repository.owner,
|
|
6266
|
+
repo: repository.repo,
|
|
6267
|
+
timeoutMs,
|
|
6268
|
+
url: factoryAppBrokerUrl(env)
|
|
6269
|
+
})
|
|
6270
|
+
};
|
|
6271
|
+
} catch (error) {
|
|
6272
|
+
return {
|
|
6273
|
+
failClosed: true,
|
|
6274
|
+
kind: "unavailable",
|
|
6275
|
+
message: `Factory App broker exchange failed: ${error instanceof Error ? error.message : String(error)}\n`
|
|
6276
|
+
};
|
|
6277
|
+
}
|
|
6278
|
+
};
|
|
6279
|
+
/**
|
|
6280
|
+
* Resolve a Factory App installation token for App-owned GitHub writes.
|
|
6281
|
+
*
|
|
6282
|
+
* Order: caller token, `FACTORY_GITHUB_APP_TOKEN` (Actions), local PEM mint,
|
|
6283
|
+
* Cursor OIDC broker exchange. A Cursor-managed VM with no PEM must succeed
|
|
6284
|
+
* the exchange; missing mint or publication fails closed (#949).
|
|
6285
|
+
*/
|
|
6286
|
+
const resolveFactoryAppAccess = async (repository, dependencies = {}) => {
|
|
6287
|
+
const env = dependencies.env ?? process.env;
|
|
6288
|
+
if (dependencies.token) return {
|
|
6289
|
+
appId: FACTORY_PROOF_GATE_APP_ID,
|
|
6290
|
+
kind: "ok",
|
|
6291
|
+
token: dependencies.token
|
|
6292
|
+
};
|
|
6293
|
+
const workflowToken = env.FACTORY_GITHUB_APP_TOKEN;
|
|
6294
|
+
if (workflowToken !== void 0 && workflowToken.length > 0) return {
|
|
6295
|
+
appId: FACTORY_PROOF_GATE_APP_ID,
|
|
6296
|
+
kind: "ok",
|
|
6297
|
+
token: workflowToken
|
|
6298
|
+
};
|
|
6299
|
+
const config = dependencies.githubApp ?? tryUserConfig({ env })?.config.githubApp;
|
|
6300
|
+
const request = dependencies.fetch ?? fetch;
|
|
6301
|
+
const timeoutMs = dependencies.timeoutMs ?? 5e3;
|
|
6302
|
+
if (config) try {
|
|
6303
|
+
const token = await mintInstallationToken({
|
|
6304
|
+
credentials: config,
|
|
6305
|
+
owner: repository.owner,
|
|
6306
|
+
repo: repository.repo
|
|
6307
|
+
}, {
|
|
6308
|
+
fetch: request,
|
|
6309
|
+
now: dependencies.now ?? Date.now,
|
|
6310
|
+
timeoutMs,
|
|
6311
|
+
...dependencies.transportAttempts === void 0 ? {} : { transportAttempts: dependencies.transportAttempts }
|
|
6312
|
+
});
|
|
6313
|
+
return {
|
|
6314
|
+
appId: String(config.appId),
|
|
6315
|
+
kind: "ok",
|
|
6316
|
+
token
|
|
6317
|
+
};
|
|
6318
|
+
} catch (error) {
|
|
6319
|
+
return {
|
|
6320
|
+
failClosed: true,
|
|
6321
|
+
kind: "unavailable",
|
|
6322
|
+
message: `unable to mint Factory App token: ${error instanceof Error ? error.message : String(error)}\n`
|
|
6323
|
+
};
|
|
6324
|
+
}
|
|
6325
|
+
if (isCursorManagedVm(env, dependencies.existsSync)) return exchangeFromCursorVm(repository, dependencies, env, request, timeoutMs);
|
|
6326
|
+
return {
|
|
6327
|
+
failClosed: false,
|
|
6328
|
+
kind: "unavailable",
|
|
6329
|
+
message: NO_APP_SKIP_MESSAGE
|
|
6330
|
+
};
|
|
6331
|
+
};
|
|
6332
|
+
//#endregion
|
|
6070
6333
|
//#region src/github-app-client.ts
|
|
6071
6334
|
const readExchange = async (request, url, init, timeoutMs) => {
|
|
6072
6335
|
const response = await request(url, {
|
|
@@ -6159,14 +6422,10 @@ const OUTCOMES = ["aborted", "passed"];
|
|
|
6159
6422
|
const isOutcome = (value) => OUTCOMES.includes(value);
|
|
6160
6423
|
/**
|
|
6161
6424
|
* The payload's outcome must agree with the check run's own conclusion, which
|
|
6162
|
-
* comes from `verifyProofPassed`: a
|
|
6163
|
-
*
|
|
6164
|
-
* proof with no recorded outcome is unfinished (fail closed).
|
|
6425
|
+
* comes from `verifyProofPassed`: a proof with no recorded outcome is
|
|
6426
|
+
* unfinished (fail closed).
|
|
6165
6427
|
*/
|
|
6166
|
-
const outcomeFor = (value) =>
|
|
6167
|
-
if (isOutcome(value.outcome)) return value.outcome;
|
|
6168
|
-
return value.schemaVersion === 1 ? "passed" : "aborted";
|
|
6169
|
-
};
|
|
6428
|
+
const outcomeFor = (value) => isOutcome(value.outcome) ? value.outcome : "aborted";
|
|
6170
6429
|
const asClassification = (value) => DIFF_CLASSIFICATIONS.includes(value) ? value : "unknown";
|
|
6171
6430
|
/**
|
|
6172
6431
|
* Project a local pr:verify proof onto the durable check-run payload. Accepts
|
|
@@ -6394,7 +6653,7 @@ async function postFactoryCheckRun({ config, deadlineMs, dependencies, detailsUr
|
|
|
6394
6653
|
const request = dependencies.fetch ?? fetch;
|
|
6395
6654
|
const timeoutMs = dependencies.timeoutMs ?? 5e3;
|
|
6396
6655
|
try {
|
|
6397
|
-
const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(), {
|
|
6656
|
+
const token = dependencies.token ?? await installationToken(input, config, request, (dependencies.now ?? Date.now)(), {
|
|
6398
6657
|
timeoutMs,
|
|
6399
6658
|
transportAttempts: 1
|
|
6400
6659
|
});
|
|
@@ -6488,7 +6747,7 @@ async function resolveDetailsUrlSafely(input, dependencies, onDiagnostic) {
|
|
|
6488
6747
|
async function servedFactoryCheckRun(input, config, dependencies) {
|
|
6489
6748
|
const request = dependencies.fetch ?? fetch;
|
|
6490
6749
|
const timeoutMs = dependencies.timeoutMs ?? 5e3;
|
|
6491
|
-
const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(), { timeoutMs });
|
|
6750
|
+
const token = dependencies.token ?? await installationToken(input, config, request, (dependencies.now ?? Date.now)(), { timeoutMs });
|
|
6492
6751
|
const query = new URLSearchParams({
|
|
6493
6752
|
app_id: String(config.appId),
|
|
6494
6753
|
check_name: FACTORY_CHECK_NAMES[input.gate],
|
|
@@ -6621,10 +6880,12 @@ async function confirmPublishedCheckRun({ config, createdId, dependencies, input
|
|
|
6621
6880
|
* the head SHA visible on GitHub (pushed the branch, created the PR) can make
|
|
6622
6881
|
* the proof reliably present for that SHA before it returns (#247).
|
|
6623
6882
|
*
|
|
6624
|
-
* Never
|
|
6625
|
-
*
|
|
6626
|
-
*
|
|
6627
|
-
*
|
|
6883
|
+
* Never posts a commit-status fallback: the commit status is a human-readable
|
|
6884
|
+
* mirror, not a proof surface, so a caller that needs an App-verified check
|
|
6885
|
+
* run must be told plainly whether it got one. Returns `true` only when the
|
|
6886
|
+
* App-owned check run landed. Throws when Factory App access is fail-closed
|
|
6887
|
+
* (missing mint or Cursor OIDC exchange, #949): `pr:ready` must not notice
|
|
6888
|
+
* that miss and exit 0. An optional-App skip still returns `false`.
|
|
6628
6889
|
*
|
|
6629
6890
|
* "Landed" means GitHub serves it, not that the POST was accepted (#520). On
|
|
6630
6891
|
* PR #519 the POST was accepted, `pr:ready` reported ready and armed, and the
|
|
@@ -6650,18 +6911,28 @@ async function confirmPublishedCheckRun({ config, createdId, dependencies, input
|
|
|
6650
6911
|
*/
|
|
6651
6912
|
async function ensureFactoryCheckRunPublished(input, dependencies = {}) {
|
|
6652
6913
|
const { onDiagnostic } = dependencies;
|
|
6914
|
+
const checkName = FACTORY_CHECK_NAMES[input.gate];
|
|
6915
|
+
const access = await resolveFactoryAppAccess(input, dependencies);
|
|
6916
|
+
if (access.kind !== "ok") {
|
|
6917
|
+
onDiagnostic?.(`${checkName}: ${access.message}`);
|
|
6918
|
+
if (access.failClosed) throw new Error(`${checkName}: ${access.message.trim()}`);
|
|
6919
|
+
return false;
|
|
6920
|
+
}
|
|
6653
6921
|
try {
|
|
6654
|
-
const config = dependencies.githubApp ?? tryUserConfig()?.config.githubApp
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6922
|
+
const config = dependencies.githubApp ?? tryUserConfig()?.config.githubApp ?? {
|
|
6923
|
+
appId: access.appId,
|
|
6924
|
+
privateKeyPath: "factory-app-broker"
|
|
6925
|
+
};
|
|
6926
|
+
const tokenDependencies = {
|
|
6927
|
+
...dependencies,
|
|
6928
|
+
token: access.token
|
|
6929
|
+
};
|
|
6659
6930
|
const detailsUrl = await resolveDetailsUrlSafely(input, dependencies, onDiagnostic);
|
|
6660
6931
|
const created = await postFactoryCheckRun({
|
|
6661
6932
|
config,
|
|
6662
6933
|
dependencies: {
|
|
6663
6934
|
retry: DURABLE_CHECK_RUN_RETRY,
|
|
6664
|
-
...
|
|
6935
|
+
...tokenDependencies
|
|
6665
6936
|
},
|
|
6666
6937
|
detailsUrl,
|
|
6667
6938
|
input
|
|
@@ -6669,7 +6940,7 @@ async function ensureFactoryCheckRunPublished(input, dependencies = {}) {
|
|
|
6669
6940
|
return await confirmPublishedCheckRun({
|
|
6670
6941
|
config,
|
|
6671
6942
|
createdId: typeof created.id === "number" ? created.id : void 0,
|
|
6672
|
-
dependencies,
|
|
6943
|
+
dependencies: tokenDependencies,
|
|
6673
6944
|
input,
|
|
6674
6945
|
onDiagnostic
|
|
6675
6946
|
});
|
|
@@ -6708,15 +6979,26 @@ function createFactoryCheckPublisher(output, dependencies = {}) {
|
|
|
6708
6979
|
output.stderr.write(`${checkName}: unable to post fallback status: ${message}\n`);
|
|
6709
6980
|
}
|
|
6710
6981
|
};
|
|
6711
|
-
const
|
|
6712
|
-
|
|
6713
|
-
|
|
6982
|
+
const access = await resolveFactoryAppAccess(input, {
|
|
6983
|
+
...dependencies,
|
|
6984
|
+
transportAttempts: 1
|
|
6985
|
+
});
|
|
6986
|
+
if (access.kind !== "ok") {
|
|
6987
|
+
output.stderr.write(`${checkName}: ${access.message}`);
|
|
6988
|
+
if (!access.failClosed) postFallback();
|
|
6714
6989
|
return;
|
|
6715
6990
|
}
|
|
6991
|
+
const config = dependencies.githubApp ?? tryUserConfig()?.config.githubApp ?? {
|
|
6992
|
+
appId: access.appId,
|
|
6993
|
+
privateKeyPath: "factory-app-broker"
|
|
6994
|
+
};
|
|
6716
6995
|
try {
|
|
6717
6996
|
await postFactoryCheckRun({
|
|
6718
6997
|
config,
|
|
6719
|
-
dependencies
|
|
6998
|
+
dependencies: {
|
|
6999
|
+
...dependencies,
|
|
7000
|
+
token: access.token
|
|
7001
|
+
},
|
|
6720
7002
|
detailsUrl,
|
|
6721
7003
|
input
|
|
6722
7004
|
});
|
|
@@ -6733,6 +7015,77 @@ function createFactoryCheckPublisher(output, dependencies = {}) {
|
|
|
6733
7015
|
};
|
|
6734
7016
|
}
|
|
6735
7017
|
//#endregion
|
|
7018
|
+
//#region src/repository-profile-path.ts
|
|
7019
|
+
/**
|
|
7020
|
+
* `git rev-parse --show-toplevel` answers with a real path, while a caller's
|
|
7021
|
+
* `cwd` may reach the same directory through a symlink (every macOS temporary
|
|
7022
|
+
* directory does). Both sides are resolved before they are compared, so a
|
|
7023
|
+
* symlinked checkout is not mistaken for a profile outside the repository.
|
|
7024
|
+
*/
|
|
7025
|
+
const realPath = (value) => {
|
|
7026
|
+
try {
|
|
7027
|
+
return realpathSync(value);
|
|
7028
|
+
} catch {
|
|
7029
|
+
return value;
|
|
7030
|
+
}
|
|
7031
|
+
};
|
|
7032
|
+
/**
|
|
7033
|
+
* The repository top level for `cwd`, or `cwd` itself when it is not a git
|
|
7034
|
+
* checkout. Never throws: a caller rooting a path has a usable answer either
|
|
7035
|
+
* way, and the checkout-less case keeps the previous cwd-rooted behavior.
|
|
7036
|
+
*/
|
|
7037
|
+
const repositoryRootOrCwd = ({ cwd, readRepositoryRoot = repositoryRoot }) => {
|
|
7038
|
+
try {
|
|
7039
|
+
return readRepositoryRoot(cwd);
|
|
7040
|
+
} catch {
|
|
7041
|
+
return cwd;
|
|
7042
|
+
}
|
|
7043
|
+
};
|
|
7044
|
+
/**
|
|
7045
|
+
* The repository-relative, POSIX-separated path of `profilePath`.
|
|
7046
|
+
*
|
|
7047
|
+
* A cwd that is not a git checkout, and a profile the repository top level
|
|
7048
|
+
* does not contain, both fall back to the cwd-relative path: this function
|
|
7049
|
+
* makes a path more portable, and it must never invent one. Callers that
|
|
7050
|
+
* require the profile to be inside the trusted checkout keep enforcing that
|
|
7051
|
+
* themselves.
|
|
7052
|
+
*/
|
|
7053
|
+
const repositoryRelativeProfilePath = ({ cwd, profilePath, readRepositoryRoot = repositoryRoot }) => {
|
|
7054
|
+
const cwdRelative = path.relative(cwd, profilePath).split(path.sep).join("/");
|
|
7055
|
+
const root = repositoryRootOrCwd({
|
|
7056
|
+
cwd,
|
|
7057
|
+
readRepositoryRoot
|
|
7058
|
+
});
|
|
7059
|
+
const rootRelative = path.relative(realPath(root), realPath(profilePath)).split(path.sep).join("/");
|
|
7060
|
+
return rootRelative.length > 0 && !rootRelative.startsWith("../") ? rootRelative : cwdRelative;
|
|
7061
|
+
};
|
|
7062
|
+
/** A path that names no file inside the root it was measured from. */
|
|
7063
|
+
const escapesRoot = (relativePath) => relativePath.length === 0 || relativePath === ".." || relativePath.startsWith("../") || path.isAbsolute(relativePath);
|
|
7064
|
+
/**
|
|
7065
|
+
* The same path, for a caller that must REFUSE a profile the repository does
|
|
7066
|
+
* not contain.
|
|
7067
|
+
*
|
|
7068
|
+
* Containment is measured from the repository top level, not from `cwd`.
|
|
7069
|
+
* Measuring it from `cwd` refuses the repository-root profile whenever the
|
|
7070
|
+
* command runs in a package subdirectory — a profile that is plainly inside
|
|
7071
|
+
* the trusted checkout — and it refused before the conversion above could run
|
|
7072
|
+
* (cycle-1 review finding on PR #1004).
|
|
7073
|
+
*
|
|
7074
|
+
* Fail-closed is unchanged. A profile genuinely outside the repository root
|
|
7075
|
+
* falls back to the cwd-relative path, which still escapes, and throws here.
|
|
7076
|
+
* A cwd that is not a git checkout has no root to measure from and keeps the
|
|
7077
|
+
* previous cwd-relative rule exactly.
|
|
7078
|
+
*/
|
|
7079
|
+
const repositoryContainedProfilePath = ({ cwd, profilePath, readRepositoryRoot = repositoryRoot }) => {
|
|
7080
|
+
const relativePath = repositoryRelativeProfilePath({
|
|
7081
|
+
cwd,
|
|
7082
|
+
profilePath,
|
|
7083
|
+
readRepositoryRoot
|
|
7084
|
+
});
|
|
7085
|
+
if (escapesRoot(relativePath)) throw new Error("The readiness profile must be inside the trusted checkout.");
|
|
7086
|
+
return relativePath;
|
|
7087
|
+
};
|
|
7088
|
+
//#endregion
|
|
6736
7089
|
//#region src/pr-classification.ts
|
|
6737
7090
|
const MISSING_STAMP_REASON = "impact stamp is missing; fail closed to non-trivial";
|
|
6738
7091
|
function ownershipFromStamp(stamp, file) {
|
|
@@ -6758,7 +7111,10 @@ function reasonForOwnership(file, ownership) {
|
|
|
6758
7111
|
}
|
|
6759
7112
|
}
|
|
6760
7113
|
function relativeProfilePath(options) {
|
|
6761
|
-
return
|
|
7114
|
+
return repositoryRelativeProfilePath({
|
|
7115
|
+
cwd: options.cwd,
|
|
7116
|
+
profilePath: options.profilePath
|
|
7117
|
+
});
|
|
6762
7118
|
}
|
|
6763
7119
|
function applyProfileEditGuard(files, relativeProfile, profilePath, base) {
|
|
6764
7120
|
if (!files.some((file) => file === relativeProfile || file === profilePath)) return base;
|
|
@@ -6990,9 +7346,7 @@ const planVerificationBattery = ({ commands, stamp, vetoedTargets }) => {
|
|
|
6990
7346
|
* skip this epic forbids.
|
|
6991
7347
|
*
|
|
6992
7348
|
* Rule 4 is scoped to `outcome: "passed"` because an ABORTED proof legitimately
|
|
6993
|
-
* records partial execution — the run stopped at the failing command.
|
|
6994
|
-
* also scoped to schemaVersion >= 4: `executedCommands` postdates v1–v3, so a
|
|
6995
|
-
* legacy proof that never recorded it is not making a false completeness claim.
|
|
7349
|
+
* records partial execution — the run stopped at the failing command.
|
|
6996
7350
|
*
|
|
6997
7351
|
* The check is one-directional on purpose. Every SELECTED command must be
|
|
6998
7352
|
* accounted for, but `executedCommands` may legitimately carry MORE than the
|
|
@@ -7035,7 +7389,7 @@ const assertBatteryCompleteness = (proof, context) => {
|
|
|
7035
7389
|
});
|
|
7036
7390
|
seen.add(entry.name);
|
|
7037
7391
|
}
|
|
7038
|
-
if (proof.outcome !== "passed"
|
|
7392
|
+
if (proof.outcome !== "passed") return;
|
|
7039
7393
|
const disposed = new Set([...executed, ...seen]);
|
|
7040
7394
|
const undisposed = selected.filter((name) => !disposed.has(name));
|
|
7041
7395
|
if (undisposed.length > 0) context.addIssue({
|
|
@@ -7045,7 +7399,7 @@ const assertBatteryCompleteness = (proof, context) => {
|
|
|
7045
7399
|
});
|
|
7046
7400
|
};
|
|
7047
7401
|
/**
|
|
7048
|
-
* The stamp-authorization control: a passed
|
|
7402
|
+
* The stamp-authorization control: a passed proof may only claim a command
|
|
7049
7403
|
* was NOT REQUIRED if its own recorded stamp says so.
|
|
7050
7404
|
*
|
|
7051
7405
|
* `assertBatteryCompleteness` proves the two lists partition the selected
|
|
@@ -7068,8 +7422,8 @@ const assertBatteryCompleteness = (proof, context) => {
|
|
|
7068
7422
|
* provably released.
|
|
7069
7423
|
*
|
|
7070
7424
|
* The proof is one artifact and a determined forger controls all of it; this
|
|
7071
|
-
* is internal-coherence belt-and-braces in the same trust domain as the
|
|
7072
|
-
*
|
|
7425
|
+
* is internal-coherence belt-and-braces in the same trust domain as the rest
|
|
7426
|
+
* of the proof, and write-time truth stays `pr:verify`'s job.
|
|
7073
7427
|
*
|
|
7074
7428
|
* The authorizing evidence is the proof's OWN `impactStamp` — the same stamp
|
|
7075
7429
|
* `pr:verify` computed for this candidate's identity triple and recorded here,
|
|
@@ -7082,14 +7436,13 @@ const assertBatteryCompleteness = (proof, context) => {
|
|
|
7082
7436
|
* self-contradictory stamp, an unclassified name, or an `affected` verdict all
|
|
7083
7437
|
* fail the same way scoping itself would.
|
|
7084
7438
|
*
|
|
7085
|
-
* Scoped to `outcome: "passed"`
|
|
7086
|
-
*
|
|
7087
|
-
* legacy proof predates both fields, so neither is making a false completeness
|
|
7439
|
+
* Scoped to `outcome: "passed"` for the same reason as the completeness rule:
|
|
7440
|
+
* an aborted run records partial execution, so it makes no false completeness
|
|
7088
7441
|
* claim.
|
|
7089
7442
|
*/
|
|
7090
7443
|
const assertNotRequiredStampAuthorization = (proof, context) => {
|
|
7091
7444
|
const notRequired = proof.notRequiredCommands ?? [];
|
|
7092
|
-
if (proof.outcome !== "passed" ||
|
|
7445
|
+
if (proof.outcome !== "passed" || notRequired.length === 0) return;
|
|
7093
7446
|
const { impactStamp } = proof;
|
|
7094
7447
|
if (impactStamp === void 0) {
|
|
7095
7448
|
context.addIssue({
|
|
@@ -7138,36 +7491,6 @@ const batteryScopeSummaryLines = (plan) => {
|
|
|
7138
7491
|
//#endregion
|
|
7139
7492
|
//#region src/pr-verify-proof-rules.ts
|
|
7140
7493
|
const assertPrVerifyProofRules = (proof, context) => {
|
|
7141
|
-
if (proof.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
|
|
7142
|
-
code: "custom",
|
|
7143
|
-
message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
|
|
7144
|
-
path: ["authoringSession"]
|
|
7145
|
-
});
|
|
7146
|
-
if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
|
|
7147
|
-
code: "custom",
|
|
7148
|
-
message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
7149
|
-
path: ["authoringSession"]
|
|
7150
|
-
});
|
|
7151
|
-
if (proof.schemaVersion < 4 && proof.impactStamp !== void 0) context.addIssue({
|
|
7152
|
-
code: "custom",
|
|
7153
|
-
message: "impactStamp is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
7154
|
-
path: ["impactStamp"]
|
|
7155
|
-
});
|
|
7156
|
-
if (proof.schemaVersion < 4 && proof.notRequiredCommands !== void 0) context.addIssue({
|
|
7157
|
-
code: "custom",
|
|
7158
|
-
message: "notRequiredCommands is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
7159
|
-
path: ["notRequiredCommands"]
|
|
7160
|
-
});
|
|
7161
|
-
if (proof.schemaVersion < 4 && proof.catchUpRecognition !== void 0) context.addIssue({
|
|
7162
|
-
code: "custom",
|
|
7163
|
-
message: "catchUpRecognition is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
7164
|
-
path: ["catchUpRecognition"]
|
|
7165
|
-
});
|
|
7166
|
-
if (proof.schemaVersion < 4 && proof.notDemandedRecords !== void 0) context.addIssue({
|
|
7167
|
-
code: "custom",
|
|
7168
|
-
message: "notDemandedRecords is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
7169
|
-
path: ["notDemandedRecords"]
|
|
7170
|
-
});
|
|
7171
7494
|
if (proof.notDemandedRecords !== void 0 && proof.impactStamp === void 0) context.addIssue({
|
|
7172
7495
|
code: "custom",
|
|
7173
7496
|
message: "notDemandedRecords requires impactStamp; package-level not-demanded records are bound to the stamp identity.",
|
|
@@ -7176,14 +7499,15 @@ const assertPrVerifyProofRules = (proof, context) => {
|
|
|
7176
7499
|
assertBatteryCompleteness(proof, context);
|
|
7177
7500
|
assertNotRequiredStampAuthorization(proof, context);
|
|
7178
7501
|
};
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7502
|
+
/**
|
|
7503
|
+
* The pr:verify proof versions a reader accepts: the current one only (#920
|
|
7504
|
+
* item 1). #917 narrowed the pr:ready reader the same way. A pr:verify proof
|
|
7505
|
+
* is per-candidate and short-lived — it is rewritten on every head — so no
|
|
7506
|
+
* stored v1–v3 proof can outlive the release that drops it. Pre-1.0 posture
|
|
7507
|
+
* applies: one current contract, no compat reader. A consumer on an older
|
|
7508
|
+
* factory must adopt this release before HQ ingests its pr:verify proofs.
|
|
7509
|
+
*/
|
|
7510
|
+
const SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS = [4];
|
|
7187
7511
|
const executedCommandSchema = z.object({
|
|
7188
7512
|
command: z.string().min(1),
|
|
7189
7513
|
counts: z.object({
|
|
@@ -7194,7 +7518,6 @@ const executedCommandSchema = z.object({
|
|
|
7194
7518
|
exitCode: z.number(),
|
|
7195
7519
|
name: z.string().min(1),
|
|
7196
7520
|
scope: z.enum([
|
|
7197
|
-
"always",
|
|
7198
7521
|
"docs-only",
|
|
7199
7522
|
"trivial",
|
|
7200
7523
|
"full"
|
|
@@ -7209,9 +7532,9 @@ const notDemandedRecordSchema = z.object({
|
|
|
7209
7532
|
basis: z.string().min(1),
|
|
7210
7533
|
name: z.string().min(1)
|
|
7211
7534
|
});
|
|
7212
|
-
const prVerifySchemaVersionSchema = z.
|
|
7535
|
+
const prVerifySchemaVersionSchema = z.literal(4);
|
|
7213
7536
|
const prVerifyProofSchema = z.object({
|
|
7214
|
-
authoringSession: z.string().trim().min(1)
|
|
7537
|
+
authoringSession: z.string().trim().min(1),
|
|
7215
7538
|
base: z.string().min(1),
|
|
7216
7539
|
baselineFullProofs: z.array(z.object({
|
|
7217
7540
|
base: z.string().min(1),
|
|
@@ -7257,7 +7580,6 @@ const prVerifyProofSchema = z.object({
|
|
|
7257
7580
|
impactTarget: z.string().min(1).optional(),
|
|
7258
7581
|
name: z.string().min(1),
|
|
7259
7582
|
scope: z.enum([
|
|
7260
|
-
"always",
|
|
7261
7583
|
"docs-only",
|
|
7262
7584
|
"trivial",
|
|
7263
7585
|
"full"
|
|
@@ -7288,7 +7610,7 @@ var verification_proof_exports = /* @__PURE__ */ __exportAll({
|
|
|
7288
7610
|
function readPrVerifyProof(filePath) {
|
|
7289
7611
|
return readProofFromPath(validatePrVerifyProof, filePath);
|
|
7290
7612
|
}
|
|
7291
|
-
const verifyProofPassed = (proof) => proof.outcome === "passed"
|
|
7613
|
+
const verifyProofPassed = (proof) => proof.outcome === "passed";
|
|
7292
7614
|
const prVerifyProofHeadShas = (proof, creditedHeadSha = proof?.headSha) => {
|
|
7293
7615
|
if (!proof || !verifyProofPassed(proof)) return {
|
|
7294
7616
|
docsOnlyVerifiedHeadShas: [],
|
|
@@ -7317,7 +7639,7 @@ const verifyProofHeadMatches = ({ headSha, proof }) => Boolean(proof && evidence
|
|
|
7317
7639
|
recorded: { headSha: proof.headSha },
|
|
7318
7640
|
rule: "patch-id-or-head-sha"
|
|
7319
7641
|
}));
|
|
7320
|
-
const verifyProofIdentityMatches = ({ currentIdentity, proof }) => Boolean(proof && verifyProofPassed(proof) &&
|
|
7642
|
+
const verifyProofIdentityMatches = ({ currentIdentity, proof }) => Boolean(proof && verifyProofPassed(proof) && evidenceFresh({
|
|
7321
7643
|
candidate: { patchId: currentIdentity?.patchId },
|
|
7322
7644
|
recorded: { patchId: proof.patchId },
|
|
7323
7645
|
rule: "patch-id"
|
|
@@ -7336,7 +7658,6 @@ const verifyProofIdentityMatches = ({ currentIdentity, proof }) => Boolean(proof
|
|
|
7336
7658
|
*/
|
|
7337
7659
|
const trustedImpactStamp = ({ candidate, proof }) => {
|
|
7338
7660
|
if (!proof || !verifyProofPassed(proof)) return;
|
|
7339
|
-
if (proof.schemaVersion < 4) return;
|
|
7340
7661
|
const headBound = sameHeadSha(candidate.headSha, proof.headSha);
|
|
7341
7662
|
const patchBound = evidenceFresh({
|
|
7342
7663
|
candidate: { patchId: candidate.patchId },
|
|
@@ -7348,13 +7669,13 @@ const trustedImpactStamp = ({ candidate, proof }) => {
|
|
|
7348
7669
|
/**
|
|
7349
7670
|
* #833: the recorded catch-up recognition, consumed ONLY when it is
|
|
7350
7671
|
* identity-bound to the current candidate under exactly the rules
|
|
7351
|
-
* `trustedImpactStamp` applies — the proof passed,
|
|
7672
|
+
* `trustedImpactStamp` applies — the proof passed, and its recorded
|
|
7352
7673
|
* headSha AND patchId both name this candidate. The recognition withdraws
|
|
7353
7674
|
* review and preview demands, so it earns the same strict double binding and
|
|
7354
7675
|
* the same fail-closed answer (`undefined`) on every other state.
|
|
7355
7676
|
*/
|
|
7356
7677
|
const trustedCatchUpRecognition = ({ candidate, proof }) => {
|
|
7357
|
-
if (!proof || !verifyProofPassed(proof)
|
|
7678
|
+
if (!proof || !verifyProofPassed(proof)) return;
|
|
7358
7679
|
const headBound = sameHeadSha(candidate.headSha, proof.headSha);
|
|
7359
7680
|
const patchBound = evidenceFresh({
|
|
7360
7681
|
candidate: { patchId: candidate.patchId },
|
|
@@ -7693,13 +8014,12 @@ const assertCleanInstallResolution = (repoRoot, dependencies = {}) => {
|
|
|
7693
8014
|
};
|
|
7694
8015
|
//#endregion
|
|
7695
8016
|
//#region src/pr-verify.ts
|
|
7696
|
-
const PR_VERIFY_SCHEMA_VERSION = 4;
|
|
7697
8017
|
const DEFAULT_PR_VERIFY_PROOF_PATH = ".factory-memory/pr-verify.json";
|
|
7698
8018
|
const PR_VERIFY_PROOF_DESCRIPTOR$1 = {
|
|
7699
8019
|
defaultPath: DEFAULT_PR_VERIFY_PROOF_PATH,
|
|
7700
8020
|
label: "pr:verify proof",
|
|
7701
8021
|
parse: validatePrVerifyProof,
|
|
7702
|
-
schemaVersion:
|
|
8022
|
+
schemaVersion: 4
|
|
7703
8023
|
};
|
|
7704
8024
|
const VERIFY_SCOPE_RANK = {
|
|
7705
8025
|
"docs-only": 0,
|
|
@@ -7786,7 +8106,7 @@ function buildPrVerifyProof({ authoringSession, base, baselineFullProofs, catchU
|
|
|
7786
8106
|
profilePath,
|
|
7787
8107
|
projectKey,
|
|
7788
8108
|
repository,
|
|
7789
|
-
schemaVersion:
|
|
8109
|
+
schemaVersion: 4,
|
|
7790
8110
|
startedAt: startedAt.toISOString(),
|
|
7791
8111
|
verificationCommands: commands
|
|
7792
8112
|
};
|
|
@@ -7845,7 +8165,10 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
7845
8165
|
profilePath: args.profilePath
|
|
7846
8166
|
});
|
|
7847
8167
|
const files = git.changedFiles(cwd, args.base);
|
|
7848
|
-
const relativeProfile =
|
|
8168
|
+
const relativeProfile = repositoryRelativeProfilePath({
|
|
8169
|
+
cwd,
|
|
8170
|
+
profilePath
|
|
8171
|
+
});
|
|
7849
8172
|
const { headSha, mergeBaseSha: proofMergeBaseSha, patchId } = resolveCandidateIdentity({
|
|
7850
8173
|
base: args.base,
|
|
7851
8174
|
cwd,
|
|
@@ -7863,7 +8186,7 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
7863
8186
|
changedFiles: files,
|
|
7864
8187
|
profile,
|
|
7865
8188
|
profilePath: relativeProfile,
|
|
7866
|
-
readLockfile: (side) => readLockfileAtRef(cwd, side === "base" ? proofMergeBaseSha : headSha,
|
|
8189
|
+
readLockfile: (side) => readLockfileAtRef(cwd, side === "base" ? proofMergeBaseSha : headSha, LOCKFILE_PATH)
|
|
7867
8190
|
});
|
|
7868
8191
|
} catch (error) {
|
|
7869
8192
|
if (error instanceof ImpactStampConfigError) throw error;
|
|
@@ -10415,35 +10738,38 @@ const writeGithubOutputs$1 = (outputPath, decision) => {
|
|
|
10415
10738
|
appendFileSync(outputPath, `${lines.join("\n")}\n`, "utf-8");
|
|
10416
10739
|
};
|
|
10417
10740
|
/**
|
|
10418
|
-
*
|
|
10419
|
-
* the merge-base-to-head diff, and the lockfile sides
|
|
10420
|
-
* merge base and head — the same inputs `pr:verify`
|
|
10421
|
-
* for the same candidate commits.
|
|
10422
|
-
*
|
|
10423
|
-
*
|
|
10741
|
+
* The candidate impact classifier: one exact base/head identity in, one impact
|
|
10742
|
+
* stamp out. The delta is the merge-base-to-head diff, and the lockfile sides
|
|
10743
|
+
* are read at exactly the merge base and head — the same inputs `pr:verify`
|
|
10744
|
+
* gives the impact stamp for the same candidate commits.
|
|
10745
|
+
*
|
|
10746
|
+
* It is the authority on which required checks a candidate owes (#963), so
|
|
10747
|
+
* every surface that states that demand calls this rather than matching paths
|
|
10748
|
+
* itself. Every doubt path refuses with its reason; a refusal never withdraws
|
|
10749
|
+
* anything.
|
|
10424
10750
|
*/
|
|
10425
|
-
const
|
|
10751
|
+
const classifyCandidateImpact = (args) => {
|
|
10426
10752
|
const cwd = path.resolve(args.cwd ?? process.cwd());
|
|
10427
10753
|
let profile;
|
|
10428
|
-
let profilePath;
|
|
10429
10754
|
let relativeProfile;
|
|
10430
|
-
let decision;
|
|
10431
10755
|
try {
|
|
10432
|
-
const { path:
|
|
10756
|
+
const { path: profilePath, profile: loadedProfile } = loadProjectProfile({
|
|
10433
10757
|
cwd,
|
|
10434
10758
|
profilePath: args.profilePath
|
|
10435
10759
|
});
|
|
10436
10760
|
profile = loadedProfile;
|
|
10437
|
-
|
|
10438
|
-
|
|
10761
|
+
relativeProfile = repositoryRelativeProfilePath({
|
|
10762
|
+
cwd,
|
|
10763
|
+
profilePath
|
|
10764
|
+
});
|
|
10439
10765
|
const base = assertCommitIdentity$1(args.base, "base");
|
|
10440
10766
|
const head = assertCommitIdentity$1(args.head, "head");
|
|
10441
10767
|
assertReachableCommit$1(cwd, base, "base");
|
|
10442
10768
|
assertReachableCommit$1(cwd, head, "head");
|
|
10443
10769
|
const checkedOutHead = runCapture("git", ["rev-parse", "HEAD"], cwd).stdout.trim().toLowerCase();
|
|
10444
10770
|
if (checkedOutHead !== head) throw new Error(`head commit ${head} is not the checked-out HEAD ${checkedOutHead}; refusing to mix commit identities with declarations from another tree`);
|
|
10445
|
-
const relativeProfilePath =
|
|
10446
|
-
const committedProfile = showFileAtRef(cwd, head,
|
|
10771
|
+
const relativeProfilePath = relativeProfile;
|
|
10772
|
+
const committedProfile = showFileAtRef(cwd, head, relativeProfile);
|
|
10447
10773
|
if (committedProfile === void 0) throw new Error(`profile ${relativeProfilePath} is not readable at head commit ${head}; refusing to classify from declarations the head does not carry`);
|
|
10448
10774
|
if (canonicalJson(committedProfile) === void 0 || canonicalJson(committedProfile) !== canonicalJson(readFileSync(profilePath, "utf-8"))) throw new Error(`profile ${relativeProfilePath} on disk differs from head commit ${head}; refusing to classify from declarations the head does not carry`);
|
|
10449
10775
|
const mergeBase = runCapture("git", [
|
|
@@ -10462,12 +10788,43 @@ const runCandidateImpact = (args) => {
|
|
|
10462
10788
|
const stamp = computeImpactStamp({
|
|
10463
10789
|
changedFiles,
|
|
10464
10790
|
profile,
|
|
10465
|
-
profilePath:
|
|
10466
|
-
readLockfile: (side) => showFileAtRef(cwd, side === "base" ? mergeBase : head,
|
|
10791
|
+
profilePath: relativeProfile,
|
|
10792
|
+
readLockfile: (side) => showFileAtRef(cwd, side === "base" ? mergeBase : head, LOCKFILE_PATH)
|
|
10467
10793
|
});
|
|
10468
|
-
|
|
10794
|
+
return {
|
|
10469
10795
|
baseSha: base,
|
|
10796
|
+
changedFiles,
|
|
10470
10797
|
headSha: head,
|
|
10798
|
+
kind: "classified",
|
|
10799
|
+
mergeBaseSha: mergeBase,
|
|
10800
|
+
profile,
|
|
10801
|
+
relativeProfilePath: relativeProfile,
|
|
10802
|
+
stamp
|
|
10803
|
+
};
|
|
10804
|
+
} catch (error) {
|
|
10805
|
+
return {
|
|
10806
|
+
kind: "refused",
|
|
10807
|
+
...profile ? { profile } : {},
|
|
10808
|
+
reason: `candidate impact classification refused: ${error instanceof Error ? error.message : String(error)}`,
|
|
10809
|
+
...relativeProfile ? { relativeProfilePath: relativeProfile } : {}
|
|
10810
|
+
};
|
|
10811
|
+
}
|
|
10812
|
+
};
|
|
10813
|
+
/**
|
|
10814
|
+
* Classify candidate impact from one exact base/head identity, as the routing
|
|
10815
|
+
* decision the hosted plane consumes. The result may withdraw hosted routing
|
|
10816
|
+
* work only; every doubt path returns a refused decision whose known targets
|
|
10817
|
+
* remain demanded.
|
|
10818
|
+
*/
|
|
10819
|
+
const runCandidateImpact = (args) => {
|
|
10820
|
+
const cwd = path.resolve(args.cwd ?? process.cwd());
|
|
10821
|
+
const classification = classifyCandidateImpact(args);
|
|
10822
|
+
let decision;
|
|
10823
|
+
if (classification.kind === "classified") {
|
|
10824
|
+
const { baseSha, changedFiles, headSha, mergeBaseSha: mergeBase, stamp } = classification;
|
|
10825
|
+
const core = {
|
|
10826
|
+
baseSha,
|
|
10827
|
+
headSha,
|
|
10471
10828
|
inertPaths: stamp.inertPaths,
|
|
10472
10829
|
mergeBaseSha: mergeBase,
|
|
10473
10830
|
schemaVersion: CANDIDATE_IMPACT_DECISION_SCHEMA_VERSION,
|
|
@@ -10500,16 +10857,15 @@ const runCandidateImpact = (args) => {
|
|
|
10500
10857
|
state: "demanded"
|
|
10501
10858
|
}))
|
|
10502
10859
|
};
|
|
10503
|
-
}
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
});
|
|
10510
|
-
}
|
|
10860
|
+
} else decision = refusal$1({
|
|
10861
|
+
base: args.base,
|
|
10862
|
+
head: args.head,
|
|
10863
|
+
...classification.profile ? { profile: classification.profile } : {},
|
|
10864
|
+
reason: classification.reason
|
|
10865
|
+
});
|
|
10511
10866
|
if (args.emitRunContext) {
|
|
10512
10867
|
const fullSuiteBaseRef = emptyTreeHash(cwd);
|
|
10868
|
+
const relativeProfile = classification.relativeProfilePath;
|
|
10513
10869
|
const { env } = createVerificationRunContext({
|
|
10514
10870
|
baseRef: decision.mergeBaseSha ?? fullSuiteBaseRef,
|
|
10515
10871
|
changedFiles: decision.basis.changedFiles,
|
|
@@ -13067,6 +13423,60 @@ const reviewProofCheck = ({ candidate, candidateError, proof, reviewModes }) =>
|
|
|
13067
13423
|
status: accepted ? "ok" : "warning"
|
|
13068
13424
|
};
|
|
13069
13425
|
};
|
|
13426
|
+
/**
|
|
13427
|
+
* Resolve the demand this candidate actually owes.
|
|
13428
|
+
*
|
|
13429
|
+
* The `pr:verify` stamp wins when one binds this candidate: readiness admits
|
|
13430
|
+
* on that stamp and nothing else, so the checklist must not tell a different
|
|
13431
|
+
* story about an already-verified candidate. Before `pr:verify` has stamped
|
|
13432
|
+
* anything there is no trusted stamp to read, and that is exactly the state
|
|
13433
|
+
* #963 reports from — so preflight runs the classifier itself. Preflight
|
|
13434
|
+
* never gates, so a stamp it computed can only inform; it never becomes
|
|
13435
|
+
* evidence, and readiness still requires the verified one.
|
|
13436
|
+
*/
|
|
13437
|
+
const resolveRequiredCheckDemand = ({ candidate, cwd, profile, profilePath, verifyProof }) => {
|
|
13438
|
+
const stamped = trustedImpactStamp({
|
|
13439
|
+
candidate: {
|
|
13440
|
+
headSha: candidate.headSha,
|
|
13441
|
+
patchId: candidate.patchId
|
|
13442
|
+
},
|
|
13443
|
+
proof: verifyProof
|
|
13444
|
+
});
|
|
13445
|
+
if (stamped) return {
|
|
13446
|
+
basis: `the pr:verify proof's impact stamp for this candidate — ${impactStampSummaryLines(stamped)[0]}`,
|
|
13447
|
+
kind: "resolved",
|
|
13448
|
+
stamp: stamped
|
|
13449
|
+
};
|
|
13450
|
+
if ((profile.impact?.targets ?? []).length === 0) return {
|
|
13451
|
+
basis: "the profile declares no impact targets, so every required check is unconditional",
|
|
13452
|
+
kind: "resolved"
|
|
13453
|
+
};
|
|
13454
|
+
const classification = classifyCandidateImpact({
|
|
13455
|
+
base: candidate.mergeBaseSha,
|
|
13456
|
+
cwd,
|
|
13457
|
+
head: candidate.headSha,
|
|
13458
|
+
profilePath
|
|
13459
|
+
});
|
|
13460
|
+
if (classification.kind === "refused") return {
|
|
13461
|
+
kind: "unresolved",
|
|
13462
|
+
reason: classification.reason
|
|
13463
|
+
};
|
|
13464
|
+
return {
|
|
13465
|
+
basis: `candidate:impact over ${candidate.mergeBaseSha}..${candidate.headSha} — ${impactStampSummaryLines(classification.stamp)[0]}`,
|
|
13466
|
+
kind: "resolved",
|
|
13467
|
+
stamp: classification.stamp
|
|
13468
|
+
};
|
|
13469
|
+
};
|
|
13470
|
+
/** The row that names the authority on what this candidate owes (#963). */
|
|
13471
|
+
const requiredCheckDemandCheck = (resolution) => resolution.kind === "resolved" ? {
|
|
13472
|
+
message: `Required-check demand is resolved against this candidate by ${resolution.basis}. Rows below state their own resolution; a check reported as not demanded is not owed on this candidate.`,
|
|
13473
|
+
name: "admission:required-check-demand",
|
|
13474
|
+
status: "ok"
|
|
13475
|
+
} : {
|
|
13476
|
+
message: `Required-check demand is NOT resolvable for this candidate (${resolution.reason}), so every declared required check below is shown in force (fail closed). Resolve the refusal, or read candidate:impact directly, before treating the rows below as the demand set.`,
|
|
13477
|
+
name: "admission:required-check-demand",
|
|
13478
|
+
status: "warning"
|
|
13479
|
+
};
|
|
13070
13480
|
const requiredCheckMessage = (outcome) => {
|
|
13071
13481
|
if (outcome.status === "out-of-scope") return `Not demanded for this candidate: ${outcome.scopeReason}.`;
|
|
13072
13482
|
if (outcome.status === "satisfied") return `Satisfied by a ${outcome.checkType}-type evidence envelope${outcome.matchedPath ? ` (${outcome.matchedPath})` : ""}.`;
|
|
@@ -13078,7 +13488,7 @@ const requiredCheckMessage = (outcome) => {
|
|
|
13078
13488
|
* readiness's own `evaluateRequiredChecks`, so scoping, freshness and
|
|
13079
13489
|
* independence stay exactly one implementation.
|
|
13080
13490
|
*/
|
|
13081
|
-
const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyProof }) => {
|
|
13491
|
+
const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, profilePath, verifyProof }) => {
|
|
13082
13492
|
const requiredChecks = resolveProfileRequiredChecks(profile);
|
|
13083
13493
|
if (requiredChecks.length === 0) return [{
|
|
13084
13494
|
message: "Profile declares no requiredChecks; no external evidence envelope (including a preview-deploy proof) is demanded.",
|
|
@@ -13090,13 +13500,14 @@ const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyPr
|
|
|
13090
13500
|
name: "admission:required-checks",
|
|
13091
13501
|
status: "warning"
|
|
13092
13502
|
}];
|
|
13093
|
-
const
|
|
13094
|
-
candidate
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13503
|
+
const demand = resolveRequiredCheckDemand({
|
|
13504
|
+
candidate,
|
|
13505
|
+
cwd,
|
|
13506
|
+
profile,
|
|
13507
|
+
profilePath,
|
|
13508
|
+
verifyProof
|
|
13099
13509
|
});
|
|
13510
|
+
const impactStamp = demand.kind === "resolved" ? demand.stamp : void 0;
|
|
13100
13511
|
const { outcomes } = evaluateRequiredChecks({
|
|
13101
13512
|
authoringSessionIds: resolveAuthoringSessionIds({ recorded: verifyProof?.authoringSession }),
|
|
13102
13513
|
candidate: {
|
|
@@ -13111,11 +13522,11 @@ const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyPr
|
|
|
13111
13522
|
...impactStamp ? { impactStamp } : {}
|
|
13112
13523
|
}
|
|
13113
13524
|
});
|
|
13114
|
-
return outcomes.map((outcome) => ({
|
|
13525
|
+
return [requiredCheckDemandCheck(demand), ...outcomes.map((outcome) => ({
|
|
13115
13526
|
message: requiredCheckMessage(outcome),
|
|
13116
13527
|
name: `admission:required-check:${outcome.name}`,
|
|
13117
13528
|
status: outcome.status === "unmet" ? "warning" : "ok"
|
|
13118
|
-
}));
|
|
13529
|
+
}))];
|
|
13119
13530
|
};
|
|
13120
13531
|
/**
|
|
13121
13532
|
* The full admission checklist for the current repository and candidate, in the
|
|
@@ -13124,7 +13535,7 @@ const requiredCheckChecks = ({ candidate, candidateError, cwd, profile, verifyPr
|
|
|
13124
13535
|
* requirement is reported by the doctor `github-app` check, which states the
|
|
13125
13536
|
* consequence of its absence.
|
|
13126
13537
|
*/
|
|
13127
|
-
const admissionPreflightChecks = ({ base = "origin/main", cwd, env = process.env, hqIngestCredentials, profile, reviewProofPath = DEFAULT_PR_REVIEW_PROOF_PATH, verifyProofPath = DEFAULT_PR_VERIFY_PROOF_PATH }) => {
|
|
13538
|
+
const admissionPreflightChecks = ({ base = "origin/main", cwd, env = process.env, hqIngestCredentials, profile, profilePath, reviewProofPath = DEFAULT_PR_REVIEW_PROOF_PATH, verifyProofPath = DEFAULT_PR_VERIFY_PROOF_PATH }) => {
|
|
13128
13539
|
const resolvedVerifyProofPath = path.resolve(cwd, verifyProofPath);
|
|
13129
13540
|
const preread = tryReadProof(readPrVerifyProof, resolvedVerifyProofPath);
|
|
13130
13541
|
const resolved = resolveCandidate({
|
|
@@ -13154,6 +13565,7 @@ const admissionPreflightChecks = ({ base = "origin/main", cwd, env = process.env
|
|
|
13154
13565
|
candidateError,
|
|
13155
13566
|
cwd,
|
|
13156
13567
|
profile,
|
|
13568
|
+
profilePath,
|
|
13157
13569
|
verifyProof: preread.proof
|
|
13158
13570
|
}),
|
|
13159
13571
|
hqIngestCredentialCheck({
|
|
@@ -13239,6 +13651,7 @@ async function doctorProjectProfile(input = {}) {
|
|
|
13239
13651
|
}, input.hqSpoolDependencies);
|
|
13240
13652
|
const checks = [
|
|
13241
13653
|
...buildProfileChecks(profile, cwd, {
|
|
13654
|
+
env,
|
|
13242
13655
|
userConfig: userConfig.loaded?.config,
|
|
13243
13656
|
userConfigCheck: userConfig.check
|
|
13244
13657
|
}),
|
|
@@ -13317,17 +13730,24 @@ function buildProfileChecks(profile, cwd, userConfigInput) {
|
|
|
13317
13730
|
},
|
|
13318
13731
|
reviewLadderCheck(),
|
|
13319
13732
|
userConfigInput.userConfigCheck,
|
|
13320
|
-
githubAppCheck(userConfigInput.userConfig),
|
|
13733
|
+
githubAppCheck(userConfigInput.userConfig, userConfigInput.env),
|
|
13321
13734
|
gitRemoteCheck(profile, cwd)
|
|
13322
13735
|
];
|
|
13323
13736
|
}
|
|
13324
|
-
function githubAppCheck(userConfig) {
|
|
13737
|
+
function githubAppCheck(userConfig, env) {
|
|
13325
13738
|
const app = userConfig?.githubApp;
|
|
13326
|
-
if (!app)
|
|
13327
|
-
|
|
13328
|
-
|
|
13329
|
-
|
|
13330
|
-
|
|
13739
|
+
if (!app) {
|
|
13740
|
+
if (env.CURSOR_AGENT_SOCKET !== void 0 && env.CURSOR_AGENT_SOCKET.length > 0) return {
|
|
13741
|
+
message: "Patronage Factory GitHub App PEM is not configured; this Cursor-managed VM publishes patronage-factory/pr-ready through the Factory App broker (Cursor OIDC, audience Factory). Missing exchange fails closed.",
|
|
13742
|
+
name: "github-app",
|
|
13743
|
+
status: "warning"
|
|
13744
|
+
};
|
|
13745
|
+
return {
|
|
13746
|
+
message: "Patronage Factory GitHub App credentials are not configured; gate publishing will use user-token commit statuses with Details links. Proof bindings cannot publish, so hosted required checks cannot be satisfied by local proof and must go green by their own means.",
|
|
13747
|
+
name: "github-app",
|
|
13748
|
+
status: "warning"
|
|
13749
|
+
};
|
|
13750
|
+
}
|
|
13331
13751
|
if (!existsSync(app.privateKeyPath)) return {
|
|
13332
13752
|
message: `Patronage Factory GitHub App id ${app.appId} is configured, but private key ${app.privateKeyPath} was not found.`,
|
|
13333
13753
|
name: "github-app",
|
|
@@ -13391,7 +13811,7 @@ function repositoryUrlMatches(origin, profile) {
|
|
|
13391
13811
|
//#endregion
|
|
13392
13812
|
//#region src/commands/doctor.ts
|
|
13393
13813
|
function createDoctorCommand(output) {
|
|
13394
|
-
return markCwdOptionDefault(new Command("doctor").description("Validate a project profile without mutating GitHub or local files").option("--base <ref>", "base branch or ref for --preflight", "origin/main").option("--cwd <path>", "working directory to validate", collectCwdOption).option("--json", "print the doctor report as JSON").option("--preflight", "also list the pre-checkable admission requirements for the current candidate (read-only; never blocks); findings-file shape and wave membership have no read-only pre-check").option("--profile <path>", "path to the project profile JSON file").action(async (options) => {
|
|
13814
|
+
return markCwdOptionDefault(new Command("doctor").description("Validate a project profile without mutating GitHub or local files").option("--base <ref>", "base branch or ref for --preflight", "origin/main").option("--cwd <path>", "working directory to validate", collectCwdOption).option("--json", "print the doctor report as JSON").option("--preflight", "also list the pre-checkable admission requirements for the current candidate (read-only; never blocks); each required check states whether this candidate is demanded to satisfy it, resolved by candidate:impact; findings-file shape and wave membership have no read-only pre-check").option("--profile <path>", "path to the project profile JSON file").action(async (options) => {
|
|
13395
13815
|
const report = await doctorProjectProfile({
|
|
13396
13816
|
base: options.base,
|
|
13397
13817
|
cwd: resolveCwdOption(options.cwd),
|
|
@@ -16187,10 +16607,10 @@ function defaultPrReadyGit() {
|
|
|
16187
16607
|
], cwd);
|
|
16188
16608
|
},
|
|
16189
16609
|
mergeBaseSha,
|
|
16190
|
-
objectIdAtRef: (cwd, ref,
|
|
16191
|
-
|
|
16610
|
+
objectIdAtRef: (cwd, ref, repositoryRelativePath) => {
|
|
16611
|
+
assertRepositoryRelativePath(repositoryRelativePath, "objectIdAtRef");
|
|
16192
16612
|
try {
|
|
16193
|
-
return runCapture("git", ["rev-parse", `${ref}:${
|
|
16613
|
+
return runCapture("git", ["rev-parse", `${ref}:${repositoryRelativePath}`], cwd).stdout.trim();
|
|
16194
16614
|
} catch {}
|
|
16195
16615
|
},
|
|
16196
16616
|
resolveCommitSha: (cwd, ref) => {
|
|
@@ -16425,9 +16845,9 @@ const resolveExternalEvidenceInputs = ({ args, cwd, recordedAuthoringSession, re
|
|
|
16425
16845
|
externalRequiredChecks: requiredChecks
|
|
16426
16846
|
};
|
|
16427
16847
|
};
|
|
16428
|
-
function policyProfileAtCommittedHead({ cwd, git, headSha,
|
|
16429
|
-
const content = git.showFileAtRef(cwd, headSha,
|
|
16430
|
-
const blobSha = git.objectIdAtRef(cwd, headSha,
|
|
16848
|
+
function policyProfileAtCommittedHead({ cwd, git, headSha, repositoryRelativeProfile }) {
|
|
16849
|
+
const content = git.showFileAtRef(cwd, headSha, repositoryRelativeProfile);
|
|
16850
|
+
const blobSha = git.objectIdAtRef(cwd, headSha, repositoryRelativeProfile);
|
|
16431
16851
|
const committedProfile = content ? tryParseProfileContent(content) : void 0;
|
|
16432
16852
|
if (!(committedProfile && blobSha && /^[0-9a-f]{40}$/u.test(blobSha))) throw new Error("Could not read, validate, and identify readiness policy from the committed PR head; fetch the head and re-run pr:ready.");
|
|
16433
16853
|
return {
|
|
@@ -16435,11 +16855,6 @@ function policyProfileAtCommittedHead({ cwd, git, headSha, profilePath }) {
|
|
|
16435
16855
|
profile: committedProfile
|
|
16436
16856
|
};
|
|
16437
16857
|
}
|
|
16438
|
-
function checkoutRelativeProfilePath(cwd, profilePath) {
|
|
16439
|
-
const relativePath = path.relative(cwd, profilePath);
|
|
16440
|
-
if (relativePath.length === 0 || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) throw new Error("The readiness profile must be inside the trusted checkout.");
|
|
16441
|
-
return relativePath.split(path.sep).join("/");
|
|
16442
|
-
}
|
|
16443
16858
|
function readinessProfilePath(cwd, requestedPath) {
|
|
16444
16859
|
return path.resolve(cwd, requestedPath ?? "software-factory.profile.json");
|
|
16445
16860
|
}
|
|
@@ -16499,15 +16914,18 @@ async function runPrReady(args, dependencies = {}) {
|
|
|
16499
16914
|
hudMergeBaseSha = resolvedMergeBaseSha;
|
|
16500
16915
|
hudPatchId = patchId;
|
|
16501
16916
|
const files = git.changedFiles(cwd, authoritativeBaseSha, pr.headRefOid);
|
|
16917
|
+
const committedProfilePath = repositoryContainedProfilePath({
|
|
16918
|
+
cwd,
|
|
16919
|
+
profilePath
|
|
16920
|
+
});
|
|
16502
16921
|
const committedPolicy = policyProfileAtCommittedHead({
|
|
16503
16922
|
cwd,
|
|
16504
16923
|
git,
|
|
16505
16924
|
headSha: pr.headRefOid,
|
|
16506
|
-
|
|
16925
|
+
repositoryRelativeProfile: committedProfilePath
|
|
16507
16926
|
});
|
|
16508
16927
|
const policyProfile = committedPolicy.profile;
|
|
16509
16928
|
if (repositorySlug(policyProfile.repository) !== checkoutRepositorySlug) throw new Error(`Committed readiness profile repository ${repositorySlug(policyProfile.repository)} does not match trusted checkout repository ${checkoutRepositorySlug}.`);
|
|
16510
|
-
const committedProfilePath = checkoutRelativeProfilePath(cwd, profilePath);
|
|
16511
16929
|
const policy = resolveReviewDemand({
|
|
16512
16930
|
authority: dependencies.basePolicy ?? defaultBaseReviewPolicyAuthority(),
|
|
16513
16931
|
callerBaseRef: pr.baseRefName,
|
|
@@ -16740,7 +17158,12 @@ async function runPrReady(args, dependencies = {}) {
|
|
|
16740
17158
|
repo: repository.name,
|
|
16741
17159
|
sha: presentedHeadSha
|
|
16742
17160
|
};
|
|
16743
|
-
|
|
17161
|
+
try {
|
|
17162
|
+
if (!await (dependencies.publishFinalCheckRun ? dependencies.publishFinalCheckRun(failureCheckInput) : ensureFactoryCheckRunPublished(failureCheckInput, { onDiagnostic: (diagnostic) => process.stderr.write(diagnostic) }))) console.warn(`pr:ready could not publish the failing ${FACTORY_CHECK_NAMES["pr-ready"]} check run for ${presentedHeadSha}; the required check is absent rather than red.`);
|
|
17163
|
+
} catch (publishError) {
|
|
17164
|
+
const publishMessage = publishError instanceof Error ? publishError.message : String(publishError);
|
|
17165
|
+
console.warn(`pr:ready could not publish the failing ${FACTORY_CHECK_NAMES["pr-ready"]} check run for ${presentedHeadSha}; the required check is absent rather than red. ${publishMessage}`);
|
|
17166
|
+
}
|
|
16744
17167
|
await presentHud();
|
|
16745
17168
|
}
|
|
16746
17169
|
throw error;
|
|
@@ -17081,10 +17504,20 @@ function emitLadderStageTrace({ cwd, evaluation, headSha, issue, patchId, profil
|
|
|
17081
17504
|
}
|
|
17082
17505
|
//#endregion
|
|
17083
17506
|
//#region src/review-contract.ts
|
|
17084
|
-
const
|
|
17507
|
+
const SESSION_ID_RULE = `"sessionId" is this review session's own id. It must differ from the authoring session id the candidate was verified under (FACTORY_SESSION_ID): pr:review --findings refuses a findings file whose sessionId equals the authoring session.`;
|
|
17508
|
+
const authorityRule = (waveRung) => waveRung === void 0 ? `Emit schemaVersion 1 and omit "producer" and "rung": this candidate resolves no boundary-wave review rung. If your dispatch instructions do name a rung, emit schemaVersion 2 with that rung and with "producer" instead.` : `This candidate belongs to a boundary wave that demands the ${waveRung} review rung, so schemaVersion 2 is required. Record "rung": "${waveRung}" exactly as shown, and set "producer" to the engine and model that ran this review (for example "codex-cli/gpt-5.6-sol"). Findings without both fields are recorded without authority, and pr:publish then refuses with "no passing review at that authority".`;
|
|
17509
|
+
/**
|
|
17510
|
+
* The findings-file instructions for one candidate.
|
|
17511
|
+
*
|
|
17512
|
+
* `waveRung` is the review rung the candidate's boundary wave demands, when it
|
|
17513
|
+
* resolves one. It selects the findings schema version the gate will accept.
|
|
17514
|
+
*/
|
|
17515
|
+
const reviewFooterInstructions = (waveRung) => {
|
|
17516
|
+
const schemaVersion = waveRung === void 0 ? 1 : 2;
|
|
17517
|
+
return `Do not print a machine-readable footer in your chat response. Instead, write your findings to the typed findings file at the path given in your dispatch/task instructions (the file pr:review reads via --findings <path>). The file must be valid JSON matching review-findings-file.ts's schema (schemaVersion ${schemaVersion}):
|
|
17085
17518
|
{
|
|
17086
|
-
"schemaVersion":
|
|
17087
|
-
"model": "<your model identifier>",
|
|
17519
|
+
"schemaVersion": ${schemaVersion},
|
|
17520
|
+
${waveRung === void 0 ? "" : ` "producer": "<the engine and model that ran this review, for example codex-cli/gpt-5.6-sol>",\n "rung": "${waveRung}",\n`} "model": "<your model identifier>",
|
|
17088
17521
|
"sessionId": "<this session's id>",
|
|
17089
17522
|
"patchId": "<the stable patch-id from Review scope above>",
|
|
17090
17523
|
"reviews": [
|
|
@@ -17097,10 +17530,13 @@ const REVIEW_FOOTER_INSTRUCTIONS = `Do not print a machine-readable footer in yo
|
|
|
17097
17530
|
}
|
|
17098
17531
|
]
|
|
17099
17532
|
}
|
|
17533
|
+
${authorityRule(waveRung)}
|
|
17534
|
+
${SESSION_ID_RULE}
|
|
17100
17535
|
Use category "correctness", "safety", "coordination", or "maintainability"; set blockingAfterCap true for correctness, safety, or coordination findings. citedSpan must be the exact code text the finding is about, quoted verbatim from the diff (identity is derived from this, not from the title, so quote precisely). prescribedAction must be a concise, standalone statement of the required fix (identity is derived from this text too). If this finding is a restatement of a prior finding shown in the prior review ledger below, set supersedes to {"title": "<exact prior title>", "file": "<its file>"} instead of minting a new title for the same theme.`;
|
|
17536
|
+
};
|
|
17101
17537
|
//#endregion
|
|
17102
17538
|
//#region src/review-prompt.ts
|
|
17103
|
-
const reviewScope = ({ base, changedFiles: files, headSha, patchId, profile }) => `Review scope:
|
|
17539
|
+
const reviewScope = ({ base, changedFiles: files, headSha, patchId, profile, waveRung }) => `Review scope:
|
|
17104
17540
|
- Repository: ${profile.repository.owner}/${profile.repository.name}
|
|
17105
17541
|
- Base ref: ${base}
|
|
17106
17542
|
- Head SHA: ${headSha}
|
|
@@ -17116,7 +17552,7 @@ Required constraints:
|
|
|
17116
17552
|
- Do not request elevated or unsandboxed approvals.
|
|
17117
17553
|
- When you flag a finding, report every other instance of the same class in this diff in the same response, and include a one-line coverage note.
|
|
17118
17554
|
|
|
17119
|
-
${
|
|
17555
|
+
${reviewFooterInstructions(waveRung)}`;
|
|
17120
17556
|
const MODE_FLOOR_FILES = {
|
|
17121
17557
|
correctness: "correctness.md",
|
|
17122
17558
|
security: "security/SKILL.md"
|
|
@@ -17149,7 +17585,7 @@ const readCodingStandardsAtHead = ({ cwd, git, headSha }) => {
|
|
|
17149
17585
|
} catch {
|
|
17150
17586
|
return;
|
|
17151
17587
|
}
|
|
17152
|
-
return (git?.showFileAtRef ?? showFileAtRef)(root, headSha,
|
|
17588
|
+
return (git?.showFileAtRef ?? showFileAtRef)(root, headSha, CODING_STANDARDS_FILE);
|
|
17153
17589
|
};
|
|
17154
17590
|
const codingStandardsSegment = ({ contents, headSha }) => {
|
|
17155
17591
|
const text = contents?.replace(/\n+$/u, "");
|
|
@@ -17190,7 +17626,7 @@ ${entries.map(priorLedgerEntryLine).join("\n")}
|
|
|
17190
17626
|
|
|
17191
17627
|
Do not re-raise any of the findings above under a new title: they are already adjudicated (waived, filed as a follow-up, or fixed in this thread). If new evidence genuinely supersedes one of them, say so explicitly and name the exact prior finding (its title and file) that it supersedes, rather than minting an unrelated title for the same underlying theme.`
|
|
17192
17628
|
});
|
|
17193
|
-
const assembleReviewPrompt = ({ base, changedFiles, cwd = process.cwd(), headSha, issueFocus, kind, patchId, priorLedgerEntries, profile }, dependencies = {}) => {
|
|
17629
|
+
const assembleReviewPrompt = ({ base, changedFiles, cwd = process.cwd(), headSha, issueFocus, kind, patchId, priorLedgerEntries, profile, waveRung }, dependencies = {}) => {
|
|
17194
17630
|
const priorLedger = priorLedgerEntries ? nonOpenLadderFindings(priorLedgerEntries) : [];
|
|
17195
17631
|
const optionalSegments = {
|
|
17196
17632
|
"coding-standards": codingStandardsSegment({
|
|
@@ -17210,7 +17646,8 @@ const assembleReviewPrompt = ({ base, changedFiles, cwd = process.cwd(), headSha
|
|
|
17210
17646
|
changedFiles,
|
|
17211
17647
|
headSha,
|
|
17212
17648
|
patchId,
|
|
17213
|
-
profile
|
|
17649
|
+
profile,
|
|
17650
|
+
waveRung
|
|
17214
17651
|
}) },
|
|
17215
17652
|
{ text: modeFloorInstructions(kind, changedFiles) },
|
|
17216
17653
|
...REVIEW_PROMPT_SECTION_PROVENANCES.flatMap((provenance) => {
|
|
@@ -17235,7 +17672,8 @@ const ASSEMBLED_REVIEW_PROMPT_SCHEMA_VERSION = 1;
|
|
|
17235
17672
|
const assembledReviewPromptKindSchema = z.object({
|
|
17236
17673
|
kind: z.enum(["correctness", "security"]),
|
|
17237
17674
|
prompt: z.string().min(1),
|
|
17238
|
-
sections: z.array(reviewPromptSectionSchema)
|
|
17675
|
+
sections: z.array(reviewPromptSectionSchema),
|
|
17676
|
+
waveRung: z.enum(EVIDENCE_REVIEW_RUNGS).optional()
|
|
17239
17677
|
});
|
|
17240
17678
|
const assembledReviewPromptArtifactSchema = z.object({
|
|
17241
17679
|
headSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
@@ -17264,6 +17702,51 @@ const defaultFetchIssueBody = ({ issue, owner, repo }) => runGhJson([
|
|
|
17264
17702
|
"--json",
|
|
17265
17703
|
"body"
|
|
17266
17704
|
]).body;
|
|
17705
|
+
const candidateWavePullRequestSchema = z.object({
|
|
17706
|
+
baseRefName: z.string().min(1),
|
|
17707
|
+
body: z.string(),
|
|
17708
|
+
headRefName: z.string().min(1),
|
|
17709
|
+
number: z.number().int().positive()
|
|
17710
|
+
});
|
|
17711
|
+
const defaultFetchWavePullRequest = (cwd) => runGhJsonAt([
|
|
17712
|
+
"pr",
|
|
17713
|
+
"view",
|
|
17714
|
+
"--json",
|
|
17715
|
+
"baseRefName,body,headRefName,number"
|
|
17716
|
+
], cwd);
|
|
17717
|
+
/**
|
|
17718
|
+
* The review rung this candidate's boundary wave demands (#1001).
|
|
17719
|
+
*
|
|
17720
|
+
* The assembled prompt tells the reviewer which findings schema the gate will
|
|
17721
|
+
* accept. A wave that demands a rung needs schemaVersion 2 with `producer` and
|
|
17722
|
+
* `rung`; the v1 shape is recorded without authority, and `pr:publish` then
|
|
17723
|
+
* refuses it with "no passing review at that authority".
|
|
17724
|
+
*
|
|
17725
|
+
* This is prompt enrichment and fails open, like the coding-standards read:
|
|
17726
|
+
* every unreadable step returns undefined and the prompt keeps the v1 shape.
|
|
17727
|
+
* It decides no authority — `pr:ready` resolves the same wave demand again and
|
|
17728
|
+
* is the stage that refuses on it.
|
|
17729
|
+
*/
|
|
17730
|
+
const assembleWaveRung = ({ args, cwd, dependencies, fetchIssueBody, fetchWavePullRequest, profile }) => {
|
|
17731
|
+
if (args.epic === void 0) return;
|
|
17732
|
+
try {
|
|
17733
|
+
const pullRequest = candidateWavePullRequestSchema.parse(fetchWavePullRequest(cwd));
|
|
17734
|
+
const demand = resolveWaveDemand({
|
|
17735
|
+
baseRefName: pullRequest.baseRefName,
|
|
17736
|
+
epic: args.epic,
|
|
17737
|
+
...dependencies.fetchClosingPullRequests === void 0 ? {} : { fetchClosingPullRequests: dependencies.fetchClosingPullRequests },
|
|
17738
|
+
fetchIssueBody,
|
|
17739
|
+
headRefName: pullRequest.headRefName,
|
|
17740
|
+
owner: profile.repository.owner,
|
|
17741
|
+
pr: pullRequest.number,
|
|
17742
|
+
prBody: pullRequest.body,
|
|
17743
|
+
repo: profile.repository.name
|
|
17744
|
+
});
|
|
17745
|
+
return demand.kind === "demanded" ? demand.demand.review : void 0;
|
|
17746
|
+
} catch {
|
|
17747
|
+
return;
|
|
17748
|
+
}
|
|
17749
|
+
};
|
|
17267
17750
|
const assertReviewCycleInWindow = (cycle, maxCycles) => {
|
|
17268
17751
|
if (cycle < 1 || cycle > maxCycles) throw new Error(`--cycle must be between 1 and ${maxCycles}.`);
|
|
17269
17752
|
};
|
|
@@ -17383,7 +17866,7 @@ const assembleGitDependencies = (git) => git.repositoryRoot !== void 0 && git.sh
|
|
|
17383
17866
|
repositoryRoot: git.repositoryRoot,
|
|
17384
17867
|
showFileAtRef: git.showFileAtRef
|
|
17385
17868
|
} : void 0;
|
|
17386
|
-
const writeAssembledReviewPrompt = ({ args, cwd, fetchIssueBody, files, git, headSha, kinds, output, patchId, profile }) => {
|
|
17869
|
+
const writeAssembledReviewPrompt = ({ args, cwd, fetchIssueBody, files, git, headSha, kinds, output, patchId, profile, waveRung }) => {
|
|
17387
17870
|
const priorLedger = priorLedgerFor(priorLadderStateFor({
|
|
17388
17871
|
base: args.base,
|
|
17389
17872
|
cwd,
|
|
@@ -17403,12 +17886,14 @@ const writeAssembledReviewPrompt = ({ args, cwd, fetchIssueBody, files, git, hea
|
|
|
17403
17886
|
kind,
|
|
17404
17887
|
patchId,
|
|
17405
17888
|
priorLedgerEntries: priorLedger,
|
|
17406
|
-
profile
|
|
17889
|
+
profile,
|
|
17890
|
+
...waveRung === void 0 ? {} : { waveRung }
|
|
17407
17891
|
}, assembleGit === void 0 ? {} : { git: assembleGit });
|
|
17408
17892
|
return {
|
|
17409
17893
|
kind,
|
|
17410
17894
|
prompt: assembled.prompt,
|
|
17411
|
-
sections: assembled.sections
|
|
17895
|
+
sections: assembled.sections,
|
|
17896
|
+
...waveRung === void 0 ? {} : { waveRung }
|
|
17412
17897
|
};
|
|
17413
17898
|
});
|
|
17414
17899
|
const artifactPath = path.resolve(cwd, DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH);
|
|
@@ -17416,7 +17901,7 @@ const writeAssembledReviewPrompt = ({ args, cwd, fetchIssueBody, files, git, hea
|
|
|
17416
17901
|
writeProofJson({
|
|
17417
17902
|
headSha,
|
|
17418
17903
|
patchId,
|
|
17419
|
-
reviews: existing.status === "ok" && existing.artifact.patchId === patchId && existing.artifact.headSha === headSha ? [...new Map([...existing.artifact.reviews, ...assembledReviews].map((review) => [review.kind, review])).values()] : assembledReviews,
|
|
17904
|
+
reviews: existing.status === "ok" && existing.artifact.patchId === patchId && existing.artifact.headSha === headSha ? [...new Map([...existing.artifact.reviews.filter((review) => review.waveRung === waveRung), ...assembledReviews].map((review) => [review.kind, review])).values()] : assembledReviews,
|
|
17420
17905
|
schemaVersion: ASSEMBLED_REVIEW_PROMPT_SCHEMA_VERSION
|
|
17421
17906
|
}, artifactPath);
|
|
17422
17907
|
console.log(`pr:review assembled prompt written to ${artifactPath}`);
|
|
@@ -17474,7 +17959,10 @@ const reviewPolicyIdentity = ({ args, changedFiles: files, cwd, dependencies, gi
|
|
|
17474
17959
|
changedFiles: files,
|
|
17475
17960
|
cwd,
|
|
17476
17961
|
lookupPullRequest: dependencies.lookupPullRequest ?? lookupCandidatePullRequest,
|
|
17477
|
-
profilePath:
|
|
17962
|
+
profilePath: repositoryRelativeProfilePath({
|
|
17963
|
+
cwd,
|
|
17964
|
+
profilePath
|
|
17965
|
+
}),
|
|
17478
17966
|
readCheckoutRepository: git.checkoutRepository ?? checkoutRepository
|
|
17479
17967
|
});
|
|
17480
17968
|
/**
|
|
@@ -17642,17 +18130,26 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
17642
18130
|
console.log(ASSEMBLE_FAITHFUL_MERGE_NOTICE);
|
|
17643
18131
|
return assembleProof();
|
|
17644
18132
|
}
|
|
18133
|
+
const fetchIssueBody = dependencies.fetchIssueBody ?? defaultFetchIssueBody;
|
|
17645
18134
|
writeAssembledReviewPrompt({
|
|
17646
18135
|
args,
|
|
17647
18136
|
cwd,
|
|
17648
|
-
fetchIssueBody
|
|
18137
|
+
fetchIssueBody,
|
|
17649
18138
|
files,
|
|
17650
18139
|
git,
|
|
17651
18140
|
headSha,
|
|
17652
18141
|
kinds,
|
|
17653
18142
|
output,
|
|
17654
18143
|
patchId,
|
|
17655
|
-
profile
|
|
18144
|
+
profile,
|
|
18145
|
+
waveRung: assembleWaveRung({
|
|
18146
|
+
args,
|
|
18147
|
+
cwd,
|
|
18148
|
+
dependencies,
|
|
18149
|
+
fetchIssueBody,
|
|
18150
|
+
fetchWavePullRequest: dependencies.fetchWavePullRequest ?? defaultFetchWavePullRequest,
|
|
18151
|
+
profile
|
|
18152
|
+
})
|
|
17656
18153
|
});
|
|
17657
18154
|
return assembleProof();
|
|
17658
18155
|
}
|
|
@@ -17839,11 +18336,7 @@ const defaultFollowUpRunner = (factoryCliInvocation) => (followUp, cwd) => {
|
|
|
17839
18336
|
const [command, ...args] = resolveFollowUpArgv(followUp, factoryCliInvocation);
|
|
17840
18337
|
runCapture(command, args, cwd);
|
|
17841
18338
|
};
|
|
17842
|
-
const
|
|
17843
|
-
const moduleExtension = path.extname(import.meta.filename);
|
|
17844
|
-
return defaultFactoryCliInvocation(path.join(import.meta.dirname, `index${moduleExtension}`));
|
|
17845
|
-
};
|
|
17846
|
-
const prPublishCliInvocation = (explicit) => explicit ?? defaultPrPublishCliInvocation();
|
|
18339
|
+
const prPublishCliInvocation = (explicit) => explicit ?? defaultFactoryCliInvocation(factoryCliEntry());
|
|
17847
18340
|
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
17848
18341
|
/**
|
|
17849
18342
|
* GitHub creates a pull request against a branch name, while the rest of the
|
|
@@ -18558,7 +19051,7 @@ function assertForegroundGate({ gate, env = process.env }) {
|
|
|
18558
19051
|
//#endregion
|
|
18559
19052
|
//#region src/commands/pr-review.ts
|
|
18560
19053
|
function createPrReviewCommand(output, action) {
|
|
18561
|
-
return markCwdOptionDefault(new Command("pr:review").description("Assemble a factory-owned review prompt, or validate clean-session findings and write typed review proof").option("--base <ref>", "base branch or ref", "origin/main").option("--cwd <path>", "working directory to review", collectCwdOption).option("--cycle <number>", "review cycle number", positiveInteger$1("--cycle"), 1).option("--mode <mode>", "correctness, security, or all", "all").option("--assemble", "write the factory-owned review prompt for a clean session; do not validate findings").option("--findings <path>", "typed clean-session findings JSON file").option("--historical", "record findings that read an earlier commit on this branch: the cycle joins the ladder as prior history and renders no verdict of its own, so it never admits. It leaves the candidate's recorded verdict untouched. Use --dispositions for the findings its fixes answered").option("--dispositions <path>", "JSON file of ladder disposition declarations (waived / follow-up-filed / fixed-in-thread) for previously flagged findings").option("--issue <number>", "issue number recorded on the review ladder trace; --assemble also loads Review focus from this issue", positiveInteger$1("--issue")).option("--output <path>", "write proof JSON to a file").option("--profile <path>", "path to the project profile JSON file").option("--verify-proof <path>", "pr:verify proof JSON path").action(withGateTiming({
|
|
19054
|
+
return markCwdOptionDefault(new Command("pr:review").description("Assemble a factory-owned review prompt, or validate clean-session findings and write typed review proof").option("--base <ref>", "base branch or ref", "origin/main").option("--cwd <path>", "working directory to review", collectCwdOption).option("--cycle <number>", "review cycle number", positiveInteger$1("--cycle"), 1).option("--mode <mode>", "correctness, security, or all", "all").option("--assemble", "write the factory-owned review prompt for a clean session; do not validate findings").option("--epic <number>", "issue carrying the factory-boundary manifest, which may be the work issue itself; --assemble then asks the reviewer for the findings schema the wave-demanded review rung accepts (#1001)", positiveInteger$1("--epic")).option("--findings <path>", "typed clean-session findings JSON file").option("--historical", "record findings that read an earlier commit on this branch: the cycle joins the ladder as prior history and renders no verdict of its own, so it never admits. It leaves the candidate's recorded verdict untouched. Use --dispositions for the findings its fixes answered").option("--dispositions <path>", "JSON file of ladder disposition declarations (waived / follow-up-filed / fixed-in-thread) for previously flagged findings").option("--issue <number>", "issue number recorded on the review ladder trace; --assemble also loads Review focus from this issue", positiveInteger$1("--issue")).option("--output <path>", "write proof JSON to a file").option("--profile <path>", "path to the project profile JSON file").option("--verify-proof <path>", "pr:verify proof JSON path").action(withGateTiming({
|
|
18562
19055
|
gate: "pr:review",
|
|
18563
19056
|
resolveCycle: (options) => options.cycle,
|
|
18564
19057
|
resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
|
|
@@ -18576,6 +19069,7 @@ function createPrReviewCommand(output, action) {
|
|
|
18576
19069
|
cwd: resolveCwdOption(options.cwd),
|
|
18577
19070
|
cycle: options.cycle,
|
|
18578
19071
|
dispositions: options.dispositions,
|
|
19072
|
+
epic: options.epic,
|
|
18579
19073
|
findings: options.findings,
|
|
18580
19074
|
...options.historical === true ? { historical: true } : {},
|
|
18581
19075
|
issue: options.issue,
|
|
@@ -18704,8 +19198,11 @@ const runProductionImpact = (args) => {
|
|
|
18704
19198
|
const stamp = computeImpactStamp({
|
|
18705
19199
|
changedFiles,
|
|
18706
19200
|
profile,
|
|
18707
|
-
profilePath:
|
|
18708
|
-
|
|
19201
|
+
profilePath: repositoryRelativeProfilePath({
|
|
19202
|
+
cwd,
|
|
19203
|
+
profilePath
|
|
19204
|
+
}),
|
|
19205
|
+
readLockfile: (side) => showFileAtRef(cwd, side === "base" ? before : after, LOCKFILE_PATH)
|
|
18709
19206
|
});
|
|
18710
19207
|
const core = {
|
|
18711
19208
|
afterSha: after,
|
|
@@ -19496,7 +19993,7 @@ function formatScratchFileCheckSummary(check) {
|
|
|
19496
19993
|
//#endregion
|
|
19497
19994
|
//#region src/index.ts
|
|
19498
19995
|
function createProgram(options = {}) {
|
|
19499
|
-
const factoryCliInvocation = options.factoryCliInvocation ? FactoryCliInvocationSchema.parse(options.factoryCliInvocation) : defaultFactoryCliInvocation(
|
|
19996
|
+
const factoryCliInvocation = options.factoryCliInvocation ? FactoryCliInvocationSchema.parse(options.factoryCliInvocation) : defaultFactoryCliInvocation(factoryCliEntry());
|
|
19500
19997
|
const output = options.output ?? {
|
|
19501
19998
|
stderr: process.stderr,
|
|
19502
19999
|
stdout: process.stdout
|
|
@@ -19526,24 +20023,18 @@ function createProgram(options = {}) {
|
|
|
19526
20023
|
program.addCommand(createDemandWaiveCommand(output, options.actions?.demandWaive));
|
|
19527
20024
|
return program;
|
|
19528
20025
|
}
|
|
19529
|
-
|
|
20026
|
+
/**
|
|
20027
|
+
* Parse `argv` and run the matching command. `cliEntry` is the path of the
|
|
20028
|
+
* runnable bin module, which follow-up commands spawn; the bin entry passes its
|
|
20029
|
+
* own path. This function never runs itself — `./cli.js` is the only caller
|
|
20030
|
+
* that does (#386).
|
|
20031
|
+
*/
|
|
20032
|
+
async function run(argv = process.argv, cliEntry = factoryCliEntry()) {
|
|
19530
20033
|
try {
|
|
19531
|
-
await createProgram({ factoryCliInvocation: defaultFactoryCliInvocation(
|
|
20034
|
+
await createProgram({ factoryCliInvocation: defaultFactoryCliInvocation(cliEntry) }).parseAsync(argv);
|
|
19532
20035
|
} finally {
|
|
19533
20036
|
await awaitPendingHqIngest();
|
|
19534
20037
|
}
|
|
19535
20038
|
}
|
|
19536
|
-
function isDirectCliExecution() {
|
|
19537
|
-
const [, entryPath] = process.argv;
|
|
19538
|
-
if (!entryPath) return false;
|
|
19539
|
-
return import.meta.url === pathToFileURL(realpathSync(entryPath)).href;
|
|
19540
|
-
}
|
|
19541
|
-
if (isDirectCliExecution()) try {
|
|
19542
|
-
await run();
|
|
19543
|
-
} catch (error) {
|
|
19544
|
-
if (error instanceof PrPublishFollowUpError) console.error(JSON.stringify(error.result, null, 2));
|
|
19545
|
-
else console.error("\nError:", error instanceof Error ? error.message : String(error));
|
|
19546
|
-
process.exit(1);
|
|
19547
|
-
}
|
|
19548
20039
|
//#endregion
|
|
19549
20040
|
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, assertFactoryPrStatusIdentity, 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, createGitHubCheckRunMergeFreezeStore, createLocalJsonlTraceSink, createProgram, createVerificationRunContext, doctorProjectProfile, epicStructureEventId, evaluateReviewLadder, evidenceEnvelopeFilename, findingKey, followUpFromArgv, impactStampScopeDecision, inferFixedInThreadDispositions, isProductionHqUrl, loadProjectProfile, normalizeIssueComments, openLadderFindings, planFactoryPrStatusHud, 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, presentFactoryPrStatusHud, previewLifecycleTargets, publishEpicStructure, readDemandWaivers, readFactoryPrStatusSources, readPrReadyProof, readPrReviewProof, refreshFactoryPrStatusHud, refreshFactoryPrStatusHudSafely, renderFactoryPrStatusHud, 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, validatePreviewLifecyclePlan, waivedDemandNotice, waivedDemandSchema, worktree_scratch_files_exports as worktreeScratchFiles };
|