@effect-agent/pr-review 0.1.0-beta.22 → 0.1.0-beta.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 +72 -48
- package/dist/action.d.mts +13 -13
- package/dist/action.mjs +42 -25
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{fan-out-n-00ppWr.d.mts → fan-out-Bi1v0VaU.d.mts} +314 -307
- package/dist/{github-BgtP7Rdv.mjs → github-C6jrBLA2.mjs} +763 -285
- package/dist/github-C6jrBLA2.mjs.map +1 -0
- package/dist/index.d.mts +84 -126
- package/dist/index.mjs +11 -11
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-BguZK4B_.mjs → providers-2Jao2ZAX.mjs} +173 -585
- package/dist/providers-2Jao2ZAX.mjs.map +1 -0
- package/dist/testing.d.mts +6 -15
- package/dist/testing.mjs +14 -42
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +58 -34
- package/src/internal/action-entry.ts +0 -1
- package/src/internal/coverage.ts +147 -548
- package/src/internal/factory.ts +33 -54
- package/src/internal/fan-out-scripted.ts +26 -80
- package/src/internal/fan-out.ts +669 -432
- package/src/internal/fingerprint.ts +16 -10
- package/src/internal/fixtures.ts +6 -0
- package/src/internal/github.ts +36 -7
- package/src/internal/profiles.ts +8 -8
- package/src/internal/render.ts +34 -45
- package/src/internal/retirement.ts +3 -1
- package/src/internal/review-state.ts +204 -89
- package/src/internal/review-units.ts +25 -0
- package/src/internal/run.ts +231 -174
- package/src/internal/source.ts +1 -1
- package/dist/github-BgtP7Rdv.mjs.map +0 -1
- package/dist/providers-BguZK4B_.mjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
1
|
+
import { Crypto, Effect, Encoding } from "effect";
|
|
2
2
|
|
|
3
3
|
import type { ChangedFile } from "./diff.ts";
|
|
4
4
|
|
|
@@ -36,14 +36,14 @@ export const extractFingerprint = (body: string): string | undefined => {
|
|
|
36
36
|
return last;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
/**
|
|
40
|
-
const sha256Hex = (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
/** SHA-256 via `Crypto.Crypto`; unavailable crypto is a defect, not an expected failure. */
|
|
40
|
+
const sha256Hex = Effect.fn("sha256Hex")(function* (
|
|
41
|
+
text: string,
|
|
42
|
+
): Effect.fn.Return<string, never, Crypto.Crypto> {
|
|
43
|
+
const crypto = yield* Crypto.Crypto;
|
|
44
|
+
const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(text)).pipe(Effect.orDie);
|
|
45
|
+
return Encoding.encodeHex(digest);
|
|
46
|
+
});
|
|
47
47
|
|
|
48
48
|
const FIELD = "\u0000";
|
|
49
49
|
const RECORD = "\u0001";
|
|
@@ -80,4 +80,10 @@ const canonicalChangeset = (files: ReadonlyArray<ChangedFile>): string =>
|
|
|
80
80
|
export const computeChangesetFingerprint = (
|
|
81
81
|
files: ReadonlyArray<ChangedFile>,
|
|
82
82
|
signature: string,
|
|
83
|
-
): Effect.Effect<string> =>
|
|
83
|
+
): Effect.Effect<string, never, Crypto.Crypto> =>
|
|
84
|
+
sha256Hex(`${canonicalChangeset(files)}${SECTION}${signature}`);
|
|
85
|
+
|
|
86
|
+
/** Profile fingerprints are SHA-256 over configuration-only signatures. */
|
|
87
|
+
export const computeProfileFingerprint = (
|
|
88
|
+
signature: string,
|
|
89
|
+
): Effect.Effect<string, never, Crypto.Crypto> => sha256Hex(signature);
|
package/src/internal/fixtures.ts
CHANGED
|
@@ -120,6 +120,7 @@ export const staticPriorReviews = (
|
|
|
120
120
|
options: {
|
|
121
121
|
readonly state?: Option.Option<ReviewState> | undefined;
|
|
122
122
|
readonly comparison?: ReviewHeadComparison | undefined;
|
|
123
|
+
readonly treeComparison?: ReviewHeadComparison | undefined;
|
|
123
124
|
} = {},
|
|
124
125
|
): PriorReviews["Service"] =>
|
|
125
126
|
PriorReviews.of({
|
|
@@ -129,6 +130,10 @@ export const staticPriorReviews = (
|
|
|
129
130
|
options.comparison === undefined
|
|
130
131
|
? Effect.fail(PriorReviewLookupFailure.make({ reason: "no fixture comparison" }))
|
|
131
132
|
: Effect.succeed(options.comparison),
|
|
133
|
+
compareTrees: () =>
|
|
134
|
+
options.treeComparison === undefined
|
|
135
|
+
? Effect.fail(PriorReviewLookupFailure.make({ reason: "no fixture tree comparison" }))
|
|
136
|
+
: Effect.succeed(options.treeComparison),
|
|
132
137
|
});
|
|
133
138
|
|
|
134
139
|
/** Layer form for consumers whose Effect explicitly requires `PriorReviews`. */
|
|
@@ -137,6 +142,7 @@ export const staticPriorReviewsLayer = (
|
|
|
137
142
|
options: {
|
|
138
143
|
readonly state?: Option.Option<ReviewState> | undefined;
|
|
139
144
|
readonly comparison?: ReviewHeadComparison | undefined;
|
|
145
|
+
readonly treeComparison?: ReviewHeadComparison | undefined;
|
|
140
146
|
} = {},
|
|
141
147
|
): Layer.Layer<PriorReviews> =>
|
|
142
148
|
Layer.succeed(PriorReviews)(staticPriorReviews(fingerprint, options));
|
package/src/internal/github.ts
CHANGED
|
@@ -130,8 +130,9 @@ const GitHubReviewCommentWire = Schema.Struct({
|
|
|
130
130
|
node_id: Schema.String,
|
|
131
131
|
path: Schema.String,
|
|
132
132
|
body: Schema.String,
|
|
133
|
-
line
|
|
134
|
-
|
|
133
|
+
// Outdated comments omit `line` entirely instead of sending null.
|
|
134
|
+
line: Schema.optionalKey(Schema.NullOr(Schema.Int)),
|
|
135
|
+
original_line: Schema.optionalKey(Schema.NullOr(Schema.Int)),
|
|
135
136
|
start_line: Schema.optionalKey(Schema.NullOr(Schema.Int)),
|
|
136
137
|
original_start_line: Schema.optionalKey(Schema.NullOr(Schema.Int)),
|
|
137
138
|
});
|
|
@@ -578,8 +579,11 @@ export const gitHubReviewRetirementHostLayer: Layer.Layer<
|
|
|
578
579
|
}).pipe(
|
|
579
580
|
Effect.map((comments) =>
|
|
580
581
|
comments.map((comment) => {
|
|
581
|
-
const
|
|
582
|
-
|
|
582
|
+
const positiveLine = (value: number | null | undefined): number | null =>
|
|
583
|
+
value !== undefined && value !== null && value > 0 ? value : null;
|
|
584
|
+
const endLine = positiveLine(comment.line ?? comment.original_line);
|
|
585
|
+
const startLine =
|
|
586
|
+
positiveLine(comment.start_line ?? comment.original_start_line) ?? endLine;
|
|
583
587
|
return RetirableReviewComment.make({
|
|
584
588
|
nodeId: comment.node_id,
|
|
585
589
|
path: comment.path,
|
|
@@ -672,6 +676,15 @@ export class PriorReviews extends Context.Service<
|
|
|
672
676
|
baseSha: string,
|
|
673
677
|
headSha: string,
|
|
674
678
|
) => Effect.Effect<ReviewHeadComparison, PriorReviewLookupFailure>;
|
|
679
|
+
/**
|
|
680
|
+
* Two-dot tree comparison (`base..head`). Used when the reviewed head is
|
|
681
|
+
* not a git ancestor so a rebase or amend can still name the paths whose
|
|
682
|
+
* blob contents actually changed.
|
|
683
|
+
*/
|
|
684
|
+
readonly compareTrees: (
|
|
685
|
+
baseSha: string,
|
|
686
|
+
headSha: string,
|
|
687
|
+
) => Effect.Effect<ReviewHeadComparison, PriorReviewLookupFailure>;
|
|
675
688
|
}
|
|
676
689
|
>()("@effect-agent/pr-review/PriorReviews") {}
|
|
677
690
|
|
|
@@ -775,12 +788,12 @@ export const gitHubPriorReviewsLayer: Layer.Layer<
|
|
|
775
788
|
}
|
|
776
789
|
return { latestFingerprint: latest, latestState };
|
|
777
790
|
}).pipe(Effect.provideService(HttpClient.HttpClient, client));
|
|
778
|
-
const
|
|
791
|
+
const compareCommits = (baseSha: string, headSha: string, separator: "..." | "..") =>
|
|
779
792
|
Effect.gen(function* () {
|
|
780
793
|
const response = yield* HttpClient.execute(
|
|
781
794
|
withCommonHeaders(
|
|
782
795
|
HttpClientRequest.get(
|
|
783
|
-
`${target.apiUrl}/repos/${target.repository}/compare/${encodeURIComponent(baseSha)}
|
|
796
|
+
`${target.apiUrl}/repos/${target.repository}/compare/${encodeURIComponent(baseSha)}${separator}${encodeURIComponent(headSha)}`,
|
|
784
797
|
).pipe(HttpClientRequest.acceptJson),
|
|
785
798
|
target.token,
|
|
786
799
|
),
|
|
@@ -803,6 +816,21 @@ export const gitHubPriorReviewsLayer: Layer.Layer<
|
|
|
803
816
|
truncated: files.length >= MAX_CHANGED_FILES,
|
|
804
817
|
});
|
|
805
818
|
}).pipe(Effect.provideService(HttpClient.HttpClient, client));
|
|
819
|
+
const compareTrees = (baseSha: string, headSha: string) =>
|
|
820
|
+
compareCommits(baseSha, headSha, "..").pipe(
|
|
821
|
+
Effect.map((comparison) =>
|
|
822
|
+
ReviewHeadComparison.make({
|
|
823
|
+
// This is a content snapshot, not a lineage claim. Selection
|
|
824
|
+
// intersects these files with the current PR path set.
|
|
825
|
+
status: comparison.status === "identical" ? "identical" : "ahead",
|
|
826
|
+
baseSha,
|
|
827
|
+
headSha,
|
|
828
|
+
mergeBaseSha: baseSha,
|
|
829
|
+
files: comparison.files,
|
|
830
|
+
truncated: comparison.truncated,
|
|
831
|
+
}),
|
|
832
|
+
),
|
|
833
|
+
);
|
|
806
834
|
return PriorReviews.of({
|
|
807
835
|
latestFingerprint: readMarkers(Option.none()).pipe(
|
|
808
836
|
Effect.map((markers) => markers.latestFingerprint),
|
|
@@ -813,7 +841,8 @@ export const gitHubPriorReviewsLayer: Layer.Layer<
|
|
|
813
841
|
Effect.map((markers) => markers.latestState),
|
|
814
842
|
);
|
|
815
843
|
}),
|
|
816
|
-
compareHeads,
|
|
844
|
+
compareHeads: (baseSha, headSha) => compareCommits(baseSha, headSha, "..."),
|
|
845
|
+
compareTrees,
|
|
817
846
|
});
|
|
818
847
|
}),
|
|
819
848
|
);
|
package/src/internal/profiles.ts
CHANGED
|
@@ -38,18 +38,18 @@ export const pullRequestReviewerProfile = PullRequestReviewerProfile.make({
|
|
|
38
38
|
export class FanOutReviewerProfile extends Schema.Class<FanOutReviewerProfile>(
|
|
39
39
|
"@effect-agent/pr-review/FanOutReviewerProfile",
|
|
40
40
|
)({
|
|
41
|
-
/** Ephemeral runtime:
|
|
41
|
+
/** Ephemeral runtime: bounded host-scheduled child runs per invocation. */
|
|
42
42
|
deploymentClass: Schema.Literal("E"),
|
|
43
|
-
/** Every model-callable
|
|
43
|
+
/** Every model-callable surface is evidence-only; children expose no tools. */
|
|
44
44
|
readOnlyToolSurface: Schema.Literal(true),
|
|
45
45
|
/** The review is posted by the host AFTER the run settles, never by a tool. */
|
|
46
46
|
publicationOutsideAgentLoop: Schema.Literal(true),
|
|
47
47
|
/** Child findings are untrusted; anchors are validated against the parsed diff. */
|
|
48
48
|
anchorsValidatedBeforePublication: Schema.Literal(true),
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
/** A failed
|
|
52
|
-
|
|
49
|
+
/** Host code schedules every pass from the deterministic plan; no coordinator model. */
|
|
50
|
+
hostScheduledPasses: Schema.Literal(true),
|
|
51
|
+
/** A failed pass is retried once, then reported and carried as unreviewed scope. */
|
|
52
|
+
failedPassesRetriedOnceThenCarried: Schema.Literal(true),
|
|
53
53
|
/** Risk categories and required specialist passes are pure host policy. */
|
|
54
54
|
hostOwnedRiskClassification: Schema.Literal(true),
|
|
55
55
|
/** Every host-classified high-risk unit receives a fresh specialist pass. */
|
|
@@ -69,8 +69,8 @@ export const fanOutReviewerProfile = FanOutReviewerProfile.make({
|
|
|
69
69
|
readOnlyToolSurface: true,
|
|
70
70
|
publicationOutsideAgentLoop: true,
|
|
71
71
|
anchorsValidatedBeforePublication: true,
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
hostScheduledPasses: true,
|
|
73
|
+
failedPassesRetriedOnceThenCarried: true,
|
|
74
74
|
hostOwnedRiskClassification: true,
|
|
75
75
|
redundantHighRiskDiscovery: true,
|
|
76
76
|
independentCandidateVerification: true,
|
package/src/internal/render.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Schema } from "effect";
|
|
|
2
2
|
|
|
3
3
|
import { anchorViolation } from "./anchors.ts";
|
|
4
4
|
export { anchorViolation } from "./anchors.ts";
|
|
5
|
-
import type { ReviewAssurance,
|
|
5
|
+
import type { ReviewAssurance, ReviewInputCoverage } from "./coverage.ts";
|
|
6
6
|
import type { ChangedFile } from "./diff.ts";
|
|
7
7
|
import { renderFingerprintMarker } from "./fingerprint.ts";
|
|
8
8
|
import {
|
|
@@ -215,27 +215,27 @@ const renderVerdictCallout = (
|
|
|
215
215
|
options: {
|
|
216
216
|
readonly carriedFindings?: ReadonlyArray<ReviewFinding> | undefined;
|
|
217
217
|
readonly carriedConcerns?: ReadonlyArray<ReviewConcern> | undefined;
|
|
218
|
-
readonly coverage?: ReviewCoverage | undefined;
|
|
219
218
|
readonly inputCoverage?: ReviewInputCoverage | undefined;
|
|
220
219
|
readonly assurance?: ReviewAssurance | undefined;
|
|
220
|
+
readonly unreviewedPaths?: ReadonlyArray<string> | undefined;
|
|
221
221
|
},
|
|
222
222
|
): string => {
|
|
223
223
|
const counts = severityCounts(review, options.carriedFindings, options.carriedConcerns);
|
|
224
|
+
// Code findings outrank machinery gaps: a blocking finding is the
|
|
225
|
+
// actionable signal, and unsettled reviewer-side work is carried forward.
|
|
226
|
+
if (counts.blocking > 0) {
|
|
227
|
+
return `> [!CAUTION]\n> ${countNoun(counts.blocking, "blocking finding")} — do not merge before addressing ${counts.blocking === 1 ? "it" : "them"}.`;
|
|
228
|
+
}
|
|
229
|
+
const carried = options.unreviewedPaths?.length ?? 0;
|
|
224
230
|
if (
|
|
225
231
|
options.inputCoverage?.status === "incomplete" ||
|
|
226
|
-
|
|
232
|
+
options.assurance?.status === "incomplete"
|
|
227
233
|
) {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const suffix =
|
|
234
|
-
counts.blocking > 0 ? ` It also has ${countNoun(counts.blocking, "blocking finding")}.` : "";
|
|
235
|
-
return `> [!CAUTION]\n> Configured review assurance did not settle — the check must not pass.${suffix}`;
|
|
236
|
-
}
|
|
237
|
-
if (counts.blocking > 0) {
|
|
238
|
-
return `> [!CAUTION]\n> ${countNoun(counts.blocking, "blocking finding")} — do not merge before addressing ${counts.blocking === 1 ? "it" : "them"}.`;
|
|
234
|
+
const carriedNote =
|
|
235
|
+
carried > 0
|
|
236
|
+
? ` ${countNoun(carried, "affected path")} ${carried === 1 ? "is" : "are"} carried forward and retried automatically on the next run.`
|
|
237
|
+
: "";
|
|
238
|
+
return `> [!WARNING]\n> Review infrastructure did not settle — a reviewer-side gap, NOT a request to change code.${carriedNote} The check reports "incomplete" until a run settles.`;
|
|
239
239
|
}
|
|
240
240
|
if (counts.important > 0) {
|
|
241
241
|
return `> [!IMPORTANT]\n> ${countNoun(counts.important, "important finding")} to address before merging.`;
|
|
@@ -393,22 +393,20 @@ export const planPublication = (
|
|
|
393
393
|
readonly modelLabel?: string | undefined;
|
|
394
394
|
/** Workflow-run URL rendered into the footer. */
|
|
395
395
|
readonly runUrl?: string | undefined;
|
|
396
|
-
/** Observed run usage rendered into the footer. */
|
|
396
|
+
/** Observed whole-run usage rendered into the footer. */
|
|
397
397
|
readonly usage?: { readonly inputTokens: number; readonly outputTokens: number } | undefined;
|
|
398
|
-
/** What the usage observed: the whole run, or the coordinator only. */
|
|
399
|
-
readonly usageScope?: "run" | "coordinator" | undefined;
|
|
400
398
|
/**
|
|
401
399
|
* Changeset fingerprint embedded invisibly in the review body so a later
|
|
402
400
|
* run can skip re-reviewing an unchanged changeset.
|
|
403
401
|
*/
|
|
404
402
|
readonly fingerprint?: string | undefined;
|
|
405
|
-
/** Host-owned coverage; incomplete coverage is rendered and fails the check. */
|
|
406
|
-
readonly coverage?: ReviewCoverage | undefined;
|
|
407
403
|
/** Host-owned path/evidence assignment, separate from review assurance. */
|
|
408
404
|
readonly inputCoverage?: ReviewInputCoverage | undefined;
|
|
409
405
|
/** Host-owned discovery/specialist/verification settlement. */
|
|
410
406
|
readonly assurance?: ReviewAssurance | undefined;
|
|
411
|
-
/**
|
|
407
|
+
/** Retryable scope this run could not settle; carried to the next run. */
|
|
408
|
+
readonly unreviewedPaths?: ReadonlyArray<string> | undefined;
|
|
409
|
+
/** Unchanged unresolved items carried from the prior reviewed baseline. */
|
|
412
410
|
readonly carriedFindings?: ReadonlyArray<ReviewFinding> | undefined;
|
|
413
411
|
readonly carriedConcerns?: ReadonlyArray<ReviewConcern> | undefined;
|
|
414
412
|
/** Selected review scope, made visible whenever orchestration chose it. */
|
|
@@ -452,14 +450,8 @@ export const planPublication = (
|
|
|
452
450
|
|
|
453
451
|
const footerParts = ["Automated review by @effect-agent/pr-review"];
|
|
454
452
|
if (options.modelLabel !== undefined) footerParts.push(options.modelLabel);
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
// coordinator, and omitting the number is honest where mislabeling is not.
|
|
458
|
-
if (options.usage !== undefined && options.usageScope !== undefined) {
|
|
459
|
-
const scope = options.usageScope === "coordinator" ? " (coordinator)" : "";
|
|
460
|
-
footerParts.push(
|
|
461
|
-
`${options.usage.inputTokens} in / ${options.usage.outputTokens} out tokens${scope}`,
|
|
462
|
-
);
|
|
453
|
+
if (options.usage !== undefined) {
|
|
454
|
+
footerParts.push(`${options.usage.inputTokens} in / ${options.usage.outputTokens} out tokens`);
|
|
463
455
|
}
|
|
464
456
|
if (options.runUrl !== undefined) footerParts.push(`[run](${options.runUrl})`);
|
|
465
457
|
footerParts.push(`reviewed at ${options.headSha.slice(0, 7)}`);
|
|
@@ -490,9 +482,9 @@ export const planPublication = (
|
|
|
490
482
|
renderVerdictCallout(review, {
|
|
491
483
|
carriedFindings,
|
|
492
484
|
carriedConcerns,
|
|
493
|
-
coverage: options.coverage,
|
|
494
485
|
inputCoverage: options.inputCoverage,
|
|
495
486
|
assurance: options.assurance,
|
|
487
|
+
unreviewedPaths: options.unreviewedPaths,
|
|
496
488
|
}),
|
|
497
489
|
];
|
|
498
490
|
if (options.reviewMode !== undefined && options.reviewReason !== undefined) {
|
|
@@ -513,7 +505,7 @@ export const planPublication = (
|
|
|
513
505
|
if (options.inputCoverage !== undefined && options.assurance !== undefined) {
|
|
514
506
|
parts.push(
|
|
515
507
|
"",
|
|
516
|
-
`**Input coverage:** ${options.inputCoverage.status} (${options.inputCoverage.assignedPaths.length}/${options.inputCoverage.requiredPaths.length} paths assigned, ${options.inputCoverage.partialPaths.length} partial) · **Review assurance:** ${options.assurance.status} (${options.assurance.completedGeneralDiscoveryPasses}/${options.assurance.requiredGeneralDiscoveryPasses} general discovery, ${options.assurance.completedSpecialistPasses}/${options.assurance.requiredSpecialistPasses} specialist, ${options.assurance.completedVerificationPasses}/${options.assurance.requiredVerificationPasses} verification; ${options.assurance.confirmedCandidates} confirmed / ${options.assurance.rejectedCandidates} rejected / ${options.assurance.unsettledCandidates} unsettled candidates)`,
|
|
508
|
+
`**Input coverage:** ${options.inputCoverage.status} (${options.inputCoverage.assignedPaths.length}/${options.inputCoverage.requiredPaths.length} paths assigned, ${options.inputCoverage.partialPaths.length} partial) · **Review assurance:** ${options.assurance.status} (${options.assurance.completedGeneralDiscoveryPasses}/${options.assurance.requiredGeneralDiscoveryPasses} general discovery, ${options.assurance.completedSpecialistPasses}/${options.assurance.requiredSpecialistPasses} specialist, ${options.assurance.completedVerificationPasses}/${options.assurance.requiredVerificationPasses} verification; ${options.assurance.confirmedCandidates} confirmed / ${options.assurance.rejectedCandidates} rejected / ${options.assurance.unsettledCandidates} unsettled${options.assurance.discardedInvalidFindings > 0 ? ` / ${options.assurance.discardedInvalidFindings} discarded` : ""} candidates)`,
|
|
517
509
|
);
|
|
518
510
|
}
|
|
519
511
|
parts.push("", review.summary);
|
|
@@ -525,22 +517,17 @@ export const planPublication = (
|
|
|
525
517
|
if (options.inputCoverage?.status === "incomplete") {
|
|
526
518
|
parts.push(
|
|
527
519
|
"",
|
|
528
|
-
"###
|
|
520
|
+
"### ⚠️ Incomplete input coverage",
|
|
529
521
|
"",
|
|
530
522
|
...options.inputCoverage.reasons.map((reason) => `- ${reason}`),
|
|
531
523
|
);
|
|
532
|
-
} else if (options.inputCoverage === undefined && options.coverage?.status === "incomplete") {
|
|
533
|
-
parts.push(
|
|
534
|
-
"",
|
|
535
|
-
"### 🛑 Incomplete coverage",
|
|
536
|
-
"",
|
|
537
|
-
...options.coverage.reasons.map((reason) => `- ${reason}`),
|
|
538
|
-
);
|
|
539
524
|
}
|
|
540
|
-
if (options.assurance
|
|
525
|
+
if (options.assurance?.status === "incomplete") {
|
|
541
526
|
parts.push(
|
|
542
527
|
"",
|
|
543
|
-
"###
|
|
528
|
+
"### ⚠️ Unsettled review passes",
|
|
529
|
+
"",
|
|
530
|
+
"The passes below failed on the reviewer's side after a bounded retry. Their paths are carried forward and re-reviewed automatically on the next run — do not change code to satisfy this section.",
|
|
544
531
|
"",
|
|
545
532
|
...options.assurance.reasons.map((reason) => `- ${reason}`),
|
|
546
533
|
);
|
|
@@ -613,14 +600,16 @@ export const planPublication = (
|
|
|
613
600
|
options.carriedFindings ?? [],
|
|
614
601
|
options.carriedConcerns ?? [],
|
|
615
602
|
);
|
|
603
|
+
// Machinery gaps (incomplete input or unsettled passes) block APPROVE but
|
|
604
|
+
// never REQUEST_CHANGES: requesting changes for a reviewer-side fault would
|
|
605
|
+
// tell the author to edit code nobody reviewed.
|
|
606
|
+
const unclean =
|
|
607
|
+
options.inputCoverage?.status === "incomplete" || options.assurance?.status === "incomplete";
|
|
616
608
|
const event: ReviewEvent = !options.applyVerdict
|
|
617
609
|
? "COMMENT"
|
|
618
|
-
:
|
|
619
|
-
(options.inputCoverage === undefined && options.coverage?.status === "incomplete") ||
|
|
620
|
-
(options.assurance !== undefined && options.assurance.status !== "settled") ||
|
|
621
|
-
counts.blocking > 0
|
|
610
|
+
: counts.blocking > 0
|
|
622
611
|
? "REQUEST_CHANGES"
|
|
623
|
-
: review.verdict === "approve" && counts.important === 0
|
|
612
|
+
: review.verdict === "approve" && counts.important === 0 && !unclean
|
|
624
613
|
? "APPROVE"
|
|
625
614
|
: "COMMENT";
|
|
626
615
|
|
|
@@ -102,8 +102,10 @@ const findingIdentity = (finding: {
|
|
|
102
102
|
|
|
103
103
|
const REVIEW_METADATA_PATTERN = /<!-- effect-agent-pr-review metadata\n[\s\S]*?\n-->/g;
|
|
104
104
|
const FINGERPRINT_PATTERN = /<!-- effect-agent-pr-review fingerprint=sha256:[0-9a-f]{64} -->/g;
|
|
105
|
+
// Version-agnostic on purpose: retirement only RECOGNIZES machine markers to
|
|
106
|
+
// keep them byte-identical through an edit; it never authenticates them.
|
|
105
107
|
const STATE_PATTERN =
|
|
106
|
-
/<!-- effect-agent-pr-review state-
|
|
108
|
+
/<!-- effect-agent-pr-review state-v\d+:[A-Za-z0-9+/]+={0,2}\.[0-9a-f]{64} -->/g;
|
|
107
109
|
const RETIRED_ORIGINAL_PATTERN =
|
|
108
110
|
/<!-- effect-agent-pr-review retired-original:start -->\n([\s\S]*?)\n<!-- effect-agent-pr-review retired-original:end -->/;
|
|
109
111
|
const MACHINE_COMMENT_PATTERN = new RegExp(
|