@llm4ts/shell 2.1.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.
Files changed (31) hide show
  1. package/dist/Cli.d.ts +1 -1
  2. package/dist/Cli.d.ts.map +1 -1
  3. package/dist/Cli.js +32 -0
  4. package/dist/Cli.js.map +1 -1
  5. package/dist/Refine.d.ts +44 -0
  6. package/dist/Refine.d.ts.map +1 -0
  7. package/dist/Refine.js +362 -0
  8. package/dist/Refine.js.map +1 -0
  9. package/flows/lib/modernize-extract.js +217 -0
  10. package/flows/modernize-extract.js +13 -173
  11. package/flows/modernize-implement.js +28 -2
  12. package/flows/modernize-pack-check.js +7 -1
  13. package/flows/modernize-refine.js +389 -0
  14. package/flows/modernize-seed.js +50 -2
  15. package/flows/modernize-verify.js +12 -5
  16. package/kits/j2ee-nextjs/README.md +5 -4
  17. package/kits/j2ee-nextjs/fixtures/demo-bank/RUNBOOK.md +43 -0
  18. package/kits/j2ee-nextjs/fixtures/demo-bank/legacy-j2ee/PAGES.md +26 -0
  19. package/kits/j2ee-nextjs/flows/convert-all.js +56 -20
  20. package/kits/j2ee-nextjs/flows/convert-feature.js +48 -0
  21. package/kits/j2ee-nextjs/flows/lib/convert.js +292 -40
  22. package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/pack.md +16 -0
  23. package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/prompts/consolidate.md +10 -0
  24. package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/prompts/plan.md +24 -16
  25. package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/prompts/refine-propose.md +16 -0
  26. package/kits/mainframe-java/packs/cobol-springboot/pack.md +5 -0
  27. package/kits/mainframe-java/packs/cobol-springboot/prompts/consolidate.md +8 -0
  28. package/kits/mainframe-java/packs/cobol-springboot/prompts/refine-propose.md +10 -0
  29. package/package.json +5 -4
  30. package/src/Cli.ts +57 -0
  31. 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 { Sample } from "@llm4ts/core/eval/Eval";
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, FlowLlmError, Info, ReviewResult, asReadOnly, coderFromEnv, defaultPlanInstructions, loadKitPatternCards, makeChat, makeNodeWorkspace, mergeReviewResults, nodePlainFileStore, openPack, planFrom, resolveFlowInput, reviewFingerprint, runFlowMain, runNode, stage, withTurnLimit } from "@llm4ts/runner";
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
- const ModDir = "docs/modernization";
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
- // The shared Context ladder: an oversized prompt retries at half, then
351
- // quarter budget (repeating it identically cannot succeed), and every
352
- // cap or shrink is recorded and published.
353
- const judgeWithShrink = (name, spec, feature, source) => withShrink(`judge[${name}]`, (cap) => Effect.gen(function* () {
354
- const response = yield* capped(`spec[${name}]`, `${spec}\n\n${feature}`, cap);
355
- const source_ = yield* capped(`source[${name}]`, source, cap);
356
- return yield* packJudge
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) => Effect.gen(function* () {
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* () {