@llm4ts/shell 0.6.0 → 0.6.2
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/Cli.d.ts +2 -2
- package/dist/Cli.js +3 -3
- package/dist/Cli.js.map +1 -1
- package/dist/FlowCatalog.d.ts.map +1 -1
- package/dist/FlowCatalog.js +6 -2
- package/dist/FlowCatalog.js.map +1 -1
- package/flows/implement.js +29 -0
- package/flows/issue-pr.js +71 -0
- package/flows/judge-suite.js +48 -0
- package/flows/local.js +40 -0
- package/flows/modernize-bench.js +264 -0
- package/flows/modernize-extract.js +455 -0
- package/flows/modernize-implement.js +249 -0
- package/flows/modernize-review.js +237 -0
- package/flows/modernize-seed.js +189 -0
- package/flows/modernize-survey.js +0 -0
- package/flows/modernize-verify.js +435 -0
- package/flows/sdd.js +114 -0
- package/package.json +5 -5
- package/src/Cli.ts +3 -3
- package/src/FlowCatalog.ts +8 -2
- package/flows/implement.ts +0 -38
- package/flows/issue-pr.ts +0 -108
- package/flows/judge-suite.ts +0 -63
- package/flows/local.ts +0 -59
- package/flows/modernize-bench.ts +0 -368
- package/flows/modernize-extract.ts +0 -642
- package/flows/modernize-implement.ts +0 -340
- package/flows/modernize-review.ts +0 -351
- package/flows/modernize-seed.ts +0 -266
- package/flows/modernize-survey.ts +0 -0
- package/flows/modernize-verify.ts +0 -618
- package/flows/sdd.ts +0 -181
|
@@ -1,642 +0,0 @@
|
|
|
1
|
-
// Legacy modernization phase 1: reverse-engineer the estate into a judged, human-approved spec pack.
|
|
2
|
-
//
|
|
3
|
-
// Runs rooted at the LEGACY repository (`--repo <legacy>`), after modernize-survey's
|
|
4
|
-
// wave plan is approved. Extraction is PER PROGRAM and resumable
|
|
5
|
-
// (`extractProgramsResumably` skips every program whose spec already exists —
|
|
6
|
-
// delete `specs/<NAME>.md` to re-extract one program): each program gets a
|
|
7
|
-
// structured analyst call producing its four artifacts — `specs/<NAME>.md`,
|
|
8
|
-
// `features/<name>.feature`, `traceability/<NAME>.md`, `mapping/<NAME>.md` —
|
|
9
|
-
// and its own commit. The estate-wide indexes (`traceability.md`, `mapping.md`)
|
|
10
|
-
// are regenerated deterministically from the fragments before every gate round.
|
|
11
|
-
//
|
|
12
|
-
// The gate is layered and nothing auto-approves: deterministic `SpecChecks`
|
|
13
|
-
// (every legacy coverage unit must appear in the traceability matrix; features
|
|
14
|
-
// must be well-formed Gherkin; both indexes present), then an LLM-as-a-Judge
|
|
15
|
-
// pass per program against the pack's rubrics with full marks required.
|
|
16
|
-
// Findings feed one bounded fix round per sub-bar program plus one estate-wide
|
|
17
|
-
// residual turn; a still-dirty pack is committed as an explicit DRAFT and the
|
|
18
|
-
// flow halts for human triage. Even a clean pack only gets an unchecked
|
|
19
|
-
// `- [ ] Approved` marker in `docs/modernization/README.md`.
|
|
20
|
-
//
|
|
21
|
-
// Pack: LLM4TS_PACK=<dir> (default packs/cobol-springboot, resolved against the
|
|
22
|
-
// launch dir, then against the flow's own directory — the built-in packs).
|
|
23
|
-
// LLM4TS_WAVE=<name> scopes the run to one wave of the approved plan. Judge
|
|
24
|
-
// context is bounded by LLM4TS_JUDGE_SOURCES_LIMIT (chars).
|
|
25
|
-
import { join } from "node:path"
|
|
26
|
-
import * as Effect from "effect/Effect"
|
|
27
|
-
import { Sample, type EvalResult } from "@llm4ts/core/eval/Eval"
|
|
28
|
-
import { judge } from "@llm4ts/core/eval/Judge"
|
|
29
|
-
import type { JsonSchema } from "@llm4ts/core/Models"
|
|
30
|
-
import { FlowAborted, FlowLlmError } from "@llm4ts/flow/FlowError"
|
|
31
|
-
import { Info } from "@llm4ts/flow/FlowEvents"
|
|
32
|
-
import { makeChat } from "@llm4ts/flow/Chat"
|
|
33
|
-
import type { Pack } from "@llm4ts/flow/Pack"
|
|
34
|
-
import { loadPatternCards, matchingPatternCards } from "@llm4ts/flow/Patterns"
|
|
35
|
-
import { stage } from "@llm4ts/flow/PlanExecution"
|
|
36
|
-
import { defaultPlanInstructions, planFrom } from "@llm4ts/flow/Planner"
|
|
37
|
-
import { ReviewIssue, ReviewResult, mergeReviewResults } from "@llm4ts/flow/Review"
|
|
38
|
-
import { cachedReview } from "@llm4ts/flow/ReviewCache"
|
|
39
|
-
import { coverage, coverageUnits, features, matchingFiles } from "@llm4ts/flow/SpecChecks"
|
|
40
|
-
import { withDraftApproval, requireApproval } from "@llm4ts/modernize/Approval"
|
|
41
|
-
import {
|
|
42
|
-
ProgramArtifacts,
|
|
43
|
-
ProgramUnit,
|
|
44
|
-
extractProgramsResumably
|
|
45
|
-
} from "@llm4ts/modernize/Artifacts"
|
|
46
|
-
import { asReadOnly, coderFromEnv } from "@llm4ts/runner/Connectors"
|
|
47
|
-
import { resolveFlowInput } from "@llm4ts/runner/FlowArgs"
|
|
48
|
-
import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner"
|
|
49
|
-
import { reviewFingerprint } from "@llm4ts/runner/ReviewFingerprint"
|
|
50
|
-
import { nodePlainFileStore } from "@llm4ts/runner/NodePlainFileStore"
|
|
51
|
-
import { makeNodeWorkspace } from "@llm4ts/runner/NodeWorkspace"
|
|
52
|
-
import { loadUniversalPatternCards, openPack } from "@llm4ts/runner/Packs"
|
|
53
|
-
|
|
54
|
-
const ModDir = "docs/modernization"
|
|
55
|
-
const MaxRounds = 3
|
|
56
|
-
|
|
57
|
-
const judgeSourcesLimit = (): number => {
|
|
58
|
-
const raw = Number.parseInt(process.env.LLM4TS_JUDGE_SOURCES_LIMIT ?? "", 10)
|
|
59
|
-
return Number.isFinite(raw) && raw > 0 ? raw : 400_000
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Past the limit keep head + tail so entry points and trailing rules stay visible. */
|
|
63
|
-
const capText = (text: string, limit: number): string => {
|
|
64
|
-
if (text.length <= limit) {
|
|
65
|
-
return text
|
|
66
|
-
}
|
|
67
|
-
const head = Math.floor((limit * 3) / 4)
|
|
68
|
-
return `${text.slice(0, head)}\n\n… [truncated] …\n\n${text.slice(text.length - (limit - head))}`
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** `cobol/ACCTXFR.cbl` → `ACCTXFR`: the program name keying every per-program artifact. */
|
|
72
|
-
const programName = (relativePath: string): string => {
|
|
73
|
-
const base = relativePath.slice(relativePath.lastIndexOf("/") + 1)
|
|
74
|
-
const dot = base.lastIndexOf(".")
|
|
75
|
-
return dot > 0 ? base.slice(0, dot) : base
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** The `- PROG` entries of `## Wave: <name>` in the survey's wave plan. */
|
|
79
|
-
const wavePrograms = (planText: string, wave: string): ReadonlyArray<string> => {
|
|
80
|
-
const lines = planText.split(/\r?\n/)
|
|
81
|
-
const start = lines.findIndex((line) => line.trim() === `## Wave: ${wave}`)
|
|
82
|
-
if (start < 0) {
|
|
83
|
-
return []
|
|
84
|
-
}
|
|
85
|
-
const section: Array<string> = []
|
|
86
|
-
for (const line of lines.slice(start + 1)) {
|
|
87
|
-
if (line.trim().startsWith("## ")) {
|
|
88
|
-
break
|
|
89
|
-
}
|
|
90
|
-
if (line.trim().startsWith("- ")) {
|
|
91
|
-
section.push(line.trim().slice(2).trim())
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return section
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const programArtifactsJsonSchema: JsonSchema = {
|
|
98
|
-
type: "object",
|
|
99
|
-
properties: {
|
|
100
|
-
spec: { type: "string" },
|
|
101
|
-
feature: { type: "string" },
|
|
102
|
-
traceability: { type: "string" },
|
|
103
|
-
mapping: { type: "string" }
|
|
104
|
-
},
|
|
105
|
-
required: ["spec", "feature", "traceability", "mapping"]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const programAsk = (pack: Pack, relativePath: string): string =>
|
|
109
|
-
[
|
|
110
|
-
`Extract the behavioural spec for ONE source unit of this repository: ${relativePath}`,
|
|
111
|
-
"",
|
|
112
|
-
"Read the source file and anything it references (copybooks, includes, called programs)",
|
|
113
|
-
`for context, but spec ONLY ${relativePath} and do not modify legacy sources.`,
|
|
114
|
-
"",
|
|
115
|
-
'Respond only with JSON: {"spec":"…","feature":"…","traceability":"…","mapping":"…"} where:',
|
|
116
|
-
"",
|
|
117
|
-
`- "spec" — the behavioural spec for ${relativePath}, as Markdown.`,
|
|
118
|
-
pack.prompt("spec") ?? "",
|
|
119
|
-
"",
|
|
120
|
-
`- "feature" — BDD scenarios encoding that spec, as a well-formed Gherkin .feature file`,
|
|
121
|
-
" (Feature: header, Scenario: blocks, Given/When/Then steps).",
|
|
122
|
-
pack.prompt("bdd") ?? "",
|
|
123
|
-
"",
|
|
124
|
-
`- "traceability" — EVERY source unit of ${relativePath} (each COBOL paragraph, each JCL`,
|
|
125
|
-
" step) on its own line, mapped to the spec rules/scenarios that cover it:",
|
|
126
|
-
" `<UNIT-NAME> — <refs>`. Unit names verbatim as they appear in the source.",
|
|
127
|
-
"",
|
|
128
|
-
`- "mapping" — data & interface mapping for ${relativePath}: tables/record layouts → target`,
|
|
129
|
-
" entities; files/screens/queues → target service contracts."
|
|
130
|
-
].join("\n")
|
|
131
|
-
|
|
132
|
-
/** Sub-bar judge dimensions as Critical review issues, titled with their program. */
|
|
133
|
-
const judgeIssues = (
|
|
134
|
-
pack: Pack,
|
|
135
|
-
scored: {
|
|
136
|
-
readonly scores: ReadonlyArray<{
|
|
137
|
-
readonly name: string
|
|
138
|
-
readonly score: number
|
|
139
|
-
readonly reasoning: string
|
|
140
|
-
}>
|
|
141
|
-
},
|
|
142
|
-
program: string
|
|
143
|
-
): ReviewResult => {
|
|
144
|
-
const issues = scored.scores.flatMap((score) => {
|
|
145
|
-
const maxScore = pack.judgeDimensions.find((d) => d.name === score.name)?.maxScore ?? 2
|
|
146
|
-
return score.score < maxScore
|
|
147
|
-
? [
|
|
148
|
-
ReviewIssue.make({
|
|
149
|
-
severity: "Critical",
|
|
150
|
-
title: `judge[${program}]: ${score.name} scored ${score.score}`,
|
|
151
|
-
description: score.reasoning
|
|
152
|
-
})
|
|
153
|
-
]
|
|
154
|
-
: []
|
|
155
|
-
})
|
|
156
|
-
return ReviewResult.make({ issues, summary: `judge:${program}` })
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const judgeIssueProgram = (issue: ReviewIssue): string | undefined =>
|
|
160
|
-
/^judge\[([^\]]+)\]: /.exec(issue.title)?.[1]
|
|
161
|
-
|
|
162
|
-
const issueLines = (issues: ReadonlyArray<ReviewIssue>): string =>
|
|
163
|
-
issues.map((issue) => `- [${issue.severity}] ${issue.title}: ${issue.description}`).join("\n")
|
|
164
|
-
|
|
165
|
-
const programFixAsk = (
|
|
166
|
-
name: string,
|
|
167
|
-
relativePath: string,
|
|
168
|
-
issues: ReadonlyArray<ReviewIssue>
|
|
169
|
-
): string =>
|
|
170
|
-
[
|
|
171
|
-
`The spec pack for ONE program did not clear its quality gate: ${name} (source: ${relativePath}).`,
|
|
172
|
-
`Fix these findings by editing ONLY this program's files — ${ModDir}/specs/${name}.md,`,
|
|
173
|
-
`${ModDir}/features/${name.toLowerCase()}.feature, ${ModDir}/traceability/${name}.md,`,
|
|
174
|
-
`${ModDir}/mapping/${name}.md — against the source at ${relativePath}. Then stop:`,
|
|
175
|
-
issueLines(issues)
|
|
176
|
-
].join("\n")
|
|
177
|
-
|
|
178
|
-
const globalFixAsk = (issues: ReadonlyArray<ReviewIssue>): string =>
|
|
179
|
-
[
|
|
180
|
-
"The spec pack did not clear its estate-wide quality gate. Fix these findings by editing the",
|
|
181
|
-
`per-program files under ${ModDir}/ (specs/, features/, traceability/<PROGRAM>.md,`,
|
|
182
|
-
`mapping/<PROGRAM>.md). ${ModDir}/traceability.md and ${ModDir}/mapping.md are REGENERATED`,
|
|
183
|
-
"from the fragments — do not edit them directly. Fix the findings in place, then stop:",
|
|
184
|
-
issueLines(issues)
|
|
185
|
-
].join("\n")
|
|
186
|
-
|
|
187
|
-
const readmeFor = (pack: Pack, verdict: string): string =>
|
|
188
|
-
[
|
|
189
|
-
`# Modernization spec pack — ${pack.name}`,
|
|
190
|
-
"",
|
|
191
|
-
`Extracted by the modernize-extract flow. Gate verdict: ${verdict}.`,
|
|
192
|
-
"",
|
|
193
|
-
"- specs/ — behavioural specs, one per program",
|
|
194
|
-
"- features/ — BDD acceptance scenarios",
|
|
195
|
-
"- traceability.md — source-unit → spec coverage matrix (generated from traceability/)",
|
|
196
|
-
"- mapping.md — data & interface mapping (generated from mapping/)",
|
|
197
|
-
"- rules.txt — every coverage unit, one per line (the rule universe verification reports against)",
|
|
198
|
-
"- plan.md — proposed implementation tasks",
|
|
199
|
-
"",
|
|
200
|
-
"Review everything, then flip the marker below and run the seed phase."
|
|
201
|
-
].join("\n")
|
|
202
|
-
|
|
203
|
-
const program = Effect.gen(function* () {
|
|
204
|
-
const input = yield* resolveFlowInput(
|
|
205
|
-
"Extract the complete behavioural spec pack for this legacy estate"
|
|
206
|
-
)
|
|
207
|
-
const coder = coderFromEnv(process.env)
|
|
208
|
-
const files = nodePlainFileStore
|
|
209
|
-
const modDirAbs = join(input.workDir, ModDir)
|
|
210
|
-
|
|
211
|
-
yield* runNode(
|
|
212
|
-
{
|
|
213
|
-
workDir: input.workDir,
|
|
214
|
-
workspace: input.workspace,
|
|
215
|
-
userPrompt: input.prompt,
|
|
216
|
-
coder,
|
|
217
|
-
reasoning: asReadOnly(coder),
|
|
218
|
-
environment: process.env
|
|
219
|
-
},
|
|
220
|
-
(context) =>
|
|
221
|
-
Effect.gen(function* () {
|
|
222
|
-
const repo = yield* makeNodeWorkspace(input.workDir)
|
|
223
|
-
const opened = yield* stage(
|
|
224
|
-
context.events,
|
|
225
|
-
"pack",
|
|
226
|
-
openPack({
|
|
227
|
-
environment: process.env,
|
|
228
|
-
launchDir: input.workspace,
|
|
229
|
-
flowDir: import.meta.dirname
|
|
230
|
-
})
|
|
231
|
-
)
|
|
232
|
-
const pack = opened.pack
|
|
233
|
-
yield* stage(
|
|
234
|
-
context.events,
|
|
235
|
-
"branch",
|
|
236
|
-
context.git.checkoutOrCreate("modernize/spec-pack").pipe(Effect.asVoid)
|
|
237
|
-
)
|
|
238
|
-
const system = [
|
|
239
|
-
pack.prompt("analysis"),
|
|
240
|
-
pack.lessons === undefined
|
|
241
|
-
? undefined
|
|
242
|
-
: `Lessons from previous modernization runs — apply them:\n${pack.lessons}`
|
|
243
|
-
]
|
|
244
|
-
.filter((part) => part !== undefined)
|
|
245
|
-
.join("\n\n")
|
|
246
|
-
|
|
247
|
-
const all = yield* stage(
|
|
248
|
-
context.events,
|
|
249
|
-
"inventory",
|
|
250
|
-
matchingFiles(repo, pack.programs ?? pack.sources ?? ".*")
|
|
251
|
-
)
|
|
252
|
-
const wave = process.env.LLM4TS_WAVE?.trim()
|
|
253
|
-
const programs =
|
|
254
|
-
wave === undefined || wave.length === 0
|
|
255
|
-
? all
|
|
256
|
-
: yield* Effect.gen(function* () {
|
|
257
|
-
yield* stage(
|
|
258
|
-
context.events,
|
|
259
|
-
"wave approval",
|
|
260
|
-
requireApproval(files, join(modDirAbs, "wave-plan.md"))
|
|
261
|
-
)
|
|
262
|
-
const planText = (yield* files.read(join(modDirAbs, "wave-plan.md"))) ?? ""
|
|
263
|
-
const names = new Set(wavePrograms(planText, wave))
|
|
264
|
-
const scoped = all.filter((rel) => names.has(programName(rel)))
|
|
265
|
-
if (scoped.length === 0) {
|
|
266
|
-
return yield* FlowAborted.make({
|
|
267
|
-
message: `wave '${wave}' matches no programs in ${ModDir}/wave-plan.md`
|
|
268
|
-
})
|
|
269
|
-
}
|
|
270
|
-
return scoped
|
|
271
|
-
})
|
|
272
|
-
if (programs.length === 0) {
|
|
273
|
-
return yield* FlowAborted.make({
|
|
274
|
-
message: `no source units matched the pack's programs/sources regex under ${input.workDir}`
|
|
275
|
-
})
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const units = programs.map((rel) =>
|
|
279
|
-
ProgramUnit.make({ name: programName(rel), sourcePath: rel })
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
// Pattern cards are selected deterministically from the SOURCE, never by
|
|
283
|
-
// the model: implementation later injects exactly the cards cited here.
|
|
284
|
-
const cards = [
|
|
285
|
-
...(yield* loadPatternCards(opened.workspace, `${opened.dir}/patterns`)),
|
|
286
|
-
...(yield* loadUniversalPatternCards([input.workspace, import.meta.dirname]))
|
|
287
|
-
]
|
|
288
|
-
|
|
289
|
-
// One structured analyst call per program, resumable per program: a rerun
|
|
290
|
-
// skips every program whose spec exists, and each program gets its own commit.
|
|
291
|
-
yield* stage(
|
|
292
|
-
context.events,
|
|
293
|
-
"extract",
|
|
294
|
-
Effect.gen(function* () {
|
|
295
|
-
for (const [index, unit] of units.entries()) {
|
|
296
|
-
const summary = yield* extractProgramsResumably(
|
|
297
|
-
files,
|
|
298
|
-
[unit],
|
|
299
|
-
(target) =>
|
|
300
|
-
Effect.gen(function* () {
|
|
301
|
-
yield* context.events.publish(
|
|
302
|
-
Info.make({
|
|
303
|
-
message: `extracting ${target.sourcePath} (${index + 1}/${units.length})`
|
|
304
|
-
})
|
|
305
|
-
)
|
|
306
|
-
return yield* context.coder
|
|
307
|
-
.executeStructured(
|
|
308
|
-
`${system}\n\n${programAsk(pack, target.sourcePath)}`,
|
|
309
|
-
ProgramArtifacts,
|
|
310
|
-
programArtifactsJsonSchema
|
|
311
|
-
)
|
|
312
|
-
.pipe(
|
|
313
|
-
Effect.mapError(FlowLlmError.from),
|
|
314
|
-
// A turn-limit trip is the wedged-agent tail, not a
|
|
315
|
-
// failure: keep whatever the analyst already produced.
|
|
316
|
-
Effect.catchIf(
|
|
317
|
-
(error) => error.cause?._tag === "TurnLimitError",
|
|
318
|
-
(error) =>
|
|
319
|
-
Effect.gen(function* () {
|
|
320
|
-
const existing = yield* files.read(
|
|
321
|
-
join(modDirAbs, "specs", `${target.name}.md`)
|
|
322
|
-
)
|
|
323
|
-
if (existing === undefined) {
|
|
324
|
-
return yield* Effect.fail(error)
|
|
325
|
-
}
|
|
326
|
-
yield* context.events.publish(
|
|
327
|
-
Info.make({
|
|
328
|
-
message: `turn limit hit on ${target.sourcePath} after its spec was written — keeping the work`
|
|
329
|
-
})
|
|
330
|
-
)
|
|
331
|
-
return ProgramArtifacts.make({
|
|
332
|
-
spec: existing,
|
|
333
|
-
feature: "",
|
|
334
|
-
traceability: "",
|
|
335
|
-
mapping: ""
|
|
336
|
-
})
|
|
337
|
-
})
|
|
338
|
-
)
|
|
339
|
-
)
|
|
340
|
-
}),
|
|
341
|
-
modDirAbs
|
|
342
|
-
)
|
|
343
|
-
if (summary.created.length > 0) {
|
|
344
|
-
// Tag the traceability fragment with the cards this program's
|
|
345
|
-
// source matches — regex-decided, so implementation's playbook
|
|
346
|
-
// is reproducible.
|
|
347
|
-
const source = yield* files.read(join(input.workDir, unit.sourcePath))
|
|
348
|
-
const matched = source === undefined ? [] : matchingPatternCards(source, cards)
|
|
349
|
-
if (matched.length > 0) {
|
|
350
|
-
const fragmentPath = join(modDirAbs, "traceability", `${unit.name}.md`)
|
|
351
|
-
const fragment = (yield* files.read(fragmentPath)) ?? ""
|
|
352
|
-
yield* files.writeAtomic(
|
|
353
|
-
fragmentPath,
|
|
354
|
-
`${fragment.trimEnd()}\n\nPatterns: ${matched.map((card) => card.id).join(", ")}\n`
|
|
355
|
-
)
|
|
356
|
-
}
|
|
357
|
-
yield* context.git
|
|
358
|
-
.commitAll(`modernize(${pack.name}): spec ${unit.name}`)
|
|
359
|
-
.pipe(Effect.asVoid)
|
|
360
|
-
} else {
|
|
361
|
-
yield* context.events.publish(
|
|
362
|
-
Info.make({
|
|
363
|
-
message: `resume: specs/${unit.name}.md exists — skipping ${unit.sourcePath}`
|
|
364
|
-
})
|
|
365
|
-
)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
})
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
// traceability.md / mapping.md are regenerated from the fragments — fixes
|
|
372
|
-
// belong in the fragments, never the indexes.
|
|
373
|
-
const rebuildIndexes = Effect.gen(function* () {
|
|
374
|
-
for (const [fragmentDir, index] of [
|
|
375
|
-
["traceability", "traceability.md"],
|
|
376
|
-
["mapping", "mapping.md"]
|
|
377
|
-
] as const) {
|
|
378
|
-
const parts: Array<string> = []
|
|
379
|
-
for (const unit of units) {
|
|
380
|
-
const text = yield* files.read(join(modDirAbs, fragmentDir, `${unit.name}.md`))
|
|
381
|
-
if (text !== undefined && text.trim().length > 0) {
|
|
382
|
-
parts.push(`===== ${unit.name} =====\n${text.trimEnd()}`)
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
if (parts.length > 0) {
|
|
386
|
-
yield* files.writeAtomic(join(modDirAbs, index), parts.join("\n\n") + "\n")
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
const writeRules = Effect.gen(function* () {
|
|
392
|
-
const unitsByRule = yield* coverageUnits(repo, pack.coverage)
|
|
393
|
-
const allUnits = [...new Set(Object.values(unitsByRule).flat())].sort()
|
|
394
|
-
if (allUnits.length > 0) {
|
|
395
|
-
yield* files.writeAtomic(join(modDirAbs, "rules.txt"), allUnits.join("\n") + "\n")
|
|
396
|
-
}
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
// Commit the (ungated) draft: extraction is the expensive step, and a gate
|
|
400
|
-
// failure or crash must not cost it.
|
|
401
|
-
yield* stage(
|
|
402
|
-
context.events,
|
|
403
|
-
"draft",
|
|
404
|
-
rebuildIndexes.pipe(
|
|
405
|
-
Effect.andThen(writeRules),
|
|
406
|
-
Effect.andThen(
|
|
407
|
-
context.git
|
|
408
|
-
.commitAll(`modernize(${pack.name}): spec pack draft (ungated)`)
|
|
409
|
-
.pipe(Effect.asVoid)
|
|
410
|
-
)
|
|
411
|
-
)
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
const packJudge = judge(context.reasoning, pack.judgeDimensions)
|
|
415
|
-
const limit = judgeSourcesLimit()
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* An empty structured response that survives the in-run retries is
|
|
419
|
-
* usually DETERMINISTIC (context overflow), so repeating the same
|
|
420
|
-
* prompt cannot succeed — retry at half, then quarter context instead.
|
|
421
|
-
*/
|
|
422
|
-
const judgeWithShrink = (spec: string, feature: string, source: string) => {
|
|
423
|
-
const attempt = (
|
|
424
|
-
cap: number,
|
|
425
|
-
rest: ReadonlyArray<number>
|
|
426
|
-
): Effect.Effect<EvalResult, FlowLlmError> =>
|
|
427
|
-
packJudge
|
|
428
|
-
.evaluate(
|
|
429
|
-
Sample.make({
|
|
430
|
-
response: capText(`${spec}\n\n${feature}`, cap),
|
|
431
|
-
context: capText(source, cap),
|
|
432
|
-
query: input.prompt
|
|
433
|
-
})
|
|
434
|
-
)
|
|
435
|
-
.pipe(
|
|
436
|
-
Effect.mapError(FlowLlmError.from),
|
|
437
|
-
Effect.catchIf(
|
|
438
|
-
(error) => rest.length > 0 && error.message.includes("empty response"),
|
|
439
|
-
(error) => {
|
|
440
|
-
const [next, ...remaining] = rest
|
|
441
|
-
return context.events
|
|
442
|
-
.publish(
|
|
443
|
-
Info.make({
|
|
444
|
-
message: `judge returned empty at cap ${cap} chars — shrinking to ${next ?? cap}: ${error.message}`
|
|
445
|
-
})
|
|
446
|
-
)
|
|
447
|
-
.pipe(Effect.andThen(attempt(next ?? cap, remaining)))
|
|
448
|
-
}
|
|
449
|
-
)
|
|
450
|
-
)
|
|
451
|
-
return attempt(limit, [Math.floor(limit / 2), Math.floor(limit / 4)])
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Judging is resumable per program: the verdict persists under
|
|
456
|
-
* `gate/<NAME>.json`, fingerprinted over the source, spec, feature, and
|
|
457
|
-
* rubric it judged. Unchanged content reuses the stored verdict with NO
|
|
458
|
-
* model call, so a crash or quota death re-judges only what changed.
|
|
459
|
-
* Delete `gate/` to force a full re-judge.
|
|
460
|
-
*/
|
|
461
|
-
const judgeProgram = (unit: ProgramUnit) =>
|
|
462
|
-
Effect.gen(function* () {
|
|
463
|
-
const spec = (yield* files.read(join(modDirAbs, "specs", `${unit.name}.md`))) ?? ""
|
|
464
|
-
const feature =
|
|
465
|
-
(yield* files.read(
|
|
466
|
-
join(modDirAbs, "features", `${unit.name.toLowerCase()}.feature`)
|
|
467
|
-
)) ?? ""
|
|
468
|
-
const source = (yield* files.read(join(input.workDir, unit.sourcePath))) ?? ""
|
|
469
|
-
const rubric = pack.judgeDimensions
|
|
470
|
-
.map(
|
|
471
|
-
(dimension) => `${dimension.name} (0..${dimension.maxScore}): ${dimension.rubric}`
|
|
472
|
-
)
|
|
473
|
-
.join("\n")
|
|
474
|
-
return yield* cachedReview(
|
|
475
|
-
files,
|
|
476
|
-
join(modDirAbs, "gate", `${unit.name}.json`),
|
|
477
|
-
reviewFingerprint(source, spec, feature, rubric),
|
|
478
|
-
context.events.publish(Info.make({ message: `judging ${unit.name}` })).pipe(
|
|
479
|
-
Effect.andThen(judgeWithShrink(spec, feature, source)),
|
|
480
|
-
Effect.map((scored) => judgeIssues(pack, scored, unit.name))
|
|
481
|
-
)
|
|
482
|
-
)
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
const gateEvaluate = Effect.gen(function* () {
|
|
486
|
-
yield* rebuildIndexes
|
|
487
|
-
const trace = (yield* files.read(join(modDirAbs, "traceability.md"))) ?? ""
|
|
488
|
-
const mapping = (yield* files.read(join(modDirAbs, "mapping.md"))) ?? ""
|
|
489
|
-
const docs = ReviewResult.make({
|
|
490
|
-
issues: [
|
|
491
|
-
...(trace.trim().length === 0
|
|
492
|
-
? [
|
|
493
|
-
ReviewIssue.make({
|
|
494
|
-
severity: "Critical",
|
|
495
|
-
title: "missing traceability",
|
|
496
|
-
description: `no ${ModDir}/traceability/ fragments were written`
|
|
497
|
-
})
|
|
498
|
-
]
|
|
499
|
-
: []),
|
|
500
|
-
...(mapping.trim().length === 0
|
|
501
|
-
? [
|
|
502
|
-
ReviewIssue.make({
|
|
503
|
-
severity: "Critical",
|
|
504
|
-
title: "missing mapping",
|
|
505
|
-
description: `no ${ModDir}/mapping/ fragments were written`
|
|
506
|
-
})
|
|
507
|
-
]
|
|
508
|
-
: [])
|
|
509
|
-
],
|
|
510
|
-
summary: "docs"
|
|
511
|
-
})
|
|
512
|
-
const covered = yield* coverage(repo, pack.coverage, trace)
|
|
513
|
-
const wellFormed = yield* features(repo, join(ModDir, "features"))
|
|
514
|
-
const judged: Array<ReviewResult> = []
|
|
515
|
-
for (const unit of units) {
|
|
516
|
-
judged.push(yield* judgeProgram(unit))
|
|
517
|
-
}
|
|
518
|
-
return mergeReviewResults([covered, wellFormed, docs, ...judged])
|
|
519
|
-
})
|
|
520
|
-
|
|
521
|
-
// One bounded fix turn per sub-bar program (own commit) plus one residual
|
|
522
|
-
// estate-wide turn, then re-evaluate — up to MaxRounds evaluations.
|
|
523
|
-
const fixOnce = (result: ReviewResult) =>
|
|
524
|
-
Effect.gen(function* () {
|
|
525
|
-
const relOf = new Map(units.map((unit) => [unit.name, unit.sourcePath]))
|
|
526
|
-
const scoped = new Map<string, Array<ReviewIssue>>()
|
|
527
|
-
const global: Array<ReviewIssue> = []
|
|
528
|
-
for (const issue of result.issues) {
|
|
529
|
-
const name = judgeIssueProgram(issue)
|
|
530
|
-
if (name !== undefined && relOf.has(name)) {
|
|
531
|
-
const bucket = scoped.get(name) ?? []
|
|
532
|
-
bucket.push(issue)
|
|
533
|
-
scoped.set(name, bucket)
|
|
534
|
-
} else {
|
|
535
|
-
global.push(issue)
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
const turn = (ask: string, commitMessage: string) =>
|
|
539
|
-
Effect.gen(function* () {
|
|
540
|
-
const chat = yield* makeChat(context.coder, { system })
|
|
541
|
-
yield* chat.ask(ask).pipe(
|
|
542
|
-
Effect.asVoid,
|
|
543
|
-
// A wedged agent that trips its turn limit mid-fix still wrote
|
|
544
|
-
// something; re-evaluate what landed instead of failing.
|
|
545
|
-
Effect.catchIf(
|
|
546
|
-
(error) => error._tag === "Llm" && error.cause?._tag === "TurnLimitError",
|
|
547
|
-
() =>
|
|
548
|
-
context.events.publish(
|
|
549
|
-
Info.make({
|
|
550
|
-
message:
|
|
551
|
-
"turn limit hit during a fix turn — re-evaluating what was written"
|
|
552
|
-
})
|
|
553
|
-
)
|
|
554
|
-
)
|
|
555
|
-
)
|
|
556
|
-
yield* context.git.commitAll(commitMessage).pipe(Effect.asVoid)
|
|
557
|
-
})
|
|
558
|
-
for (const [name, issues] of [...scoped.entries()].sort()) {
|
|
559
|
-
yield* context.events.publish(
|
|
560
|
-
Info.make({ message: `fixing ${name} — ${issues.length} finding(s)` })
|
|
561
|
-
)
|
|
562
|
-
const relativePath = relOf.get(name)
|
|
563
|
-
if (relativePath !== undefined) {
|
|
564
|
-
yield* turn(
|
|
565
|
-
programFixAsk(name, relativePath, issues),
|
|
566
|
-
`modernize(${pack.name}): gate fixes ${name}`
|
|
567
|
-
)
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
if (global.length > 0) {
|
|
571
|
-
yield* context.events.publish(
|
|
572
|
-
Info.make({ message: `fixing estate-wide findings — ${global.length}` })
|
|
573
|
-
)
|
|
574
|
-
yield* turn(globalFixAsk(global), `modernize(${pack.name}): gate fixes (estate-wide)`)
|
|
575
|
-
}
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
let result = yield* stage(context.events, "gate", gateEvaluate)
|
|
579
|
-
for (let round = 2; round <= MaxRounds && result.issues.length > 0; round += 1) {
|
|
580
|
-
yield* stage(context.events, `gate fixes (round ${round})`, fixOnce(result))
|
|
581
|
-
result = yield* stage(context.events, `gate (round ${round})`, gateEvaluate)
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
if (result.issues.length === 0) {
|
|
585
|
-
yield* stage(
|
|
586
|
-
context.events,
|
|
587
|
-
"plan",
|
|
588
|
-
Effect.gen(function* () {
|
|
589
|
-
const specTexts: Array<string> = []
|
|
590
|
-
for (const unit of units) {
|
|
591
|
-
const spec = yield* files.read(join(modDirAbs, "specs", `${unit.name}.md`))
|
|
592
|
-
if (spec !== undefined) {
|
|
593
|
-
specTexts.push(spec)
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
const plan = yield* planFrom(
|
|
597
|
-
context.reasoning,
|
|
598
|
-
capText(specTexts.join("\n\n"), limit),
|
|
599
|
-
`${defaultPlanInstructions}\n\n${pack.prompt("plan") ?? ""}`
|
|
600
|
-
)
|
|
601
|
-
yield* files.writeAtomic(join(modDirAbs, "plan.md"), plan.render)
|
|
602
|
-
})
|
|
603
|
-
)
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const verdict =
|
|
607
|
-
result.issues.length === 0
|
|
608
|
-
? "PASSED — pending human approval"
|
|
609
|
-
: `DRAFT — ${result.issues.length} open issue(s)`
|
|
610
|
-
yield* stage(
|
|
611
|
-
context.events,
|
|
612
|
-
"commit",
|
|
613
|
-
files
|
|
614
|
-
.writeAtomic(join(modDirAbs, "README.md"), withDraftApproval(readmeFor(pack, verdict)))
|
|
615
|
-
.pipe(
|
|
616
|
-
Effect.andThen(
|
|
617
|
-
context.git
|
|
618
|
-
.commitAll(`modernize(${pack.name}): spec pack (${verdict})`)
|
|
619
|
-
.pipe(Effect.asVoid)
|
|
620
|
-
)
|
|
621
|
-
)
|
|
622
|
-
)
|
|
623
|
-
|
|
624
|
-
if (result.issues.length > 0) {
|
|
625
|
-
return yield* FlowAborted.make({
|
|
626
|
-
message:
|
|
627
|
-
`extraction gate not cleared after ${MaxRounds} round(s) — spec pack committed as draft:\n` +
|
|
628
|
-
result.issues.map((issue) => `- ${issue.title}`).join("\n")
|
|
629
|
-
})
|
|
630
|
-
}
|
|
631
|
-
yield* context.events.publish(
|
|
632
|
-
Info.make({
|
|
633
|
-
message:
|
|
634
|
-
`spec pack ready — review ${ModDir}/README.md, set '- [x] Approved', ` +
|
|
635
|
-
"then run the seed phase"
|
|
636
|
-
})
|
|
637
|
-
)
|
|
638
|
-
})
|
|
639
|
-
)
|
|
640
|
-
})
|
|
641
|
-
|
|
642
|
-
runFlowMain(program)
|