@llm4ts/shell 2.36.0 → 2.38.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 +18 -1
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +89 -0
- package/dist/Cli.js.map +1 -1
- package/flows/lib/modernize-extract.js +9 -0
- package/flows/modernize-extract.js +17 -8
- package/flows/modernize-pack-check.js +65 -5
- package/flows/modernize-refine.js +4 -5
- package/flows/modernize-survey.js +80 -87
- package/kits/j2ee-nextjs/flows/lib/convert.js +4 -3
- package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/pack.md +38 -5
- package/kits/mainframe-java/packs/cobol-kafka/pack.md +24 -0
- package/kits/mainframe-java/packs/cobol-springboot/pack.md +24 -0
- package/kits/port/README.md +5 -4
- package/kits/port/packs/cobol-springboot-port/pack.md +48 -0
- package/kits/port/packs/cobol-springboot-port/patterns/pitfalls-cobol-springboot-port.md +36 -0
- package/kits/port/packs/cobol-springboot-port/prompts/porting.md +92 -0
- package/kits/port/packs/cobol-springboot-port/reviewers/spring-fidelity.md +15 -0
- package/package.json +4 -4
- package/src/Cli.ts +183 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Legacy modernization phase 0: inventory the estate, refine its dependency graph, triage, and propose a human-approved wave plan.
|
|
2
2
|
//
|
|
3
3
|
// Runs rooted at the LEGACY repository (`--repo <legacy>`). Deterministic first:
|
|
4
|
-
// the graph comes from the pack's `sources:` regex and `## Survey:`
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// the graph comes from the pack's `sources:` regex and its `## Survey:` /
|
|
5
|
+
// `## Node:` / `## Edge:` / `## Join:` rules (no LLM; ADR 0030), cached under
|
|
6
|
+
// `.llm4ts/graph/<pack>.json` by file hash. What the scanner could not resolve
|
|
7
|
+
// becomes a bounded worklist for a read-only reasoning pass (attr fills and
|
|
8
|
+
// edges between EXISTING nodes, each verified against the cited source line),
|
|
9
|
+
// and finally triage classifies every unit rewrite | retire | wrap and slices
|
|
10
|
+
// the rewrites into dependency-coherent waves. The wave plan lands
|
|
9
11
|
// at docs/modernization/wave-plan.md with an UNCHECKED approval marker — a
|
|
10
12
|
// human reviews, flips `- [x] Approved`, and only then does modernize-extract
|
|
11
13
|
// start (LLM4TS_WAVE=<name> scopes extraction to one wave).
|
|
@@ -27,9 +29,13 @@ import { FlowAborted, Info, asReadOnly, coderFromEnv, makeNodeWorkspace, nodePla
|
|
|
27
29
|
import { FlowEvents } from "@llm4ts/flow/FlowEvents";
|
|
28
30
|
import { structuredAndPublish } from "@llm4ts/flow/Flow";
|
|
29
31
|
import { loadBenchRecords, renderBenchReport } from "@llm4ts/flow/BenchReport";
|
|
30
|
-
import {
|
|
32
|
+
import { nodeKind, renderSurveyGraphJson, renderSurveyInventory, surveyTriagePrompt } from "@llm4ts/flow/Survey";
|
|
31
33
|
import { discoveryOverflowAdvice, legacySourceWorkspaceLimits, workspaceLimitsFromEnv } from "@llm4ts/flow/Workspace";
|
|
32
34
|
import { withDraftApproval } from "@llm4ts/flow/Approval";
|
|
35
|
+
import { freshGraph, graphCachePath, updateGraphCache } from "@llm4ts/flow/GraphCache";
|
|
36
|
+
import { WorklistAnswer, mergeWorklist, renderWorklistReport, verifyWorklistAnswer, worklistAnswerJsonSchema, worklistBatches, worklistOf, worklistPrompt } from "@llm4ts/flow/GraphWorklist";
|
|
37
|
+
import { wholeView } from "@llm4ts/flow/GraphQuery";
|
|
38
|
+
import { renderClusterDiagrams, renderEntryPaths, renderGraphDot } from "@llm4ts/flow/GraphRender";
|
|
33
39
|
const ModDir = "docs/modernization";
|
|
34
40
|
class NodeTriage extends Schema.Class("NodeTriage")({
|
|
35
41
|
name: Schema.String,
|
|
@@ -80,63 +86,21 @@ const surveyOutcomeJsonSchema = {
|
|
|
80
86
|
},
|
|
81
87
|
required: ["triage", "waves", "notes"]
|
|
82
88
|
};
|
|
83
|
-
class RefinedEdge extends Schema.Class("RefinedEdge")({
|
|
84
|
-
from: Schema.String,
|
|
85
|
-
to: Schema.String,
|
|
86
|
-
kind: Schema.String,
|
|
87
|
-
evidence: Schema.String
|
|
88
|
-
}) {
|
|
89
|
-
}
|
|
90
|
-
class GraphRefinement extends Schema.Class("GraphRefinement")({
|
|
91
|
-
edges: Schema.Array(RefinedEdge),
|
|
92
|
-
notes: Schema.Array(Schema.String)
|
|
93
|
-
}) {
|
|
94
|
-
}
|
|
95
|
-
const graphRefinementJsonSchema = {
|
|
96
|
-
type: "object",
|
|
97
|
-
properties: {
|
|
98
|
-
edges: {
|
|
99
|
-
type: "array",
|
|
100
|
-
items: {
|
|
101
|
-
type: "object",
|
|
102
|
-
properties: {
|
|
103
|
-
from: { type: "string" },
|
|
104
|
-
to: { type: "string" },
|
|
105
|
-
kind: { type: "string" },
|
|
106
|
-
evidence: { type: "string" }
|
|
107
|
-
},
|
|
108
|
-
required: ["from", "to", "kind", "evidence"]
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
notes: { type: "array", items: { type: "string" } }
|
|
112
|
-
},
|
|
113
|
-
required: ["edges", "notes"]
|
|
114
|
-
};
|
|
115
89
|
const tableCell = (text) => text.split(/\r?\n/).join(" ").replaceAll("|", "\\|");
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
`${kept.length} of ${refinement.edges.length} proposed edge(s) merged into \`graph.json\` (kind prefixed \`llm-\`).`,
|
|
131
|
-
"Each carries the source statement that establishes it — verify before trusting a wave built on it.",
|
|
132
|
-
"",
|
|
133
|
-
"| From | To | Kind | Status | Evidence |",
|
|
134
|
-
"| ---- | -- | ---- | ------ | -------- |",
|
|
135
|
-
...rows,
|
|
136
|
-
"",
|
|
137
|
-
...notes
|
|
138
|
-
].join("\n") + "\n");
|
|
139
|
-
};
|
|
90
|
+
/** inventory.md (units, cluster diagrams, entry paths), graph.json and graph.dot. */
|
|
91
|
+
const writeGraphArtifacts = (repo, pack, graph) => Effect.gen(function* () {
|
|
92
|
+
const programs = graph.nodes
|
|
93
|
+
.filter((node) => nodeKind(node) === "file")
|
|
94
|
+
.map((node) => node.name);
|
|
95
|
+
const inventory = [
|
|
96
|
+
renderSurveyInventory(graph),
|
|
97
|
+
renderClusterDiagrams(graph, pack.consolidate ?? { cluster: [], context: [] }, programs),
|
|
98
|
+
renderEntryPaths(graph)
|
|
99
|
+
].join("\n");
|
|
100
|
+
yield* repo.write(join(ModDir, "inventory.md"), inventory);
|
|
101
|
+
yield* repo.write(join(ModDir, "graph.json"), renderSurveyGraphJson(graph));
|
|
102
|
+
yield* repo.write(join(ModDir, "graph.dot"), renderGraphDot(wholeView(graph), graph));
|
|
103
|
+
});
|
|
140
104
|
const renderWavePlan = (outcome, graph, projection) => {
|
|
141
105
|
const triageRows = [...outcome.triage]
|
|
142
106
|
.sort((left, right) => left.name.localeCompare(right.name))
|
|
@@ -182,16 +146,15 @@ const program = Effect.gen(function* () {
|
|
|
182
146
|
launchDir: input.workspace,
|
|
183
147
|
flowDir: import.meta.dirname
|
|
184
148
|
}));
|
|
185
|
-
if (pack.
|
|
149
|
+
if (pack.graph.edges.length === 0 && pack.graph.joins.length === 0) {
|
|
186
150
|
return yield* FlowAborted.make({
|
|
187
|
-
message: `pack '${pack.name}' has no '## Survey:' sections — add the
|
|
188
|
-
"
|
|
151
|
+
message: `pack '${pack.name}' has no '## Survey:', '## Edge:' or '## Join:' sections — add the ` +
|
|
152
|
+
"dependency-edge rules (CALL/COPY/EXEC PGM, ajax → servlet…) the graph should be built from"
|
|
189
153
|
});
|
|
190
154
|
}
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}).pipe(
|
|
155
|
+
const cachePath = graphCachePath(input.workDir, pack.name);
|
|
156
|
+
const fresh = yield* stage(context.events, "graph", Effect.gen(function* () {
|
|
157
|
+
const built = yield* freshGraph(nodePlainFileStore, cachePath, pack, repo).pipe(
|
|
195
158
|
// The cap is a guard against runaway trees, not a verdict on the
|
|
196
159
|
// estate: name the three knobs instead of a bare limit number.
|
|
197
160
|
Effect.catchTag("WorkspaceLimit", (error) => Effect.fail(error.operation === "discovery results"
|
|
@@ -199,33 +162,63 @@ const program = Effect.gen(function* () {
|
|
|
199
162
|
message: `${input.workDir}: ${discoveryOverflowAdvice(limits)}`
|
|
200
163
|
})
|
|
201
164
|
: error)));
|
|
202
|
-
yield* repo
|
|
203
|
-
yield*
|
|
204
|
-
|
|
165
|
+
yield* writeGraphArtifacts(repo, pack, built.graph);
|
|
166
|
+
yield* context.events.publish(Info.make({
|
|
167
|
+
message: `${built.graph.nodes.length} node(s), ${built.graph.edges.length} edge(s), ` +
|
|
168
|
+
`${built.graph.unresolved.length} unresolved` +
|
|
169
|
+
(built.reused ? " (graph cache reused)" : "")
|
|
170
|
+
}));
|
|
205
171
|
return built;
|
|
206
172
|
}));
|
|
173
|
+
const graph = fresh.graph;
|
|
207
174
|
const refined = yield* stage(context.events, "graph refine", refineDisabled(process.env)
|
|
208
175
|
? context.events
|
|
209
176
|
.publish(Info.make({ message: "LLM4TS_GRAPH_REFINE=off — regex graph ships unrefined" }))
|
|
210
177
|
.pipe(Effect.as(graph))
|
|
211
178
|
: Effect.gen(function* () {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
179
|
+
const contents = new Map();
|
|
180
|
+
const load = (path) => contents.has(path)
|
|
181
|
+
? Effect.void
|
|
182
|
+
: repo.read(path).pipe(Effect.orElseSucceed(() => ""), Effect.map((text) => void contents.set(path, text)));
|
|
183
|
+
const read = (path) => contents.get(path);
|
|
184
|
+
for (const file of new Set(graph.unresolved.map((item) => item.file))) {
|
|
185
|
+
yield* load(file);
|
|
186
|
+
}
|
|
187
|
+
const items = worklistOf(graph, read, pack.graph.worklistMax, pack.graph);
|
|
188
|
+
if (items.length === 0) {
|
|
189
|
+
yield* context.events.publish(Info.make({ message: "nothing unresolved — no LLM pass needed" }));
|
|
190
|
+
return graph;
|
|
191
|
+
}
|
|
192
|
+
let merged = graph;
|
|
193
|
+
const accepted = [];
|
|
194
|
+
const notes = [];
|
|
195
|
+
for (const batch of worklistBatches(items, pack.graph.batchSize)) {
|
|
196
|
+
const answer = yield* withShrink("graph worklist", (cap) => Effect.gen(function* () {
|
|
197
|
+
const prompt = yield* capped("worklist", worklistPrompt(batch, pack.prompt("survey-refine")), cap);
|
|
198
|
+
return yield* structuredAndPublish(context.reasoning, context.events, prompt, WorklistAnswer, worklistAnswerJsonSchema);
|
|
199
|
+
})).pipe(Effect.provideService(FlowEvents, context.events));
|
|
200
|
+
for (const file of new Set([
|
|
201
|
+
...answer.edges.map((edge) => edge.evidence.file),
|
|
202
|
+
...answer.attrs.map((attr) => attr.evidence.file)
|
|
203
|
+
])) {
|
|
204
|
+
yield* load(file);
|
|
205
|
+
}
|
|
206
|
+
const verified = verifyWorklistAnswer(answer, merged, read);
|
|
207
|
+
merged = mergeWorklist(merged, verified, pack.graph);
|
|
208
|
+
accepted.push(verified);
|
|
209
|
+
notes.push(...answer.notes);
|
|
225
210
|
}
|
|
211
|
+
const all = {
|
|
212
|
+
edges: accepted.flatMap((verified) => verified.edges),
|
|
213
|
+
fills: accepted.flatMap((verified) => verified.fills),
|
|
214
|
+
dropped: accepted.flatMap((verified) => verified.dropped)
|
|
215
|
+
};
|
|
216
|
+
yield* repo.write(join(ModDir, "graph-refine.md"), renderWorklistReport(items, all, notes));
|
|
217
|
+
yield* updateGraphCache(nodePlainFileStore, cachePath, fresh.cache, merged);
|
|
218
|
+
yield* writeGraphArtifacts(repo, pack, merged);
|
|
226
219
|
yield* context.events.publish(Info.make({
|
|
227
|
-
message: `${
|
|
228
|
-
|
|
220
|
+
message: `${all.edges.length} edge(s) and ${all.fills.length} fill(s) accepted from ` +
|
|
221
|
+
`${items.length} unresolved item(s); ${all.dropped.length} dropped`
|
|
229
222
|
}));
|
|
230
223
|
return merged;
|
|
231
224
|
}));
|
|
@@ -19,7 +19,8 @@ import { openApiFor, openApiForFeature, parsePageSpec, renderPageSpec } from "@l
|
|
|
19
19
|
import { loadPatternCards, matchingPatternCards } from "@llm4ts/flow/Patterns";
|
|
20
20
|
import { Task } from "@llm4ts/flow/Plan";
|
|
21
21
|
import { judgeAllPrograms } from "@llm4ts/flow/ProgramJudge";
|
|
22
|
-
import { closureFor
|
|
22
|
+
import { closureFor } from "@llm4ts/flow/Survey";
|
|
23
|
+
import { freshGraph, graphCachePath } from "@llm4ts/flow/GraphCache";
|
|
23
24
|
import { estimatedUsageOptionsFromEnv, makeEstimatedUsageMeter } from "@llm4ts/flow/EstimatedUsage";
|
|
24
25
|
import { legacySourceWorkspaceLimits, workspaceLimitsFromEnv } from "@llm4ts/flow/Workspace";
|
|
25
26
|
export const positiveEnvInt = (environment, name, fallback) => {
|
|
@@ -213,7 +214,7 @@ const legacyEvidence = Effect.fn("convert.legacyEvidence")(function* (deps, page
|
|
|
213
214
|
const source = sourcePath === undefined
|
|
214
215
|
? ""
|
|
215
216
|
: yield* deps.legacy.read(sourcePath).pipe(Effect.orElseSucceed(() => ""));
|
|
216
|
-
const graph = yield*
|
|
217
|
+
const graph = (yield* freshGraph(deps.files, graphCachePath(deps.legacyDir, deps.pack.name), deps.pack, deps.legacy)).graph;
|
|
217
218
|
const closure = closureFor(graph, page, positiveEnvInt(deps.environment, "LLM4TS_MAX_CLOSURE_FILES", 12));
|
|
218
219
|
const parts = [];
|
|
219
220
|
if (sourcePath !== undefined) {
|
|
@@ -480,7 +481,7 @@ export const convertFeature = Effect.fn("convert.feature")(function* (deps, feat
|
|
|
480
481
|
for (const page of pages) {
|
|
481
482
|
specs.set(page, yield* readSpec(deps, page));
|
|
482
483
|
}
|
|
483
|
-
const graph = yield*
|
|
484
|
+
const graph = (yield* freshGraph(deps.files, graphCachePath(deps.legacyDir, deps.pack.name), deps.pack, deps.legacy)).graph;
|
|
484
485
|
const order = navigationOrder({ ...feature, programs: pages }, graph, deps.pack.consolidate ?? { cluster: [], context: [] });
|
|
485
486
|
const contractPath = `contracts/${feature.id}.openapi.yaml`;
|
|
486
487
|
const contract = yield* openApiForFeature(feature, order.map((page) => specs.get(page)?.spec).filter((spec) => spec !== undefined));
|
|
@@ -49,15 +49,48 @@ unit: <jsp:include page="([^"]+)"
|
|
|
49
49
|
files: .*web\.xml
|
|
50
50
|
unit: <servlet-class>[a-z.]*\.([A-Za-z0-9]+)</servlet-class>
|
|
51
51
|
|
|
52
|
-
##
|
|
52
|
+
## Node: ajax-call
|
|
53
53
|
|
|
54
|
-
files: .*\.jsp
|
|
55
|
-
|
|
54
|
+
files: .*\.(jsp|js)
|
|
55
|
+
pattern: (?:["']?url["']?\s*:\s*|\$\.(?:get|post|getJSON)\(\s*)['"](?<name>[^'"]+)['"]
|
|
56
|
+
attrs: url=name
|
|
56
57
|
|
|
57
|
-
##
|
|
58
|
+
## Node: form
|
|
58
59
|
|
|
59
60
|
files: .*\.jsp
|
|
60
|
-
|
|
61
|
+
pattern: <(?:[a-z]+:)?[Ff][Oo][Rr][Mm]\b[^>]*\b[Aa][Cc][Tt][Ii][Oo][Nn]=["'](?<name>[^"']+)["']
|
|
62
|
+
attrs: url=name
|
|
63
|
+
|
|
64
|
+
## Node: servlet-mapping
|
|
65
|
+
|
|
66
|
+
files: .*web\.xml
|
|
67
|
+
pattern: <servlet-mapping>\s*<servlet-name>(?<name>[^<]+)</servlet-name>\s*<url-pattern>(?<url>[^<]+)</url-pattern>
|
|
68
|
+
descriptor: yes
|
|
69
|
+
|
|
70
|
+
## Node: servlet-decl
|
|
71
|
+
|
|
72
|
+
files: .*web\.xml
|
|
73
|
+
pattern: <servlet>(?:(?!</servlet>)[\s\S])*?<servlet-name>(?<name>[^<]+)</servlet-name>(?:(?!</servlet>)[\s\S])*?<servlet-class>(?:[a-z0-9_]+\.)*(?<class>[A-Za-z0-9_]+)</servlet-class>
|
|
74
|
+
descriptor: yes
|
|
75
|
+
anchor: class
|
|
76
|
+
|
|
77
|
+
## Join: jsp-ajax-target
|
|
78
|
+
|
|
79
|
+
from: ajax-call.url
|
|
80
|
+
to: servlet-mapping.url
|
|
81
|
+
match: url
|
|
82
|
+
|
|
83
|
+
## Join: jsp-form-action
|
|
84
|
+
|
|
85
|
+
from: form.url
|
|
86
|
+
to: servlet-mapping.url
|
|
87
|
+
match: url
|
|
88
|
+
|
|
89
|
+
## Join: servlet-wiring
|
|
90
|
+
|
|
91
|
+
from: servlet-mapping.name
|
|
92
|
+
to: servlet-decl.name
|
|
93
|
+
scope: file
|
|
61
94
|
|
|
62
95
|
## Consolidate
|
|
63
96
|
|
|
@@ -49,3 +49,27 @@ unit: ^ {6}[ ]*COPY +([A-Z0-9]+)
|
|
|
49
49
|
|
|
50
50
|
files: .*\.(jcl|JCL)
|
|
51
51
|
unit: EXEC +PGM=([A-Z0-9]+)
|
|
52
|
+
|
|
53
|
+
## Node: cobol-section
|
|
54
|
+
|
|
55
|
+
files: .*\.(cbl|CBL)
|
|
56
|
+
pattern: ^ {7}(?<name>[A-Z0-9][A-Z0-9-]*) +SECTION\.
|
|
57
|
+
|
|
58
|
+
## Node: cobol-paragraph
|
|
59
|
+
|
|
60
|
+
files: .*\.(cbl|CBL)
|
|
61
|
+
pattern: ^ {7}(?<name>\d{4}-[A-Z0-9-]+)\.
|
|
62
|
+
|
|
63
|
+
## Edge: performs
|
|
64
|
+
|
|
65
|
+
files: .*\.(cbl|CBL)
|
|
66
|
+
pattern: PERFORM +(?<to>\d{4}-[A-Z0-9-]+)(?: +THRU +(?<thru>\d{4}-[A-Z0-9-]+))?
|
|
67
|
+
from: cobol-paragraph
|
|
68
|
+
to: cobol-paragraph
|
|
69
|
+
|
|
70
|
+
## Edge: goes-to
|
|
71
|
+
|
|
72
|
+
files: .*\.(cbl|CBL)
|
|
73
|
+
pattern: GO +TO +(?<to>\d{4}-[A-Z0-9-]+)
|
|
74
|
+
from: cobol-paragraph
|
|
75
|
+
to: cobol-paragraph
|
|
@@ -50,6 +50,30 @@ unit: ^ {6}[ ]*COPY +([A-Z0-9]+)
|
|
|
50
50
|
files: .*\.(jcl|JCL)
|
|
51
51
|
unit: EXEC +PGM=([A-Z0-9]+)
|
|
52
52
|
|
|
53
|
+
## Node: cobol-section
|
|
54
|
+
|
|
55
|
+
files: .*\.(cbl|CBL)
|
|
56
|
+
pattern: ^ {7}(?<name>[A-Z0-9][A-Z0-9-]*) +SECTION\.
|
|
57
|
+
|
|
58
|
+
## Node: cobol-paragraph
|
|
59
|
+
|
|
60
|
+
files: .*\.(cbl|CBL)
|
|
61
|
+
pattern: ^ {7}(?<name>\d{4}-[A-Z0-9-]+)\.
|
|
62
|
+
|
|
63
|
+
## Edge: performs
|
|
64
|
+
|
|
65
|
+
files: .*\.(cbl|CBL)
|
|
66
|
+
pattern: PERFORM +(?<to>\d{4}-[A-Z0-9-]+)(?: +THRU +(?<thru>\d{4}-[A-Z0-9-]+))?
|
|
67
|
+
from: cobol-paragraph
|
|
68
|
+
to: cobol-paragraph
|
|
69
|
+
|
|
70
|
+
## Edge: goes-to
|
|
71
|
+
|
|
72
|
+
files: .*\.(cbl|CBL)
|
|
73
|
+
pattern: GO +TO +(?<to>\d{4}-[A-Z0-9-]+)
|
|
74
|
+
from: cobol-paragraph
|
|
75
|
+
to: cobol-paragraph
|
|
76
|
+
|
|
53
77
|
## Consolidate
|
|
54
78
|
|
|
55
79
|
- cluster: calls, exec-pgm
|
package/kits/port/README.md
CHANGED
|
@@ -26,9 +26,10 @@ another pair copies one of these and replaces the rulebook, the pitfall card,
|
|
|
26
26
|
`target:`, the diagnostics command, the ledger regex and the two test
|
|
27
27
|
commands.
|
|
28
28
|
|
|
29
|
-
| Pack
|
|
30
|
-
|
|
|
31
|
-
| `zig-rust`
|
|
32
|
-
| `scala-ts`
|
|
29
|
+
| Pack | Source → target | Diagnostics | Ledger units |
|
|
30
|
+
| ----------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------- |
|
|
31
|
+
| `zig-rust` | Zig → Rust | `cargo check --message-format=json` | pointer and slice fields |
|
|
32
|
+
| `scala-ts` | Scala 3 / ZIO 2 → TS / Effect 4 | `tsc --pretty false` | classes, objects, traits |
|
|
33
|
+
| `cobol-springboot-port` | COBOL → Java / Spring Boot 3 (file by file; the clean-room sibling is `mainframe-java/cobol-springboot`) | `mvn -q -B -DskipTests compile` (javac) | level-01 records, FDs, paragraphs |
|
|
33
34
|
|
|
34
35
|
`## Diagnostics` reads `json` (one object per line), `cargo` (`--message-format=json`), `tsc` (`--pretty false`) or `javac` (javac's and Maven's error lines, the Maven module as the unit).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Pack: cobol-springboot-port
|
|
2
|
+
|
|
3
|
+
source: cobol
|
|
4
|
+
sources: .*\.(cbl|cob|cpy|CBL|COB|CPY)$
|
|
5
|
+
exclude: (^|/)(target|build|\.git|node_modules)/
|
|
6
|
+
target: src/main/java/legacy/{{dir}}/{{base}}.java
|
|
7
|
+
comment: //
|
|
8
|
+
specs-dir: docs/port
|
|
9
|
+
features-dir: docs/port/features
|
|
10
|
+
|
|
11
|
+
## Gates
|
|
12
|
+
|
|
13
|
+
- compile: mvn -q -B -DskipTests compile
|
|
14
|
+
- test: mvn -q -B test
|
|
15
|
+
|
|
16
|
+
## Diagnostics
|
|
17
|
+
|
|
18
|
+
- command: mvn -q -B -DskipTests compile
|
|
19
|
+
- format: javac
|
|
20
|
+
|
|
21
|
+
## Ledger
|
|
22
|
+
|
|
23
|
+
- unit: ^\s{0,7}(?:01|77)\s+([A-Z][A-Z0-9-]*)|^\s{0,7}FD\s+([A-Z][A-Z0-9-]*)|^\s{0,7}([A-Z0-9][A-Z0-9-]*)\s*(?:SECTION)?\.\s*$
|
|
24
|
+
- classes: RECORD, FILE, PARAGRAPH, SQL, CICS, COPYBOOK, REPORT, UTIL, UNKNOWN
|
|
25
|
+
- question: What does this COBOL unit become in a Spring Boot port? RECORD (a level-01 or 77 data item → a Java record or class with BigDecimal for numerics), FILE (an FD / SELECT → a JPA entity with a Spring Data repository, or a reader for a flat file), PARAGRAPH (a paragraph or section → a private method on the program's @Service), SQL (a paragraph whose body is EXEC SQL → a repository query), CICS (EXEC CICS send/receive → a controller endpoint or a service boundary), COPYBOOK (a shared record → a class in the copybook package, ported once), REPORT (a print layout → a formatter), UTIL (a pure routine).
|
|
26
|
+
|
|
27
|
+
## Differential
|
|
28
|
+
|
|
29
|
+
- tests: ^tests/.*\.(txt|dat|json)$
|
|
30
|
+
- legacy: scripts/legacy-run.sh {{file}}
|
|
31
|
+
- target: scripts/target-run.sh {{file}}
|
|
32
|
+
- timeout: 120
|
|
33
|
+
|
|
34
|
+
## Audit
|
|
35
|
+
|
|
36
|
+
- dimensions: data division and record layouts, PIC clauses and numeric semantics, files and embedded SQL, control flow (PERFORM, GO TO, fall-through), copybooks and shared records, batch versus online (CICS), transaction boundaries, test idioms, what not to translate
|
|
37
|
+
|
|
38
|
+
## Review rules
|
|
39
|
+
|
|
40
|
+
A `double` or `float` holding money, a quantity or any `COMP-3` field is a
|
|
41
|
+
finding: numerics are `BigDecimal` with the PIC's scale, or `long` for
|
|
42
|
+
unsigned integer PICs. A `MOVE` whose truncation or padding the source relied
|
|
43
|
+
on must be written out, not assumed. `GO TO` is flattened into structured
|
|
44
|
+
control flow with a `// PORTED: GO TO` note, never a `while (true)` with a
|
|
45
|
+
state variable unless the source was a state machine. Every file and SQL
|
|
46
|
+
access sits behind a Spring Data repository or a dedicated reader; every
|
|
47
|
+
program that commits has one `@Transactional` boundary. A paragraph the
|
|
48
|
+
source has that the draft lacks is a finding.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: COBOL → Java/Spring Boot pitfalls — alike on the page, different at runtime
|
|
3
|
+
matches: .
|
|
4
|
+
tags: port, cobol, java, spring-boot
|
|
5
|
+
---
|
|
6
|
+
1. **Decimal scale.** `PIC S9(7)V99 COMP-3` has an implied point: `1234567.89`
|
|
7
|
+
is stored as nine digits. A `double` loses cents; a `BigDecimal` without
|
|
8
|
+
`setScale(2)` prints `1234567.9`.
|
|
9
|
+
2. **Truncation on MOVE.** Moving `PIC X(10)` into `PIC X(5)` keeps the left
|
|
10
|
+
five characters; moving `PIC 9(5)` into `PIC 9(3)` keeps the right three
|
|
11
|
+
digits. Java assignment keeps everything.
|
|
12
|
+
3. **Padding.** An alphanumeric field is space-padded to its width, so
|
|
13
|
+
`"ABC" = "ABC "` is true in COBOL and false in Java. Trim or pad at the
|
|
14
|
+
boundary, in one place.
|
|
15
|
+
4. **Signed overpunch.** `PIC S9(3)` in display format carries the sign in
|
|
16
|
+
the last byte (`12}` is `-120`). Parse it; never `Long.parseLong` the raw
|
|
17
|
+
bytes.
|
|
18
|
+
5. **PERFORM THRU and fall-through.** Control falls into the next paragraph
|
|
19
|
+
unless something stops it; a `PERFORM A THRU C` runs `B` too. A method per
|
|
20
|
+
paragraph needs the range written out.
|
|
21
|
+
6. **GO TO out of a PERFORM.** The paragraph's exit point moves. Flatten with
|
|
22
|
+
care, and say which exit the source used.
|
|
23
|
+
7. **REDEFINES.** Two layouts over the same bytes; writing one view changes
|
|
24
|
+
the other. Two Java fields are two values; make the conversion explicit.
|
|
25
|
+
8. **OCCURS DEPENDING ON.** The table's length is another field's value at
|
|
26
|
+
that moment; a fixed array hides out-of-range reads the source tolerated.
|
|
27
|
+
9. **88-levels and VALUE.** A condition name tests the parent's current
|
|
28
|
+
value; `SET name TO TRUE` writes the first `VALUE`. Keep both directions.
|
|
29
|
+
10. **Zero-based indexing.** COBOL subscripts start at 1. Every `OCCURS`
|
|
30
|
+
access shifts by one.
|
|
31
|
+
11. **ROUNDED and ON SIZE ERROR.** `COMPUTE` truncates by default and
|
|
32
|
+
rounds only with `ROUNDED`; `RoundingMode.HALF_UP` is not the default.
|
|
33
|
+
12. **Dates.** `YYMMDD` with a century window, `ACCEPT FROM DATE` in local
|
|
34
|
+
time: pin the rule and the clock.
|
|
35
|
+
13. **Sequential file state.** `READ ... AT END` and `FILE STATUS` codes are
|
|
36
|
+
the control flow; a reader that throws on end-of-file changes it.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
You are translating one COBOL source unit (a program or a copybook) to Java
|
|
2
|
+
on Spring Boot 3. Read this whole document before writing any code. The first
|
|
3
|
+
pass is a **draft** `.java` at the path the flow gives you, that captures the
|
|
4
|
+
logic faithfully; it does **not** need to compile. The compile pass makes it
|
|
5
|
+
compile module by module.
|
|
6
|
+
|
|
7
|
+
## Ground rules
|
|
8
|
+
|
|
9
|
+
- Same unit, same names, same order. The public class is named exactly as
|
|
10
|
+
the file the flow asked for (Java requires it); paragraphs become private
|
|
11
|
+
methods in the order they appear, named in camelCase after the paragraph
|
|
12
|
+
(`2000-READ-INPUT` → `readInput2000`, the number kept as a suffix so the
|
|
13
|
+
reviewer can diff the two files side by side). Data items keep their names
|
|
14
|
+
in camelCase (`WS-TOTAL-AMT` → `wsTotalAmt`).
|
|
15
|
+
- A program is one `@Service` bean. Its `PROCEDURE DIVISION` is a public
|
|
16
|
+
`run(...)` method whose parameters are the `ACCEPT`ed values and the
|
|
17
|
+
`LINKAGE SECTION` items; its `WORKING-STORAGE` becomes fields on a private
|
|
18
|
+
state class created per run, never static fields.
|
|
19
|
+
- A copybook is one class in the `legacy.copybook` package, ported once and
|
|
20
|
+
imported by every program that `COPY`s it. Do not inline it.
|
|
21
|
+
- Numerics are exact. `PIC S9(7)V99 COMP-3` is `BigDecimal` with scale 2;
|
|
22
|
+
`PIC 9(5)` is `long`; `PIC X(n)` is `String` with the width recorded in a
|
|
23
|
+
comment. Money is never `double`.
|
|
24
|
+
- Files and SQL are repositories. An `FD` with `SELECT ... ASSIGN` becomes a
|
|
25
|
+
JPA `@Entity` plus a Spring Data repository when the file is a keyed VSAM
|
|
26
|
+
or DB table, or a dedicated reader class for a sequential flat file.
|
|
27
|
+
`EXEC SQL` becomes a repository method; the host variables are its
|
|
28
|
+
parameters. One `@Transactional` on `run` where the program `COMMIT`s at
|
|
29
|
+
the end; explicit boundaries where it commits mid-way.
|
|
30
|
+
- `EXEC CICS` is the online boundary: `SEND MAP` / `RECEIVE MAP` become a
|
|
31
|
+
request and response record on a `@RestController` that calls the service;
|
|
32
|
+
`LINK` / `XCTL` become a call to the other program's service bean.
|
|
33
|
+
- `DISPLAY` is a logger call at `info`; `DISPLAY` of an error is `warn`.
|
|
34
|
+
`ACCEPT FROM DATE` is `LocalDate.now(clock)` with an injected `Clock`.
|
|
35
|
+
`STOP RUN` is a `return`; `GOBACK` is a `return` from the program method.
|
|
36
|
+
- Leave `// TODO(port): <reason>` for anything you cannot translate
|
|
37
|
+
confidently. Do not guess.
|
|
38
|
+
- Do not translate JCL, compile options, SORT control cards or `IDENTIFICATION
|
|
39
|
+
DIVISION` metadata; note them as `// SKIPPED(port): <what>`.
|
|
40
|
+
|
|
41
|
+
## Type map
|
|
42
|
+
|
|
43
|
+
| COBOL | Java / Spring Boot |
|
|
44
|
+
| ---------------------------------- | --------------------------------------------------- |
|
|
45
|
+
| `PIC X(n)` | `String` (width `n` noted; pad or trim on `MOVE`) |
|
|
46
|
+
| `PIC 9(n)` | `long` (`int` when `n` ≤ 9 and the source never exceeds it) |
|
|
47
|
+
| `PIC S9(n)V9(m)`, `COMP-3` | `BigDecimal` with scale `m`, `RoundingMode.DOWN` unless `ROUNDED` |
|
|
48
|
+
| `COMP` / `BINARY` | `int` or `long` by the PIC |
|
|
49
|
+
| level-01 record | a `record` when read-only after construction, else a class |
|
|
50
|
+
| `OCCURS n` | an array or `List` sized `n`; `OCCURS DEPENDING ON` → `List` |
|
|
51
|
+
| `REDEFINES` | a second view class with explicit conversion methods |
|
|
52
|
+
| 88-level condition name | a `boolean` method on the owning record (`isActive()`) |
|
|
53
|
+
| `FD` + `SELECT` | `@Entity` + `JpaRepository`, or a flat-file reader |
|
|
54
|
+
| `EXEC SQL` | a repository method or `JdbcTemplate` call |
|
|
55
|
+
| `EXEC CICS SEND/RECEIVE MAP` | `@RestController` request/response records |
|
|
56
|
+
| `CALL 'PROG' USING ...` | a call to `Prog.run(...)` on the injected bean |
|
|
57
|
+
| `PERFORM para` | a method call |
|
|
58
|
+
| `PERFORM para THRU other` | calls to each paragraph in source order, in a method named after the range |
|
|
59
|
+
| `PERFORM ... UNTIL cond` | `while (!cond) { … }` (test before) or `do { … } while` (`WITH TEST AFTER`) |
|
|
60
|
+
| `PERFORM VARYING` | a `for` loop |
|
|
61
|
+
| `EVALUATE` | `switch` with pattern matching, or an `if` chain for `EVALUATE TRUE` |
|
|
62
|
+
| `GO TO` | structured flow with a `// PORTED: GO TO` note |
|
|
63
|
+
| `STRING` / `UNSTRING` | `StringBuilder` / `split` with the delimiters written out |
|
|
64
|
+
| `INSPECT ... TALLYING/REPLACING` | explicit counting / `replace` |
|
|
65
|
+
| `SORT` / `MERGE` | `Comparator` on a `List`, or a stream `sorted` |
|
|
66
|
+
| `DISPLAY` / `ACCEPT` | `log.info` / a method parameter |
|
|
67
|
+
|
|
68
|
+
## Idiom map
|
|
69
|
+
|
|
70
|
+
- A `MOVE` between items of different widths truncates on the right for
|
|
71
|
+
alphanumerics and on the left for numerics; write the truncation out
|
|
72
|
+
(`substring`, `remainder`) where the source relies on it, and say so.
|
|
73
|
+
- `ON SIZE ERROR` is an explicit range check before the assignment.
|
|
74
|
+
- `ADD 1 TO WS-COUNT` is `wsCount++` only when the PIC cannot overflow;
|
|
75
|
+
otherwise the modulus the PIC implies.
|
|
76
|
+
- A paragraph that is both `PERFORM`ed and fallen into is split: the
|
|
77
|
+
fall-through becomes an explicit call at the end of the preceding method.
|
|
78
|
+
- Dates `PIC 9(6)` `YYMMDD` become `LocalDate` with the century rule the
|
|
79
|
+
program used, written in one place.
|
|
80
|
+
- `FILE STATUS` checks become typed exceptions on the reader or repository.
|
|
81
|
+
|
|
82
|
+
## Output format
|
|
83
|
+
|
|
84
|
+
End the file with the trailer the flow reads:
|
|
85
|
+
|
|
86
|
+
```java
|
|
87
|
+
// PORT STATUS
|
|
88
|
+
// source: <path>.cbl
|
|
89
|
+
// confidence: high | medium | low
|
|
90
|
+
// todos: <count of TODO(port)>
|
|
91
|
+
// notes: <one line>
|
|
92
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
files: \.java$
|
|
3
|
+
---
|
|
4
|
+
Review a Java draft against the rulebook as a port reviewer would, with the
|
|
5
|
+
COBOL source in mind: the diff is the draft. Report as findings: a paragraph,
|
|
6
|
+
data item or 88-level the source has that the draft lacks; a `double` or
|
|
7
|
+
`float` holding a `COMP-3`, money or quantity field; a `BigDecimal` whose
|
|
8
|
+
scale differs from the PIC; a `MOVE` truncation or padding the source relied
|
|
9
|
+
on that the draft assumes away; a `GO TO` turned into a state variable loop
|
|
10
|
+
when the flow was structured; a `PERFORM THRU` range that skips a paragraph;
|
|
11
|
+
file or SQL access outside a repository or reader; a program that commits
|
|
12
|
+
with no `@Transactional` boundary; `WORKING-STORAGE` turned into static
|
|
13
|
+
fields; a copybook inlined instead of imported; a guessed translation where a
|
|
14
|
+
`TODO(port)` was honest. Do not report imports or beans that cannot resolve
|
|
15
|
+
yet: the compile pass owns those.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llm4ts/shell",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.38.0",
|
|
4
4
|
"description": "Interactive shell and CLI for llm4ts: flow discovery, run-a-flow, and view",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@effect/platform-node": "4.0.0",
|
|
54
54
|
"@effect/platform-node-shared": "4.0.0",
|
|
55
|
-
"@llm4ts/
|
|
56
|
-
"@llm4ts/
|
|
57
|
-
"@llm4ts/flow": "2.
|
|
55
|
+
"@llm4ts/core": "2.38.0",
|
|
56
|
+
"@llm4ts/runner": "2.38.0",
|
|
57
|
+
"@llm4ts/flow": "2.38.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"effect": "4.0.0"
|