@llm4ts/flow 2.37.0 → 2.39.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/Domains.d.ts +1 -1
- package/dist/Domains.d.ts.map +1 -1
- package/dist/Domains.js +7 -3
- package/dist/Domains.js.map +1 -1
- package/dist/Flow.d.ts +3 -1
- package/dist/Flow.d.ts.map +1 -1
- package/dist/Flow.js +1 -0
- package/dist/Flow.js.map +1 -1
- package/dist/GraphCache.d.ts +70 -0
- package/dist/GraphCache.d.ts.map +1 -0
- package/dist/GraphCache.js +111 -0
- package/dist/GraphCache.js.map +1 -0
- package/dist/GraphQuery.d.ts +67 -0
- package/dist/GraphQuery.d.ts.map +1 -0
- package/dist/GraphQuery.js +212 -0
- package/dist/GraphQuery.js.map +1 -0
- package/dist/GraphRender.d.ts +26 -0
- package/dist/GraphRender.d.ts.map +1 -0
- package/dist/GraphRender.js +154 -0
- package/dist/GraphRender.js.map +1 -0
- package/dist/GraphRules.d.ts +86 -0
- package/dist/GraphRules.d.ts.map +1 -0
- package/dist/GraphRules.js +246 -0
- package/dist/GraphRules.js.map +1 -0
- package/dist/GraphWorklist.d.ts +64 -0
- package/dist/GraphWorklist.d.ts.map +1 -0
- package/dist/GraphWorklist.js +283 -0
- package/dist/GraphWorklist.js.map +1 -0
- package/dist/Pack.d.ts +7 -0
- package/dist/Pack.d.ts.map +1 -1
- package/dist/Pack.js +9 -5
- package/dist/Pack.js.map +1 -1
- package/dist/Review.d.ts +12 -0
- package/dist/Review.d.ts.map +1 -1
- package/dist/Review.js +11 -1
- package/dist/Review.js.map +1 -1
- package/dist/Stories.d.ts +16 -0
- package/dist/Stories.d.ts.map +1 -1
- package/dist/Stories.js +42 -7
- package/dist/Stories.js.map +1 -1
- package/dist/Survey.d.ts +112 -3
- package/dist/Survey.d.ts.map +1 -1
- package/dist/Survey.js +644 -41
- package/dist/Survey.js.map +1 -1
- package/package.json +7 -2
- package/src/Domains.ts +7 -4
- package/src/Flow.ts +5 -1
- package/src/GraphCache.ts +191 -0
- package/src/GraphQuery.ts +315 -0
- package/src/GraphRender.ts +208 -0
- package/src/GraphRules.ts +293 -0
- package/src/GraphWorklist.ts +405 -0
- package/src/Pack.ts +15 -5
- package/src/Review.ts +25 -1
- package/src/Stories.ts +66 -12
- package/src/Survey.ts +846 -52
package/src/Pack.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as Schema from "effect/Schema"
|
|
|
4
4
|
import { Dimension } from "@llm4ts/core/eval/Eval"
|
|
5
5
|
import type { ConsolidateRules } from "./Domains.ts"
|
|
6
6
|
import { PlanParseError } from "./FlowError.ts"
|
|
7
|
+
import { edgeKindsOf, parseGraphRules, type GraphRules } from "./GraphRules.ts"
|
|
7
8
|
import { oracleRulesFrom, type OracleRules } from "./OracleGuard.ts"
|
|
8
9
|
import { Reviewer, parseReviewer } from "./Reviewer.ts"
|
|
9
10
|
import { CoverageRule } from "./SpecChecks.ts"
|
|
@@ -37,6 +38,12 @@ export interface Pack {
|
|
|
37
38
|
readonly judgeDimensions: ReadonlyArray<Dimension>
|
|
38
39
|
readonly coverage: ReadonlyArray<CoverageRule>
|
|
39
40
|
readonly survey: ReadonlyArray<CoverageRule>
|
|
41
|
+
/**
|
|
42
|
+
* The graph vocabulary (ADR 0030): `## Node:`, `## Edge:` (with every
|
|
43
|
+
* `## Survey:` folded in as a file-to-file edge), `## Join:`, `## Probe:`
|
|
44
|
+
* and the `## Graph` budgets.
|
|
45
|
+
*/
|
|
46
|
+
readonly graph: GraphRules
|
|
40
47
|
readonly prompts: Readonly<Record<string, string>>
|
|
41
48
|
readonly lenses: ReadonlyArray<Reviewer>
|
|
42
49
|
readonly lessons: string | undefined
|
|
@@ -391,16 +398,18 @@ export const loadPack = Effect.fn("@llm4ts/flow/Pack.load")(function* (
|
|
|
391
398
|
cluster: commaList(consolidateValues.cluster),
|
|
392
399
|
context: commaList(consolidateValues.context)
|
|
393
400
|
}
|
|
401
|
+
const survey = rules(manifest.sections, "## Survey: ")
|
|
402
|
+
const graph = yield* parseGraphRules(manifest.sections, survey)
|
|
394
403
|
if (consolidate !== undefined) {
|
|
395
|
-
const
|
|
404
|
+
const knownKinds = new Set([...survey.map((rule) => rule.name), ...edgeKindsOf(graph)])
|
|
396
405
|
const unknown = [...consolidate.cluster, ...consolidate.context].filter(
|
|
397
|
-
(kind) => !
|
|
406
|
+
(kind) => !knownKinds.has(kind) && !kind.startsWith("llm-") && !kind.endsWith("*")
|
|
398
407
|
)
|
|
399
408
|
if (unknown.length > 0) {
|
|
400
409
|
return yield* PlanParseError.make({
|
|
401
410
|
message:
|
|
402
|
-
`pack manifest '## Consolidate' names edge kinds no '## Survey:' rule produces: ${unknown.join(", ")} ` +
|
|
403
|
-
`(known: ${[...
|
|
411
|
+
`pack manifest '## Consolidate' names edge kinds no '## Survey:', '## Edge:' or '## Join:' rule produces: ${unknown.join(", ")} ` +
|
|
412
|
+
`(known: ${[...knownKinds].join(", ") || "none"}; 'llm-*' matches refined edges)`
|
|
404
413
|
})
|
|
405
414
|
}
|
|
406
415
|
const both = consolidate.cluster.filter((kind) => consolidate.context.includes(kind))
|
|
@@ -438,7 +447,8 @@ export const loadPack = Effect.fn("@llm4ts/flow/Pack.load")(function* (
|
|
|
438
447
|
}),
|
|
439
448
|
judgeDimensions: dimensions(section(manifest.sections, "Judge")),
|
|
440
449
|
coverage: rules(manifest.sections, "## Coverage: "),
|
|
441
|
-
survey
|
|
450
|
+
survey,
|
|
451
|
+
graph,
|
|
442
452
|
prompts,
|
|
443
453
|
lenses,
|
|
444
454
|
lessons: lessons === undefined || lessons.length === 0 ? undefined : lessons,
|
package/src/Review.ts
CHANGED
|
@@ -735,8 +735,24 @@ export interface ReviewAndFixOptions {
|
|
|
735
735
|
readonly fixWith?: (prompt: string) => Effect.Effect<string, FlowError>
|
|
736
736
|
/** End the task, typed `Stalled`, when a fix round leaves the diff byte-identical (ADR 0027 decision 11). */
|
|
737
737
|
readonly stallOnIdenticalDiff?: boolean
|
|
738
|
+
/**
|
|
739
|
+
* When a round settles before `maxRounds` (ADR 0031): "clean" (default)
|
|
740
|
+
* when it has no findings; "blocking" also when its findings are all
|
|
741
|
+
* non-blocking. A red gate is always blocking.
|
|
742
|
+
*/
|
|
743
|
+
readonly settle?: SettleRule
|
|
738
744
|
}
|
|
739
745
|
|
|
746
|
+
/** When a review round stops sending findings back to the coder (ADR 0031). */
|
|
747
|
+
export type SettleRule = "clean" | "blocking"
|
|
748
|
+
|
|
749
|
+
/** A finding that must be fixed before the work moves on: Critical (ADR 0031). */
|
|
750
|
+
export const isBlocking = (issue: ReviewIssue): boolean => issue.severity === "Critical"
|
|
751
|
+
|
|
752
|
+
/** The findings that may wait: everything not blocking (ADR 0031). */
|
|
753
|
+
export const nonBlockingIssues = (result: ReviewResult): ReadonlyArray<ReviewIssue> =>
|
|
754
|
+
result.issues.filter((issue) => !isBlocking(issue))
|
|
755
|
+
|
|
740
756
|
export interface ReviewCacheLocation {
|
|
741
757
|
readonly files: PlainFileStoreShape
|
|
742
758
|
readonly dir: string
|
|
@@ -881,6 +897,10 @@ export const reviewAndFixLoop = Effect.fn("@llm4ts/flow/Review.reviewAndFixLoop"
|
|
|
881
897
|
const reported = yield* Ref.make<ReadonlySet<string>>(new Set())
|
|
882
898
|
const oracleNoted = yield* Ref.make(false)
|
|
883
899
|
|
|
900
|
+
const settle = options.settle ?? "clean"
|
|
901
|
+
// A red gate is never settled early, whatever its findings' severities.
|
|
902
|
+
const gateRed = yield* Ref.make(false)
|
|
903
|
+
|
|
884
904
|
const reviewOnce = (
|
|
885
905
|
round: number,
|
|
886
906
|
previous: ReviewResult | undefined
|
|
@@ -893,6 +913,7 @@ export const reviewAndFixLoop = Effect.fn("@llm4ts/flow/Review.reviewAndFixLoop"
|
|
|
893
913
|
options.events,
|
|
894
914
|
oracleNoted
|
|
895
915
|
)
|
|
916
|
+
yield* Ref.set(gateRed, !lint.isClean)
|
|
896
917
|
if (!lint.isClean) {
|
|
897
918
|
return lint
|
|
898
919
|
}
|
|
@@ -1017,7 +1038,10 @@ export const reviewAndFixLoop = Effect.fn("@llm4ts/flow/Review.reviewAndFixLoop"
|
|
|
1017
1038
|
}
|
|
1018
1039
|
yield* Ref.set(lastDiff, diffNow)
|
|
1019
1040
|
const result = yield* reviewOnce(round, previous)
|
|
1020
|
-
const settled =
|
|
1041
|
+
const settled =
|
|
1042
|
+
result.isClean ||
|
|
1043
|
+
round >= maxRounds ||
|
|
1044
|
+
(settle === "blocking" && !(yield* Ref.get(gateRed)) && !result.issues.some(isBlocking))
|
|
1021
1045
|
yield* options.events.publish(
|
|
1022
1046
|
ReviewFindings.make({
|
|
1023
1047
|
round,
|
package/src/Stories.ts
CHANGED
|
@@ -74,7 +74,9 @@ import {
|
|
|
74
74
|
ReviewIssue,
|
|
75
75
|
ReviewResult,
|
|
76
76
|
applyTriage,
|
|
77
|
+
isBlocking,
|
|
77
78
|
loadRepoReviewRules,
|
|
79
|
+
nonBlockingIssues,
|
|
78
80
|
type FixPromptOptions,
|
|
79
81
|
type GateTriageOptions
|
|
80
82
|
} from "./Review.ts"
|
|
@@ -722,6 +724,14 @@ export interface StoriesOptions {
|
|
|
722
724
|
readonly failFast?: boolean
|
|
723
725
|
readonly reviewers?: ReadonlyArray<Reviewer>
|
|
724
726
|
readonly maxRounds?: number
|
|
727
|
+
/**
|
|
728
|
+
* Defer what does not block (ADR 0031): a task's review settles once its
|
|
729
|
+
* findings are all non-blocking, the judge clears a story whose findings
|
|
730
|
+
* are all non-blocking, and what is left is written to
|
|
731
|
+
* `stories/<id>.deferred.md` (`deferredPath`) when the story finishes,
|
|
732
|
+
* for a follow-up round. Default false: every finding is fixed or fails.
|
|
733
|
+
*/
|
|
734
|
+
readonly deferNonBlocking?: boolean
|
|
725
735
|
/**
|
|
726
736
|
* Second opinion on a BLOCKED_ON claim the plan cannot settle (a path no
|
|
727
737
|
* story owns, or a parallel story's). `real: false` sends the coder back
|
|
@@ -762,6 +772,24 @@ const combined = (first: ReviewResult, second: ReviewResult): ReviewResult =>
|
|
|
762
772
|
summary: [first.summary, second.summary].filter((part) => part.length > 0).join("; ")
|
|
763
773
|
})
|
|
764
774
|
|
|
775
|
+
/**
|
|
776
|
+
* Where a finished story's deferred findings are kept (ADR 0031): one
|
|
777
|
+
* Markdown list per story under the executor's state directory, absent when
|
|
778
|
+
* nothing was deferred.
|
|
779
|
+
*/
|
|
780
|
+
export const deferredPath = (stateDir: string, storyId: string): string =>
|
|
781
|
+
join(stateDir, `stories/${storyId}.deferred.md`)
|
|
782
|
+
|
|
783
|
+
/** A deferred finding as one Markdown bullet, prefixed with where it was found. */
|
|
784
|
+
export const deferredLine = (source: string, issue: ReviewIssue): string => {
|
|
785
|
+
const where =
|
|
786
|
+
issue.file === undefined
|
|
787
|
+
? ""
|
|
788
|
+
: ` (${issue.file}${issue.line === undefined ? "" : `:${issue.line}`})`
|
|
789
|
+
const detail = issue.description.trim().length === 0 ? "" : `: ${issue.description.trim()}`
|
|
790
|
+
return `- [${issue.severity}] ${source} — ${issue.title}${where}${detail}`
|
|
791
|
+
}
|
|
792
|
+
|
|
765
793
|
const issueLines = (result: ReviewResult): string =>
|
|
766
794
|
result.issues.map((issue) => `- ${issue.title}: ${issue.description}`).join("\n")
|
|
767
795
|
|
|
@@ -843,6 +871,7 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
843
871
|
const epicBranch = options.epicBranch ?? `epic/${plan.epicId}`
|
|
844
872
|
const concurrency = Math.max(1, options.concurrency ?? 3)
|
|
845
873
|
const judgeRounds = Math.max(1, options.judgeRounds ?? 2)
|
|
874
|
+
const deferring = options.deferNonBlocking === true
|
|
846
875
|
const statePath = (story: Story): string => join(options.stateDir, `stories/${story.id}.json`)
|
|
847
876
|
const planPath = (story: Story): string => join(options.stateDir, `stories/${story.id}.plan.md`)
|
|
848
877
|
/** The judge's last verdict beside a fingerprint of what it judged. */
|
|
@@ -1196,6 +1225,8 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1196
1225
|
)
|
|
1197
1226
|
/** What the coder claimed to have run and did not, task by task, for the judge. */
|
|
1198
1227
|
const evidenceNotes: Array<string> = []
|
|
1228
|
+
/** Findings left for a follow-up round (ADR 0031), written when the story finishes. */
|
|
1229
|
+
const deferred: Array<string> = []
|
|
1199
1230
|
const checkEvidence = (
|
|
1200
1231
|
task: Task,
|
|
1201
1232
|
trailer: Trailer,
|
|
@@ -1513,13 +1544,21 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1513
1544
|
// the story's findings log for people to read.
|
|
1514
1545
|
reviewCache: { files, dir: reviewCacheDir(story) },
|
|
1515
1546
|
onReview: (task, round, result, settled) =>
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1547
|
+
Effect.gen(function* () {
|
|
1548
|
+
// A settled round's leftovers are deferred, not dropped: when the
|
|
1549
|
+
// rounds ran out, blocking ones too, since the task commits anyway.
|
|
1550
|
+
const left = settled && deferring ? result.issues : []
|
|
1551
|
+
for (const issue of left) {
|
|
1552
|
+
deferred.push(deferredLine(`review "${task.title}"`, issue))
|
|
1553
|
+
}
|
|
1554
|
+
yield* appendFindings(
|
|
1555
|
+
story,
|
|
1556
|
+
`review "${task.title}" round ${round} — ${
|
|
1557
|
+
result.isClean ? "clean" : `${result.issues.length} issue(s)`
|
|
1558
|
+
}${settled ? (left.length === 0 ? "" : ", deferred") : ", fixing"}`,
|
|
1559
|
+
result
|
|
1560
|
+
)
|
|
1561
|
+
}),
|
|
1523
1562
|
onTaskReply: (task, _reply, trailer, startedAt) => checkEvidence(task, trailer, startedAt),
|
|
1524
1563
|
// The repository's own review rules ride along as one extra lens
|
|
1525
1564
|
// (ADR 0027 decision 9), read from the epic checkout.
|
|
@@ -1528,7 +1567,8 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1528
1567
|
...(options.fixer === undefined ? {} : { fixer: options.fixer }),
|
|
1529
1568
|
...(options.stall === undefined ? {} : { stall: options.stall }),
|
|
1530
1569
|
...(options.reviewers === undefined ? {} : { reviewers: options.reviewers }),
|
|
1531
|
-
...(options.maxRounds === undefined ? {} : { maxRounds: options.maxRounds })
|
|
1570
|
+
...(options.maxRounds === undefined ? {} : { maxRounds: options.maxRounds }),
|
|
1571
|
+
...(deferring ? { settle: "blocking" as const } : {})
|
|
1532
1572
|
})
|
|
1533
1573
|
yield* guarded(implementTasks, implementTasks)
|
|
1534
1574
|
|
|
@@ -1684,11 +1724,17 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1684
1724
|
dimensions: verdict instanceof StoryVerdict ? verdict.dimensions : []
|
|
1685
1725
|
})
|
|
1686
1726
|
)
|
|
1687
|
-
|
|
1727
|
+
// Deferring, a verdict with nothing blocking clears: what it found
|
|
1728
|
+
// waits for the follow-up round instead of another revision.
|
|
1729
|
+
if (verdict.isClean || (deferring && !verdict.issues.some(isBlocking))) {
|
|
1730
|
+
const waiting = nonBlockingIssues(verdict)
|
|
1731
|
+
for (const issue of waiting) {
|
|
1732
|
+
deferred.push(deferredLine(`judge round ${round}`, issue))
|
|
1733
|
+
}
|
|
1688
1734
|
inPlace = empty
|
|
1689
|
-
judgeNote = empty
|
|
1690
|
-
|
|
1691
|
-
|
|
1735
|
+
judgeNote = `${empty ? "verified already in place" : "judge cleared"} (round ${round}${
|
|
1736
|
+
waiting.length === 0 ? "" : `, ${waiting.length} finding(s) deferred`
|
|
1737
|
+
})`
|
|
1692
1738
|
break
|
|
1693
1739
|
}
|
|
1694
1740
|
if (round >= judgeRounds) {
|
|
@@ -1709,6 +1755,14 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1709
1755
|
|
|
1710
1756
|
const changed = yield* git.changedFilesVsBase(epicBranch)
|
|
1711
1757
|
yield* enforcePerimeter(changed, story)
|
|
1758
|
+
if (deferring) {
|
|
1759
|
+
yield* deferred.length === 0
|
|
1760
|
+
? files.remove(deferredPath(options.stateDir, story.id))
|
|
1761
|
+
: files.writeAtomic(
|
|
1762
|
+
deferredPath(options.stateDir, story.id),
|
|
1763
|
+
[`# Deferred from ${story.id}: ${story.title}`, "", ...deferred, ""].join("\n")
|
|
1764
|
+
)
|
|
1765
|
+
}
|
|
1712
1766
|
const totals = seats.totals === undefined ? undefined : yield* seats.totals
|
|
1713
1767
|
const history = roster === undefined ? [] : yield* roster.history
|
|
1714
1768
|
return {
|