@patronage/software-factory 1.0.0-alpha.23 → 1.0.0-alpha.24
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/README.md +2 -0
- package/dist/index.d.ts +262 -182
- package/dist/index.js +828 -324
- package/dist/schemas.d.ts +73 -36
- package/dist/schemas.js +111 -51
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
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
|
+
import { Buffer as Buffer$1 } from "node:buffer";
|
|
10
11
|
import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "node:fs/promises";
|
|
11
12
|
import os, { homedir, tmpdir } from "node:os";
|
|
12
13
|
import { setImmediate } from "node:timers";
|
|
@@ -17,15 +18,15 @@ import { parse } from "yaml";
|
|
|
17
18
|
import { FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, previewProofInventory, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
|
|
18
19
|
import { promisify } from "node:util";
|
|
19
20
|
//#region package.json
|
|
20
|
-
var version = "1.0.0-alpha.
|
|
21
|
+
var version = "1.0.0-alpha.24";
|
|
21
22
|
//#endregion
|
|
22
23
|
//#region src/review-rungs.ts
|
|
23
|
-
const EVIDENCE_REVIEW_RUNGS
|
|
24
|
+
const EVIDENCE_REVIEW_RUNGS = [
|
|
24
25
|
"independent-model",
|
|
25
26
|
"oracle",
|
|
26
27
|
"human"
|
|
27
28
|
];
|
|
28
|
-
const RUNG_ORDER = Object.fromEntries(EVIDENCE_REVIEW_RUNGS
|
|
29
|
+
const RUNG_ORDER = Object.fromEntries(EVIDENCE_REVIEW_RUNGS.map((rung, index) => [rung, index]));
|
|
29
30
|
//#endregion
|
|
30
31
|
//#region src/boundary-manifest.ts
|
|
31
32
|
var boundary_manifest_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -43,7 +44,7 @@ var boundary_manifest_exports = /* @__PURE__ */ __exportAll({
|
|
|
43
44
|
const FACTORY_BOUNDARY_FENCE_LANG = "factory-boundary";
|
|
44
45
|
const BOUNDARY_CLOSEOUT_DEFAULT_RUNG = "oracle";
|
|
45
46
|
const rungMeetsMinimum = (rung, minimum) => RUNG_ORDER[rung] >= RUNG_ORDER[minimum];
|
|
46
|
-
const rungSchema = z.enum(EVIDENCE_REVIEW_RUNGS
|
|
47
|
+
const rungSchema = z.enum(EVIDENCE_REVIEW_RUNGS);
|
|
47
48
|
const BOUNDARY_TOPOLOGIES = [
|
|
48
49
|
"each-to-main",
|
|
49
50
|
"flagged",
|
|
@@ -358,7 +359,7 @@ const boundaryReviewProofSchema$1 = z.object({
|
|
|
358
359
|
}).passthrough(),
|
|
359
360
|
outcome: z.enum(["pass", "fail"]),
|
|
360
361
|
producer: z.string().min(1),
|
|
361
|
-
rung: z.enum(EVIDENCE_REVIEW_RUNGS
|
|
362
|
+
rung: z.enum(EVIDENCE_REVIEW_RUNGS),
|
|
362
363
|
schemaVersion: z.literal(1),
|
|
363
364
|
sessionId: z.string().min(1).optional(),
|
|
364
365
|
specHash: z.object({
|
|
@@ -999,6 +1000,9 @@ function runInheritedTee(command, args, cwd, extraEnv = {}) {
|
|
|
999
1000
|
function currentHeadSha(cwd) {
|
|
1000
1001
|
return runCapture("git", ["rev-parse", "HEAD"], cwd).stdout.trim();
|
|
1001
1002
|
}
|
|
1003
|
+
function repositoryRoot(cwd) {
|
|
1004
|
+
return runCapture("git", ["rev-parse", "--show-toplevel"], cwd).stdout.trim();
|
|
1005
|
+
}
|
|
1002
1006
|
function showFileAtRef(cwd, ref, absolutePath) {
|
|
1003
1007
|
const rel = path.relative(cwd, absolutePath);
|
|
1004
1008
|
const spec = rel.startsWith(".") ? rel : `./${rel}`;
|
|
@@ -1199,7 +1203,33 @@ const normalizeIssueComments = (value) => {
|
|
|
1199
1203
|
const runGhCapture = (args, cwd = process.cwd(), acceptableExitCodes = [0]) => runCapture("gh", args, cwd, { acceptableExitCodes });
|
|
1200
1204
|
const runGhJsonAt = (args, cwd) => JSON.parse(runGhCapture(args, cwd).stdout);
|
|
1201
1205
|
const runGhJson = (args) => runGhJsonAt(args, process.cwd());
|
|
1202
|
-
z.object({ object: z.object({ sha: z.string().regex(/^[0-9a-f]{40}$/u) }) });
|
|
1206
|
+
const gitReferenceSchema = z.object({ object: z.object({ sha: z.string().regex(/^[0-9a-f]{40}$/u) }) });
|
|
1207
|
+
/** Read a branch tip from GitHub rather than a checkout's cached remote ref. */
|
|
1208
|
+
const fetchGitHubBranchTipSha = (repository, branch, cwd) => {
|
|
1209
|
+
return gitReferenceSchema.parse(runGhJsonAt(["api", `/repos/${repository.owner}/${repository.name}/git/ref/heads/${encodeURIComponent(branch)}`], cwd)).object.sha;
|
|
1210
|
+
};
|
|
1211
|
+
const repositoryFileSchema = z.object({
|
|
1212
|
+
content: z.string(),
|
|
1213
|
+
encoding: z.literal("base64"),
|
|
1214
|
+
type: z.literal("file")
|
|
1215
|
+
});
|
|
1216
|
+
/**
|
|
1217
|
+
* Read one file's content at an exact commit from GitHub.
|
|
1218
|
+
*
|
|
1219
|
+
* #922: the authoritative base policy is the profile GitHub serves at the base
|
|
1220
|
+
* tip. A checkout is candidate-controlled, so it is not an authority for it.
|
|
1221
|
+
* There is exactly one implementation on purpose — no local-first read with an
|
|
1222
|
+
* API fallback, which would let a checkout answer the authority's question.
|
|
1223
|
+
*
|
|
1224
|
+
* Throws for every failure — an absent path, a directory, a file too large for
|
|
1225
|
+
* the contents endpoint to inline, an unreachable or unauthorized API. The
|
|
1226
|
+
* caller decides what an unreadable authority means; it is never an absent one.
|
|
1227
|
+
*/
|
|
1228
|
+
const fetchGitHubFileAtRef = (repository, filePath, ref, cwd) => {
|
|
1229
|
+
const encodedPath = filePath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
1230
|
+
const response = repositoryFileSchema.parse(runGhJsonAt(["api", `/repos/${repository.owner}/${repository.name}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`], cwd));
|
|
1231
|
+
return Buffer$1.from(response.content, "base64").toString("utf-8");
|
|
1232
|
+
};
|
|
1203
1233
|
/**
|
|
1204
1234
|
* Fetch and normalize the issue/PR comment thread for `<owner>/<repo>`.
|
|
1205
1235
|
*
|
|
@@ -1260,15 +1290,15 @@ function matchesAnyGlob(patterns, file) {
|
|
|
1260
1290
|
}
|
|
1261
1291
|
//#endregion
|
|
1262
1292
|
//#region src/diff-classification.ts
|
|
1263
|
-
const DIFF_CLASSIFICATIONS
|
|
1293
|
+
const DIFF_CLASSIFICATIONS = [
|
|
1264
1294
|
"docs/process-only",
|
|
1265
1295
|
"trivial",
|
|
1266
1296
|
"non-trivial"
|
|
1267
1297
|
];
|
|
1268
1298
|
//#endregion
|
|
1269
1299
|
//#region src/required-check-policy.ts
|
|
1270
|
-
const EVIDENCE_CHECK_TYPES
|
|
1271
|
-
const requiredCheckScopeSchema
|
|
1300
|
+
const EVIDENCE_CHECK_TYPES = ["review", "verify"];
|
|
1301
|
+
const requiredCheckScopeSchema = z.object({ classifications: z.array(z.enum(DIFF_CLASSIFICATIONS)).min(1) }).strict();
|
|
1272
1302
|
const requiredCheckInScope = ({ context, scope }) => {
|
|
1273
1303
|
if (!scope) return {
|
|
1274
1304
|
inScope: true,
|
|
@@ -1316,6 +1346,20 @@ const commandSchema = z.object({
|
|
|
1316
1346
|
]).default("full")
|
|
1317
1347
|
});
|
|
1318
1348
|
const conditionalReviewSchema = z.object({ paths: z.array(z.string().min(1).refine(isValidGlob, "must be a valid glob")).min(1) }).strict();
|
|
1349
|
+
/**
|
|
1350
|
+
* The `review` subtree: the repository-declared paths that additionally demand
|
|
1351
|
+
* a security review.
|
|
1352
|
+
*
|
|
1353
|
+
* Exported so the authoritative-base-policy reader (#922) can validate the
|
|
1354
|
+
* subtree WITHOUT the rest of the profile. That reader needs this fact and only
|
|
1355
|
+
* this fact, and it reads a profile committed on the base ref, which may sit at
|
|
1356
|
+
* the previous `schemaVersion` while a bump is in flight. Validating the whole
|
|
1357
|
+
* document there would refuse every open pull request in a repository for the
|
|
1358
|
+
* length of a schema bump, with no in-factory route to land the fix (the sf#489
|
|
1359
|
+
* class: a guardrail must not lock the operator out). One definition, two parse
|
|
1360
|
+
* scopes.
|
|
1361
|
+
*/
|
|
1362
|
+
const reviewPolicySchema = z.object({ conditional: z.array(conditionalReviewSchema).min(1) }).strict();
|
|
1319
1363
|
const hqEndpointDeclarationSchema = z.intersection(z.string().url(), z.string().regex(/^https:\/\/[^/@\s]+(?:[/?#]|$)/u, "Must use HTTPS without embedded credentials"));
|
|
1320
1364
|
const hqIngestConfigSchema = z.object({
|
|
1321
1365
|
enabled: z.boolean(),
|
|
@@ -1328,7 +1372,7 @@ const hqIngestConfigSchema = z.object({
|
|
|
1328
1372
|
}
|
|
1329
1373
|
}, "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.")
|
|
1330
1374
|
}).strict();
|
|
1331
|
-
const profileRequiredCheckScopeSchema = requiredCheckScopeSchema
|
|
1375
|
+
const profileRequiredCheckScopeSchema = requiredCheckScopeSchema.extend({ classifications: z.array(z.enum(["docs/process-only", "non-trivial"])).min(1) }).strict();
|
|
1332
1376
|
const requiredCheckSchema = z.object({
|
|
1333
1377
|
name: z.string().min(1),
|
|
1334
1378
|
scope: profileRequiredCheckScopeSchema.optional()
|
|
@@ -1369,7 +1413,7 @@ const factoryProjectProfileSchema = z.object({
|
|
|
1369
1413
|
message: "requiredChecks entries must have distinct names."
|
|
1370
1414
|
});
|
|
1371
1415
|
}).optional(),
|
|
1372
|
-
review:
|
|
1416
|
+
review: reviewPolicySchema.optional(),
|
|
1373
1417
|
schemaVersion: z.literal(6),
|
|
1374
1418
|
verification: z.object({ commands: z.array(commandSchema).min(1) })
|
|
1375
1419
|
}).strict().superRefine((profile, context) => {
|
|
@@ -3765,7 +3809,7 @@ const qualifierIsMintable = (family, qualifier) => {
|
|
|
3765
3809
|
if (!QUALIFIER_PATTERN.test(qualifier)) return false;
|
|
3766
3810
|
const decoded = decodeQualifier(qualifier);
|
|
3767
3811
|
if (decoded === void 0 || demandQualifier(decoded) !== qualifier) return false;
|
|
3768
|
-
if (family === "review-rung") return EVIDENCE_REVIEW_RUNGS
|
|
3812
|
+
if (family === "review-rung") return EVIDENCE_REVIEW_RUNGS.includes(decoded);
|
|
3769
3813
|
if (family === "review-mode") return REVIEW_MODE_DEMAND_VALUES.includes(decoded);
|
|
3770
3814
|
return true;
|
|
3771
3815
|
};
|
|
@@ -3928,8 +3972,64 @@ const authorizeDemandWaiver = ({ authenticatedLogin, session }) => {
|
|
|
3928
3972
|
} };
|
|
3929
3973
|
};
|
|
3930
3974
|
//#endregion
|
|
3975
|
+
//#region src/policy-resolution.ts
|
|
3976
|
+
const COMMIT_SHA_PATTERN$2 = /^[0-9a-f]{40}$/u;
|
|
3977
|
+
const digestSchema = z.string().regex(/^[0-9a-f]{64}$/u);
|
|
3978
|
+
/**
|
|
3979
|
+
* The authority resolved: the live tip of the PR's own base ref served a
|
|
3980
|
+
* parseable profile, and demand was resolved from the union of that policy and
|
|
3981
|
+
* the candidate's.
|
|
3982
|
+
*
|
|
3983
|
+
* Every identity field is REQUIRED. A cross-stage reader compares these digests
|
|
3984
|
+
* to decide whether two stages judged the same candidate under the same policy;
|
|
3985
|
+
* an `authoritative` record with a digest missing would read to that reader as
|
|
3986
|
+
* "nothing disagrees", which is a silent downgrade of exactly the demand this
|
|
3987
|
+
* record exists to protect. `reason` is forbidden here — a resolved authority
|
|
3988
|
+
* has nothing to explain.
|
|
3989
|
+
*/
|
|
3990
|
+
const authoritativePolicyResolutionSchema = z.object({
|
|
3991
|
+
/** sha256 of the canonical base `review` subtree. */
|
|
3992
|
+
basePolicyDigest: digestSchema,
|
|
3993
|
+
/** The PR's own base ref, whose live tip is the authority. */
|
|
3994
|
+
baseRefName: z.string().min(1),
|
|
3995
|
+
/** sha256 of the canonical candidate `review` subtree. */
|
|
3996
|
+
candidatePolicyDigest: digestSchema,
|
|
3997
|
+
/** sha256 of the merged (base ∪ candidate) protected-path set. */
|
|
3998
|
+
effectiveDigest: digestSchema,
|
|
3999
|
+
/** The base-ref tip the base policy was read at. */
|
|
4000
|
+
policyBaseSha: z.string().regex(COMMIT_SHA_PATTERN$2),
|
|
4001
|
+
/**
|
|
4002
|
+
* Whether the candidate's `review` subtree differs from the base's. The
|
|
4003
|
+
* digests prove which policies were read; this says whether they agreed,
|
|
4004
|
+
* which is the fact that explains a demand the candidate's own profile
|
|
4005
|
+
* would not have produced.
|
|
4006
|
+
*/
|
|
4007
|
+
reviewPolicyChanged: z.boolean(),
|
|
4008
|
+
status: z.literal("authoritative")
|
|
4009
|
+
}).strict();
|
|
4010
|
+
/**
|
|
4011
|
+
* The authority could not be read, and `reason` says why — required, because an
|
|
4012
|
+
* unresolved record with no reason is an unactionable refusal. `pr:ready`
|
|
4013
|
+
* refuses before it writes a proof, so a Slice A ready proof never carries one;
|
|
4014
|
+
* this member is the recorded form of that refusal for the stages that carry it
|
|
4015
|
+
* forward.
|
|
4016
|
+
*/
|
|
4017
|
+
const unresolvedPolicyResolutionSchema = z.object({
|
|
4018
|
+
/** The base ref whose policy could not be read. */
|
|
4019
|
+
baseRefName: z.string().min(1),
|
|
4020
|
+
/** Why the authority is unresolved. */
|
|
4021
|
+
reason: z.string().min(1),
|
|
4022
|
+
status: z.literal("unresolved")
|
|
4023
|
+
}).strict();
|
|
4024
|
+
/** How a gate resolved the review policy it evaluated a candidate under. */
|
|
4025
|
+
const policyResolutionSchema = z.discriminatedUnion("status", [authoritativePolicyResolutionSchema, unresolvedPolicyResolutionSchema]);
|
|
4026
|
+
//#endregion
|
|
3931
4027
|
//#region src/packages/review-prompt-sections/schema.ts
|
|
3932
|
-
const REVIEW_PROMPT_SECTION_PROVENANCES = [
|
|
4028
|
+
const REVIEW_PROMPT_SECTION_PROVENANCES = [
|
|
4029
|
+
"coding-standards",
|
|
4030
|
+
"prior-review-ledger",
|
|
4031
|
+
"issue-review-focus"
|
|
4032
|
+
];
|
|
3933
4033
|
const nonBlankString$2 = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
|
|
3934
4034
|
const reviewPromptSectionSchema = z.object({
|
|
3935
4035
|
provenance: z.enum(REVIEW_PROMPT_SECTION_PROVENANCES),
|
|
@@ -4780,7 +4880,7 @@ const prReviewResultSchema = z.object({
|
|
|
4780
4880
|
]),
|
|
4781
4881
|
producer: nonBlankString$1.optional(),
|
|
4782
4882
|
promptSections: reviewPromptSectionsSchema.optional(),
|
|
4783
|
-
rung: z.enum(EVIDENCE_REVIEW_RUNGS
|
|
4883
|
+
rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
|
|
4784
4884
|
sessionId: nonBlankString$1.optional(),
|
|
4785
4885
|
startedAt: parseableDateString,
|
|
4786
4886
|
summary: z.string().refine((value) => value.trim().length > 0 && value !== "<one sentence>", { message: "summary must not be blank or a placeholder" }),
|
|
@@ -4867,6 +4967,47 @@ const PR_REVIEW_PROOF_DESCRIPTOR = {
|
|
|
4867
4967
|
schemaVersion: 2
|
|
4868
4968
|
};
|
|
4869
4969
|
//#endregion
|
|
4970
|
+
//#region src/review-proof-applicability.ts
|
|
4971
|
+
const REVIEW_STATUS_VALUES = [
|
|
4972
|
+
"not-required",
|
|
4973
|
+
"current",
|
|
4974
|
+
"stale",
|
|
4975
|
+
"missing",
|
|
4976
|
+
"blocked"
|
|
4977
|
+
];
|
|
4978
|
+
const reviewStatusSchema = z.enum(REVIEW_STATUS_VALUES);
|
|
4979
|
+
const isProofCurrent = ({ reviewedHeadSha, reviewedPatchId, headSha, patchId }) => evidenceFresh({
|
|
4980
|
+
candidate: {
|
|
4981
|
+
headSha,
|
|
4982
|
+
patchId
|
|
4983
|
+
},
|
|
4984
|
+
recorded: {
|
|
4985
|
+
headSha: reviewedHeadSha,
|
|
4986
|
+
patchId: reviewedPatchId
|
|
4987
|
+
},
|
|
4988
|
+
rule: "patch-id-or-head-sha"
|
|
4989
|
+
});
|
|
4990
|
+
const reviewStatus = ({ blocked, required, reviewedHeadSha, reviewedPatchId, headSha, patchId }) => {
|
|
4991
|
+
if (blocked) {
|
|
4992
|
+
if ((reviewedHeadSha || reviewedPatchId) && !isProofCurrent({
|
|
4993
|
+
headSha,
|
|
4994
|
+
patchId,
|
|
4995
|
+
reviewedHeadSha,
|
|
4996
|
+
reviewedPatchId
|
|
4997
|
+
})) return "stale";
|
|
4998
|
+
return "blocked";
|
|
4999
|
+
}
|
|
5000
|
+
if (!required) return "not-required";
|
|
5001
|
+
if (!(reviewedHeadSha || reviewedPatchId)) return "missing";
|
|
5002
|
+
if (isProofCurrent({
|
|
5003
|
+
headSha,
|
|
5004
|
+
patchId,
|
|
5005
|
+
reviewedHeadSha,
|
|
5006
|
+
reviewedPatchId
|
|
5007
|
+
})) return "current";
|
|
5008
|
+
return "stale";
|
|
5009
|
+
};
|
|
5010
|
+
//#endregion
|
|
4870
5011
|
//#region src/review-state.ts
|
|
4871
5012
|
const reviewStateIssue = (message, field) => [{
|
|
4872
5013
|
code: "custom",
|
|
@@ -5783,24 +5924,12 @@ const closeoutArtifactSchema = z.object({
|
|
|
5783
5924
|
}).strict();
|
|
5784
5925
|
//#endregion
|
|
5785
5926
|
//#region src/schemas.ts
|
|
5786
|
-
const
|
|
5787
|
-
const
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
const DIFF_CLASSIFICATIONS = [
|
|
5793
|
-
"docs/process-only",
|
|
5794
|
-
"trivial",
|
|
5795
|
-
"non-trivial"
|
|
5796
|
-
];
|
|
5797
|
-
const REVIEW_STATUS_VALUES$1 = [
|
|
5798
|
-
"not-required",
|
|
5799
|
-
"current",
|
|
5800
|
-
"stale",
|
|
5801
|
-
"missing",
|
|
5802
|
-
"blocked"
|
|
5803
|
-
];
|
|
5927
|
+
const evidenceCheckTypeSchema = z.enum(EVIDENCE_CHECK_TYPES);
|
|
5928
|
+
const evidenceReviewRungSchema = z.enum(EVIDENCE_REVIEW_RUNGS);
|
|
5929
|
+
const diffClassificationSchema = z.enum(DIFF_CLASSIFICATIONS);
|
|
5930
|
+
const ladderStageSchema = z.enum(LADDER_STAGES);
|
|
5931
|
+
const ladderNextActionSchema = z.enum(LADDER_NEXT_ACTION_KINDS);
|
|
5932
|
+
const ladderForcedTransitionSchema = z.enum(LADDER_FORCED_TRANSITION_KINDS);
|
|
5804
5933
|
const followUpActionSchema = z.object({
|
|
5805
5934
|
argv: z.array(z.string()),
|
|
5806
5935
|
command: z.string()
|
|
@@ -5808,7 +5937,7 @@ const followUpActionSchema = z.object({
|
|
|
5808
5937
|
const evidenceShaSchema = z.string().regex(/^[0-9a-f]{7,40}$/u, { message: "must be a 7-40 char lowercase hex git SHA" });
|
|
5809
5938
|
z.object({
|
|
5810
5939
|
check: z.string().min(1),
|
|
5811
|
-
checkType:
|
|
5940
|
+
checkType: evidenceCheckTypeSchema,
|
|
5812
5941
|
findingsPointer: z.string().min(1).optional(),
|
|
5813
5942
|
headSha: evidenceShaSchema,
|
|
5814
5943
|
mergeBaseSha: evidenceShaSchema,
|
|
@@ -5818,7 +5947,7 @@ z.object({
|
|
|
5818
5947
|
policyVersion: z.string().min(1).optional(),
|
|
5819
5948
|
producer: z.string().min(1),
|
|
5820
5949
|
requestId: z.string().min(1).optional(),
|
|
5821
|
-
rung:
|
|
5950
|
+
rung: evidenceReviewRungSchema.optional(),
|
|
5822
5951
|
schemaVersion: z.literal(1),
|
|
5823
5952
|
sessionId: z.string().trim().min(1).optional(),
|
|
5824
5953
|
timestamp: z.iso.datetime().optional()
|
|
@@ -5881,7 +6010,7 @@ z.object({
|
|
|
5881
6010
|
}).passthrough(),
|
|
5882
6011
|
outcome: z.enum(["pass", "fail"]),
|
|
5883
6012
|
producer: z.string().min(1),
|
|
5884
|
-
rung:
|
|
6013
|
+
rung: evidenceReviewRungSchema,
|
|
5885
6014
|
schemaVersion: z.literal(1),
|
|
5886
6015
|
sessionId: z.string().min(1).optional(),
|
|
5887
6016
|
specHash: z.object({
|
|
@@ -5901,7 +6030,7 @@ const boundaryCheckProofSchema = z.object({
|
|
|
5901
6030
|
state: z.enum(["merged", "open"]),
|
|
5902
6031
|
wave: z.string().min(1)
|
|
5903
6032
|
})),
|
|
5904
|
-
demandedRung:
|
|
6033
|
+
demandedRung: evidenceReviewRungSchema.optional(),
|
|
5905
6034
|
epicIssue: z.number().int().positive(),
|
|
5906
6035
|
manifestHash: z.object({
|
|
5907
6036
|
algo: z.literal("sha256"),
|
|
@@ -5913,7 +6042,7 @@ const boundaryCheckProofSchema = z.object({
|
|
|
5913
6042
|
commentId: z.union([z.string(), z.number()]).optional(),
|
|
5914
6043
|
commentUrl: z.string().optional(),
|
|
5915
6044
|
producer: z.string().min(1),
|
|
5916
|
-
rung:
|
|
6045
|
+
rung: evidenceReviewRungSchema,
|
|
5917
6046
|
sessionId: z.string().min(1).optional(),
|
|
5918
6047
|
specHash: z.object({
|
|
5919
6048
|
algo: z.literal("sha256"),
|
|
@@ -5928,7 +6057,6 @@ const boundaryCheckProofSchema = z.object({
|
|
|
5928
6057
|
}).optional(),
|
|
5929
6058
|
status: z.enum(["ready", "blocked"])
|
|
5930
6059
|
});
|
|
5931
|
-
const requiredCheckScopeSchema = z.object({ classifications: z.array(z.enum(DIFF_CLASSIFICATIONS)).min(1) }).strict();
|
|
5932
6060
|
const readinessRepairSchema$1 = z.object({
|
|
5933
6061
|
action: z.string().min(1),
|
|
5934
6062
|
code: z.enum([
|
|
@@ -5942,7 +6070,7 @@ const reviewStateSchema$1 = z.object({
|
|
|
5942
6070
|
required: z.boolean(),
|
|
5943
6071
|
reviewedHeadSha: z.string().optional(),
|
|
5944
6072
|
reviewedPatchId: z.string().optional(),
|
|
5945
|
-
status:
|
|
6073
|
+
status: reviewStatusSchema
|
|
5946
6074
|
}).strict().superRefine((state, context) => {
|
|
5947
6075
|
for (const issue of reviewStateIssues(state)) context.addIssue(issue);
|
|
5948
6076
|
});
|
|
@@ -5963,9 +6091,9 @@ const historicalHandledHumanCommentSchema = z.object({
|
|
|
5963
6091
|
const managedReadinessLedgerSchema$1 = z.object({
|
|
5964
6092
|
baseSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
5965
6093
|
blockingReasons: z.array(z.string()),
|
|
5966
|
-
classification:
|
|
6094
|
+
classification: diffClassificationSchema,
|
|
5967
6095
|
externalChecks: z.array(z.object({
|
|
5968
|
-
checkType:
|
|
6096
|
+
checkType: evidenceCheckTypeSchema,
|
|
5969
6097
|
name: z.string().min(1),
|
|
5970
6098
|
reason: z.string().optional(),
|
|
5971
6099
|
scope: requiredCheckScopeSchema.optional(),
|
|
@@ -6038,25 +6166,9 @@ const managedReadinessLedgerSchema$1 = z.object({
|
|
|
6038
6166
|
gate: z.number().int().nonnegative(),
|
|
6039
6167
|
interior: z.number().int().nonnegative()
|
|
6040
6168
|
}),
|
|
6041
|
-
forcedTransition:
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
"interior-takeover"
|
|
6045
|
-
]).optional(),
|
|
6046
|
-
nextAction: z.enum([
|
|
6047
|
-
"run-interior-cycle",
|
|
6048
|
-
"advance-to-gate",
|
|
6049
|
-
"run-gate-cycle",
|
|
6050
|
-
"accept-nonblocking-findings",
|
|
6051
|
-
"escalate-to-triage",
|
|
6052
|
-
"ready-for-human"
|
|
6053
|
-
]),
|
|
6054
|
-
stage: z.enum([
|
|
6055
|
-
"interior",
|
|
6056
|
-
"interior-takeover",
|
|
6057
|
-
"interior-complete",
|
|
6058
|
-
"gate"
|
|
6059
|
-
])
|
|
6169
|
+
forcedTransition: ladderForcedTransitionSchema.optional(),
|
|
6170
|
+
nextAction: ladderNextActionSchema,
|
|
6171
|
+
stage: ladderStageSchema
|
|
6060
6172
|
}).optional(),
|
|
6061
6173
|
reviewRuns: z.array(prReviewResultSchema).optional(),
|
|
6062
6174
|
reviewTerminalState: z.enum([
|
|
@@ -6122,6 +6234,7 @@ z.object({
|
|
|
6122
6234
|
humanBlockingReasons: z.array(z.string()).default([]),
|
|
6123
6235
|
ledger: managedReadinessLedgerSchema$1,
|
|
6124
6236
|
notices: z.array(z.string().min(1)).optional(),
|
|
6237
|
+
policyResolution: policyResolutionSchema.optional(),
|
|
6125
6238
|
profileBlobSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
6126
6239
|
profilePath: z.string().min(1).optional(),
|
|
6127
6240
|
repairs: z.array(readinessRepairSchema$1).default([]),
|
|
@@ -9687,10 +9800,10 @@ const recognizeCatchUpCandidate = ({ base, cwd }) => {
|
|
|
9687
9800
|
//#endregion
|
|
9688
9801
|
//#region src/pr-readiness/external-evidence.ts
|
|
9689
9802
|
var external_evidence_exports = /* @__PURE__ */ __exportAll({
|
|
9690
|
-
EVIDENCE_CHECK_TYPES: () => EVIDENCE_CHECK_TYPES
|
|
9803
|
+
EVIDENCE_CHECK_TYPES: () => EVIDENCE_CHECK_TYPES,
|
|
9691
9804
|
EVIDENCE_DIR: () => EVIDENCE_DIR,
|
|
9692
9805
|
EVIDENCE_ENVELOPE_SCHEMA_VERSION: () => 1,
|
|
9693
|
-
EVIDENCE_REVIEW_RUNGS: () => EVIDENCE_REVIEW_RUNGS
|
|
9806
|
+
EVIDENCE_REVIEW_RUNGS: () => EVIDENCE_REVIEW_RUNGS,
|
|
9694
9807
|
boundFailingCheckNames: () => boundFailingCheckNames,
|
|
9695
9808
|
evaluateRequiredChecks: () => evaluateRequiredChecks,
|
|
9696
9809
|
evidenceBindingFresh: () => evidenceBindingFresh,
|
|
@@ -9699,13 +9812,13 @@ var external_evidence_exports = /* @__PURE__ */ __exportAll({
|
|
|
9699
9812
|
loadEvidenceEnvelopes: () => loadEvidenceEnvelopes,
|
|
9700
9813
|
parseEvidenceEnvelope: () => parseEvidenceEnvelope,
|
|
9701
9814
|
requiredCheckInScope: () => requiredCheckInScope,
|
|
9702
|
-
requiredCheckScopeSchema: () => requiredCheckScopeSchema
|
|
9815
|
+
requiredCheckScopeSchema: () => requiredCheckScopeSchema,
|
|
9703
9816
|
resolveAuthoringSessionIds: () => resolveAuthoringSessionIds
|
|
9704
9817
|
});
|
|
9705
9818
|
const shaSchema$4 = z.string().regex(/^[0-9a-f]{7,40}$/u, { message: "must be a 7-40 char lowercase hex git SHA" });
|
|
9706
9819
|
const evidenceEnvelopeSchema = z.object({
|
|
9707
9820
|
check: z.string().min(1),
|
|
9708
|
-
checkType: z.enum(EVIDENCE_CHECK_TYPES
|
|
9821
|
+
checkType: z.enum(EVIDENCE_CHECK_TYPES),
|
|
9709
9822
|
findingsPointer: z.string().min(1).optional(),
|
|
9710
9823
|
headSha: shaSchema$4,
|
|
9711
9824
|
mergeBaseSha: shaSchema$4,
|
|
@@ -9715,7 +9828,7 @@ const evidenceEnvelopeSchema = z.object({
|
|
|
9715
9828
|
policyVersion: z.string().min(1).optional(),
|
|
9716
9829
|
producer: z.string().min(1),
|
|
9717
9830
|
requestId: z.string().min(1).optional(),
|
|
9718
|
-
rung: z.enum(EVIDENCE_REVIEW_RUNGS
|
|
9831
|
+
rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
|
|
9719
9832
|
schemaVersion: z.literal(1),
|
|
9720
9833
|
sessionId: z.string().trim().min(1).optional(),
|
|
9721
9834
|
timestamp: z.iso.datetime().optional()
|
|
@@ -10207,21 +10320,29 @@ const readExchange = async (request, url, init, timeoutMs) => {
|
|
|
10207
10320
|
async function requestGitHubJson(request, url, init, timeoutMs = GITHUB_PUBLISH_TIMEOUT_MS) {
|
|
10208
10321
|
return (await readExchange(request, url, init, timeoutMs)).json;
|
|
10209
10322
|
}
|
|
10210
|
-
/**
|
|
10211
|
-
|
|
10323
|
+
/**
|
|
10324
|
+
* Mint one repository-scoped installation token for the Patronage Factory App.
|
|
10325
|
+
*
|
|
10326
|
+
* `budget.transportAttempts` is the caller's, not this function's: a proof
|
|
10327
|
+
* registration must survive a stale keep-alive socket, while a best-effort
|
|
10328
|
+
* publish with a fallback must give up at once. Omitted, it stays at the
|
|
10329
|
+
* package default of three.
|
|
10330
|
+
*/
|
|
10331
|
+
const mintFactoryInstallationToken = (repository, config, request, now, budget = {}) => mintInstallationToken({
|
|
10212
10332
|
credentials: config,
|
|
10213
10333
|
owner: repository.owner,
|
|
10214
10334
|
repo: repository.repo
|
|
10215
10335
|
}, {
|
|
10216
10336
|
fetch: request,
|
|
10217
10337
|
now: () => now,
|
|
10218
|
-
timeoutMs
|
|
10338
|
+
timeoutMs: budget.timeoutMs ?? 5e3,
|
|
10339
|
+
...budget.transportAttempts === void 0 ? {} : { transportAttempts: budget.transportAttempts }
|
|
10219
10340
|
});
|
|
10220
10341
|
const resolveFactoryAppToken = async (input, dependencies, request, timeoutMs) => {
|
|
10221
10342
|
if (dependencies.token) return dependencies.token;
|
|
10222
10343
|
const config = dependencies.githubApp ?? tryUserConfig()?.config.githubApp;
|
|
10223
10344
|
if (!config) throw new Error("The Patronage Factory GitHub App is required for App-owned GitHub requests.");
|
|
10224
|
-
return mintFactoryInstallationToken(input, config, request, (dependencies.now ?? Date.now)(), timeoutMs);
|
|
10345
|
+
return mintFactoryInstallationToken(input, config, request, (dependencies.now ?? Date.now)(), { timeoutMs });
|
|
10225
10346
|
};
|
|
10226
10347
|
const factoryAppInit = (input, token) => ({
|
|
10227
10348
|
...input.body === void 0 ? {} : { body: JSON.stringify(input.body) },
|
|
@@ -10272,7 +10393,7 @@ const outcomeFor = (value) => {
|
|
|
10272
10393
|
if (isOutcome(value.outcome)) return value.outcome;
|
|
10273
10394
|
return value.schemaVersion === 1 ? "passed" : "aborted";
|
|
10274
10395
|
};
|
|
10275
|
-
const asClassification = (value) => DIFF_CLASSIFICATIONS
|
|
10396
|
+
const asClassification = (value) => DIFF_CLASSIFICATIONS.includes(value) ? value : "unknown";
|
|
10276
10397
|
/**
|
|
10277
10398
|
* Project a local pr:verify proof onto the durable check-run payload. Accepts
|
|
10278
10399
|
* `unknown` because the publisher is handed an opaque proof value; anything
|
|
@@ -10402,8 +10523,16 @@ Re-verify only if the tree changes: a new commit, an amend, or a rebase.
|
|
|
10402
10523
|
* had grown a second copy of it. What stays here is what is this repository's:
|
|
10403
10524
|
* where the credentials come from (`user-config`), and the publish timeout the
|
|
10404
10525
|
* rest of these calls are bound by. Nothing is cached, as before.
|
|
10526
|
+
*
|
|
10527
|
+
* Retry budget is the caller's. The fire-and-forget publisher passes one
|
|
10528
|
+
* attempt because it already has a commit-status fallback (#938). The
|
|
10529
|
+
* confirmation read-back leaves the default, because it has no fallback and
|
|
10530
|
+
* a stale keep-alive is the class #923 covers.
|
|
10405
10531
|
*/
|
|
10406
|
-
const installationToken = (repository, config, request, now,
|
|
10532
|
+
const installationToken = (repository, config, request, now, budget = {}) => mintFactoryInstallationToken(repository, config, request, now, {
|
|
10533
|
+
timeoutMs: budget.timeoutMs ?? 5e3,
|
|
10534
|
+
...budget.transportAttempts === void 0 ? {} : { transportAttempts: budget.transportAttempts }
|
|
10535
|
+
});
|
|
10407
10536
|
function verifyOutput(proof) {
|
|
10408
10537
|
const value = proof;
|
|
10409
10538
|
const commands = value.executedCommands?.length ?? 0;
|
|
@@ -10491,7 +10620,10 @@ async function postFactoryCheckRun({ config, deadlineMs, dependencies, detailsUr
|
|
|
10491
10620
|
const request = dependencies.fetch ?? fetch;
|
|
10492
10621
|
const timeoutMs = dependencies.timeoutMs ?? 5e3;
|
|
10493
10622
|
try {
|
|
10494
|
-
const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(),
|
|
10623
|
+
const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(), {
|
|
10624
|
+
timeoutMs,
|
|
10625
|
+
transportAttempts: 1
|
|
10626
|
+
});
|
|
10495
10627
|
return await requestGitHubJson(request, `https://api.github.com/repos/${input.owner}/${input.repo}/check-runs`, {
|
|
10496
10628
|
body: JSON.stringify(checkRunRequestBody(input, detailsUrl)),
|
|
10497
10629
|
headers: {
|
|
@@ -10582,7 +10714,7 @@ async function resolveDetailsUrlSafely(input, dependencies, onDiagnostic) {
|
|
|
10582
10714
|
async function servedFactoryCheckRun(input, config, dependencies) {
|
|
10583
10715
|
const request = dependencies.fetch ?? fetch;
|
|
10584
10716
|
const timeoutMs = dependencies.timeoutMs ?? 5e3;
|
|
10585
|
-
const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(), timeoutMs);
|
|
10717
|
+
const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(), { timeoutMs });
|
|
10586
10718
|
const query = new URLSearchParams({
|
|
10587
10719
|
app_id: String(config.appId),
|
|
10588
10720
|
check_name: FACTORY_CHECK_NAMES[input.gate],
|
|
@@ -11817,6 +11949,204 @@ function createDemandWaiveCommand(_output, action = runDemandWaive) {
|
|
|
11817
11949
|
const resolveApplicableReviewModes = (profile, changedFiles) => {
|
|
11818
11950
|
return (profile.review?.conditional ?? []).some((scope) => changedFiles.some((file) => matchesAnyGlob(scope.paths, file))) ? ["correctness", "security"] : ["correctness"];
|
|
11819
11951
|
};
|
|
11952
|
+
const compareStrings = (left, right) => {
|
|
11953
|
+
if (left === right) return 0;
|
|
11954
|
+
return left < right ? -1 : 1;
|
|
11955
|
+
};
|
|
11956
|
+
/**
|
|
11957
|
+
* The `review` subtree as one comparable string.
|
|
11958
|
+
*
|
|
11959
|
+
* Groups are anonymous in schema v6 (`paths` only, `additionalProperties:
|
|
11960
|
+
* false`), so a group has no identity beyond its path list. Paths are sorted
|
|
11961
|
+
* inside each group and groups are sorted by their path list, so a pure
|
|
11962
|
+
* reordering is the same policy and only a real change reads as a change.
|
|
11963
|
+
* `undefined` (no `review` object at all) canonicalizes to `null`, which is
|
|
11964
|
+
* distinct from every declared policy including an empty one.
|
|
11965
|
+
*/
|
|
11966
|
+
const canonicalReviewPolicy = (review) => {
|
|
11967
|
+
if (review === void 0) return JSON.stringify(null);
|
|
11968
|
+
const groups = review.conditional.map((group) => ({ paths: [...group.paths].toSorted(compareStrings) }));
|
|
11969
|
+
return JSON.stringify({ conditional: groups.toSorted((left, right) => compareStrings(JSON.stringify(left.paths), JSON.stringify(right.paths))) });
|
|
11970
|
+
};
|
|
11971
|
+
/** Every protected path a policy declares, across all of its groups. */
|
|
11972
|
+
const reviewPolicyPaths = (review) => (review?.conditional ?? []).flatMap((group) => group.paths);
|
|
11973
|
+
/**
|
|
11974
|
+
* Resolve review demand from the base policy and the candidate policy.
|
|
11975
|
+
*
|
|
11976
|
+
* Two terms, both decided:
|
|
11977
|
+
*
|
|
11978
|
+
* 1. The merge rule is the UNION of the flattened protected-path sets, matched
|
|
11979
|
+
* by the same globs as before. Policy may add demand; it can never remove
|
|
11980
|
+
* demand the base established for this candidate.
|
|
11981
|
+
* 2. Self-protection: a candidate whose `review` subtree differs from the
|
|
11982
|
+
* base's demands a security review whatever else it touches. It is scoped
|
|
11983
|
+
* to `review` and not to the whole profile deliberately — widening it would
|
|
11984
|
+
* make every `verification.commands` edit owe a security review.
|
|
11985
|
+
*
|
|
11986
|
+
* The demand is not scoped by classification. A docs-only candidate that
|
|
11987
|
+
* touches a protected path still owes the security review: classification
|
|
11988
|
+
* scopes verification work, not review authority.
|
|
11989
|
+
*/
|
|
11990
|
+
const resolveAuthoritativeReviewDemand = ({ basePolicy, candidatePolicy, changedFiles }) => {
|
|
11991
|
+
const baseCanonical = canonicalReviewPolicy(basePolicy);
|
|
11992
|
+
const candidateCanonical = canonicalReviewPolicy(candidatePolicy);
|
|
11993
|
+
const reviewPolicyChanged = baseCanonical !== candidateCanonical;
|
|
11994
|
+
const candidatePaths = reviewPolicyPaths(candidatePolicy);
|
|
11995
|
+
const effectivePaths = [...new Set([...reviewPolicyPaths(basePolicy), ...candidatePaths])].toSorted(compareStrings);
|
|
11996
|
+
const candidateOwned = new Set(candidatePaths);
|
|
11997
|
+
const baseOnlyMatchedPaths = effectivePaths.filter((protectedPath) => !candidateOwned.has(protectedPath) && changedFiles.some((file) => matchesAnyGlob([protectedPath], file)));
|
|
11998
|
+
const candidateAloneDemandsSecurity = changedFiles.some((file) => matchesAnyGlob(candidatePaths, file));
|
|
11999
|
+
const securityApplies = reviewPolicyChanged || candidateAloneDemandsSecurity || baseOnlyMatchedPaths.length > 0;
|
|
12000
|
+
return {
|
|
12001
|
+
baseOnlyMatchedPaths,
|
|
12002
|
+
basePolicyDigest: sha256Hex(baseCanonical),
|
|
12003
|
+
candidateAloneDemandsSecurity,
|
|
12004
|
+
candidatePolicyDigest: sha256Hex(candidateCanonical),
|
|
12005
|
+
effectiveDigest: sha256Hex(JSON.stringify(effectivePaths)),
|
|
12006
|
+
effectivePaths,
|
|
12007
|
+
reviewModes: securityApplies ? ["correctness", "security"] : ["correctness"],
|
|
12008
|
+
reviewPolicyChanged
|
|
12009
|
+
};
|
|
12010
|
+
};
|
|
12011
|
+
/**
|
|
12012
|
+
* Why the security demand rose, when the candidate's own profile would not have
|
|
12013
|
+
* produced it. `undefined` when the candidate's own policy already demanded the
|
|
12014
|
+
* review, or when nothing demanded it — an operator learns nothing from being
|
|
12015
|
+
* told their own profile did what they wrote it to do.
|
|
12016
|
+
*/
|
|
12017
|
+
const baseDerivedSecurityNotice = ({ baseRefName, demand }) => {
|
|
12018
|
+
if (!demand.reviewModes.includes("security") || demand.candidateAloneDemandsSecurity) return;
|
|
12019
|
+
return `Security review is demanded by the base policy, not by this candidate's own profile: ${[...demand.baseOnlyMatchedPaths.length > 0 ? [`${baseRefName} protects ${demand.baseOnlyMatchedPaths.join(", ")}, and this candidate changes a file it matches`] : [], ...demand.reviewPolicyChanged ? [`this candidate's review policy differs from the one ${baseRefName} declares`] : []].join("; ")}. Policy may add review demand; it cannot remove demand the base established (#922).`;
|
|
12020
|
+
};
|
|
12021
|
+
const defaultBaseReviewPolicyAuthority = () => ({
|
|
12022
|
+
branchTipSha: ({ branch, cwd, repository }) => fetchGitHubBranchTipSha(repository, branch, cwd),
|
|
12023
|
+
profileContentAtRef: ({ cwd, profilePath, ref, repository }) => fetchGitHubFileAtRef(repository, profilePath, ref, cwd)
|
|
12024
|
+
});
|
|
12025
|
+
const failureText = (error) => error instanceof Error ? error.message : String(error);
|
|
12026
|
+
/**
|
|
12027
|
+
* The base document, read at exactly the scope of the fact being read — and no
|
|
12028
|
+
* wider, but no narrower either.
|
|
12029
|
+
*
|
|
12030
|
+
* NARROW, because the reader needs the `review` subtree and nothing else. It
|
|
12031
|
+
* does NOT pin `schemaVersion` to the CLI's own constant. A base profile one
|
|
12032
|
+
* version behind still answers the question it is being asked, which is what
|
|
12033
|
+
* lets the pull request that bumps the schema — and every pull request open
|
|
12034
|
+
* beside it — reach `pr:ready` at all.
|
|
12035
|
+
*
|
|
12036
|
+
* BOUNDED, because `review` is optional and "no protected paths" is a real,
|
|
12037
|
+
* demand-lowering answer. A schema that accepted any JSON object would read
|
|
12038
|
+
* `{}`, a truncated write, or an unrelated JSON file at that path as a valid
|
|
12039
|
+
* statement that the base protects nothing — a degrade-to-no-base-demand path,
|
|
12040
|
+
* which is the failure this whole change exists to close. So the document must
|
|
12041
|
+
* be RECOGNIZABLE as a factory profile before its `review` subtree is trusted:
|
|
12042
|
+
* `schemaVersion` must be a positive integer, and `repository` must be present
|
|
12043
|
+
* (the caller then checks it against the trusted checkout).
|
|
12044
|
+
*
|
|
12045
|
+
* Nothing about the refusal relaxes: unreadable JSON, a non-object document,
|
|
12046
|
+
* and a `review` value that violates the review shape all still refuse.
|
|
12047
|
+
*/
|
|
12048
|
+
const baseReviewDocumentSchema = z.object({
|
|
12049
|
+
repository: z.object({
|
|
12050
|
+
name: z.string().min(1),
|
|
12051
|
+
owner: z.string().min(1)
|
|
12052
|
+
}).strict(),
|
|
12053
|
+
review: reviewPolicySchema.optional(),
|
|
12054
|
+
schemaVersion: z.number().int().positive()
|
|
12055
|
+
});
|
|
12056
|
+
const parseBaseReviewPolicy = (content) => {
|
|
12057
|
+
let document;
|
|
12058
|
+
try {
|
|
12059
|
+
document = JSON.parse(content);
|
|
12060
|
+
} catch (error) {
|
|
12061
|
+
return {
|
|
12062
|
+
ok: false,
|
|
12063
|
+
reason: `it is not valid JSON (${failureText(error)})`
|
|
12064
|
+
};
|
|
12065
|
+
}
|
|
12066
|
+
const parsed = baseReviewDocumentSchema.safeParse(document);
|
|
12067
|
+
if (!parsed.success) return {
|
|
12068
|
+
ok: false,
|
|
12069
|
+
reason: parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")
|
|
12070
|
+
};
|
|
12071
|
+
return {
|
|
12072
|
+
ok: true,
|
|
12073
|
+
policy: parsed.data.review,
|
|
12074
|
+
repository: parsed.data.repository
|
|
12075
|
+
};
|
|
12076
|
+
};
|
|
12077
|
+
/**
|
|
12078
|
+
* Read the review policy the PR's base ref actually declares, at its live tip.
|
|
12079
|
+
*
|
|
12080
|
+
* The base ref name comes from GitHub, the repository from the trusted
|
|
12081
|
+
* checkout, and the tip from GitHub's ref API — never from the candidate
|
|
12082
|
+
* profile and never from a caller-supplied ref, both of which the candidate
|
|
12083
|
+
* controls. Every failure returns `unresolved` with a reason; the caller
|
|
12084
|
+
* refuses on it. An absent or unparseable profile at the base tip is a
|
|
12085
|
+
* refusal, never "no base demand".
|
|
12086
|
+
*/
|
|
12087
|
+
const readAuthoritativeBasePolicy = ({ authority, baseRefName, crossRepository, cwd, profilePath, repository }) => {
|
|
12088
|
+
if (crossRepository !== false) return {
|
|
12089
|
+
baseRefName,
|
|
12090
|
+
kind: "unresolved",
|
|
12091
|
+
reason: crossRepository === true ? `this pull request is opened from a fork, so its base repository is not the trusted checkout repository ${repositorySlug(repository)}, and base policy must never be resolved from a candidate-controlled fork.` : `GitHub did not state whether this pull request comes from a fork (isCrossRepository was ${JSON.stringify(crossRepository)}), and an unstated answer is not a "no": base policy must never be resolved from a repository that may be a candidate-controlled fork.`
|
|
12092
|
+
};
|
|
12093
|
+
let policyBaseSha;
|
|
12094
|
+
try {
|
|
12095
|
+
policyBaseSha = authority.branchTipSha({
|
|
12096
|
+
branch: baseRefName,
|
|
12097
|
+
cwd,
|
|
12098
|
+
repository
|
|
12099
|
+
});
|
|
12100
|
+
} catch (error) {
|
|
12101
|
+
return {
|
|
12102
|
+
baseRefName,
|
|
12103
|
+
kind: "unresolved",
|
|
12104
|
+
reason: `the live tip of ${baseRefName} could not be read from GitHub: ${failureText(error)}`
|
|
12105
|
+
};
|
|
12106
|
+
}
|
|
12107
|
+
let content;
|
|
12108
|
+
try {
|
|
12109
|
+
content = authority.profileContentAtRef({
|
|
12110
|
+
cwd,
|
|
12111
|
+
profilePath,
|
|
12112
|
+
ref: policyBaseSha,
|
|
12113
|
+
repository
|
|
12114
|
+
});
|
|
12115
|
+
} catch (error) {
|
|
12116
|
+
return {
|
|
12117
|
+
baseRefName,
|
|
12118
|
+
kind: "unresolved",
|
|
12119
|
+
reason: `${profilePath} could not be read at base tip ${policyBaseSha}: ${failureText(error)}`
|
|
12120
|
+
};
|
|
12121
|
+
}
|
|
12122
|
+
const parsed = parseBaseReviewPolicy(content);
|
|
12123
|
+
if (!parsed.ok) return {
|
|
12124
|
+
baseRefName,
|
|
12125
|
+
kind: "unresolved",
|
|
12126
|
+
reason: `${profilePath} at base tip ${policyBaseSha} does not carry a readable review policy: ${parsed.reason}`
|
|
12127
|
+
};
|
|
12128
|
+
if (repositorySlug(parsed.repository) !== repositorySlug(repository)) return {
|
|
12129
|
+
baseRefName,
|
|
12130
|
+
kind: "unresolved",
|
|
12131
|
+
reason: `${profilePath} at base tip ${policyBaseSha} declares repository ${repositorySlug(parsed.repository)}, which is not the trusted checkout repository ${repositorySlug(repository)}.`
|
|
12132
|
+
};
|
|
12133
|
+
return {
|
|
12134
|
+
baseRefName,
|
|
12135
|
+
kind: "resolved",
|
|
12136
|
+
policy: parsed.policy,
|
|
12137
|
+
policyBaseSha
|
|
12138
|
+
};
|
|
12139
|
+
};
|
|
12140
|
+
/** The policy identity a gate records once the authority resolved (#922). */
|
|
12141
|
+
const authoritativePolicyResolution = ({ base, demand }) => ({
|
|
12142
|
+
basePolicyDigest: demand.basePolicyDigest,
|
|
12143
|
+
baseRefName: base.baseRefName,
|
|
12144
|
+
candidatePolicyDigest: demand.candidatePolicyDigest,
|
|
12145
|
+
effectiveDigest: demand.effectiveDigest,
|
|
12146
|
+
policyBaseSha: base.policyBaseSha,
|
|
12147
|
+
reviewPolicyChanged: demand.reviewPolicyChanged,
|
|
12148
|
+
status: "authoritative"
|
|
12149
|
+
});
|
|
11820
12150
|
/**
|
|
11821
12151
|
* Convert schema-v5 profile requiredChecks into the executable evidence
|
|
11822
12152
|
* demands consumed by readiness and its preflight presentation. Schema-v5
|
|
@@ -11866,7 +12196,7 @@ const defaultFetchClosingPullRequests = ({ issue, owner, repo }) => {
|
|
|
11866
12196
|
`issue=${issue}`
|
|
11867
12197
|
]).data?.repository?.issue?.closedByPullRequestsReferences?.nodes ?? [];
|
|
11868
12198
|
};
|
|
11869
|
-
const defaultFetchIssueBody = ({ issue, owner, repo }) => runGhJson([
|
|
12199
|
+
const defaultFetchIssueBody$1 = ({ issue, owner, repo }) => runGhJson([
|
|
11870
12200
|
"issue",
|
|
11871
12201
|
"view",
|
|
11872
12202
|
String(issue),
|
|
@@ -11891,7 +12221,7 @@ const wrapperBoundaryNotices = ({ epic, manifest }) => {
|
|
|
11891
12221
|
* closed: an absent, unparseable, or unmatched manifest binds nothing and
|
|
11892
12222
|
* names its refusal. The boundary issue may be the work issue itself (#789).
|
|
11893
12223
|
*/
|
|
11894
|
-
const resolveWaveDemand = ({ baseRefName, epic, fetchClosingPullRequests = defaultFetchClosingPullRequests, fetchIssueBody = defaultFetchIssueBody, headRefName, owner, pr, repo }) => {
|
|
12224
|
+
const resolveWaveDemand = ({ baseRefName, epic, fetchClosingPullRequests = defaultFetchClosingPullRequests, fetchIssueBody = defaultFetchIssueBody$1, headRefName, owner, pr, repo }) => {
|
|
11895
12225
|
const parsed = parseBoundaryManifest(fetchIssueBody({
|
|
11896
12226
|
issue: epic,
|
|
11897
12227
|
owner,
|
|
@@ -12152,47 +12482,6 @@ function hqIngestCredentialCheck({ env, resolve = defaultSecretReferenceProbe, r
|
|
|
12152
12482
|
};
|
|
12153
12483
|
}
|
|
12154
12484
|
//#endregion
|
|
12155
|
-
//#region src/review-proof-applicability.ts
|
|
12156
|
-
const REVIEW_STATUS_VALUES = [
|
|
12157
|
-
"not-required",
|
|
12158
|
-
"current",
|
|
12159
|
-
"stale",
|
|
12160
|
-
"missing",
|
|
12161
|
-
"blocked"
|
|
12162
|
-
];
|
|
12163
|
-
const reviewStatusSchema = z.enum(REVIEW_STATUS_VALUES);
|
|
12164
|
-
const isProofCurrent = ({ reviewedHeadSha, reviewedPatchId, headSha, patchId }) => evidenceFresh({
|
|
12165
|
-
candidate: {
|
|
12166
|
-
headSha,
|
|
12167
|
-
patchId
|
|
12168
|
-
},
|
|
12169
|
-
recorded: {
|
|
12170
|
-
headSha: reviewedHeadSha,
|
|
12171
|
-
patchId: reviewedPatchId
|
|
12172
|
-
},
|
|
12173
|
-
rule: "patch-id-or-head-sha"
|
|
12174
|
-
});
|
|
12175
|
-
const reviewStatus = ({ blocked, required, reviewedHeadSha, reviewedPatchId, headSha, patchId }) => {
|
|
12176
|
-
if (blocked) {
|
|
12177
|
-
if ((reviewedHeadSha || reviewedPatchId) && !isProofCurrent({
|
|
12178
|
-
headSha,
|
|
12179
|
-
patchId,
|
|
12180
|
-
reviewedHeadSha,
|
|
12181
|
-
reviewedPatchId
|
|
12182
|
-
})) return "stale";
|
|
12183
|
-
return "blocked";
|
|
12184
|
-
}
|
|
12185
|
-
if (!required) return "not-required";
|
|
12186
|
-
if (!(reviewedHeadSha || reviewedPatchId)) return "missing";
|
|
12187
|
-
if (isProofCurrent({
|
|
12188
|
-
headSha,
|
|
12189
|
-
patchId,
|
|
12190
|
-
reviewedHeadSha,
|
|
12191
|
-
reviewedPatchId
|
|
12192
|
-
})) return "current";
|
|
12193
|
-
return "stale";
|
|
12194
|
-
};
|
|
12195
|
-
//#endregion
|
|
12196
12485
|
//#region src/admission-preflight.ts
|
|
12197
12486
|
const errorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
|
|
12198
12487
|
const resolveCandidate = ({ base, cwd, verifyProof }) => {
|
|
@@ -13963,10 +14252,10 @@ const managedReadinessLedgerSchema = z.object({
|
|
|
13963
14252
|
"non-trivial"
|
|
13964
14253
|
]),
|
|
13965
14254
|
externalChecks: z.array(z.object({
|
|
13966
|
-
checkType: z.enum(EVIDENCE_CHECK_TYPES
|
|
14255
|
+
checkType: z.enum(EVIDENCE_CHECK_TYPES),
|
|
13967
14256
|
name: z.string().min(1),
|
|
13968
14257
|
reason: z.string().optional(),
|
|
13969
|
-
scope: requiredCheckScopeSchema
|
|
14258
|
+
scope: requiredCheckScopeSchema.optional(),
|
|
13970
14259
|
scopeReason: z.string().min(1),
|
|
13971
14260
|
status: z.enum([
|
|
13972
14261
|
"satisfied",
|
|
@@ -15422,6 +15711,7 @@ const prReadyProofSchema = z.object({
|
|
|
15422
15711
|
humanBlockingReasons: z.array(z.string()).default([]),
|
|
15423
15712
|
ledger: managedReadinessLedgerSchema,
|
|
15424
15713
|
notices: z.array(z.string().min(1)).optional(),
|
|
15714
|
+
policyResolution: policyResolutionSchema.optional(),
|
|
15425
15715
|
profileBlobSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
15426
15716
|
profilePath: z.string().min(1).optional(),
|
|
15427
15717
|
repairs: z.array(readinessRepairSchema).default([]),
|
|
@@ -15444,6 +15734,7 @@ const PR_VIEW_JSON_FIELDS = [
|
|
|
15444
15734
|
"body",
|
|
15445
15735
|
"headRefName",
|
|
15446
15736
|
"headRefOid",
|
|
15737
|
+
"isCrossRepository",
|
|
15447
15738
|
"isDraft",
|
|
15448
15739
|
"mergeStateStatus",
|
|
15449
15740
|
"mergeable",
|
|
@@ -15647,6 +15938,20 @@ async function runPrReady(args, dependencies = {}) {
|
|
|
15647
15938
|
const policyProfile = committedPolicy.profile;
|
|
15648
15939
|
if (repositorySlug(policyProfile.repository) !== checkoutRepositorySlug) throw new Error(`Committed readiness profile repository ${repositorySlug(policyProfile.repository)} does not match trusted checkout repository ${checkoutRepositorySlug}.`);
|
|
15649
15940
|
const committedProfilePath = checkoutRelativeProfilePath(cwd, profilePath);
|
|
15941
|
+
const basePolicyReading = readAuthoritativeBasePolicy({
|
|
15942
|
+
authority: dependencies.basePolicy ?? defaultBaseReviewPolicyAuthority(),
|
|
15943
|
+
baseRefName: pr.baseRefName,
|
|
15944
|
+
crossRepository: pr.isCrossRepository,
|
|
15945
|
+
cwd,
|
|
15946
|
+
profilePath: committedProfilePath,
|
|
15947
|
+
repository
|
|
15948
|
+
});
|
|
15949
|
+
if (basePolicyReading.kind === "unresolved") throw new Error(`pr:ready could not read the authoritative review policy from the live tip of base ref ${basePolicyReading.baseRefName}: ${basePolicyReading.reason} Review demand is resolved from the base policy as well as the candidate's, so an unreadable base authority refuses admission rather than falling back to the candidate's own profile. Restore ${committedProfilePath} on ${basePolicyReading.baseRefName}, or re-run once GitHub is reachable.`);
|
|
15950
|
+
const authoritativeReview = resolveAuthoritativeReviewDemand({
|
|
15951
|
+
basePolicy: basePolicyReading.policy,
|
|
15952
|
+
candidatePolicy: policyProfile.review,
|
|
15953
|
+
changedFiles: files
|
|
15954
|
+
});
|
|
15650
15955
|
const verifyProofPath = path.resolve(cwd, args.verifyProof ?? ".factory-memory/pr-verify.json");
|
|
15651
15956
|
const reviewProofPath = path.resolve(cwd, args.reviewProof ?? ".factory-memory/pr-review.json");
|
|
15652
15957
|
const verifyProofRead = tryReadProof(readPrVerifyProof, verifyProofPath);
|
|
@@ -15655,11 +15960,10 @@ async function runPrReady(args, dependencies = {}) {
|
|
|
15655
15960
|
recorded: { patchId: verifyProofRead.proof.patchId },
|
|
15656
15961
|
rule: "patch-id"
|
|
15657
15962
|
}) ? verifyProofRead.proof.classification : "non-trivial";
|
|
15658
|
-
const
|
|
15963
|
+
const { ladderPolicy, requiredChecks } = resolveCandidateDemands({
|
|
15659
15964
|
changedFiles: files,
|
|
15660
15965
|
profile: policyProfile
|
|
15661
15966
|
});
|
|
15662
|
-
const { ladderPolicy } = demands;
|
|
15663
15967
|
const waveDemand = args.epic === void 0 ? void 0 : resolveWaveDemand({
|
|
15664
15968
|
baseRefName: pr.baseRefName,
|
|
15665
15969
|
epic: args.epic,
|
|
@@ -15693,7 +15997,7 @@ async function runPrReady(args, dependencies = {}) {
|
|
|
15693
15997
|
args,
|
|
15694
15998
|
cwd,
|
|
15695
15999
|
recordedAuthoringSession: verifyProofRead.proof?.authoringSession,
|
|
15696
|
-
requiredChecks
|
|
16000
|
+
requiredChecks
|
|
15697
16001
|
});
|
|
15698
16002
|
const output = path.resolve(cwd, args.output ?? ".factory-memory/pr-ready.json");
|
|
15699
16003
|
const externalCheckState = statusCheckState(pr.statusCheckRollup, isFactoryReadyCheck);
|
|
@@ -15729,7 +16033,7 @@ async function runPrReady(args, dependencies = {}) {
|
|
|
15729
16033
|
prTitle: pr.title,
|
|
15730
16034
|
requiredChecks: externalCheckState,
|
|
15731
16035
|
reviewDecision: pr.reviewDecision,
|
|
15732
|
-
reviewModes:
|
|
16036
|
+
reviewModes: authoritativeReview.reviewModes,
|
|
15733
16037
|
reviewProof,
|
|
15734
16038
|
unresolvedReviewThreads: pr.unresolvedReviewThreads,
|
|
15735
16039
|
verificationProof: verification.verificationProof,
|
|
@@ -15768,8 +16072,13 @@ async function runPrReady(args, dependencies = {}) {
|
|
|
15768
16072
|
if (evaluation.status === "ready") followUp = arming?.outcome === "not-armed" ? readyRedispatch() : void 0;
|
|
15769
16073
|
else if (undraftRepair && evaluation.humanBlockingReasons.length === 0) followUp = prUndraftFollowUp(args.pr);
|
|
15770
16074
|
else if (!pendingExternalChecksOnly) followUp = prVerifyFollowUp(authoringSession);
|
|
16075
|
+
const basePolicyNotice = baseDerivedSecurityNotice({
|
|
16076
|
+
baseRefName: basePolicyReading.baseRefName,
|
|
16077
|
+
demand: authoritativeReview
|
|
16078
|
+
});
|
|
15771
16079
|
const notices = [
|
|
15772
16080
|
...evaluation.notices,
|
|
16081
|
+
...basePolicyNotice && evaluation.ledger.reviews.security ? [basePolicyNotice] : [],
|
|
15773
16082
|
...arming ? [armingNotice(arming)] : [],
|
|
15774
16083
|
...waveDemand?.notices ?? [],
|
|
15775
16084
|
...evaluation.status === "ready" && !boundary ? ["No boundary wave authorized this candidate, so nothing was armed: pr:ready admits it but schedules no merge. Re-run with --epic <issue> to resolve the wave demand and arm. That issue can be the work issue itself, carrying its own fence; do not file a second boundary issue for one candidate."] : [],
|
|
@@ -15784,6 +16093,10 @@ async function runPrReady(args, dependencies = {}) {
|
|
|
15784
16093
|
humanBlockingReasons: evaluation.humanBlockingReasons,
|
|
15785
16094
|
ledger: evaluation.ledger,
|
|
15786
16095
|
...notices.length > 0 ? { notices } : {},
|
|
16096
|
+
policyResolution: authoritativePolicyResolution({
|
|
16097
|
+
base: basePolicyReading,
|
|
16098
|
+
demand: authoritativeReview
|
|
16099
|
+
}),
|
|
15787
16100
|
profileBlobSha: committedPolicy.blobSha,
|
|
15788
16101
|
profilePath: committedProfilePath,
|
|
15789
16102
|
repairs: evaluation.repairs,
|
|
@@ -15930,7 +16243,7 @@ const reviewFindingsFileSchema = z.object({
|
|
|
15930
16243
|
patchId: nonBlankString,
|
|
15931
16244
|
producer: nonBlankString.optional(),
|
|
15932
16245
|
reviews: z.array(findingsReviewSchema),
|
|
15933
|
-
rung: z.enum(EVIDENCE_REVIEW_RUNGS
|
|
16246
|
+
rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
|
|
15934
16247
|
schemaVersion: z.union([z.literal(1), z.literal(2)]),
|
|
15935
16248
|
sessionId: nonBlankString
|
|
15936
16249
|
}).strict().superRefine((file, context) => {
|
|
@@ -15981,40 +16294,92 @@ const parseReviewFindingsFile = (findingsPath, cwd = process.cwd()) => {
|
|
|
15981
16294
|
}
|
|
15982
16295
|
};
|
|
15983
16296
|
//#endregion
|
|
15984
|
-
//#region src/
|
|
15985
|
-
const
|
|
15986
|
-
const
|
|
15987
|
-
|
|
15988
|
-
|
|
15989
|
-
|
|
15990
|
-
|
|
15991
|
-
|
|
15992
|
-
|
|
15993
|
-
|
|
15994
|
-
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
16000
|
-
|
|
16001
|
-
|
|
16002
|
-
|
|
16003
|
-
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16297
|
+
//#region src/issue-body-sections.ts
|
|
16298
|
+
const HEADING_PATTERN = /^#{2,6}\s+(?<title>.+?)\s*$/u;
|
|
16299
|
+
const FENCE_PATTERN = /^\s*(?<fence>`{3,}|~{3,})(?<rest>.*)$/u;
|
|
16300
|
+
/**
|
|
16301
|
+
* Extracts the body of a markdown section (heading depth `##`–`######`,
|
|
16302
|
+
* case-insensitive title match) from an issue body. Duplicate sections
|
|
16303
|
+
* resolve to the first occurrence.
|
|
16304
|
+
*
|
|
16305
|
+
* The scan is fence-aware: heading-like lines inside fenced code blocks
|
|
16306
|
+
* (``` or ~~~, including indented and language-tagged fences) neither start
|
|
16307
|
+
* nor terminate a section. Fences are matched by marker character (backtick
|
|
16308
|
+
* vs. tilde) and require a closing fence with at least as many marker
|
|
16309
|
+
* characters as the opener, per CommonMark; an unclosed fence extends to the
|
|
16310
|
+
* end of the body.
|
|
16311
|
+
*/
|
|
16312
|
+
const sectionBody = (body, heading) => {
|
|
16313
|
+
const lines = body.split(/\r?\n/u);
|
|
16314
|
+
const target = heading.toLowerCase();
|
|
16315
|
+
let openFence;
|
|
16316
|
+
let startIndex = -1;
|
|
16317
|
+
for (const [index, line] of lines.entries()) {
|
|
16318
|
+
const fence = FENCE_PATTERN.exec(line)?.groups;
|
|
16319
|
+
const marker = fence?.fence;
|
|
16320
|
+
if (openFence) {
|
|
16321
|
+
if (marker && marker[0] === openFence.char && marker.length >= openFence.length && (fence?.rest ?? "").trim() === "") openFence = void 0;
|
|
16322
|
+
continue;
|
|
16323
|
+
}
|
|
16324
|
+
if (marker) {
|
|
16325
|
+
openFence = {
|
|
16326
|
+
char: marker[0],
|
|
16327
|
+
length: marker.length
|
|
16328
|
+
};
|
|
16329
|
+
continue;
|
|
16330
|
+
}
|
|
16331
|
+
const title = HEADING_PATTERN.exec(line)?.groups?.title;
|
|
16332
|
+
if (title === void 0) continue;
|
|
16333
|
+
if (startIndex === -1) {
|
|
16334
|
+
if (title.toLowerCase() === target) startIndex = index;
|
|
16335
|
+
continue;
|
|
16336
|
+
}
|
|
16337
|
+
return lines.slice(startIndex + 1, index).join("\n").trim();
|
|
16338
|
+
}
|
|
16339
|
+
if (startIndex === -1) return;
|
|
16340
|
+
return lines.slice(startIndex + 1).join("\n").trim();
|
|
16341
|
+
};
|
|
16342
|
+
//#endregion
|
|
16343
|
+
//#region src/review-focus.ts
|
|
16344
|
+
const REVIEW_FOCUS_SECTION = "Review focus";
|
|
16345
|
+
const reviewFocusFromIssueBody = (body) => {
|
|
16346
|
+
return sectionBody(body, "Review focus") || void 0;
|
|
16347
|
+
};
|
|
16348
|
+
//#endregion
|
|
16349
|
+
//#region src/review-ladder-trace.ts
|
|
16350
|
+
const eventTime = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
16351
|
+
const buildReviewLadderStageTraceEvent = ({ createdAt = eventTime(), event, headSha, identity, patchId }) => {
|
|
16352
|
+
const trace = {
|
|
16353
|
+
createdAt,
|
|
16354
|
+
cycle: event.cycle,
|
|
16355
|
+
engine: event.engine,
|
|
16356
|
+
eventId: `review-ladder-stage-${crypto.randomUUID()}`,
|
|
16357
|
+
eventType: "review-ladder-stage",
|
|
16358
|
+
findingsSummary: event.findingsSummary,
|
|
16359
|
+
observedAt: createdAt,
|
|
16360
|
+
repo: identity.repo,
|
|
16361
|
+
schemaVersion: 1,
|
|
16362
|
+
stage: event.stage,
|
|
16363
|
+
typeVersion: 1
|
|
16364
|
+
};
|
|
16365
|
+
if (event.cyclesToClean !== void 0) trace.cyclesToClean = event.cyclesToClean;
|
|
16366
|
+
if (event.forcedTransition !== void 0) trace.forcedTransition = event.forcedTransition;
|
|
16367
|
+
if (event.model !== void 0) trace.model = event.model;
|
|
16368
|
+
if (event.session !== void 0) trace.session = event.session;
|
|
16369
|
+
if (event.usage !== void 0) trace.usage = event.usage;
|
|
16370
|
+
if (headSha !== void 0) trace.headSha = headSha;
|
|
16371
|
+
if (patchId !== void 0) trace.patchId = patchId;
|
|
16372
|
+
if (typeof identity.issue === "number") trace.issue = identity.issue;
|
|
16373
|
+
if (typeof identity.pr === "number") trace.pr = identity.pr;
|
|
16374
|
+
if (identity.threadId !== void 0) trace.threadId = identity.threadId;
|
|
16375
|
+
if (identity.workerId !== void 0) trace.workerId = identity.workerId;
|
|
16376
|
+
return trace;
|
|
16377
|
+
};
|
|
16378
|
+
const appendReviewLadderStageTraceEvent = ({ createdAt, event, headSha, identity, mirror, patchId, repoRoot }) => appendFactoryTraceEvent(repoRoot, buildReviewLadderStageTraceEvent({
|
|
16379
|
+
createdAt,
|
|
16380
|
+
event,
|
|
16381
|
+
headSha,
|
|
16382
|
+
identity,
|
|
16018
16383
|
patchId
|
|
16019
16384
|
}), { mirror });
|
|
16020
16385
|
const tryAppendReviewLadderStageTraceEvent = (input) => {
|
|
@@ -16128,15 +16493,189 @@ function emitLadderStageTrace({ cwd, evaluation, headSha, issue, patchId, profil
|
|
|
16128
16493
|
else console.error(`review-ladder-stage trace event not written: ${traceResult.errorMessage}`);
|
|
16129
16494
|
}
|
|
16130
16495
|
//#endregion
|
|
16496
|
+
//#region src/review-contract.ts
|
|
16497
|
+
const REVIEW_FOOTER_INSTRUCTIONS = `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 1):
|
|
16498
|
+
{
|
|
16499
|
+
"schemaVersion": 1,
|
|
16500
|
+
"model": "<your model identifier>",
|
|
16501
|
+
"sessionId": "<this session's id>",
|
|
16502
|
+
"patchId": "<the stable patch-id from Review scope above>",
|
|
16503
|
+
"reviews": [
|
|
16504
|
+
{
|
|
16505
|
+
"kind": "correctness" or "security" (this review's mode, matching the mode-floor instructions below),
|
|
16506
|
+
"outcome": "passed" or "failed",
|
|
16507
|
+
"issuesFlagged": an integer count of actionable findings (must equal findings.length),
|
|
16508
|
+
"summary": one concise sentence; use "No actionable findings." only when issuesFlagged is 0,
|
|
16509
|
+
"findings": [] when issuesFlagged is 0, otherwise an array of objects with title, body, and optional file, line, priority, category, blockingAfterCap, citedSpan, prescribedAction, and supersedes fields
|
|
16510
|
+
}
|
|
16511
|
+
]
|
|
16512
|
+
}
|
|
16513
|
+
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.`;
|
|
16514
|
+
//#endregion
|
|
16515
|
+
//#region src/review-prompt.ts
|
|
16516
|
+
const reviewScope = ({ base, changedFiles: files, headSha, patchId, profile }) => `Review scope:
|
|
16517
|
+
- Repository: ${profile.repository.owner}/${profile.repository.name}
|
|
16518
|
+
- Base ref: ${base}
|
|
16519
|
+
- Head SHA: ${headSha}
|
|
16520
|
+
- Stable patch-id: ${patchId}
|
|
16521
|
+
- Changed files:
|
|
16522
|
+
${files.length > 0 ? files.map((file) => ` - ${file}`).join("\n") : " - None"}
|
|
16523
|
+
|
|
16524
|
+
Required constraints:
|
|
16525
|
+
- Review only the PR-owned diff against ${base} and the changed files listed above.
|
|
16526
|
+
- Prefer git diff, git grep, and file-specific reads.
|
|
16527
|
+
- Do not inspect ignored generated output such as dist, build, coverage, or sourcemaps.
|
|
16528
|
+
- Do not rerun broad build or verification commands. If pr:verify proof already exists, treat build verification as out of scope for this review runner.
|
|
16529
|
+
- Do not request elevated or unsandboxed approvals.
|
|
16530
|
+
- 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.
|
|
16531
|
+
|
|
16532
|
+
${REVIEW_FOOTER_INSTRUCTIONS}`;
|
|
16533
|
+
const MODE_FLOOR_FILES = {
|
|
16534
|
+
correctness: "correctness.md",
|
|
16535
|
+
security: "security/SKILL.md"
|
|
16536
|
+
};
|
|
16537
|
+
const reviewPromptsDir = path.join(import.meta.dirname, "review-prompts");
|
|
16538
|
+
const modeFloorCache = /* @__PURE__ */ new Map();
|
|
16539
|
+
const securityReferenceFiles = (changedFiles) => {
|
|
16540
|
+
const references = /* @__PURE__ */ new Set();
|
|
16541
|
+
for (const file of changedFiles) {
|
|
16542
|
+
if (/^\.github\/(?:actions|workflows)\//u.test(file)) references.add("security/references/github-workflows.md");
|
|
16543
|
+
if (/\.(?:[cm]?[jt]sx?)$/u.test(file)) references.add("security/references/javascript-typescript.md");
|
|
16544
|
+
if (/\.py(?:i)?$/u.test(file)) references.add("security/references/python.md");
|
|
16545
|
+
}
|
|
16546
|
+
return [...references];
|
|
16547
|
+
};
|
|
16548
|
+
function modeFloorInstructions(kind, changedFiles = []) {
|
|
16549
|
+
const referenceFiles = kind === "security" ? securityReferenceFiles(changedFiles) : [];
|
|
16550
|
+
const cacheKey = `${kind}:${referenceFiles.join(",")}`;
|
|
16551
|
+
const cached = modeFloorCache.get(cacheKey);
|
|
16552
|
+
if (cached !== void 0) return cached;
|
|
16553
|
+
const text = [MODE_FLOOR_FILES[kind], ...referenceFiles].map((file) => readFileSync(path.join(reviewPromptsDir, file), "utf-8")).map((contents) => contents.replace(/\n+$/u, "")).join("\n\n---\n\n");
|
|
16554
|
+
modeFloorCache.set(cacheKey, text);
|
|
16555
|
+
return text;
|
|
16556
|
+
}
|
|
16557
|
+
const CODING_STANDARDS_FILE = "CODING_STANDARDS.md";
|
|
16558
|
+
const readCodingStandardsAtHead = ({ cwd, git, headSha }) => {
|
|
16559
|
+
let root;
|
|
16560
|
+
try {
|
|
16561
|
+
root = (git?.repositoryRoot ?? repositoryRoot)(cwd);
|
|
16562
|
+
} catch {
|
|
16563
|
+
return;
|
|
16564
|
+
}
|
|
16565
|
+
return (git?.showFileAtRef ?? showFileAtRef)(root, headSha, path.join(root, CODING_STANDARDS_FILE));
|
|
16566
|
+
};
|
|
16567
|
+
const codingStandardsSegment = ({ contents, headSha }) => {
|
|
16568
|
+
const text = contents?.replace(/\n+$/u, "");
|
|
16569
|
+
if (!text?.trim()) return;
|
|
16570
|
+
const source = `${CODING_STANDARDS_FILE} at head ${headSha}`;
|
|
16571
|
+
return {
|
|
16572
|
+
section: {
|
|
16573
|
+
provenance: "coding-standards",
|
|
16574
|
+
source,
|
|
16575
|
+
text
|
|
16576
|
+
},
|
|
16577
|
+
text: `Repository coding standards (source: ${source}, appended verbatim). Check the diff against these rules:
|
|
16578
|
+
${text}`
|
|
16579
|
+
};
|
|
16580
|
+
};
|
|
16581
|
+
const issueFocusSegment = (focus) => ({
|
|
16582
|
+
section: {
|
|
16583
|
+
provenance: "issue-review-focus",
|
|
16584
|
+
source: `issue #${focus.issueNumber} "${REVIEW_FOCUS_SECTION}" section`,
|
|
16585
|
+
text: focus.text
|
|
16586
|
+
},
|
|
16587
|
+
text: `Per-issue review focus (source: issue #${focus.issueNumber} "${REVIEW_FOCUS_SECTION}" section, appended verbatim):
|
|
16588
|
+
${focus.text}`
|
|
16589
|
+
});
|
|
16590
|
+
const priorLedgerEntryLine = (entry) => {
|
|
16591
|
+
const location = entry.file ? ` (${entry.file})` : "";
|
|
16592
|
+
const reference = entry.reference ? ` — ${entry.reference}` : "";
|
|
16593
|
+
return `- [${entry.disposition}] "${entry.title}"${location}${reference} (flagged ${entry.flagCount}x)`;
|
|
16594
|
+
};
|
|
16595
|
+
const priorLedgerSegment = (entries) => ({
|
|
16596
|
+
section: {
|
|
16597
|
+
provenance: "prior-review-ledger",
|
|
16598
|
+
source: "persisted pr:review disposition ledger (prior cycles, #233)",
|
|
16599
|
+
text: entries.map(priorLedgerEntryLine).join("\n")
|
|
16600
|
+
},
|
|
16601
|
+
text: `Prior review ledger (source: persisted pr:review disposition ledger from earlier cycles on this PR; already adjudicated — do not re-litigate):
|
|
16602
|
+
${entries.map(priorLedgerEntryLine).join("\n")}
|
|
16603
|
+
|
|
16604
|
+
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.`
|
|
16605
|
+
});
|
|
16606
|
+
const assembleReviewPrompt = ({ base, changedFiles, cwd = process.cwd(), headSha, issueFocus, kind, patchId, priorLedgerEntries, profile }, dependencies = {}) => {
|
|
16607
|
+
if (!resolveApplicableReviewModes(profile, changedFiles).includes(kind)) throw new Error(`${kind} review mode does not apply to the changed files.`);
|
|
16608
|
+
const priorLedger = priorLedgerEntries ? nonOpenLadderFindings(priorLedgerEntries) : [];
|
|
16609
|
+
const optionalSegments = {
|
|
16610
|
+
"coding-standards": codingStandardsSegment({
|
|
16611
|
+
contents: kind === "correctness" ? readCodingStandardsAtHead({
|
|
16612
|
+
cwd,
|
|
16613
|
+
git: dependencies.git,
|
|
16614
|
+
headSha
|
|
16615
|
+
}) : void 0,
|
|
16616
|
+
headSha
|
|
16617
|
+
}),
|
|
16618
|
+
"issue-review-focus": issueFocus ? issueFocusSegment(issueFocus) : void 0,
|
|
16619
|
+
"prior-review-ledger": priorLedger.length > 0 ? priorLedgerSegment(priorLedger) : void 0
|
|
16620
|
+
};
|
|
16621
|
+
const segments = [
|
|
16622
|
+
{ text: reviewScope({
|
|
16623
|
+
base,
|
|
16624
|
+
changedFiles,
|
|
16625
|
+
headSha,
|
|
16626
|
+
patchId,
|
|
16627
|
+
profile
|
|
16628
|
+
}) },
|
|
16629
|
+
{ text: modeFloorInstructions(kind, changedFiles) },
|
|
16630
|
+
...REVIEW_PROMPT_SECTION_PROVENANCES.flatMap((provenance) => {
|
|
16631
|
+
const segment = optionalSegments[provenance];
|
|
16632
|
+
return segment ? [segment] : [];
|
|
16633
|
+
})
|
|
16634
|
+
];
|
|
16635
|
+
return {
|
|
16636
|
+
prompt: segments.map((segment) => segment.text).join("\n\n"),
|
|
16637
|
+
sections: segments.flatMap((segment) => segment.section ? [segment.section] : [])
|
|
16638
|
+
};
|
|
16639
|
+
};
|
|
16640
|
+
//#endregion
|
|
16131
16641
|
//#region src/pr-review.ts
|
|
16642
|
+
const DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH = ".factory-memory/pr-review-prompt.json";
|
|
16643
|
+
const MISSING_ASSEMBLED_PROMPT_NOTICE = "pr:review notice: assembled review prompt not found at .factory-memory/pr-review-prompt.json; accepting findings without promptSections.";
|
|
16644
|
+
const STALE_ASSEMBLED_PROMPT_NOTICE = "pr:review notice: assembled review prompt patch-id does not match the candidate; accepting findings without promptSections.";
|
|
16645
|
+
const UNREADABLE_ASSEMBLED_PROMPT_NOTICE = "pr:review notice: assembled review prompt at .factory-memory/pr-review-prompt.json is unreadable; accepting findings without promptSections.";
|
|
16646
|
+
const ASSEMBLE_NO_KIND_NOTICE = "pr:review --assemble: no review kind applies; prompt not written.";
|
|
16647
|
+
const ASSEMBLE_FAITHFUL_MERGE_NOTICE = "pr:review --assemble: faithful-merge; prompt not written.";
|
|
16648
|
+
const ASSEMBLED_REVIEW_PROMPT_SCHEMA_VERSION = 1;
|
|
16649
|
+
const assembledReviewPromptKindSchema = z.object({
|
|
16650
|
+
kind: z.enum(["correctness", "security"]),
|
|
16651
|
+
prompt: z.string().min(1),
|
|
16652
|
+
sections: z.array(reviewPromptSectionSchema)
|
|
16653
|
+
});
|
|
16654
|
+
const assembledReviewPromptArtifactSchema = z.object({
|
|
16655
|
+
headSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
16656
|
+
patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
|
|
16657
|
+
reviews: z.array(assembledReviewPromptKindSchema),
|
|
16658
|
+
schemaVersion: z.literal(ASSEMBLED_REVIEW_PROMPT_SCHEMA_VERSION)
|
|
16659
|
+
});
|
|
16132
16660
|
const defaultPrReviewGit = () => ({
|
|
16133
16661
|
changedFiles,
|
|
16134
16662
|
currentHeadSha,
|
|
16135
16663
|
isAncestor,
|
|
16136
16664
|
recognizeCatchUp: recognizeCatchUpCandidate,
|
|
16665
|
+
repositoryRoot,
|
|
16666
|
+
showFileAtRef,
|
|
16137
16667
|
stablePatchId: runStablePatchId,
|
|
16138
16668
|
statusPorcelain
|
|
16139
16669
|
});
|
|
16670
|
+
const defaultFetchIssueBody = ({ issue, owner, repo }) => runGhJson([
|
|
16671
|
+
"issue",
|
|
16672
|
+
"view",
|
|
16673
|
+
String(issue),
|
|
16674
|
+
"--repo",
|
|
16675
|
+
`${owner}/${repo}`,
|
|
16676
|
+
"--json",
|
|
16677
|
+
"body"
|
|
16678
|
+
]).body;
|
|
16140
16679
|
const assertReviewCycleInWindow = (cycle, maxCycles) => {
|
|
16141
16680
|
if (cycle < 1 || cycle > maxCycles) throw new Error(`--cycle must be between 1 and ${maxCycles}.`);
|
|
16142
16681
|
};
|
|
@@ -16231,7 +16770,99 @@ const assertReviewOutcomes = (proof, policy, cycle, maxCycles) => {
|
|
|
16231
16770
|
console.error(cycle >= maxCycles ? `Review cycle limit reached (${cycle}/${maxCycles}). Stop and request human direction before rerunning.` : `Fix ${state.openFindings} actionable finding(s) before rerunning pr:review as cycle ${cycle + 1}/${maxCycles}.`);
|
|
16232
16771
|
throw new Error("pr:review found actionable review findings.");
|
|
16233
16772
|
};
|
|
16773
|
+
const loadIssueFocus = (issue, profile, fetchIssueBody) => {
|
|
16774
|
+
const text = reviewFocusFromIssueBody(fetchIssueBody({
|
|
16775
|
+
issue,
|
|
16776
|
+
owner: profile.repository.owner,
|
|
16777
|
+
repo: profile.repository.name
|
|
16778
|
+
}));
|
|
16779
|
+
return text === void 0 ? void 0 : {
|
|
16780
|
+
issueNumber: issue,
|
|
16781
|
+
text
|
|
16782
|
+
};
|
|
16783
|
+
};
|
|
16784
|
+
const assembleGitDependencies = (git) => git.repositoryRoot !== void 0 && git.showFileAtRef !== void 0 ? {
|
|
16785
|
+
repositoryRoot: git.repositoryRoot,
|
|
16786
|
+
showFileAtRef: git.showFileAtRef
|
|
16787
|
+
} : void 0;
|
|
16788
|
+
const writeAssembledReviewPrompt = ({ args, cwd, fetchIssueBody, files, git, headSha, kinds, output, patchId, profile }) => {
|
|
16789
|
+
const priorLedger = priorLedgerFor(priorLadderStateFor({
|
|
16790
|
+
base: args.base,
|
|
16791
|
+
cwd,
|
|
16792
|
+
headSha,
|
|
16793
|
+
isAncestor: (ancestor, descendant) => git.isAncestor(cwd, ancestor, descendant),
|
|
16794
|
+
output
|
|
16795
|
+
}));
|
|
16796
|
+
const issueFocus = args.issue === void 0 ? void 0 : loadIssueFocus(args.issue, profile, fetchIssueBody);
|
|
16797
|
+
const assembleGit = assembleGitDependencies(git);
|
|
16798
|
+
const assembledReviews = kinds.map((kind) => {
|
|
16799
|
+
const assembled = assembleReviewPrompt({
|
|
16800
|
+
base: args.base,
|
|
16801
|
+
changedFiles: files,
|
|
16802
|
+
cwd,
|
|
16803
|
+
headSha,
|
|
16804
|
+
...issueFocus === void 0 ? {} : { issueFocus },
|
|
16805
|
+
kind,
|
|
16806
|
+
patchId,
|
|
16807
|
+
priorLedgerEntries: priorLedger,
|
|
16808
|
+
profile
|
|
16809
|
+
}, assembleGit === void 0 ? {} : { git: assembleGit });
|
|
16810
|
+
return {
|
|
16811
|
+
kind,
|
|
16812
|
+
prompt: assembled.prompt,
|
|
16813
|
+
sections: assembled.sections
|
|
16814
|
+
};
|
|
16815
|
+
});
|
|
16816
|
+
const artifactPath = path.resolve(cwd, DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH);
|
|
16817
|
+
const existing = readAssembledReviewPrompt(artifactPath);
|
|
16818
|
+
writeProofJson({
|
|
16819
|
+
headSha,
|
|
16820
|
+
patchId,
|
|
16821
|
+
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,
|
|
16822
|
+
schemaVersion: ASSEMBLED_REVIEW_PROMPT_SCHEMA_VERSION
|
|
16823
|
+
}, artifactPath);
|
|
16824
|
+
console.log(`pr:review assembled prompt written to ${artifactPath}`);
|
|
16825
|
+
return artifactPath;
|
|
16826
|
+
};
|
|
16827
|
+
const readAssembledReviewPrompt = (artifactPath) => {
|
|
16828
|
+
if (!existsSync(artifactPath)) return { status: "missing" };
|
|
16829
|
+
try {
|
|
16830
|
+
return {
|
|
16831
|
+
artifact: assembledReviewPromptArtifactSchema.parse(JSON.parse(readFileSync(artifactPath, "utf-8"))),
|
|
16832
|
+
status: "ok"
|
|
16833
|
+
};
|
|
16834
|
+
} catch {
|
|
16835
|
+
return { status: "unreadable" };
|
|
16836
|
+
}
|
|
16837
|
+
};
|
|
16838
|
+
const promptSectionsFromAssembledArtifact = ({ cwd, patchId }) => {
|
|
16839
|
+
const read = readAssembledReviewPrompt(path.resolve(cwd, DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH));
|
|
16840
|
+
if (read.status === "missing") {
|
|
16841
|
+
console.error(MISSING_ASSEMBLED_PROMPT_NOTICE);
|
|
16842
|
+
return;
|
|
16843
|
+
}
|
|
16844
|
+
if (read.status === "unreadable") {
|
|
16845
|
+
console.error(UNREADABLE_ASSEMBLED_PROMPT_NOTICE);
|
|
16846
|
+
return;
|
|
16847
|
+
}
|
|
16848
|
+
if (read.artifact.patchId !== patchId) {
|
|
16849
|
+
console.error(STALE_ASSEMBLED_PROMPT_NOTICE);
|
|
16850
|
+
return;
|
|
16851
|
+
}
|
|
16852
|
+
return new Map(read.artifact.reviews.flatMap((review) => review.sections.length > 0 ? [[review.kind, review.sections]] : []));
|
|
16853
|
+
};
|
|
16854
|
+
const withAssembledPromptSections = (reviews, sectionsByKind) => {
|
|
16855
|
+
if (sectionsByKind === void 0) return reviews;
|
|
16856
|
+
return reviews.map((review) => {
|
|
16857
|
+
const promptSections = sectionsByKind.get(review.kind);
|
|
16858
|
+
return promptSections === void 0 ? review : {
|
|
16859
|
+
...review,
|
|
16860
|
+
promptSections
|
|
16861
|
+
};
|
|
16862
|
+
});
|
|
16863
|
+
};
|
|
16234
16864
|
async function runPrReview(args, dependencies = {}) {
|
|
16865
|
+
if (args.assemble === true && args.findings !== void 0) throw new Error("--assemble and --findings cannot be combined; they are sequential steps.");
|
|
16235
16866
|
const cwd = path.resolve(args.cwd ?? process.cwd());
|
|
16236
16867
|
const git = dependencies.git ?? defaultPrReviewGit();
|
|
16237
16868
|
const files = git.changedFiles(cwd, args.base);
|
|
@@ -16268,6 +16899,40 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
16268
16899
|
const startedAt = /* @__PURE__ */ new Date();
|
|
16269
16900
|
const findingsFile = args.findings ? parseReviewFindingsFile(args.findings, cwd) : void 0;
|
|
16270
16901
|
const kinds = enabledKinds(profile, args.mode, files, findingsFile?.reviews.map((review) => review.kind) ?? []);
|
|
16902
|
+
if (args.assemble === true) {
|
|
16903
|
+
const assembleProof = () => emptyProof({
|
|
16904
|
+
base: args.base,
|
|
16905
|
+
cycle: args.cycle,
|
|
16906
|
+
files,
|
|
16907
|
+
headSha,
|
|
16908
|
+
maxCycles,
|
|
16909
|
+
patchId
|
|
16910
|
+
});
|
|
16911
|
+
if (kinds.length === 0) {
|
|
16912
|
+
console.log(ASSEMBLE_NO_KIND_NOTICE);
|
|
16913
|
+
return assembleProof();
|
|
16914
|
+
}
|
|
16915
|
+
if ((git.recognizeCatchUp ?? recognizeCatchUpCandidate)({
|
|
16916
|
+
base: args.base,
|
|
16917
|
+
cwd
|
|
16918
|
+
}).recognized) {
|
|
16919
|
+
console.log(ASSEMBLE_FAITHFUL_MERGE_NOTICE);
|
|
16920
|
+
return assembleProof();
|
|
16921
|
+
}
|
|
16922
|
+
writeAssembledReviewPrompt({
|
|
16923
|
+
args,
|
|
16924
|
+
cwd,
|
|
16925
|
+
fetchIssueBody: dependencies.fetchIssueBody ?? defaultFetchIssueBody,
|
|
16926
|
+
files,
|
|
16927
|
+
git,
|
|
16928
|
+
headSha,
|
|
16929
|
+
kinds,
|
|
16930
|
+
output,
|
|
16931
|
+
patchId,
|
|
16932
|
+
profile
|
|
16933
|
+
});
|
|
16934
|
+
return assembleProof();
|
|
16935
|
+
}
|
|
16271
16936
|
const mintNotRequiredProof = (reviewRequirement) => {
|
|
16272
16937
|
const proof = emptyProof({
|
|
16273
16938
|
base: args.base,
|
|
@@ -16335,7 +17000,10 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
16335
17000
|
const priorLedger = priorLedgerFor(priorLadder);
|
|
16336
17001
|
const declaredDispositions = declaredDispositionsFor(args.dispositions, cwd);
|
|
16337
17002
|
assertDeclaredDispositionsMatch(priorLedger, declaredDispositions);
|
|
16338
|
-
const reviews = reviewResults(findingsFile, kinds, startedAt, /* @__PURE__ */ new Date())
|
|
17003
|
+
const reviews = withAssembledPromptSections(reviewResults(findingsFile, kinds, startedAt, /* @__PURE__ */ new Date()), promptSectionsFromAssembledArtifact({
|
|
17004
|
+
cwd,
|
|
17005
|
+
patchId
|
|
17006
|
+
}));
|
|
16339
17007
|
const runs = reviews.map((review) => ({ review }));
|
|
16340
17008
|
const { evaluation, ladder } = ladderStateForRun({
|
|
16341
17009
|
cycle: args.cycle,
|
|
@@ -17070,7 +17738,7 @@ function assertForegroundGate({ gate, env = process.env }) {
|
|
|
17070
17738
|
//#endregion
|
|
17071
17739
|
//#region src/commands/pr-review.ts
|
|
17072
17740
|
function createPrReviewCommand(output, action) {
|
|
17073
|
-
return markCwdOptionDefault(new Command("pr:review").description("
|
|
17741
|
+
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("--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({
|
|
17074
17742
|
gate: "pr:review",
|
|
17075
17743
|
resolveCycle: (options) => options.cycle,
|
|
17076
17744
|
resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
|
|
@@ -17083,6 +17751,7 @@ function createPrReviewCommand(output, action) {
|
|
|
17083
17751
|
"all"
|
|
17084
17752
|
].includes(options.mode)) throw new Error("--mode requires correctness, security, or all");
|
|
17085
17753
|
const args = {
|
|
17754
|
+
...options.assemble === true ? { assemble: true } : {},
|
|
17086
17755
|
base: options.base,
|
|
17087
17756
|
cwd: resolveCwdOption(options.cwd),
|
|
17088
17757
|
cycle: options.cycle,
|
|
@@ -17399,171 +18068,6 @@ function formatScratchFileCheckSummary(check) {
|
|
|
17399
18068
|
return "Scratch files: not checked (no readable worktree path)";
|
|
17400
18069
|
}
|
|
17401
18070
|
//#endregion
|
|
17402
|
-
//#region src/issue-body-sections.ts
|
|
17403
|
-
const HEADING_PATTERN = /^#{2,6}\s+(?<title>.+?)\s*$/u;
|
|
17404
|
-
const FENCE_PATTERN = /^\s*(?<fence>`{3,}|~{3,})(?<rest>.*)$/u;
|
|
17405
|
-
/**
|
|
17406
|
-
* Extracts the body of a markdown section (heading depth `##`–`######`,
|
|
17407
|
-
* case-insensitive title match) from an issue body. Duplicate sections
|
|
17408
|
-
* resolve to the first occurrence.
|
|
17409
|
-
*
|
|
17410
|
-
* The scan is fence-aware: heading-like lines inside fenced code blocks
|
|
17411
|
-
* (``` or ~~~, including indented and language-tagged fences) neither start
|
|
17412
|
-
* nor terminate a section. Fences are matched by marker character (backtick
|
|
17413
|
-
* vs. tilde) and require a closing fence with at least as many marker
|
|
17414
|
-
* characters as the opener, per CommonMark; an unclosed fence extends to the
|
|
17415
|
-
* end of the body.
|
|
17416
|
-
*/
|
|
17417
|
-
const sectionBody = (body, heading) => {
|
|
17418
|
-
const lines = body.split(/\r?\n/u);
|
|
17419
|
-
const target = heading.toLowerCase();
|
|
17420
|
-
let openFence;
|
|
17421
|
-
let startIndex = -1;
|
|
17422
|
-
for (const [index, line] of lines.entries()) {
|
|
17423
|
-
const fence = FENCE_PATTERN.exec(line)?.groups;
|
|
17424
|
-
const marker = fence?.fence;
|
|
17425
|
-
if (openFence) {
|
|
17426
|
-
if (marker && marker[0] === openFence.char && marker.length >= openFence.length && (fence?.rest ?? "").trim() === "") openFence = void 0;
|
|
17427
|
-
continue;
|
|
17428
|
-
}
|
|
17429
|
-
if (marker) {
|
|
17430
|
-
openFence = {
|
|
17431
|
-
char: marker[0],
|
|
17432
|
-
length: marker.length
|
|
17433
|
-
};
|
|
17434
|
-
continue;
|
|
17435
|
-
}
|
|
17436
|
-
const title = HEADING_PATTERN.exec(line)?.groups?.title;
|
|
17437
|
-
if (title === void 0) continue;
|
|
17438
|
-
if (startIndex === -1) {
|
|
17439
|
-
if (title.toLowerCase() === target) startIndex = index;
|
|
17440
|
-
continue;
|
|
17441
|
-
}
|
|
17442
|
-
return lines.slice(startIndex + 1, index).join("\n").trim();
|
|
17443
|
-
}
|
|
17444
|
-
if (startIndex === -1) return;
|
|
17445
|
-
return lines.slice(startIndex + 1).join("\n").trim();
|
|
17446
|
-
};
|
|
17447
|
-
//#endregion
|
|
17448
|
-
//#region src/review-focus.ts
|
|
17449
|
-
const REVIEW_FOCUS_SECTION = "Review focus";
|
|
17450
|
-
const reviewFocusFromIssueBody = (body) => {
|
|
17451
|
-
return sectionBody(body, "Review focus") || void 0;
|
|
17452
|
-
};
|
|
17453
|
-
//#endregion
|
|
17454
|
-
//#region src/review-contract.ts
|
|
17455
|
-
const REVIEW_FOOTER_INSTRUCTIONS = `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 1):
|
|
17456
|
-
{
|
|
17457
|
-
"schemaVersion": 1,
|
|
17458
|
-
"model": "<your model identifier>",
|
|
17459
|
-
"sessionId": "<this session's id>",
|
|
17460
|
-
"patchId": "<the stable patch-id from Review scope above>",
|
|
17461
|
-
"reviews": [
|
|
17462
|
-
{
|
|
17463
|
-
"kind": "correctness" or "security" (this review's mode, matching the mode-floor instructions below),
|
|
17464
|
-
"outcome": "passed" or "failed",
|
|
17465
|
-
"issuesFlagged": an integer count of actionable findings (must equal findings.length),
|
|
17466
|
-
"summary": one concise sentence; use "No actionable findings." only when issuesFlagged is 0,
|
|
17467
|
-
"findings": [] when issuesFlagged is 0, otherwise an array of objects with title, body, and optional file, line, priority, category, blockingAfterCap, citedSpan, prescribedAction, and supersedes fields
|
|
17468
|
-
}
|
|
17469
|
-
]
|
|
17470
|
-
}
|
|
17471
|
-
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.`;
|
|
17472
|
-
//#endregion
|
|
17473
|
-
//#region src/review-prompt.ts
|
|
17474
|
-
const reviewScope = ({ base, changedFiles: files, headSha, patchId, profile }) => `Review scope:
|
|
17475
|
-
- Repository: ${profile.repository.owner}/${profile.repository.name}
|
|
17476
|
-
- Base ref: ${base}
|
|
17477
|
-
- Head SHA: ${headSha}
|
|
17478
|
-
- Stable patch-id: ${patchId}
|
|
17479
|
-
- Changed files:
|
|
17480
|
-
${files.length > 0 ? files.map((file) => ` - ${file}`).join("\n") : " - None"}
|
|
17481
|
-
|
|
17482
|
-
Required constraints:
|
|
17483
|
-
- Review only the PR-owned diff against ${base} and the changed files listed above.
|
|
17484
|
-
- Prefer git diff, git grep, and file-specific reads.
|
|
17485
|
-
- Do not inspect ignored generated output such as dist, build, coverage, or sourcemaps.
|
|
17486
|
-
- Do not rerun broad build or verification commands. If pr:verify proof already exists, treat build verification as out of scope for this review runner.
|
|
17487
|
-
- Do not request elevated or unsandboxed approvals.
|
|
17488
|
-
- 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.
|
|
17489
|
-
|
|
17490
|
-
${REVIEW_FOOTER_INSTRUCTIONS}`;
|
|
17491
|
-
const MODE_FLOOR_FILES = {
|
|
17492
|
-
correctness: "correctness.md",
|
|
17493
|
-
security: "security/SKILL.md"
|
|
17494
|
-
};
|
|
17495
|
-
const reviewPromptsDir = path.join(import.meta.dirname, "review-prompts");
|
|
17496
|
-
const modeFloorCache = /* @__PURE__ */ new Map();
|
|
17497
|
-
const securityReferenceFiles = (changedFiles) => {
|
|
17498
|
-
const references = /* @__PURE__ */ new Set();
|
|
17499
|
-
for (const file of changedFiles) {
|
|
17500
|
-
if (/^\.github\/(?:actions|workflows)\//u.test(file)) references.add("security/references/github-workflows.md");
|
|
17501
|
-
if (/\.(?:[cm]?[jt]sx?)$/u.test(file)) references.add("security/references/javascript-typescript.md");
|
|
17502
|
-
if (/\.py(?:i)?$/u.test(file)) references.add("security/references/python.md");
|
|
17503
|
-
}
|
|
17504
|
-
return [...references];
|
|
17505
|
-
};
|
|
17506
|
-
function modeFloorInstructions(kind, changedFiles = []) {
|
|
17507
|
-
const referenceFiles = kind === "security" ? securityReferenceFiles(changedFiles) : [];
|
|
17508
|
-
const cacheKey = `${kind}:${referenceFiles.join(",")}`;
|
|
17509
|
-
const cached = modeFloorCache.get(cacheKey);
|
|
17510
|
-
if (cached !== void 0) return cached;
|
|
17511
|
-
const text = [MODE_FLOOR_FILES[kind], ...referenceFiles].map((file) => readFileSync(path.join(reviewPromptsDir, file), "utf-8")).map((contents) => contents.replace(/\n+$/u, "")).join("\n\n---\n\n");
|
|
17512
|
-
modeFloorCache.set(cacheKey, text);
|
|
17513
|
-
return text;
|
|
17514
|
-
}
|
|
17515
|
-
const issueFocusSegment = (focus) => ({
|
|
17516
|
-
section: {
|
|
17517
|
-
provenance: "issue-review-focus",
|
|
17518
|
-
source: `issue #${focus.issueNumber} "${REVIEW_FOCUS_SECTION}" section`,
|
|
17519
|
-
text: focus.text
|
|
17520
|
-
},
|
|
17521
|
-
text: `Per-issue review focus (source: issue #${focus.issueNumber} "${REVIEW_FOCUS_SECTION}" section, appended verbatim):
|
|
17522
|
-
${focus.text}`
|
|
17523
|
-
});
|
|
17524
|
-
const priorLedgerEntryLine = (entry) => {
|
|
17525
|
-
const location = entry.file ? ` (${entry.file})` : "";
|
|
17526
|
-
const reference = entry.reference ? ` — ${entry.reference}` : "";
|
|
17527
|
-
return `- [${entry.disposition}] "${entry.title}"${location}${reference} (flagged ${entry.flagCount}x)`;
|
|
17528
|
-
};
|
|
17529
|
-
const priorLedgerSegment = (entries) => ({
|
|
17530
|
-
section: {
|
|
17531
|
-
provenance: "prior-review-ledger",
|
|
17532
|
-
source: "persisted pr:review disposition ledger (prior cycles, #233)",
|
|
17533
|
-
text: entries.map(priorLedgerEntryLine).join("\n")
|
|
17534
|
-
},
|
|
17535
|
-
text: `Prior review ledger (source: persisted pr:review disposition ledger from earlier cycles on this PR; already adjudicated — do not re-litigate):
|
|
17536
|
-
${entries.map(priorLedgerEntryLine).join("\n")}
|
|
17537
|
-
|
|
17538
|
-
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.`
|
|
17539
|
-
});
|
|
17540
|
-
const assembleReviewPrompt = ({ base, changedFiles, headSha, issueFocus, kind, patchId, priorLedgerEntries, profile }) => {
|
|
17541
|
-
if (!resolveApplicableReviewModes(profile, changedFiles).includes(kind)) throw new Error(`${kind} review mode does not apply to the changed files.`);
|
|
17542
|
-
const priorLedger = priorLedgerEntries ? nonOpenLadderFindings(priorLedgerEntries) : [];
|
|
17543
|
-
const optionalSegments = {
|
|
17544
|
-
"issue-review-focus": issueFocus ? issueFocusSegment(issueFocus) : void 0,
|
|
17545
|
-
"prior-review-ledger": priorLedger.length > 0 ? priorLedgerSegment(priorLedger) : void 0
|
|
17546
|
-
};
|
|
17547
|
-
const segments = [
|
|
17548
|
-
{ text: reviewScope({
|
|
17549
|
-
base,
|
|
17550
|
-
changedFiles,
|
|
17551
|
-
headSha,
|
|
17552
|
-
patchId,
|
|
17553
|
-
profile
|
|
17554
|
-
}) },
|
|
17555
|
-
{ text: modeFloorInstructions(kind, changedFiles) },
|
|
17556
|
-
...REVIEW_PROMPT_SECTION_PROVENANCES.flatMap((provenance) => {
|
|
17557
|
-
const segment = optionalSegments[provenance];
|
|
17558
|
-
return segment ? [segment] : [];
|
|
17559
|
-
})
|
|
17560
|
-
];
|
|
17561
|
-
return {
|
|
17562
|
-
prompt: segments.map((segment) => segment.text).join("\n\n"),
|
|
17563
|
-
sections: segments.flatMap((segment) => segment.section ? [segment.section] : [])
|
|
17564
|
-
};
|
|
17565
|
-
};
|
|
17566
|
-
//#endregion
|
|
17567
18071
|
//#region src/index.ts
|
|
17568
18072
|
function createProgram(options = {}) {
|
|
17569
18073
|
const factoryCliInvocation = options.factoryCliInvocation ? FactoryCliInvocationSchema.parse(options.factoryCliInvocation) : defaultFactoryCliInvocation(import.meta.filename);
|