@llm4ts/shell 2.0.0 → 2.2.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/Cli.d.ts +1 -1
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +32 -0
- package/dist/Cli.js.map +1 -1
- package/dist/Refine.d.ts +44 -0
- package/dist/Refine.d.ts.map +1 -0
- package/dist/Refine.js +362 -0
- package/dist/Refine.js.map +1 -0
- package/flows/epic-stories.js +1 -1
- package/flows/fixtures/epic-stories/conto-bonifico.md +3 -3
- package/flows/lib/modernize-extract.js +217 -0
- package/flows/modernize-extract.js +13 -173
- package/flows/modernize-implement.js +28 -2
- package/flows/modernize-pack-check.js +7 -1
- package/flows/modernize-refine.js +389 -0
- package/flows/modernize-seed.js +50 -2
- package/flows/modernize-verify.js +12 -5
- package/kits/j2ee-nextjs/README.md +5 -4
- package/kits/j2ee-nextjs/fixtures/demo-bank/RUNBOOK.md +43 -0
- package/kits/j2ee-nextjs/fixtures/demo-bank/legacy-j2ee/PAGES.md +26 -0
- package/kits/j2ee-nextjs/flows/convert-all.js +56 -20
- package/kits/j2ee-nextjs/flows/convert-feature.js +48 -0
- package/kits/j2ee-nextjs/flows/lib/convert.js +292 -40
- package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/pack.md +16 -0
- package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/prompts/consolidate.md +10 -0
- package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/prompts/plan.md +24 -16
- package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/prompts/refine-propose.md +16 -0
- package/kits/mainframe-java/packs/cobol-springboot/pack.md +5 -0
- package/kits/mainframe-java/packs/cobol-springboot/prompts/consolidate.md +8 -0
- package/kits/mainframe-java/packs/cobol-springboot/prompts/refine-propose.md +10 -0
- package/package.json +5 -4
- package/src/Cli.ts +57 -0
- package/src/Refine.ts +504 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Shared core of the extraction phase: the per-program analyst ask, the
|
|
2
|
+
// per-program cached judge, the fix turn, and the spec-pack README. Used by
|
|
3
|
+
// `modernize-extract` (every program of a wave) and `modernize-refine`
|
|
4
|
+
// (one program at a time, deepened with a focus — ADR 0015).
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import { Sample } from "@llm4ts/core/eval/Eval";
|
|
8
|
+
import { judge } from "@llm4ts/core/eval/Judge";
|
|
9
|
+
import { capped, withShrink } from "@llm4ts/flow/Context";
|
|
10
|
+
import { FlowEvents } from "@llm4ts/flow/FlowEvents";
|
|
11
|
+
import { ReviewIssue } from "@llm4ts/flow/Review";
|
|
12
|
+
import { cachedReview } from "@llm4ts/flow/ReviewCache";
|
|
13
|
+
import { FlowLlmError, Info, ReviewResult, makeChat, reviewFingerprint } from "@llm4ts/runner";
|
|
14
|
+
export const ModDir = "docs/modernization";
|
|
15
|
+
export const positiveEnvInt = (name, fallback) => {
|
|
16
|
+
const raw = Number.parseInt(process.env[name] ?? "", 10);
|
|
17
|
+
return Number.isFinite(raw) && raw > 0 ? raw : fallback;
|
|
18
|
+
};
|
|
19
|
+
// Per-program turn budget — bounds a wedged agent, generous for real work.
|
|
20
|
+
export const analystTurns = () => positiveEnvInt("LLM4TS_ANALYST_TURNS", 48);
|
|
21
|
+
/**
|
|
22
|
+
* Max files named in one program's include closure. A program pulling more
|
|
23
|
+
* than this gets a bounded, visible subset rather than an unbounded read.
|
|
24
|
+
*/
|
|
25
|
+
export const maxClosureFiles = () => positiveEnvInt("LLM4TS_MAX_CLOSURE_FILES", 40);
|
|
26
|
+
/** `cobol/ACCTXFR.cbl` → `ACCTXFR`: the program name keying every per-program artifact. */
|
|
27
|
+
export const programName = (relativePath) => {
|
|
28
|
+
const base = relativePath.slice(relativePath.lastIndexOf("/") + 1);
|
|
29
|
+
const dot = base.lastIndexOf(".");
|
|
30
|
+
return dot > 0 ? base.slice(0, dot) : base;
|
|
31
|
+
};
|
|
32
|
+
/** The `- PROG` entries of `## Wave: <name>` in the survey's wave plan. */
|
|
33
|
+
export const wavePrograms = (planText, wave) => {
|
|
34
|
+
const lines = planText.split(/\r?\n/);
|
|
35
|
+
const start = lines.findIndex((line) => line.trim() === `## Wave: ${wave}`);
|
|
36
|
+
if (start < 0) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const section = [];
|
|
40
|
+
for (const line of lines.slice(start + 1)) {
|
|
41
|
+
if (line.trim().startsWith("## ")) {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
if (line.trim().startsWith("- ")) {
|
|
45
|
+
section.push(line.trim().slice(2).trim());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return section;
|
|
49
|
+
};
|
|
50
|
+
export const programArtifactsJsonSchema = {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
spec: { type: "string" },
|
|
54
|
+
feature: { type: "string" },
|
|
55
|
+
traceability: { type: "string" },
|
|
56
|
+
mapping: { type: "string" }
|
|
57
|
+
},
|
|
58
|
+
required: ["spec", "feature", "traceability", "mapping"]
|
|
59
|
+
};
|
|
60
|
+
/** The analyst's system prompt: the pack's analysis sidecar plus its lessons. */
|
|
61
|
+
export const analystSystem = (pack) => [
|
|
62
|
+
pack.prompt("analysis"),
|
|
63
|
+
pack.lessons === undefined
|
|
64
|
+
? undefined
|
|
65
|
+
: `Lessons from previous modernization runs — apply them:\n${pack.lessons}`
|
|
66
|
+
]
|
|
67
|
+
.filter((part) => part !== undefined)
|
|
68
|
+
.join("\n\n");
|
|
69
|
+
// The analyst gets a deterministically resolved include closure, not an open
|
|
70
|
+
// "read anything it references" instruction: told to chase references itself,
|
|
71
|
+
// the coding agent pulls files into its own context inside a single turn —
|
|
72
|
+
// which is how extract blew a 1M-token window while its own cap sat untouched.
|
|
73
|
+
export const programAsk = (pack, relativePath, closure, revision) => [
|
|
74
|
+
revision === undefined
|
|
75
|
+
? `Extract the behavioural spec for ONE source unit of this repository: ${relativePath}`
|
|
76
|
+
: `DEEPEN the behavioural spec of ONE source unit of this repository: ${relativePath}`,
|
|
77
|
+
"",
|
|
78
|
+
...(closure.length === 0
|
|
79
|
+
? [`Read ${relativePath}. It has no resolved dependencies.`]
|
|
80
|
+
: [
|
|
81
|
+
`Read ${relativePath} and EXACTLY these resolved dependencies — do not go looking for others:`,
|
|
82
|
+
...closure.map((file) => `- ${file}`)
|
|
83
|
+
]),
|
|
84
|
+
`Spec ONLY ${relativePath} and do not modify legacy sources.`,
|
|
85
|
+
...(revision === undefined
|
|
86
|
+
? []
|
|
87
|
+
: [
|
|
88
|
+
"",
|
|
89
|
+
"This is a REVISION, not a fresh extraction. Start from the previous artifacts below and",
|
|
90
|
+
"revise them: keep every existing scenario title UNCHANGED unless the behaviour it names",
|
|
91
|
+
"no longer exists in the source, add what is missing, correct what is wrong. The focus of",
|
|
92
|
+
`this revision, which the result MUST address explicitly: ${revision.focus}`,
|
|
93
|
+
"",
|
|
94
|
+
"Previous spec:",
|
|
95
|
+
revision.previous.spec,
|
|
96
|
+
"",
|
|
97
|
+
"Previous feature file:",
|
|
98
|
+
revision.previous.feature,
|
|
99
|
+
"",
|
|
100
|
+
"Previous traceability fragment:",
|
|
101
|
+
revision.previous.traceability,
|
|
102
|
+
"",
|
|
103
|
+
"Previous mapping fragment:",
|
|
104
|
+
revision.previous.mapping
|
|
105
|
+
]),
|
|
106
|
+
"",
|
|
107
|
+
'Respond only with JSON: {"spec":"…","feature":"…","traceability":"…","mapping":"…"} where:',
|
|
108
|
+
"",
|
|
109
|
+
`- "spec" — the behavioural spec for ${relativePath}, as Markdown.`,
|
|
110
|
+
pack.prompt("spec") ?? "",
|
|
111
|
+
"",
|
|
112
|
+
`- "feature" — BDD scenarios encoding that spec, as a well-formed Gherkin .feature file`,
|
|
113
|
+
" (Feature: header, Scenario: blocks, Given/When/Then steps).",
|
|
114
|
+
pack.prompt("bdd") ?? "",
|
|
115
|
+
"",
|
|
116
|
+
`- "traceability" — EVERY source unit of ${relativePath} (each COBOL paragraph, each JCL`,
|
|
117
|
+
" step) on its own line, mapped to the spec rules/scenarios that cover it:",
|
|
118
|
+
" `<UNIT-NAME> — <refs>`. Unit names verbatim as they appear in the source.",
|
|
119
|
+
"",
|
|
120
|
+
`- "mapping" — data & interface mapping for ${relativePath}: tables/record layouts → target`,
|
|
121
|
+
" entities; files/screens/queues → target service contracts."
|
|
122
|
+
].join("\n");
|
|
123
|
+
/** Sub-bar judge dimensions as Critical review issues, titled with their program. */
|
|
124
|
+
export const judgeIssues = (pack, scored, program) => {
|
|
125
|
+
const issues = scored.scores.flatMap((score) => {
|
|
126
|
+
const maxScore = pack.judgeDimensions.find((d) => d.name === score.name)?.maxScore ?? 2;
|
|
127
|
+
return score.score < maxScore
|
|
128
|
+
? [
|
|
129
|
+
ReviewIssue.make({
|
|
130
|
+
severity: "Critical",
|
|
131
|
+
title: `judge[${program}]: ${score.name} scored ${score.score}`,
|
|
132
|
+
description: score.reasoning
|
|
133
|
+
})
|
|
134
|
+
]
|
|
135
|
+
: [];
|
|
136
|
+
});
|
|
137
|
+
return ReviewResult.make({ issues, summary: `judge:${program}` });
|
|
138
|
+
};
|
|
139
|
+
export const judgeIssueProgram = (issue) => /^judge\[([^\]]+)\]: /.exec(issue.title)?.[1];
|
|
140
|
+
export const issueLines = (issues) => issues.map((issue) => `- [${issue.severity}] ${issue.title}: ${issue.description}`).join("\n");
|
|
141
|
+
export const programFixAsk = (name, relativePath, issues) => [
|
|
142
|
+
`The spec pack for ONE program did not clear its quality gate: ${name} (source: ${relativePath}).`,
|
|
143
|
+
`Fix these findings by editing ONLY this program's files — ${ModDir}/specs/${name}.md,`,
|
|
144
|
+
`${ModDir}/features/${name.toLowerCase()}.feature, ${ModDir}/traceability/${name}.md,`,
|
|
145
|
+
`${ModDir}/mapping/${name}.md — against the source at ${relativePath}. Then stop:`,
|
|
146
|
+
issueLines(issues)
|
|
147
|
+
].join("\n");
|
|
148
|
+
export const globalFixAsk = (issues) => [
|
|
149
|
+
"The spec pack did not clear its estate-wide quality gate. Fix these findings by editing the",
|
|
150
|
+
`per-program files under ${ModDir}/ (specs/, features/, traceability/<PROGRAM>.md,`,
|
|
151
|
+
`mapping/<PROGRAM>.md). ${ModDir}/traceability.md and ${ModDir}/mapping.md are REGENERATED`,
|
|
152
|
+
"from the fragments — do not edit them directly. Fix the findings in place, then stop:",
|
|
153
|
+
issueLines(issues)
|
|
154
|
+
].join("\n");
|
|
155
|
+
/** The spec pack README; `notes` records what a refinement changed after the gate passed. */
|
|
156
|
+
export const readmeFor = (pack, verdict, notes = []) => [
|
|
157
|
+
`# Modernization spec pack — ${pack.name}`,
|
|
158
|
+
"",
|
|
159
|
+
`Extracted by the modernize-extract flow. Gate verdict: ${verdict}.`,
|
|
160
|
+
"",
|
|
161
|
+
"- specs/ — behavioural specs, one per program",
|
|
162
|
+
"- features/ — BDD acceptance scenarios",
|
|
163
|
+
"- traceability.md — source-unit → spec coverage matrix (generated from traceability/)",
|
|
164
|
+
"- mapping.md — data & interface mapping (generated from mapping/)",
|
|
165
|
+
"- rules.txt — every coverage unit, one per line (the rule universe verification reports against)",
|
|
166
|
+
"- plan.md — proposed implementation tasks",
|
|
167
|
+
"- decisions.md / domains.md — the refinement overlays, when modernize-refine has run (ADR 0015)",
|
|
168
|
+
...(notes.length === 0
|
|
169
|
+
? []
|
|
170
|
+
: ["", "Refined after the gate passed:", ...notes.map((n) => `- ${n}`)]),
|
|
171
|
+
"",
|
|
172
|
+
"Review everything, then flip the marker below and run the seed phase."
|
|
173
|
+
].join("\n");
|
|
174
|
+
/**
|
|
175
|
+
* Judging is resumable per program: the verdict persists under
|
|
176
|
+
* `gate/<NAME>.json`, fingerprinted over the source, spec, feature, and
|
|
177
|
+
* rubric it judged. Unchanged content reuses the stored verdict with NO
|
|
178
|
+
* model call, so a crash or quota death re-judges only what changed.
|
|
179
|
+
* Delete `gate/` to force a full re-judge. `extraRubric` (a deepen focus)
|
|
180
|
+
* joins the rubric and therefore the fingerprint.
|
|
181
|
+
*/
|
|
182
|
+
export const makeProgramJudge = (deps) => {
|
|
183
|
+
const { context, files, pack, modDirAbs, workDir, limit } = deps;
|
|
184
|
+
const packJudge = judge(context.reasoning, pack.judgeDimensions);
|
|
185
|
+
// The shared Context ladder: an oversized prompt retries at half, then
|
|
186
|
+
// quarter budget (repeating it identically cannot succeed), and every
|
|
187
|
+
// cap or shrink is recorded and published.
|
|
188
|
+
const judgeWithShrink = (name, spec, feature, source) => withShrink(`judge[${name}]`, (cap) => Effect.gen(function* () {
|
|
189
|
+
const response = yield* capped(`spec[${name}]`, `${spec}\n\n${feature}`, cap);
|
|
190
|
+
const source_ = yield* capped(`source[${name}]`, source, cap);
|
|
191
|
+
return yield* packJudge
|
|
192
|
+
.evaluate(Sample.make({ response, context: source_, query: context.userPrompt }))
|
|
193
|
+
.pipe(Effect.mapError(FlowLlmError.from));
|
|
194
|
+
}), { start: limit }).pipe(Effect.provideService(FlowEvents, context.events));
|
|
195
|
+
return (unit, extraRubric) => Effect.gen(function* () {
|
|
196
|
+
const spec = (yield* files.read(join(modDirAbs, "specs", `${unit.name}.md`))) ?? "";
|
|
197
|
+
const feature = (yield* files.read(join(modDirAbs, "features", `${unit.name.toLowerCase()}.feature`))) ?? "";
|
|
198
|
+
const source = (yield* files.read(join(workDir, unit.sourcePath))) ?? "";
|
|
199
|
+
const rubric = [
|
|
200
|
+
...pack.judgeDimensions.map((dimension) => `${dimension.name} (0..${dimension.maxScore}): ${dimension.rubric}`),
|
|
201
|
+
...(extraRubric === undefined ? [] : [extraRubric])
|
|
202
|
+
].join("\n");
|
|
203
|
+
return yield* cachedReview(files, join(modDirAbs, "gate", `${unit.name}.json`), reviewFingerprint(source, spec, feature, rubric), context.events.publish(Info.make({ message: `judging ${unit.name}` })).pipe(Effect.andThen(judgeWithShrink(unit.name, spec, feature, source)), Effect.map((scored) => judgeIssues(pack, scored, unit.name))));
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* One bounded fix turn: the coder edits the pack against `ask`, then the
|
|
208
|
+
* working tree is committed. A wedged agent that trips its turn limit
|
|
209
|
+
* mid-fix still wrote something; the caller re-evaluates what landed.
|
|
210
|
+
*/
|
|
211
|
+
export const fixTurn = (context, system, ask, commitMessage) => Effect.gen(function* () {
|
|
212
|
+
const chat = yield* makeChat(context.coder, { system });
|
|
213
|
+
yield* chat.ask(ask).pipe(Effect.asVoid, Effect.catchIf((error) => error._tag === "Llm" && error.cause?._tag === "TurnLimitError", () => context.events.publish(Info.make({
|
|
214
|
+
message: "turn limit hit during a fix turn — re-evaluating what was written"
|
|
215
|
+
}))));
|
|
216
|
+
yield* context.git.commitAll(commitMessage).pipe(Effect.asVoid);
|
|
217
|
+
});
|
|
@@ -31,33 +31,19 @@ import { join } from "node:path";
|
|
|
31
31
|
import * as Effect from "effect/Effect";
|
|
32
32
|
import * as Ref from "effect/Ref";
|
|
33
33
|
import * as Semaphore from "effect/Semaphore";
|
|
34
|
-
import {
|
|
35
|
-
import { judge } from "@llm4ts/core/eval/Judge";
|
|
36
|
-
import { budget, capped, withShrink } from "@llm4ts/flow/Context";
|
|
34
|
+
import { budget, capped } from "@llm4ts/flow/Context";
|
|
37
35
|
import { structuredAndPublish } from "@llm4ts/flow/Flow";
|
|
38
36
|
import { FlowEvents } from "@llm4ts/flow/FlowEvents";
|
|
39
|
-
import { FlowAborted,
|
|
37
|
+
import { FlowAborted, Info, ReviewResult, asReadOnly, coderFromEnv, defaultPlanInstructions, loadKitPatternCards, makeNodeWorkspace, mergeReviewResults, nodePlainFileStore, openPack, planFrom, resolveFlowInput, runFlowMain, runNode, stage, withTurnLimit } from "@llm4ts/runner";
|
|
40
38
|
import { loadPatternCards, matchingPatternCards } from "@llm4ts/flow/Patterns";
|
|
41
39
|
import { legacySourceWorkspaceLimits, workspaceLimitsFromEnv } from "@llm4ts/flow/Workspace";
|
|
42
40
|
import { ReviewIssue } from "@llm4ts/flow/Review";
|
|
43
|
-
import { cachedReview } from "@llm4ts/flow/ReviewCache";
|
|
44
41
|
import { coverageReport, coverageUnits, features, matchingFiles, specSchemaIssues } from "@llm4ts/flow/SpecChecks";
|
|
45
42
|
import { SurveyGraph, closureFor, surveyGraph } from "@llm4ts/flow/Survey";
|
|
46
43
|
import { withDraftApproval, requireApproval } from "@llm4ts/flow/Approval";
|
|
47
44
|
import { ProgramArtifacts, ProgramUnit, extractProgramsResumably, programArtifactPaths } from "@llm4ts/flow/Artifacts";
|
|
48
|
-
|
|
45
|
+
import { ModDir, analystSystem, analystTurns, fixTurn, globalFixAsk, judgeIssueProgram, makeProgramJudge, maxClosureFiles, positiveEnvInt, programArtifactsJsonSchema, programAsk, programFixAsk, programName, readmeFor, wavePrograms } from "./lib/modernize-extract.js";
|
|
49
46
|
const MaxRounds = 3;
|
|
50
|
-
const positiveEnvInt = (name, fallback) => {
|
|
51
|
-
const raw = Number.parseInt(process.env[name] ?? "", 10);
|
|
52
|
-
return Number.isFinite(raw) && raw > 0 ? raw : fallback;
|
|
53
|
-
};
|
|
54
|
-
// Per-program turn budget — bounds a wedged agent, generous for real work.
|
|
55
|
-
const analystTurns = () => positiveEnvInt("LLM4TS_ANALYST_TURNS", 48);
|
|
56
|
-
/**
|
|
57
|
-
* Max files named in one program's include closure. A program pulling more
|
|
58
|
-
* than this gets a bounded, visible subset rather than an unbounded read.
|
|
59
|
-
*/
|
|
60
|
-
const maxClosureFiles = () => positiveEnvInt("LLM4TS_MAX_CLOSURE_FILES", 40);
|
|
61
47
|
/**
|
|
62
48
|
* Programs extracted (and judged) at once. The programs of a wave are
|
|
63
49
|
* independent — each analyst reads its source and resolved closure, writes
|
|
@@ -66,117 +52,6 @@ const maxClosureFiles = () => positiveEnvInt("LLM4TS_MAX_CLOSURE_FILES", 40);
|
|
|
66
52
|
* narration; the bound is about the coder seat's quota, not correctness.
|
|
67
53
|
*/
|
|
68
54
|
const extractConcurrency = () => positiveEnvInt("LLM4TS_EXTRACT_CONCURRENCY", 1);
|
|
69
|
-
/** `cobol/ACCTXFR.cbl` → `ACCTXFR`: the program name keying every per-program artifact. */
|
|
70
|
-
const programName = (relativePath) => {
|
|
71
|
-
const base = relativePath.slice(relativePath.lastIndexOf("/") + 1);
|
|
72
|
-
const dot = base.lastIndexOf(".");
|
|
73
|
-
return dot > 0 ? base.slice(0, dot) : base;
|
|
74
|
-
};
|
|
75
|
-
/** The `- PROG` entries of `## Wave: <name>` in the survey's wave plan. */
|
|
76
|
-
const wavePrograms = (planText, wave) => {
|
|
77
|
-
const lines = planText.split(/\r?\n/);
|
|
78
|
-
const start = lines.findIndex((line) => line.trim() === `## Wave: ${wave}`);
|
|
79
|
-
if (start < 0) {
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
82
|
-
const section = [];
|
|
83
|
-
for (const line of lines.slice(start + 1)) {
|
|
84
|
-
if (line.trim().startsWith("## ")) {
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
if (line.trim().startsWith("- ")) {
|
|
88
|
-
section.push(line.trim().slice(2).trim());
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return section;
|
|
92
|
-
};
|
|
93
|
-
const programArtifactsJsonSchema = {
|
|
94
|
-
type: "object",
|
|
95
|
-
properties: {
|
|
96
|
-
spec: { type: "string" },
|
|
97
|
-
feature: { type: "string" },
|
|
98
|
-
traceability: { type: "string" },
|
|
99
|
-
mapping: { type: "string" }
|
|
100
|
-
},
|
|
101
|
-
required: ["spec", "feature", "traceability", "mapping"]
|
|
102
|
-
};
|
|
103
|
-
// The analyst gets a deterministically resolved include closure, not an open
|
|
104
|
-
// "read anything it references" instruction: told to chase references itself,
|
|
105
|
-
// the coding agent pulls files into its own context inside a single turn —
|
|
106
|
-
// which is how extract blew a 1M-token window while its own cap sat untouched.
|
|
107
|
-
const programAsk = (pack, relativePath, closure) => [
|
|
108
|
-
`Extract the behavioural spec for ONE source unit of this repository: ${relativePath}`,
|
|
109
|
-
"",
|
|
110
|
-
...(closure.length === 0
|
|
111
|
-
? [`Read ${relativePath}. It has no resolved dependencies.`]
|
|
112
|
-
: [
|
|
113
|
-
`Read ${relativePath} and EXACTLY these resolved dependencies — do not go looking for others:`,
|
|
114
|
-
...closure.map((file) => `- ${file}`)
|
|
115
|
-
]),
|
|
116
|
-
`Spec ONLY ${relativePath} and do not modify legacy sources.`,
|
|
117
|
-
"",
|
|
118
|
-
'Respond only with JSON: {"spec":"…","feature":"…","traceability":"…","mapping":"…"} where:',
|
|
119
|
-
"",
|
|
120
|
-
`- "spec" — the behavioural spec for ${relativePath}, as Markdown.`,
|
|
121
|
-
pack.prompt("spec") ?? "",
|
|
122
|
-
"",
|
|
123
|
-
`- "feature" — BDD scenarios encoding that spec, as a well-formed Gherkin .feature file`,
|
|
124
|
-
" (Feature: header, Scenario: blocks, Given/When/Then steps).",
|
|
125
|
-
pack.prompt("bdd") ?? "",
|
|
126
|
-
"",
|
|
127
|
-
`- "traceability" — EVERY source unit of ${relativePath} (each COBOL paragraph, each JCL`,
|
|
128
|
-
" step) on its own line, mapped to the spec rules/scenarios that cover it:",
|
|
129
|
-
" `<UNIT-NAME> — <refs>`. Unit names verbatim as they appear in the source.",
|
|
130
|
-
"",
|
|
131
|
-
`- "mapping" — data & interface mapping for ${relativePath}: tables/record layouts → target`,
|
|
132
|
-
" entities; files/screens/queues → target service contracts."
|
|
133
|
-
].join("\n");
|
|
134
|
-
/** Sub-bar judge dimensions as Critical review issues, titled with their program. */
|
|
135
|
-
const judgeIssues = (pack, scored, program) => {
|
|
136
|
-
const issues = scored.scores.flatMap((score) => {
|
|
137
|
-
const maxScore = pack.judgeDimensions.find((d) => d.name === score.name)?.maxScore ?? 2;
|
|
138
|
-
return score.score < maxScore
|
|
139
|
-
? [
|
|
140
|
-
ReviewIssue.make({
|
|
141
|
-
severity: "Critical",
|
|
142
|
-
title: `judge[${program}]: ${score.name} scored ${score.score}`,
|
|
143
|
-
description: score.reasoning
|
|
144
|
-
})
|
|
145
|
-
]
|
|
146
|
-
: [];
|
|
147
|
-
});
|
|
148
|
-
return ReviewResult.make({ issues, summary: `judge:${program}` });
|
|
149
|
-
};
|
|
150
|
-
const judgeIssueProgram = (issue) => /^judge\[([^\]]+)\]: /.exec(issue.title)?.[1];
|
|
151
|
-
const issueLines = (issues) => issues.map((issue) => `- [${issue.severity}] ${issue.title}: ${issue.description}`).join("\n");
|
|
152
|
-
const programFixAsk = (name, relativePath, issues) => [
|
|
153
|
-
`The spec pack for ONE program did not clear its quality gate: ${name} (source: ${relativePath}).`,
|
|
154
|
-
`Fix these findings by editing ONLY this program's files — ${ModDir}/specs/${name}.md,`,
|
|
155
|
-
`${ModDir}/features/${name.toLowerCase()}.feature, ${ModDir}/traceability/${name}.md,`,
|
|
156
|
-
`${ModDir}/mapping/${name}.md — against the source at ${relativePath}. Then stop:`,
|
|
157
|
-
issueLines(issues)
|
|
158
|
-
].join("\n");
|
|
159
|
-
const globalFixAsk = (issues) => [
|
|
160
|
-
"The spec pack did not clear its estate-wide quality gate. Fix these findings by editing the",
|
|
161
|
-
`per-program files under ${ModDir}/ (specs/, features/, traceability/<PROGRAM>.md,`,
|
|
162
|
-
`mapping/<PROGRAM>.md). ${ModDir}/traceability.md and ${ModDir}/mapping.md are REGENERATED`,
|
|
163
|
-
"from the fragments — do not edit them directly. Fix the findings in place, then stop:",
|
|
164
|
-
issueLines(issues)
|
|
165
|
-
].join("\n");
|
|
166
|
-
const readmeFor = (pack, verdict) => [
|
|
167
|
-
`# Modernization spec pack — ${pack.name}`,
|
|
168
|
-
"",
|
|
169
|
-
`Extracted by the modernize-extract flow. Gate verdict: ${verdict}.`,
|
|
170
|
-
"",
|
|
171
|
-
"- specs/ — behavioural specs, one per program",
|
|
172
|
-
"- features/ — BDD acceptance scenarios",
|
|
173
|
-
"- traceability.md — source-unit → spec coverage matrix (generated from traceability/)",
|
|
174
|
-
"- mapping.md — data & interface mapping (generated from mapping/)",
|
|
175
|
-
"- rules.txt — every coverage unit, one per line (the rule universe verification reports against)",
|
|
176
|
-
"- plan.md — proposed implementation tasks",
|
|
177
|
-
"",
|
|
178
|
-
"Review everything, then flip the marker below and run the seed phase."
|
|
179
|
-
].join("\n");
|
|
180
55
|
const program = Effect.gen(function* () {
|
|
181
56
|
const input = yield* resolveFlowInput("Extract the complete behavioural spec pack for this legacy estate");
|
|
182
57
|
const coder = withTurnLimit(coderFromEnv(process.env), analystTurns());
|
|
@@ -198,14 +73,7 @@ const program = Effect.gen(function* () {
|
|
|
198
73
|
}));
|
|
199
74
|
const pack = opened.pack;
|
|
200
75
|
yield* stage(context.events, "branch", context.git.checkoutOrCreate("modernize/spec-pack").pipe(Effect.asVoid));
|
|
201
|
-
const system =
|
|
202
|
-
pack.prompt("analysis"),
|
|
203
|
-
pack.lessons === undefined
|
|
204
|
-
? undefined
|
|
205
|
-
: `Lessons from previous modernization runs — apply them:\n${pack.lessons}`
|
|
206
|
-
]
|
|
207
|
-
.filter((part) => part !== undefined)
|
|
208
|
-
.join("\n\n");
|
|
76
|
+
const system = analystSystem(pack);
|
|
209
77
|
const all = yield* stage(context.events, "inventory", matchingFiles(repo, pack.programs ?? pack.sources ?? ".*", pack.exclude));
|
|
210
78
|
const wave = process.env.LLM4TS_WAVE?.trim();
|
|
211
79
|
const programs = wave === undefined || wave.length === 0
|
|
@@ -345,33 +213,14 @@ const program = Effect.gen(function* () {
|
|
|
345
213
|
yield* stage(context.events, "draft", rebuildIndexes.pipe(Effect.andThen(writeRules), Effect.andThen(context.git
|
|
346
214
|
.commitAll(`modernize(${pack.name}): spec pack draft (ungated)`)
|
|
347
215
|
.pipe(Effect.asVoid))));
|
|
348
|
-
const packJudge = judge(context.reasoning, pack.judgeDimensions);
|
|
349
216
|
const limit = budget();
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
.evaluate(Sample.make({ response, context: source_, query: input.prompt }))
|
|
358
|
-
.pipe(Effect.mapError(FlowLlmError.from));
|
|
359
|
-
}), { start: limit }).pipe(Effect.provideService(FlowEvents, context.events));
|
|
360
|
-
/**
|
|
361
|
-
* Judging is resumable per program: the verdict persists under
|
|
362
|
-
* `gate/<NAME>.json`, fingerprinted over the source, spec, feature, and
|
|
363
|
-
* rubric it judged. Unchanged content reuses the stored verdict with NO
|
|
364
|
-
* model call, so a crash or quota death re-judges only what changed.
|
|
365
|
-
* Delete `gate/` to force a full re-judge.
|
|
366
|
-
*/
|
|
367
|
-
const judgeProgram = (unit) => Effect.gen(function* () {
|
|
368
|
-
const spec = (yield* files.read(join(modDirAbs, "specs", `${unit.name}.md`))) ?? "";
|
|
369
|
-
const feature = (yield* files.read(join(modDirAbs, "features", `${unit.name.toLowerCase()}.feature`))) ?? "";
|
|
370
|
-
const source = (yield* files.read(join(input.workDir, unit.sourcePath))) ?? "";
|
|
371
|
-
const rubric = pack.judgeDimensions
|
|
372
|
-
.map((dimension) => `${dimension.name} (0..${dimension.maxScore}): ${dimension.rubric}`)
|
|
373
|
-
.join("\n");
|
|
374
|
-
return yield* cachedReview(files, join(modDirAbs, "gate", `${unit.name}.json`), reviewFingerprint(source, spec, feature, rubric), context.events.publish(Info.make({ message: `judging ${unit.name}` })).pipe(Effect.andThen(judgeWithShrink(unit.name, spec, feature, source)), Effect.map((scored) => judgeIssues(pack, scored, unit.name))));
|
|
217
|
+
const judgeProgram = makeProgramJudge({
|
|
218
|
+
context,
|
|
219
|
+
files,
|
|
220
|
+
pack,
|
|
221
|
+
modDirAbs,
|
|
222
|
+
workDir: input.workDir,
|
|
223
|
+
limit
|
|
375
224
|
});
|
|
376
225
|
const gateEvaluate = Effect.gen(function* () {
|
|
377
226
|
yield* rebuildIndexes;
|
|
@@ -426,7 +275,7 @@ const program = Effect.gen(function* () {
|
|
|
426
275
|
const wellFormed = yield* features(repo, join(ModDir, "features"));
|
|
427
276
|
// Verdicts are per program and cached per program, so they judge
|
|
428
277
|
// under the same bound as extraction; the merge is order-stable.
|
|
429
|
-
const judged = yield* Effect.forEach(units, judgeProgram, { concurrency });
|
|
278
|
+
const judged = yield* Effect.forEach(units, (unit) => judgeProgram(unit), { concurrency });
|
|
430
279
|
return mergeReviewResults([covered, wellFormed, docs, ...judged]);
|
|
431
280
|
});
|
|
432
281
|
// One bounded fix turn per sub-bar program (own commit) plus one residual
|
|
@@ -446,16 +295,7 @@ const program = Effect.gen(function* () {
|
|
|
446
295
|
global.push(issue);
|
|
447
296
|
}
|
|
448
297
|
}
|
|
449
|
-
const turn = (ask, commitMessage) =>
|
|
450
|
-
const chat = yield* makeChat(context.coder, { system });
|
|
451
|
-
yield* chat.ask(ask).pipe(Effect.asVoid,
|
|
452
|
-
// A wedged agent that trips its turn limit mid-fix still wrote
|
|
453
|
-
// something; re-evaluate what landed instead of failing.
|
|
454
|
-
Effect.catchIf((error) => error._tag === "Llm" && error.cause?._tag === "TurnLimitError", () => context.events.publish(Info.make({
|
|
455
|
-
message: "turn limit hit during a fix turn — re-evaluating what was written"
|
|
456
|
-
}))));
|
|
457
|
-
yield* context.git.commitAll(commitMessage).pipe(Effect.asVoid);
|
|
458
|
-
});
|
|
298
|
+
const turn = (ask, commitMessage) => fixTurn(context, system, ask, commitMessage);
|
|
459
299
|
for (const [name, issues] of [...scoped.entries()].sort()) {
|
|
460
300
|
yield* context.events.publish(Info.make({ message: `fixing ${name} — ${issues.length} finding(s)` }));
|
|
461
301
|
const relativePath = relOf.get(name);
|
|
@@ -30,6 +30,7 @@ import { Provenance, makeProvenanceStore } from "@llm4ts/flow/Provenance";
|
|
|
30
30
|
import { loadPatternCards, taggedPatternIds } from "@llm4ts/flow/Patterns";
|
|
31
31
|
import { ReviewIssue } from "@llm4ts/flow/Review";
|
|
32
32
|
import { checkWall, wallBreachMessage } from "@llm4ts/flow/Wall";
|
|
33
|
+
import { parseDecisions } from "@llm4ts/flow/Decisions";
|
|
33
34
|
const ModDir = "docs/modernization";
|
|
34
35
|
const judgeRounds = () => {
|
|
35
36
|
const raw = Number.parseInt(process.env.LLM4TS_JUDGE_ROUNDS ?? "", 10);
|
|
@@ -66,9 +67,32 @@ const specPrograms = Effect.fn("modernize-implement.specPrograms")(function* (ta
|
|
|
66
67
|
.map((path) => path.split("/").at(-1) ?? path)
|
|
67
68
|
.filter((name) => name.endsWith(".md"))
|
|
68
69
|
.map((name) => name.slice(0, -".md".length))
|
|
69
|
-
.filter((name) => !["traceability", "mapping", "README"].includes(name))
|
|
70
|
+
.filter((name) => !["traceability", "mapping", "README", "decisions", "domains"].includes(name))
|
|
70
71
|
.sort();
|
|
71
72
|
});
|
|
73
|
+
/**
|
|
74
|
+
* The decisions overlay seeded beside the specs (ADR 0015), rendered for a
|
|
75
|
+
* brief: what is out of scope and must not be built or scored, and which
|
|
76
|
+
* target capability a `provided` entry points at.
|
|
77
|
+
*/
|
|
78
|
+
const decisionsBrief = Effect.fn("modernize-implement.decisionsBrief")(function* (files, specsDirAbs) {
|
|
79
|
+
const text = yield* files.read(join(specsDirAbs, "decisions.md"));
|
|
80
|
+
if (text === undefined) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const decisions = yield* parseDecisions(text, "decisions.md");
|
|
84
|
+
const lines = [
|
|
85
|
+
...decisions.programs.map((entry) => `- ${entry.program} (whole program): ${entry.disposition} — ${entry.reason}` +
|
|
86
|
+
(entry.pointer === undefined ? "" : ` [provided by ${entry.pointer}]`)),
|
|
87
|
+
...decisions.scenarios.map((entry) => `- ${entry.program} / ${entry.scenario}: ${entry.disposition} — ${entry.reason}` +
|
|
88
|
+
(entry.pointer === undefined ? "" : ` [provided by ${entry.pointer}]`))
|
|
89
|
+
];
|
|
90
|
+
return lines.length === 0
|
|
91
|
+
? undefined
|
|
92
|
+
: "Out of scope by decision (do not implement, do not test, do not score their absence; " +
|
|
93
|
+
"use the target capability a `provided` entry points at instead of rebuilding it):\n" +
|
|
94
|
+
lines.join("\n");
|
|
95
|
+
});
|
|
72
96
|
/**
|
|
73
97
|
* One bounded estate-wide pass: the traceability index plus the changed-file
|
|
74
98
|
* NAMES (never contents). Per-program judging cannot see cross-program
|
|
@@ -175,8 +199,10 @@ const program = Effect.gen(function* () {
|
|
|
175
199
|
];
|
|
176
200
|
const cited = new Set(taggedPatternIds(specText));
|
|
177
201
|
const playbook = cards.filter((card) => cited.has(card.id));
|
|
202
|
+
const scope = yield* decisionsBrief(files, join(input.workDir, pack.specsDir));
|
|
178
203
|
const system = [
|
|
179
204
|
pack.prompt("implement"),
|
|
205
|
+
scope,
|
|
180
206
|
pack.lessons === undefined
|
|
181
207
|
? undefined
|
|
182
208
|
: `Lessons from previous modernization runs — apply them:\n${pack.lessons}`,
|
|
@@ -265,7 +291,7 @@ const program = Effect.gen(function* () {
|
|
|
265
291
|
programs,
|
|
266
292
|
specFor: (program) => files
|
|
267
293
|
.read(join(specsDirAbs, `${program}.md`))
|
|
268
|
-
.pipe(Effect.map((text) => text ?? "")),
|
|
294
|
+
.pipe(Effect.map((text) => scope === undefined ? (text ?? "") : `${text ?? ""}\n\n${scope}`)),
|
|
269
295
|
query: input.prompt,
|
|
270
296
|
fingerprint: reviewFingerprint
|
|
271
297
|
});
|
|
@@ -32,7 +32,9 @@ const phasePrompts = [
|
|
|
32
32
|
["plan", "extract"],
|
|
33
33
|
["implement", "implement"],
|
|
34
34
|
["review", "review"],
|
|
35
|
-
["vectors", "verify"]
|
|
35
|
+
["vectors", "verify"],
|
|
36
|
+
["refine-propose", "refine"],
|
|
37
|
+
["consolidate", "refine"]
|
|
36
38
|
];
|
|
37
39
|
const sampleSize = 5;
|
|
38
40
|
const sample = (units) => units.length === 0
|
|
@@ -103,6 +105,10 @@ const program = Effect.gen(function* () {
|
|
|
103
105
|
if (pack.lenses.length === 0) {
|
|
104
106
|
warnings.push("no reviewers/*.md sidecar — review runs without pack lenses");
|
|
105
107
|
}
|
|
108
|
+
yield* say(pack.consolidate === undefined
|
|
109
|
+
? "consolidate: (none — refine treats every program as its own domain feature)"
|
|
110
|
+
: `consolidate: cluster on ${pack.consolidate.cluster.join(", ") || "(nothing)"} · ` +
|
|
111
|
+
`context via ${pack.consolidate.context.join(", ") || "(nothing)"}`);
|
|
106
112
|
yield* say(`lessons: ${pack.lessons === undefined ? "(none yet)" : "present"}`);
|
|
107
113
|
}));
|
|
108
114
|
yield* stage(context.events, "estate", Effect.gen(function* () {
|