@llm4ts/flow 2.50.0 → 2.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FlowEvents.d.ts +2 -0
- package/dist/FlowEvents.d.ts.map +1 -1
- package/dist/FlowEvents.js +3 -1
- package/dist/FlowEvents.js.map +1 -1
- package/dist/Recheck.d.ts +69 -0
- package/dist/Recheck.d.ts.map +1 -0
- package/dist/Recheck.js +111 -0
- package/dist/Recheck.js.map +1 -0
- package/dist/Review.d.ts +32 -33
- package/dist/Review.d.ts.map +1 -1
- package/dist/Review.js +104 -45
- package/dist/Review.js.map +1 -1
- package/dist/ReviewCache.d.ts +1 -1
- package/dist/ReviewCache.d.ts.map +1 -1
- package/dist/ReviewCache.js +1 -1
- package/dist/ReviewCache.js.map +1 -1
- package/dist/ReviewModel.d.ts +36 -0
- package/dist/ReviewModel.d.ts.map +1 -0
- package/dist/ReviewModel.js +50 -0
- package/dist/ReviewModel.js.map +1 -0
- package/dist/Stories.d.ts +36 -2
- package/dist/Stories.d.ts.map +1 -1
- package/dist/Stories.js +255 -90
- package/dist/Stories.js.map +1 -1
- package/package.json +4 -2
- package/src/FlowEvents.ts +3 -1
- package/src/Recheck.ts +160 -0
- package/src/Review.ts +178 -58
- package/src/ReviewCache.ts +1 -1
- package/src/ReviewModel.ts +63 -0
- package/src/Stories.ts +270 -18
package/src/Review.ts
CHANGED
|
@@ -34,6 +34,16 @@ import {
|
|
|
34
34
|
parseUnifiedDiff,
|
|
35
35
|
passedCountIn
|
|
36
36
|
} from "./OracleGuard.ts"
|
|
37
|
+
import { ReviewMemory } from "./Recheck.ts"
|
|
38
|
+
import {
|
|
39
|
+
type Candidate,
|
|
40
|
+
diffChunks,
|
|
41
|
+
planRound,
|
|
42
|
+
recheckLead,
|
|
43
|
+
remember,
|
|
44
|
+
standing,
|
|
45
|
+
touchedFiles
|
|
46
|
+
} from "./Recheck.ts"
|
|
37
47
|
import { cachedReview, fingerprintOf } from "./ReviewCache.ts"
|
|
38
48
|
import { withCallPurpose } from "./Timing.ts"
|
|
39
49
|
import {
|
|
@@ -61,46 +71,8 @@ import {
|
|
|
61
71
|
type JudgmentPolicy
|
|
62
72
|
} from "./Judgment.ts"
|
|
63
73
|
|
|
64
|
-
export
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
export class ReviewIssue extends Schema.Class<ReviewIssue>("ReviewIssue")({
|
|
68
|
-
severity: Severity,
|
|
69
|
-
title: Schema.String,
|
|
70
|
-
description: Schema.String.pipe(
|
|
71
|
-
Schema.withConstructorDefault(Effect.succeed("")),
|
|
72
|
-
Schema.withDecodingDefaultKey(Effect.succeed(""))
|
|
73
|
-
),
|
|
74
|
-
file: Schema.optionalKey(Schema.String),
|
|
75
|
-
line: Schema.optionalKey(Schema.Int),
|
|
76
|
-
suggestion: Schema.optionalKey(Schema.String),
|
|
77
|
-
confidence: Schema.Number.pipe(
|
|
78
|
-
Schema.withConstructorDefault(Effect.succeed(1)),
|
|
79
|
-
Schema.withDecodingDefaultKey(Effect.succeed(1))
|
|
80
|
-
),
|
|
81
|
-
/** A gate failure's triage against the base (ADR 0027); absent for review findings. */
|
|
82
|
-
origin: Schema.optionalKey(Schema.Literals(["new", "base", "flaky"])),
|
|
83
|
-
/** How a gate ended: a red exit, killed at the timeout, or a signal exit. */
|
|
84
|
-
gateClass: Schema.optionalKey(Schema.Literals(["red", "hang", "crash"])),
|
|
85
|
-
/** Why a test gate's output could not stand for green: no tests ran, or test files failed to load. */
|
|
86
|
-
gateReason: Schema.optionalKey(Schema.Literals(["zero-tests", "load-failure"])),
|
|
87
|
-
/** Where the gate's full output was written, when the caller asked for a log. */
|
|
88
|
-
logPath: Schema.optionalKey(Schema.String)
|
|
89
|
-
}) {}
|
|
90
|
-
|
|
91
|
-
export class ReviewResult extends Schema.Class<ReviewResult>("ReviewResult")({
|
|
92
|
-
issues: Schema.Array(ReviewIssue),
|
|
93
|
-
summary: Schema.String.pipe(
|
|
94
|
-
Schema.withConstructorDefault(Effect.succeed("")),
|
|
95
|
-
Schema.withDecodingDefaultKey(Effect.succeed(""))
|
|
96
|
-
),
|
|
97
|
-
/** A test gate's passed-test count, when its output had a summary the oracle guard reads (ADR 0027). */
|
|
98
|
-
passed: Schema.optionalKey(Schema.Int)
|
|
99
|
-
}) {
|
|
100
|
-
get isClean(): boolean {
|
|
101
|
-
return this.issues.length === 0
|
|
102
|
-
}
|
|
103
|
-
}
|
|
74
|
+
export { ReviewIssue, ReviewResult, Severity, reviewPrompt } from "./ReviewModel.ts"
|
|
75
|
+
import { ReviewIssue, ReviewResult, reviewPrompt } from "./ReviewModel.ts"
|
|
104
76
|
|
|
105
77
|
export const reviewJsonSchema: JsonSchema = {
|
|
106
78
|
type: "object",
|
|
@@ -300,19 +272,6 @@ const fileSlug = (name: string): string =>
|
|
|
300
272
|
.replace(/[^a-z0-9]+/g, "-")
|
|
301
273
|
.replace(/^-+|-+$/g, "") || "lens"
|
|
302
274
|
|
|
303
|
-
export const reviewPrompt = (task: string, diff: string): string =>
|
|
304
|
-
[
|
|
305
|
-
`Review the change below for task "${task}". Report problems as JSON:`,
|
|
306
|
-
'{"issues":[{"severity":"Critical|Warning|Info","title":"...",',
|
|
307
|
-
'"description":"..."}],"summary":"..."}.',
|
|
308
|
-
'An empty "issues" array means the change is acceptable. Respond with JSON only.',
|
|
309
|
-
"The diff below is the whole subject: judge what it shows. Do not explore the",
|
|
310
|
-
"repository, read other files, or run anything; answer from the diff.",
|
|
311
|
-
"",
|
|
312
|
-
"Diff:",
|
|
313
|
-
diff
|
|
314
|
-
].join("\n")
|
|
315
|
-
|
|
316
275
|
export interface FixPromptOptions {
|
|
317
276
|
/** Characters of a gate's output kept in the prompt, from the end. Default 4000. */
|
|
318
277
|
readonly tailChars?: number
|
|
@@ -460,6 +419,17 @@ export const applyTriage = Effect.fn("@llm4ts/flow/Review.applyTriage")(function
|
|
|
460
419
|
return triaged.blocking
|
|
461
420
|
})
|
|
462
421
|
|
|
422
|
+
/** Names `lens` as the source of every finding in `result` that names none yet (ADR 0044). */
|
|
423
|
+
export const tagLens = (lens: string, result: ReviewResult): ReviewResult =>
|
|
424
|
+
result.issues.every((issue) => issue.lens !== undefined)
|
|
425
|
+
? result
|
|
426
|
+
: ReviewResult.make({
|
|
427
|
+
...result,
|
|
428
|
+
issues: result.issues.map((issue) =>
|
|
429
|
+
issue.lens === undefined ? ReviewIssue.make({ ...issue, lens }) : issue
|
|
430
|
+
)
|
|
431
|
+
})
|
|
432
|
+
|
|
463
433
|
export const mergeReviewResults = (results: ReadonlyArray<ReviewResult>): ReviewResult => {
|
|
464
434
|
const counts = results.flatMap((result) => (result.passed === undefined ? [] : [result.passed]))
|
|
465
435
|
return ReviewResult.make({
|
|
@@ -755,6 +725,32 @@ export interface ReviewAndFixOptions {
|
|
|
755
725
|
* non-blocking. A red gate is always blocking.
|
|
756
726
|
*/
|
|
757
727
|
readonly settle?: SettleRule
|
|
728
|
+
/**
|
|
729
|
+
* Incremental re-review (ADR 0044): each reviewer's findings are kept
|
|
730
|
+
* across rounds, a later round asks only the reviewers with findings (a
|
|
731
|
+
* re-check of them) or with files they have not seen, and the result is
|
|
732
|
+
* every finding still standing. Absent, every lens reviews every round.
|
|
733
|
+
*/
|
|
734
|
+
readonly recheck?: RecheckOptions
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** A reviewer beside the lenses, such as the story judge (ADR 0044). */
|
|
738
|
+
export interface ExtraReview {
|
|
739
|
+
readonly name: string
|
|
740
|
+
/** `prior`: its own standing findings when this run is a re-check; undefined for a full review. */
|
|
741
|
+
readonly review: (
|
|
742
|
+
diff: string,
|
|
743
|
+
prior: ReadonlyArray<ReviewIssue> | undefined
|
|
744
|
+
) => Effect.Effect<ReviewResult, FlowError>
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export interface RecheckOptions {
|
|
748
|
+
readonly extra?: ReadonlyArray<ExtraReview>
|
|
749
|
+
/** Where the reviewers' memory is kept, so a restart resumes at the next round. */
|
|
750
|
+
readonly memory?: {
|
|
751
|
+
readonly load: Effect.Effect<ReviewMemory | undefined, FlowError>
|
|
752
|
+
readonly save: (memory: ReviewMemory) => Effect.Effect<void, FlowError>
|
|
753
|
+
}
|
|
758
754
|
}
|
|
759
755
|
|
|
760
756
|
/** When a review round stops sending findings back to the coder (ADR 0031). */
|
|
@@ -865,9 +861,13 @@ export const reviewWith = (
|
|
|
865
861
|
events: FlowEventsShape,
|
|
866
862
|
lens: Reviewer,
|
|
867
863
|
taskTitle: string,
|
|
868
|
-
diff: string
|
|
864
|
+
diff: string,
|
|
865
|
+
/** A re-check (ADR 0044): the files the fix touched and the lens's own earlier findings. */
|
|
866
|
+
recheck?: { readonly touched: ReadonlyArray<string>; readonly prior: ReadonlyArray<ReviewIssue> }
|
|
869
867
|
): Effect.Effect<ReviewResult, FlowLlmError> => {
|
|
870
|
-
const
|
|
868
|
+
const lead =
|
|
869
|
+
recheck === undefined ? "" : `${recheckLead(taskTitle, recheck.touched, recheck.prior)}\n\n`
|
|
870
|
+
const prompt = `${lensPrompt(lens)}\n\n${lead}${reviewPrompt(taskTitle, diff)}`
|
|
871
871
|
// Usage is published per attempt — a schema retry costs real tokens too.
|
|
872
872
|
const attempt = (text: string): Effect.Effect<ReviewResult, LlmError> =>
|
|
873
873
|
service.executeStructuredWithUsage(text, ReviewResult, reviewJsonSchema).pipe(
|
|
@@ -915,6 +915,104 @@ export const reviewAndFixLoop = Effect.fn("@llm4ts/flow/Review.reviewAndFixLoop"
|
|
|
915
915
|
// A red gate is never settled early, whatever its findings' severities.
|
|
916
916
|
const gateRed = yield* Ref.make(false)
|
|
917
917
|
|
|
918
|
+
const recheck = options.recheck
|
|
919
|
+
const loaded = recheck?.memory === undefined ? undefined : yield* recheck.memory.load
|
|
920
|
+
const memory = yield* Ref.make<ReviewMemory | undefined>(loaded)
|
|
921
|
+
|
|
922
|
+
/** One round of incremental re-review (ADR 0044): returns what still stands. */
|
|
923
|
+
const recheckRound = (
|
|
924
|
+
settings: RecheckOptions,
|
|
925
|
+
round: number,
|
|
926
|
+
diff: string,
|
|
927
|
+
files: ReadonlyArray<string>
|
|
928
|
+
): Effect.Effect<ReviewResult, FlowError> =>
|
|
929
|
+
Effect.gen(function* () {
|
|
930
|
+
const before = yield* Ref.get(memory)
|
|
931
|
+
const chunks = diffChunks(diff)
|
|
932
|
+
const shown = files.length > 0 ? files : Object.keys(chunks)
|
|
933
|
+
const touched = before === undefined ? [] : touchedFiles(before.chunks, chunks)
|
|
934
|
+
const extras = settings.extra ?? []
|
|
935
|
+
const candidates: ReadonlyArray<Candidate> = [
|
|
936
|
+
...options.reviewers.map((lens) => ({
|
|
937
|
+
name: lens.name,
|
|
938
|
+
matches: (changed: ReadonlyArray<string>) => lens.matches(changed)
|
|
939
|
+
})),
|
|
940
|
+
...extras.map((extra) => ({ name: extra.name, matches: () => true, onlyWhenDirty: true }))
|
|
941
|
+
]
|
|
942
|
+
const plan = planRound(candidates, before, shown, touched)
|
|
943
|
+
const priorOf = (name: string): ReadonlyArray<ReviewIssue> =>
|
|
944
|
+
before?.reviewers.find((entry) => entry.name === name)?.findings ?? []
|
|
945
|
+
const asked = [...plan.full, ...plan.recheck]
|
|
946
|
+
if (asked.length > 0) {
|
|
947
|
+
yield* options.events.publish(
|
|
948
|
+
Info.make({
|
|
949
|
+
message: `review round ${round} of "${options.taskTitle}": ${asked
|
|
950
|
+
.map((name) => (plan.recheck.includes(name) ? `${name} (re-check)` : name))
|
|
951
|
+
.join(", ")}`
|
|
952
|
+
})
|
|
953
|
+
)
|
|
954
|
+
}
|
|
955
|
+
const cache = options.cache
|
|
956
|
+
const ask = (
|
|
957
|
+
name: string
|
|
958
|
+
): Effect.Effect<{ readonly name: string; readonly result: ReviewResult }, FlowError> => {
|
|
959
|
+
const again = plan.recheck.includes(name)
|
|
960
|
+
const prior = priorOf(name)
|
|
961
|
+
const lens = options.reviewers.find((candidate) => candidate.name === name)
|
|
962
|
+
const extra = extras.find((candidate) => candidate.name === name)
|
|
963
|
+
const lensAnswer = (lens: Reviewer): Effect.Effect<ReviewResult, FlowError> => {
|
|
964
|
+
const call = withCallPurpose(
|
|
965
|
+
`${name} lens${again ? " · re-check" : ""}`,
|
|
966
|
+
reviewWith(
|
|
967
|
+
options.reviewerService,
|
|
968
|
+
options.events,
|
|
969
|
+
lens,
|
|
970
|
+
options.taskTitle,
|
|
971
|
+
diff,
|
|
972
|
+
again ? { touched, prior } : undefined
|
|
973
|
+
)
|
|
974
|
+
)
|
|
975
|
+
const answered =
|
|
976
|
+
cache === undefined
|
|
977
|
+
? call
|
|
978
|
+
: cachedReview(
|
|
979
|
+
cache.files,
|
|
980
|
+
join(cache.dir, `${fileSlug(name)}.json`),
|
|
981
|
+
fingerprintOf([
|
|
982
|
+
name,
|
|
983
|
+
lensPrompt(lens),
|
|
984
|
+
"0",
|
|
985
|
+
options.taskTitle,
|
|
986
|
+
diff,
|
|
987
|
+
...(again ? ["recheck", ...prior.map((issue) => issue.title)] : [])
|
|
988
|
+
]),
|
|
989
|
+
call
|
|
990
|
+
)
|
|
991
|
+
return Effect.flatMap(answered, (result) =>
|
|
992
|
+
demoteUnplaced(lens, result, files, options.events)
|
|
993
|
+
)
|
|
994
|
+
}
|
|
995
|
+
const answer: Effect.Effect<ReviewResult, FlowError> =
|
|
996
|
+
lens !== undefined
|
|
997
|
+
? lensAnswer(lens)
|
|
998
|
+
: extra !== undefined
|
|
999
|
+
? extra.review(diff, again ? prior : undefined)
|
|
1000
|
+
: Effect.succeed(ReviewResult.make({ issues: [] }))
|
|
1001
|
+
return Effect.map(answer, (result) => ({ name, result: tagLens(name, result) }))
|
|
1002
|
+
}
|
|
1003
|
+
const parallelism = options.parallelism ?? 0
|
|
1004
|
+
const ran =
|
|
1005
|
+
parallelism > 0
|
|
1006
|
+
? yield* Effect.forEach(asked, ask, { concurrency: parallelism })
|
|
1007
|
+
: yield* Effect.forEach(asked, ask, { concurrency: "unbounded" })
|
|
1008
|
+
const next = remember(before, round, chunks, ran, shown)
|
|
1009
|
+
yield* Ref.set(memory, next)
|
|
1010
|
+
if (settings.memory !== undefined) {
|
|
1011
|
+
yield* settings.memory.save(next)
|
|
1012
|
+
}
|
|
1013
|
+
return standing(next)
|
|
1014
|
+
})
|
|
1015
|
+
|
|
918
1016
|
const reviewOnce = (
|
|
919
1017
|
round: number,
|
|
920
1018
|
previous: ReviewResult | undefined
|
|
@@ -933,6 +1031,9 @@ export const reviewAndFixLoop = Effect.fn("@llm4ts/flow/Review.reviewAndFixLoop"
|
|
|
933
1031
|
}
|
|
934
1032
|
const diff = yield* options.currentDiff
|
|
935
1033
|
const files = yield* changedFiles
|
|
1034
|
+
if (recheck !== undefined) {
|
|
1035
|
+
return yield* recheckRound(recheck, round, diff, files)
|
|
1036
|
+
}
|
|
936
1037
|
const selected = yield* selector.select(options.reviewers, files, round, previous)
|
|
937
1038
|
const screened =
|
|
938
1039
|
options.prescreen === undefined
|
|
@@ -974,7 +1075,7 @@ export const reviewAndFixLoop = Effect.fn("@llm4ts/flow/Review.reviewAndFixLoop"
|
|
|
974
1075
|
Effect.flatMap(review(lens), (result) =>
|
|
975
1076
|
Effect.map(demoteUnplaced(lens, result, files, options.events), (placed) => ({
|
|
976
1077
|
lens,
|
|
977
|
-
result: placed
|
|
1078
|
+
result: tagLens(lens.name, placed)
|
|
978
1079
|
}))
|
|
979
1080
|
)
|
|
980
1081
|
const parallelism = options.parallelism ?? 0
|
|
@@ -1056,10 +1157,12 @@ export const reviewAndFixLoop = Effect.fn("@llm4ts/flow/Review.reviewAndFixLoop"
|
|
|
1056
1157
|
result.isClean ||
|
|
1057
1158
|
round >= maxRounds ||
|
|
1058
1159
|
(settle === "blocking" && !(yield* Ref.get(gateRed)) && !result.issues.some(isBlocking))
|
|
1160
|
+
const reviewed = !(yield* Ref.get(gateRed)) && diffNow.length > 0
|
|
1059
1161
|
yield* options.events.publish(
|
|
1060
1162
|
ReviewFindings.make({
|
|
1061
1163
|
round,
|
|
1062
1164
|
settled,
|
|
1165
|
+
...(reviewed ? { diffLines: diffNow.split("\n").length } : {}),
|
|
1063
1166
|
issues: result.issues.map((issue) =>
|
|
1064
1167
|
ReviewFinding.make({
|
|
1065
1168
|
severity: issue.severity,
|
|
@@ -1076,10 +1179,27 @@ export const reviewAndFixLoop = Effect.fn("@llm4ts/flow/Review.reviewAndFixLoop"
|
|
|
1076
1179
|
if (settled) {
|
|
1077
1180
|
return result
|
|
1078
1181
|
}
|
|
1182
|
+
yield* applyFix(result)
|
|
1183
|
+
return yield* loop(round + 1, result)
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
/** Sends the findings to the fixer; in recheck mode, records that the fix landed (ADR 0044). */
|
|
1187
|
+
const applyFix = (result: ReviewResult): Effect.Effect<void, FlowError> =>
|
|
1188
|
+
Effect.gen(function* () {
|
|
1079
1189
|
const fix = fixPrompt(result, options.fix)
|
|
1080
1190
|
yield* options.fixWith === undefined ? options.coder.ask(fix) : options.fixWith(fix)
|
|
1081
|
-
|
|
1191
|
+
const remembered = yield* Ref.get(memory)
|
|
1192
|
+
if (recheck?.memory !== undefined && remembered !== undefined) {
|
|
1193
|
+
const fixed = ReviewMemory.make({ ...remembered, fixed: true })
|
|
1194
|
+
yield* Ref.set(memory, fixed)
|
|
1195
|
+
yield* recheck.memory.save(fixed)
|
|
1196
|
+
}
|
|
1082
1197
|
})
|
|
1083
1198
|
|
|
1084
|
-
|
|
1199
|
+
// A restart resumes after the last round its reviewers answered (ADR 0044),
|
|
1200
|
+
// first applying that round's fix when it never landed.
|
|
1201
|
+
if (loaded !== undefined && loaded.fixed !== true && !standing(loaded).isClean) {
|
|
1202
|
+
yield* applyFix(standing(loaded))
|
|
1203
|
+
}
|
|
1204
|
+
return yield* loop(loaded === undefined ? 1 : Math.min(loaded.round + 1, maxRounds), undefined)
|
|
1085
1205
|
})
|
package/src/ReviewCache.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect"
|
|
|
3
3
|
import * as Schema from "effect/Schema"
|
|
4
4
|
import { PersistenceError, type FlowError } from "./FlowError.ts"
|
|
5
5
|
import type { PlainFileStoreShape } from "./Persistence.ts"
|
|
6
|
-
import { ReviewResult } from "./
|
|
6
|
+
import { ReviewResult } from "./ReviewModel.ts"
|
|
7
7
|
|
|
8
8
|
/** A stable key over parts, length-prefixed so a split never collides with another. */
|
|
9
9
|
export const fingerprintOf = (parts: ReadonlyArray<string>): string => {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a review answers: findings and their result. Kept apart from
|
|
3
|
+
* `Review.ts` so modules that `Review.ts` imports can build schemas over them
|
|
4
|
+
* at load time without an import cycle (ADR 0044).
|
|
5
|
+
*/
|
|
6
|
+
import * as Effect from "effect/Effect"
|
|
7
|
+
import * as Schema from "effect/Schema"
|
|
8
|
+
|
|
9
|
+
export const Severity = Schema.Literals(["Critical", "Warning", "Info"])
|
|
10
|
+
export type Severity = typeof Severity.Type
|
|
11
|
+
|
|
12
|
+
export class ReviewIssue extends Schema.Class<ReviewIssue>("ReviewIssue")({
|
|
13
|
+
severity: Severity,
|
|
14
|
+
title: Schema.String,
|
|
15
|
+
description: Schema.String.pipe(
|
|
16
|
+
Schema.withConstructorDefault(Effect.succeed("")),
|
|
17
|
+
Schema.withDecodingDefaultKey(Effect.succeed(""))
|
|
18
|
+
),
|
|
19
|
+
file: Schema.optionalKey(Schema.String),
|
|
20
|
+
line: Schema.optionalKey(Schema.Int),
|
|
21
|
+
suggestion: Schema.optionalKey(Schema.String),
|
|
22
|
+
confidence: Schema.Number.pipe(
|
|
23
|
+
Schema.withConstructorDefault(Effect.succeed(1)),
|
|
24
|
+
Schema.withDecodingDefaultKey(Effect.succeed(1))
|
|
25
|
+
),
|
|
26
|
+
/** A gate failure's triage against the base (ADR 0027); absent for review findings. */
|
|
27
|
+
origin: Schema.optionalKey(Schema.Literals(["new", "base", "flaky"])),
|
|
28
|
+
/** How a gate ended: a red exit, killed at the timeout, or a signal exit. */
|
|
29
|
+
gateClass: Schema.optionalKey(Schema.Literals(["red", "hang", "crash"])),
|
|
30
|
+
/** Why a test gate's output could not stand for green: no tests ran, or test files failed to load. */
|
|
31
|
+
gateReason: Schema.optionalKey(Schema.Literals(["zero-tests", "load-failure"])),
|
|
32
|
+
/** Where the gate's full output was written, when the caller asked for a log. */
|
|
33
|
+
logPath: Schema.optionalKey(Schema.String),
|
|
34
|
+
/** The reviewer that raised it (ADR 0044): a lens name, or "judge"; absent for gate findings. */
|
|
35
|
+
lens: Schema.optionalKey(Schema.String)
|
|
36
|
+
}) {}
|
|
37
|
+
|
|
38
|
+
export class ReviewResult extends Schema.Class<ReviewResult>("ReviewResult")({
|
|
39
|
+
issues: Schema.Array(ReviewIssue),
|
|
40
|
+
summary: Schema.String.pipe(
|
|
41
|
+
Schema.withConstructorDefault(Effect.succeed("")),
|
|
42
|
+
Schema.withDecodingDefaultKey(Effect.succeed(""))
|
|
43
|
+
),
|
|
44
|
+
/** A test gate's passed-test count, when its output had a summary the oracle guard reads (ADR 0027). */
|
|
45
|
+
passed: Schema.optionalKey(Schema.Int)
|
|
46
|
+
}) {
|
|
47
|
+
get isClean(): boolean {
|
|
48
|
+
return this.issues.length === 0
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const reviewPrompt = (task: string, diff: string): string =>
|
|
53
|
+
[
|
|
54
|
+
`Review the change below for task "${task}". Report problems as JSON:`,
|
|
55
|
+
'{"issues":[{"severity":"Critical|Warning|Info","title":"...",',
|
|
56
|
+
'"description":"..."}],"summary":"..."}.',
|
|
57
|
+
'An empty "issues" array means the change is acceptable. Respond with JSON only.',
|
|
58
|
+
"The diff below is the whole subject: judge what it shows. Do not explore the",
|
|
59
|
+
"repository, read other files, or run anything; answer from the diff.",
|
|
60
|
+
"",
|
|
61
|
+
"Diff:",
|
|
62
|
+
diff
|
|
63
|
+
].join("\n")
|