@llm4ts/flow 2.4.2 → 2.6.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.
Files changed (82) hide show
  1. package/dist/Classified.d.ts +2 -0
  2. package/dist/Classified.d.ts.map +1 -1
  3. package/dist/Classified.js +4 -0
  4. package/dist/Classified.js.map +1 -1
  5. package/dist/CostReport.d.ts +110 -0
  6. package/dist/CostReport.d.ts.map +1 -0
  7. package/dist/CostReport.js +313 -0
  8. package/dist/CostReport.js.map +1 -0
  9. package/dist/EstimatedUsage.d.ts.map +1 -1
  10. package/dist/EstimatedUsage.js +4 -0
  11. package/dist/EstimatedUsage.js.map +1 -1
  12. package/dist/Flow.d.ts +14 -0
  13. package/dist/Flow.d.ts.map +1 -1
  14. package/dist/Flow.js +45 -2
  15. package/dist/Flow.js.map +1 -1
  16. package/dist/FlowContext.d.ts +7 -0
  17. package/dist/FlowContext.d.ts.map +1 -1
  18. package/dist/FlowContext.js.map +1 -1
  19. package/dist/FlowEvents.d.ts +44 -1
  20. package/dist/FlowEvents.d.ts.map +1 -1
  21. package/dist/FlowEvents.js +35 -0
  22. package/dist/FlowEvents.js.map +1 -1
  23. package/dist/GitTool.d.ts +7 -0
  24. package/dist/GitTool.d.ts.map +1 -1
  25. package/dist/GitTool.js +15 -1
  26. package/dist/GitTool.js.map +1 -1
  27. package/dist/Judgment.d.ts +88 -0
  28. package/dist/Judgment.d.ts.map +1 -0
  29. package/dist/Judgment.js +281 -0
  30. package/dist/Judgment.js.map +1 -0
  31. package/dist/JudgmentDataset.d.ts +79 -0
  32. package/dist/JudgmentDataset.d.ts.map +1 -0
  33. package/dist/JudgmentDataset.js +164 -0
  34. package/dist/JudgmentDataset.js.map +1 -0
  35. package/dist/JudgmentEval.d.ts +143 -0
  36. package/dist/JudgmentEval.d.ts.map +1 -0
  37. package/dist/JudgmentEval.js +242 -0
  38. package/dist/JudgmentEval.js.map +1 -0
  39. package/dist/JudgmentLog.d.ts +46 -0
  40. package/dist/JudgmentLog.d.ts.map +1 -0
  41. package/dist/JudgmentLog.js +62 -0
  42. package/dist/JudgmentLog.js.map +1 -0
  43. package/dist/JudgmentTypes.d.ts +13 -0
  44. package/dist/JudgmentTypes.d.ts.map +1 -0
  45. package/dist/JudgmentTypes.js +11 -0
  46. package/dist/JudgmentTypes.js.map +1 -0
  47. package/dist/ProgramJudge.d.ts +15 -2
  48. package/dist/ProgramJudge.d.ts.map +1 -1
  49. package/dist/ProgramJudge.js +58 -3
  50. package/dist/ProgramJudge.js.map +1 -1
  51. package/dist/Replay.d.ts.map +1 -1
  52. package/dist/Replay.js +9 -4
  53. package/dist/Replay.js.map +1 -1
  54. package/dist/Review.d.ts +35 -0
  55. package/dist/Review.d.ts.map +1 -1
  56. package/dist/Review.js +92 -13
  57. package/dist/Review.js.map +1 -1
  58. package/dist/Reviewer.d.ts +8 -0
  59. package/dist/Reviewer.d.ts.map +1 -1
  60. package/dist/Reviewer.js +12 -1
  61. package/dist/Reviewer.js.map +1 -1
  62. package/dist/TransientRetry.d.ts.map +1 -1
  63. package/dist/TransientRetry.js +1 -0
  64. package/dist/TransientRetry.js.map +1 -1
  65. package/package.json +8 -3
  66. package/src/Classified.ts +5 -0
  67. package/src/CostReport.ts +402 -0
  68. package/src/EstimatedUsage.ts +15 -0
  69. package/src/Flow.ts +77 -2
  70. package/src/FlowContext.ts +7 -0
  71. package/src/FlowEvents.ts +42 -0
  72. package/src/GitTool.ts +19 -1
  73. package/src/Judgment.ts +418 -0
  74. package/src/JudgmentDataset.ts +254 -0
  75. package/src/JudgmentEval.ts +306 -0
  76. package/src/JudgmentLog.ts +96 -0
  77. package/src/JudgmentTypes.ts +15 -0
  78. package/src/ProgramJudge.ts +89 -4
  79. package/src/Replay.ts +23 -17
  80. package/src/Review.ts +163 -13
  81. package/src/Reviewer.ts +15 -1
  82. package/src/TransientRetry.ts +1 -0
package/src/Review.ts CHANGED
@@ -8,9 +8,24 @@ import { Capabilities } from "@llm4ts/core/Capability"
8
8
  import { guarded } from "./CapabilityGuard.ts"
9
9
  import type { Chat } from "./Chat.ts"
10
10
  import { FlowLlmError, ProcessError, describeFlowError, type FlowError } from "./FlowError.ts"
11
- import { Info, type FlowEventsShape } from "./FlowEvents.ts"
11
+ import {
12
+ Info,
13
+ JudgmentObserved,
14
+ publishJudgmentObserved,
15
+ type FlowEventsShape
16
+ } from "./FlowEvents.ts"
12
17
  import { Reviewer } from "./Reviewer.ts"
13
18
  import { publishUsage } from "./Usage.ts"
19
+ import type { JudgmentShape } from "@llm4ts/core/judgment/Judgment"
20
+ import { truth, type JudgmentResult, type TruthAnswer } from "@llm4ts/core/judgment/Schemas"
21
+ import {
22
+ certaintyOf,
23
+ decide,
24
+ defaultJudgmentPolicy,
25
+ type Decision,
26
+ type JudgmentMode,
27
+ type JudgmentPolicy
28
+ } from "./Judgment.ts"
14
29
 
15
30
  export const Severity = Schema.Literals(["Critical", "Warning", "Info"])
16
31
  export type Severity = typeof Severity.Type
@@ -67,8 +82,8 @@ export const reviewJsonSchema: JsonSchema = {
67
82
  required: ["issues", "summary"]
68
83
  }
69
84
 
70
- const reviewer = (name: string, systemPrompt: string, files = ".*"): Reviewer =>
71
- Reviewer.make({ name, systemPrompt, files })
85
+ const reviewer = (name: string, systemPrompt: string, screen: string, files = ".*"): Reviewer =>
86
+ Reviewer.make({ name, systemPrompt, files, screen })
72
87
 
73
88
  export const correctnessReviewer = reviewer(
74
89
  "code-functionality",
@@ -76,7 +91,8 @@ export const correctnessReviewer = reviewer(
76
91
  "Review for functional correctness. Check that the change implements the task's intent,",
77
92
  "handles obvious edge cases, and does not regress existing behavior. Report only concrete",
78
93
  "logic errors, wrong conditions, mishandled error paths, and missing cases. Ignore style."
79
- ].join("\n")
94
+ ].join("\n"),
95
+ "The diff plausibly contains a logic error, a wrong condition, a mishandled error path, or a missing case."
80
96
  )
81
97
 
82
98
  export const readabilityReviewer = reviewer(
@@ -84,7 +100,8 @@ export const readabilityReviewer = reviewer(
84
100
  [
85
101
  "Review for readability and clarity. Check names, focused functions, understandable control",
86
102
  "flow, and useful comments. Report only concrete, actionable readability problems."
87
- ].join("\n")
103
+ ].join("\n"),
104
+ "The diff plausibly introduces an unclear name, a tangled function, or confusing control flow."
88
105
  )
89
106
 
90
107
  export const testReviewer = reviewer(
@@ -92,27 +109,32 @@ export const testReviewer = reviewer(
92
109
  [
93
110
  "Review test coverage and quality. Check that new behavior and important error paths are",
94
111
  "covered by tests that assert real outcomes and would fail on regression. Report concrete gaps."
95
- ].join("\n")
112
+ ].join("\n"),
113
+ "The diff changes behavior that is not covered by a test in the same diff."
96
114
  )
97
115
 
98
116
  export const structureReviewer = reviewer(
99
117
  "code-structure",
100
- "Review module boundaries, dependency direction, cohesion, and unnecessary coupling. Report concrete structural problems."
118
+ "Review module boundaries, dependency direction, cohesion, and unnecessary coupling. Report concrete structural problems.",
119
+ "The diff plausibly crosses a module boundary, reverses a dependency direction, or adds coupling."
101
120
  )
102
121
 
103
122
  export const performanceReviewer = reviewer(
104
123
  "performance",
105
- "Review for material performance regressions, unbounded work, avoidable repeated I/O, and resource leaks."
124
+ "Review for material performance regressions, unbounded work, avoidable repeated I/O, and resource leaks.",
125
+ "The diff plausibly changes performance characteristics: unbounded work, repeated I/O, or a resource leak."
106
126
  )
107
127
 
108
128
  export const securityReviewer = reviewer(
109
129
  "security",
110
- "Review trust boundaries, input handling, secrets, authorization, injection risks, and unsafe defaults."
130
+ "Review trust boundaries, input handling, secrets, authorization, injection risks, and unsafe defaults.",
131
+ "The diff plausibly touches a trust boundary, secret, authorization check, input parser, or unsafe default."
111
132
  )
112
133
 
113
134
  export const effectReviewer = reviewer(
114
135
  "effect-ts",
115
136
  "Review Effect usage: typed errors, scoped resources, service boundaries, interruption, concurrency, and runtime ownership.",
137
+ "The diff plausibly misuses Effect: an untyped error, an unscoped resource, a leaked service, or unmanaged concurrency.",
116
138
  ".*\\.(ts|tsx|mts|cts)$"
117
139
  )
118
140
 
@@ -277,9 +299,103 @@ export interface ReviewAndFixOptions {
277
299
  readonly lint?: Effect.Effect<ReviewResult, FlowError>
278
300
  readonly parallelism?: number
279
301
  readonly format?: Effect.Effect<void, FlowError>
302
+ /**
303
+ * A judgment pre-screen (ADR 0017): one Truth question per lens over the
304
+ * diff. Observes by default, keeping every selected lens; only explicit
305
+ * act mode skips lenses. Omit to disable the judgment entirely.
306
+ */
307
+ readonly prescreen?: ReviewPrescreen
308
+ }
309
+
310
+ export interface ReviewPrescreen {
311
+ readonly judgment: JudgmentShape
312
+ readonly policy?: JudgmentPolicy
313
+ readonly mode?: JudgmentMode
314
+ }
315
+
316
+ export interface ReviewPrescreenResult {
317
+ readonly reviewers: ReadonlyArray<Reviewer>
318
+ readonly observations: ReadonlyArray<{
319
+ readonly key: string
320
+ readonly answer: TruthAnswer
321
+ readonly question: ReturnType<typeof truth>
322
+ readonly state: { readonly diff: string }
323
+ readonly judgmentIdentity: string
324
+ readonly decision: Decision
325
+ }>
280
326
  }
281
327
 
282
- const reviewWith = (
328
+ /**
329
+ * Selected lenses and their answers for publication after review. In act mode
330
+ * a lens is skipped only when its screen
331
+ * answered with `act` certainty that the diff has nothing for it; doubt,
332
+ * failure, and escalation all run the lens. Never skip on doubt.
333
+ */
334
+ export const prescreenReviewers = Effect.fn("@llm4ts/flow/Review.prescreen")(function* (
335
+ prescreen: ReviewPrescreen,
336
+ events: FlowEventsShape,
337
+ diff: string,
338
+ lenses: ReadonlyArray<Reviewer>
339
+ ): Effect.fn.Return<ReviewPrescreenResult, FlowError> {
340
+ if (lenses.length === 0) {
341
+ return { reviewers: lenses, observations: [] }
342
+ }
343
+ const policy = prescreen.policy ?? defaultJudgmentPolicy
344
+ const state = { diff }
345
+ const questions = Object.fromEntries(
346
+ lenses.map((lens) => [lens.name, truth(lens.screeningStatement)])
347
+ )
348
+ const result = yield* prescreen.judgment.judge({ state, questions }).pipe(
349
+ Effect.catch((error) =>
350
+ events
351
+ .publish(
352
+ Info.make({
353
+ message: `review pre-screen unavailable (${error.message}); running every lens`
354
+ })
355
+ )
356
+ .pipe(Effect.as<JudgmentResult | undefined>(undefined))
357
+ )
358
+ )
359
+ if (result === undefined) {
360
+ return { reviewers: lenses, observations: [] }
361
+ }
362
+ const observations = lenses.flatMap((lens) => {
363
+ const answer = result.answers[lens.name]
364
+ return answer?.type === "truth"
365
+ ? [
366
+ {
367
+ key: lens.name,
368
+ answer,
369
+ decision: decide(answer, policy),
370
+ state,
371
+ question: questions[lens.name],
372
+ judgmentIdentity: prescreen.judgment.identity
373
+ }
374
+ ]
375
+ : []
376
+ })
377
+ const kept =
378
+ prescreen.mode !== "act"
379
+ ? lenses
380
+ : lenses.filter(
381
+ (lens) =>
382
+ !observations.some(
383
+ ({ key, answer, decision }) =>
384
+ key === lens.name && answer.truth < 0.5 && decision === "act"
385
+ )
386
+ )
387
+ const skipped = lenses.filter((lens) => !kept.includes(lens))
388
+ if (skipped.length > 0) {
389
+ yield* events.publish(
390
+ Info.make({
391
+ message: `review pre-screen skipped ${skipped.map((lens) => lens.name).join(", ")}`
392
+ })
393
+ )
394
+ }
395
+ return { reviewers: kept, observations }
396
+ })
397
+
398
+ export const reviewWith = (
283
399
  service: LlmServiceShape,
284
400
  events: FlowEventsShape,
285
401
  lens: Reviewer,
@@ -339,15 +455,49 @@ export const reviewAndFixLoop = Effect.fn("@llm4ts/flow/Review.reviewAndFixLoop"
339
455
  }
340
456
  const diff = yield* options.currentDiff
341
457
  const files = yield* changedFiles
342
- const chosen = yield* selector.select(options.reviewers, files, round, previous)
458
+ const selected = yield* selector.select(options.reviewers, files, round, previous)
459
+ const screened =
460
+ options.prescreen === undefined
461
+ ? { reviewers: selected, observations: [] }
462
+ : yield* prescreenReviewers(options.prescreen, options.events, diff, selected)
463
+ const chosen = screened.reviewers
343
464
  const run = (lens: Reviewer) =>
344
- reviewWith(options.reviewerService, options.events, lens, options.taskTitle, diff)
465
+ reviewWith(options.reviewerService, options.events, lens, options.taskTitle, diff).pipe(
466
+ Effect.map((result) => ({ lens, result }))
467
+ )
345
468
  const parallelism = options.parallelism ?? 0
346
469
  const results =
347
470
  parallelism > 0
348
471
  ? yield* Effect.forEach(chosen, run, { concurrency: parallelism })
349
472
  : yield* Effect.forEach(chosen, run, { concurrency: "unbounded" })
350
- return mergeReviewResults(results)
473
+ const mode = options.prescreen?.mode ?? "observe"
474
+ if (mode !== "act") {
475
+ for (const { lens, result } of results) {
476
+ const observation = screened.observations.find(({ key }) => key === lens.name)
477
+ if (observation === undefined) continue
478
+ const { key, answer, decision, state, question, judgmentIdentity } = observation
479
+ const counts = { Critical: 0, Warning: 0, Info: 0 }
480
+ for (const issue of result.issues) counts[issue.severity] += 1
481
+ yield* publishJudgmentObserved(
482
+ options.events,
483
+ JudgmentObserved.make({
484
+ consumer: "review-prescreen",
485
+ state,
486
+ question,
487
+ answer,
488
+ judgmentIdentity,
489
+ key,
490
+ decision,
491
+ certainty: certaintyOf(answer),
492
+ support: answer.support,
493
+ origin: answer.origin,
494
+ outcome: { _tag: "ReviewPrescreen", lens: lens.name, issues: counts },
495
+ mode
496
+ })
497
+ )
498
+ }
499
+ }
500
+ return mergeReviewResults(results.map(({ result }) => result))
351
501
  })
352
502
 
353
503
  const loop = (
package/src/Reviewer.ts CHANGED
@@ -3,8 +3,22 @@ import * as Schema from "effect/Schema"
3
3
  export class Reviewer extends Schema.Class<Reviewer>("Reviewer")({
4
4
  name: Schema.String,
5
5
  systemPrompt: Schema.String,
6
- files: Schema.optionalKey(Schema.String)
6
+ files: Schema.optionalKey(Schema.String),
7
+ /**
8
+ * The yes/no statement a judgment pre-screen evaluates against a diff to
9
+ * decide whether this lens is worth a full pass (ADR 0017). Absent, one is
10
+ * derived from the system prompt.
11
+ */
12
+ screen: Schema.optionalKey(Schema.String)
7
13
  }) {
14
+ /** The screening statement, explicit or derived. */
15
+ get screeningStatement(): string {
16
+ return (
17
+ this.screen ??
18
+ `A reviewer following these instructions would report at least one concrete issue in the diff: ${this.systemPrompt}`
19
+ )
20
+ }
21
+
8
22
  matches(changedFiles: ReadonlyArray<string>): boolean {
9
23
  if (this.files === undefined || changedFiles.length === 0) {
10
24
  return true
@@ -353,6 +353,7 @@ export const makeTransientRetry = Effect.fn("@llm4ts/flow/TransientRetry.make")(
353
353
  options,
354
354
  events
355
355
  ),
356
+ scoreLabels: underlying.scoreLabels,
356
357
  isAvailable: underlying.isAvailable
357
358
  })
358
359
  })