@llm4ts/shell 2.37.0 → 2.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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/epic-stories.js +156 -11
- 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/package.json +4 -4
- package/src/Cli.ts +183 -0
|
@@ -39,10 +39,11 @@ import { loadPatternCards, matchingPatternCards } from "@llm4ts/flow/Patterns";
|
|
|
39
39
|
import { legacySourceWorkspaceLimits, workspaceLimitsFromEnv } from "@llm4ts/flow/Workspace";
|
|
40
40
|
import { ReviewIssue } from "@llm4ts/flow/Review";
|
|
41
41
|
import { coverageReport, coverageUnits, features, matchingFiles, specSchemaIssues } from "@llm4ts/flow/SpecChecks";
|
|
42
|
-
import { SurveyGraph, closureFor
|
|
42
|
+
import { SurveyGraph, closureFor } from "@llm4ts/flow/Survey";
|
|
43
|
+
import { freshGraph, graphCachePath } from "@llm4ts/flow/GraphCache";
|
|
43
44
|
import { withDraftApproval, requireApproval } from "@llm4ts/flow/Approval";
|
|
44
45
|
import { ProgramArtifacts, ProgramUnit, extractProgramsResumably, programArtifactPaths } from "@llm4ts/flow/Artifacts";
|
|
45
|
-
import { ModDir, analystSystem, analystTurns, fixTurn, globalFixAsk, judgeIssueProgram, makeProgramJudge, maxClosureFiles, positiveEnvInt, programArtifactsJsonSchema, programAsk, programFixAsk, programName, readmeFor, wavePrograms } from "./lib/modernize-extract.js";
|
|
46
|
+
import { ModDir, analystSystem, analystTurns, fixTurn, globalFixAsk, judgeIssueProgram, makeProgramJudge, maxClosureFiles, positiveEnvInt, programArtifactsJsonSchema, programAsk, programFixAsk, programName, readmeFor, wavePrograms, closureSection } from "./lib/modernize-extract.js";
|
|
46
47
|
const MaxRounds = 3;
|
|
47
48
|
/**
|
|
48
49
|
* Programs extracted (and judged) at once. The programs of a wave are
|
|
@@ -109,16 +110,19 @@ const program = Effect.gen(function* () {
|
|
|
109
110
|
...(yield* loadKitPatternCards(opened))
|
|
110
111
|
];
|
|
111
112
|
// The dependency graph the analysts' include closures are resolved
|
|
112
|
-
// from
|
|
113
|
-
|
|
113
|
+
// from: the cached code graph (ADR 0030), rebuilt from the pack's rules
|
|
114
|
+
// when a file changed, with the survey's LLM edges carried along.
|
|
115
|
+
const graph = yield* stage(context.events, "graph", pack.graph.edges.length === 0 && pack.graph.joins.length === 0
|
|
114
116
|
? context.events
|
|
115
117
|
.publish(Info.make({
|
|
116
|
-
message: "pack has no
|
|
118
|
+
message: "pack has no graph rules — the analyst gets no resolved closure"
|
|
117
119
|
}))
|
|
118
120
|
.pipe(Effect.as(SurveyGraph.make({ nodes: [], edges: [] })))
|
|
119
|
-
:
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
: freshGraph(files, graphCachePath(input.workDir, pack.name), pack, repo).pipe(Effect.tap((fresh) => context.events.publish(Info.make({
|
|
122
|
+
message: fresh.reused
|
|
123
|
+
? "graph cache reused"
|
|
124
|
+
: "graph rebuilt (scanner layer); LLM edges carried where their evidence is unchanged"
|
|
125
|
+
}))), Effect.map((fresh) => fresh.graph)));
|
|
122
126
|
// One structured analyst call per program, resumable per program: a rerun
|
|
123
127
|
// skips every program whose spec exists, and each program gets its own
|
|
124
128
|
// commit scoped to its four files. LLM4TS_EXTRACT_CONCURRENCY runs that
|
|
@@ -138,6 +142,11 @@ const program = Effect.gen(function* () {
|
|
|
138
142
|
message: `extracting ${target.sourcePath} (${ordinal}/${units.length})`
|
|
139
143
|
}));
|
|
140
144
|
return yield* structuredAndPublish(context.coder, context.events, `${system}\n\n${programAsk(pack, target.sourcePath, closureFor(graph, target.name, maxClosureFiles()))}`, ProgramArtifacts, programArtifactsJsonSchema, "coder").pipe(
|
|
145
|
+
// The spec opens with the program's closure diagram (ADR 0030).
|
|
146
|
+
Effect.map((artifacts) => ProgramArtifacts.make({
|
|
147
|
+
...artifacts,
|
|
148
|
+
spec: `${closureSection(graph, target.name)}\n\n${artifacts.spec}`
|
|
149
|
+
})),
|
|
141
150
|
// A turn-limit trip is the wedged-agent tail, not a
|
|
142
151
|
// failure: keep whatever the analyst already produced.
|
|
143
152
|
Effect.catchIf((error) => error.cause?._tag === "TurnLimitError", (error) => Effect.gen(function* () {
|
|
@@ -10,19 +10,23 @@
|
|
|
10
10
|
// 2. estate — the files `sources:` selects (minus `exclude:`), the files
|
|
11
11
|
// `programs:` selects, and for every `## Coverage:` and `## Survey:` rule
|
|
12
12
|
// the units it captures, with a sample of each so a regex typo is
|
|
13
|
-
// visible at a glance
|
|
13
|
+
// visible at a glance; then the code graph (ADR 0030): what every
|
|
14
|
+
// `## Node:`, `## Edge:` and `## Join:` rule produced, the unresolved
|
|
15
|
+
// items by reason, and one verdict line per `## Probe:`.
|
|
14
16
|
// 3. verdict — hard failures (the pack does not load, `sources:` or
|
|
15
|
-
// `programs:` match nothing
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// warnings and the flow exits 0.
|
|
17
|
+
// `programs:` match nothing, a probe is broken or names an unknown node)
|
|
18
|
+
// abort with exit code 1; likely mistakes (a rule capturing no unit, a
|
|
19
|
+
// missing phase prompt, a scaffold path that does not exist, no reviewer
|
|
20
|
+
// lens, no judge dimension) are listed as warnings and the flow exits 0.
|
|
19
21
|
//
|
|
20
22
|
// Run: LLM4TS_PACK=packs/my-pack modernize-pack-check --repo ~/estates/legacy
|
|
21
23
|
import { existsSync } from "node:fs";
|
|
22
24
|
import { join } from "node:path";
|
|
23
25
|
import * as Effect from "effect/Effect";
|
|
24
26
|
import { FlowAborted, Info, makeNodeWorkspace, mock, openPack, resolveFlowInput, runFlowMain, runNode, stage } from "@llm4ts/runner";
|
|
27
|
+
import { graphStats, probeResults } from "@llm4ts/flow/GraphQuery";
|
|
25
28
|
import { coverageUnits, matchingFiles } from "@llm4ts/flow/SpecChecks";
|
|
29
|
+
import { buildCodeGraph, nodeId, nodeKind } from "@llm4ts/flow/Survey";
|
|
26
30
|
import { legacySourceWorkspaceLimits, workspaceLimitsFromEnv } from "@llm4ts/flow/Workspace";
|
|
27
31
|
/** Prompt sidecars the modernization phases read, with the phase that reads each. */
|
|
28
32
|
const phasePrompts = [
|
|
@@ -42,6 +46,8 @@ const sample = (units) => units.length === 0
|
|
|
42
46
|
: units.slice(0, sampleSize).join(", ") +
|
|
43
47
|
(units.length > sampleSize ? ` … +${units.length - sampleSize} more` : "");
|
|
44
48
|
const plural = (count, noun, many = `${noun}s`) => `${count} ${count === 1 ? noun : many}`;
|
|
49
|
+
const probeFrom = (path) => path[0]?.from ?? "";
|
|
50
|
+
const probeTo = (path) => path.at(-1)?.to ?? "";
|
|
45
51
|
const program = Effect.gen(function* () {
|
|
46
52
|
const input = yield* resolveFlowInput("Check the pack against the estate");
|
|
47
53
|
yield* runNode({
|
|
@@ -147,6 +153,60 @@ const program = Effect.gen(function* () {
|
|
|
147
153
|
});
|
|
148
154
|
yield* report("coverage", pack.coverage);
|
|
149
155
|
yield* report("survey", pack.survey);
|
|
156
|
+
// The code graph (ADR 0030): rule by rule, then the holes, then the probes.
|
|
157
|
+
const build = yield* buildCodeGraph(repo, {
|
|
158
|
+
sources: sourcesRegex,
|
|
159
|
+
...(pack.exclude === undefined ? {} : { exclude: pack.exclude }),
|
|
160
|
+
coverage: pack.coverage,
|
|
161
|
+
rules: pack.graph
|
|
162
|
+
});
|
|
163
|
+
const stats = graphStats(build.graph);
|
|
164
|
+
for (const rule of pack.graph.nodes) {
|
|
165
|
+
const count = stats.nodes[rule.kind] ?? 0;
|
|
166
|
+
const samples = build.graph.nodes
|
|
167
|
+
.filter((node) => nodeKind(node) === rule.kind)
|
|
168
|
+
.map((node) => node.name);
|
|
169
|
+
yield* say(`node '${rule.kind}': ${plural(count, "node")} — ${sample(samples)}`);
|
|
170
|
+
if (count === 0) {
|
|
171
|
+
warnings.push(`node rule '${rule.kind}' matched nothing: files '${rule.files}', pattern '${rule.pattern}'`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const duplicates = build.graph.nodes.filter((node) => nodeId(node).includes("~"));
|
|
175
|
+
if (duplicates.length > 0) {
|
|
176
|
+
warnings.push(`${plural(duplicates.length, "node")} share a name within one file (ids with '~n'): ${sample(duplicates.map(nodeId))}`);
|
|
177
|
+
}
|
|
178
|
+
for (const rule of pack.graph.edges.filter((rule) => rule.fromKind !== "file" || rule.toKind !== "file")) {
|
|
179
|
+
const count = stats.edges[rule.kind] ?? 0;
|
|
180
|
+
yield* say(`edge '${rule.kind}': ${plural(count, "edge")}`);
|
|
181
|
+
if (count === 0) {
|
|
182
|
+
warnings.push(`edge rule '${rule.kind}' produced no edge`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const rule of pack.graph.joins) {
|
|
186
|
+
const count = build.graph.edges.filter((edge) => edge.kind === rule.kind && (edge.mechanism ?? "").startsWith("join:")).length;
|
|
187
|
+
yield* say(`join '${rule.kind}' (${rule.fromKind}.${rule.fromAttr} → ${rule.toKind}.${rule.toAttr}, ${rule.match}, scope ${rule.scope}): ${plural(count, "edge")}`);
|
|
188
|
+
if (count === 0) {
|
|
189
|
+
warnings.push(`join '${rule.kind}' produced no edge`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const byReason = Object.entries(stats.unresolved)
|
|
193
|
+
.map(([reason, count]) => `${reason} ${count}`)
|
|
194
|
+
.join(", ");
|
|
195
|
+
yield* say(`unresolved: ${build.graph.unresolved.length === 0 ? "none" : byReason}`);
|
|
196
|
+
for (const item of build.graph.unresolved.slice(0, 20)) {
|
|
197
|
+
yield* say(` ${item.reason} [${item.rule}] ${item.node}${item.reference === undefined ? "" : ` → ${item.reference}`} (${item.file}:${item.line})`);
|
|
198
|
+
}
|
|
199
|
+
for (const result of probeResults(build.graph, pack.graph.probes)) {
|
|
200
|
+
const via = result.path === undefined
|
|
201
|
+
? ""
|
|
202
|
+
: result.path.length === 0
|
|
203
|
+
? " (same node)"
|
|
204
|
+
: ` (${probeFrom(result.path)} → ${probeTo(result.path)} via ${result.path.map((edge) => edge.kind).join(" → ")})`;
|
|
205
|
+
yield* say(`probe '${result.probe.name}': ${result.status}${via}`);
|
|
206
|
+
if (result.status !== "ok") {
|
|
207
|
+
failures.push(`probe '${result.probe.name}' is ${result.status} (${result.probe.from} → ${result.probe.to})`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
150
210
|
}));
|
|
151
211
|
yield* stage(context.events, "verdict", Effect.gen(function* () {
|
|
152
212
|
for (const warning of warnings) {
|
|
@@ -37,7 +37,8 @@ import { structuredAndPublish } from "@llm4ts/flow/Flow";
|
|
|
37
37
|
import { FlowEvents } from "@llm4ts/flow/FlowEvents";
|
|
38
38
|
import { Task } from "@llm4ts/flow/Plan";
|
|
39
39
|
import { coverageUnits, matchingFiles } from "@llm4ts/flow/SpecChecks";
|
|
40
|
-
import { SurveyGraph, closureFor
|
|
40
|
+
import { SurveyGraph, closureFor } from "@llm4ts/flow/Survey";
|
|
41
|
+
import { freshGraph, graphCachePath } from "@llm4ts/flow/GraphCache";
|
|
41
42
|
import { withDraftApproval } from "@llm4ts/flow/Approval";
|
|
42
43
|
import { legacySourceWorkspaceLimits, workspaceLimitsFromEnv } from "@llm4ts/flow/Workspace";
|
|
43
44
|
import { FlowAborted, Info, Plan, asReadOnly, coderFromEnv, defaultPlanInstructions, makeNodeWorkspace, nodePlainFileStore, openPack, planFrom, resolveFlowInput, runFlowMain, runNode, stage, withTurnLimit } from "@llm4ts/runner";
|
|
@@ -112,11 +113,9 @@ const program = Effect.gen(function* () {
|
|
|
112
113
|
const writeDecisions = Effect.gen(function* () {
|
|
113
114
|
yield* files.writeAtomic(decisionsPath, renderDecisions(decisions));
|
|
114
115
|
});
|
|
115
|
-
const graph = yield* stage(context.events, "graph", pack.
|
|
116
|
+
const graph = yield* stage(context.events, "graph", pack.graph.edges.length === 0 && pack.graph.joins.length === 0
|
|
116
117
|
? Effect.succeed(SurveyGraph.make({ nodes: [], edges: [] }))
|
|
117
|
-
:
|
|
118
|
-
...(pack.exclude === undefined ? {} : { exclude: pack.exclude })
|
|
119
|
-
}));
|
|
118
|
+
: freshGraph(files, graphCachePath(input.workDir, pack.name), pack, repo).pipe(Effect.map((fresh) => fresh.graph)));
|
|
120
119
|
const system = analystSystem(pack);
|
|
121
120
|
const limit = budget();
|
|
122
121
|
const judgeProgram = makeProgramJudge({
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llm4ts/shell",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.39.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/core": "2.
|
|
56
|
-
"@llm4ts/flow": "2.
|
|
57
|
-
"@llm4ts/runner": "2.
|
|
55
|
+
"@llm4ts/core": "2.39.0",
|
|
56
|
+
"@llm4ts/flow": "2.39.0",
|
|
57
|
+
"@llm4ts/runner": "2.39.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"effect": "4.0.0"
|