@llm4ts/shell 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Cli.d.ts +1 -1
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +32 -0
- package/dist/Cli.js.map +1 -1
- package/dist/Refine.d.ts +44 -0
- package/dist/Refine.d.ts.map +1 -0
- package/dist/Refine.js +362 -0
- package/dist/Refine.js.map +1 -0
- package/flows/lib/modernize-extract.js +226 -0
- package/flows/modernize-extract.js +13 -173
- package/flows/modernize-implement.js +28 -2
- package/flows/modernize-pack-check.js +7 -1
- package/flows/modernize-pack-upgrade.js +246 -0
- package/flows/modernize-refine.js +389 -0
- package/flows/modernize-seed.js +50 -2
- package/flows/modernize-verify.js +12 -5
- package/kits/j2ee-nextjs/README.md +5 -4
- package/kits/j2ee-nextjs/fixtures/demo-bank/RUNBOOK.md +43 -0
- package/kits/j2ee-nextjs/fixtures/demo-bank/legacy-j2ee/PAGES.md +26 -0
- package/kits/j2ee-nextjs/flows/convert-all.js +56 -20
- package/kits/j2ee-nextjs/flows/convert-feature.js +48 -0
- package/kits/j2ee-nextjs/flows/lib/convert.js +292 -40
- package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/pack.md +16 -0
- package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/prompts/consolidate.md +10 -0
- package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/prompts/plan.md +24 -16
- package/kits/j2ee-nextjs/packs/j2ee-nextjs-spa/prompts/refine-propose.md +16 -0
- package/kits/mainframe-java/packs/cobol-springboot/pack.md +5 -0
- package/kits/mainframe-java/packs/cobol-springboot/prompts/consolidate.md +8 -0
- package/kits/mainframe-java/packs/cobol-springboot/prompts/refine-propose.md +10 -0
- package/package.json +5 -4
- package/src/Cli.ts +57 -0
- package/src/Refine.ts +504 -0
package/flows/modernize-seed.js
CHANGED
|
@@ -27,6 +27,7 @@ import { packageVersion } from "@llm4ts/flow/Package";
|
|
|
27
27
|
import { Provenance, makeProvenanceStore } from "@llm4ts/flow/Provenance";
|
|
28
28
|
import { matchingFiles } from "@llm4ts/flow/SpecChecks";
|
|
29
29
|
import { requireApproval } from "@llm4ts/flow/Approval";
|
|
30
|
+
import { Decisions, filterFeature, parseDecisions } from "@llm4ts/flow/Decisions";
|
|
30
31
|
const ModDir = "docs/modernization";
|
|
31
32
|
const skipped = new Set([".git", "target", "node_modules", "dist"]);
|
|
32
33
|
const Seats = Schema.Record(Schema.String, Schema.String);
|
|
@@ -84,7 +85,20 @@ const program = Effect.gen(function* () {
|
|
|
84
85
|
}));
|
|
85
86
|
const pack = opened.pack;
|
|
86
87
|
const specPackRoot = join(legacyRepo, ModDir);
|
|
87
|
-
|
|
88
|
+
// The README always; each refinement overlay when it exists (ADR 0015):
|
|
89
|
+
// a pack refined after its gate passed is seeded only as approved.
|
|
90
|
+
yield* stage(context.events, "approval", Effect.gen(function* () {
|
|
91
|
+
yield* requireApproval(files, join(specPackRoot, "README.md"));
|
|
92
|
+
for (const overlay of ["decisions.md", "domains.md"]) {
|
|
93
|
+
if ((yield* files.read(join(specPackRoot, overlay))) !== undefined) {
|
|
94
|
+
yield* requireApproval(files, join(specPackRoot, overlay));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}));
|
|
98
|
+
const decisionsText = yield* files.read(join(specPackRoot, "decisions.md"));
|
|
99
|
+
const decisions = decisionsText === undefined
|
|
100
|
+
? Decisions.empty()
|
|
101
|
+
: yield* parseDecisions(decisionsText, `${ModDir}/decisions.md`);
|
|
88
102
|
yield* stage(context.events, "scaffold", Effect.gen(function* () {
|
|
89
103
|
const existing = (yield* target.discover().pipe(Effect.orElseSucceed(() => []))).filter((path) => !path.startsWith(".git/"));
|
|
90
104
|
if (existing.length > 0) {
|
|
@@ -116,8 +130,42 @@ const program = Effect.gen(function* () {
|
|
|
116
130
|
"check LLM4TS_LEGACY_REPO and that extraction wrote its spec pack"
|
|
117
131
|
});
|
|
118
132
|
}
|
|
133
|
+
// The projection (ADR 0015): a feature file reaches the target
|
|
134
|
+
// with only its surviving scenarios, so the coder never sees a
|
|
135
|
+
// scenario it must not encode; a program disposed as a whole
|
|
136
|
+
// ships no feature file at all. Specs are copied unchanged, with
|
|
137
|
+
// the overlays beside them for the judges.
|
|
119
138
|
const features = yield* copyTree(legacy, target, `${ModDir}/features`, pack.featuresDir);
|
|
120
|
-
|
|
139
|
+
let projected = 0;
|
|
140
|
+
if (!decisions.isEmpty) {
|
|
141
|
+
const featurePaths = yield* target
|
|
142
|
+
.discover(`${pack.featuresDir}/**`)
|
|
143
|
+
.pipe(Effect.orElseSucceed(() => []));
|
|
144
|
+
for (const path of featurePaths) {
|
|
145
|
+
const stem = (path.split("/").at(-1) ?? "").replace(/\.feature$/, "").toLowerCase();
|
|
146
|
+
const program = [...decisions.programs, ...decisions.scenarios]
|
|
147
|
+
.map((entry) => entry.program)
|
|
148
|
+
.find((name) => name.toLowerCase() === stem);
|
|
149
|
+
if (program === undefined) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
projected += 1;
|
|
153
|
+
if (decisions.programDecision(program) !== undefined) {
|
|
154
|
+
yield* target.write(path, "");
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const text = yield* target.read(path);
|
|
158
|
+
yield* target.write(path, filterFeature(text, decisions.disposedScenarios(program)));
|
|
159
|
+
}
|
|
160
|
+
yield* context.events.publish(Info.make({ message: `projected decisions onto ${projected} feature file(s)` }));
|
|
161
|
+
}
|
|
162
|
+
for (const index of [
|
|
163
|
+
"traceability.md",
|
|
164
|
+
"mapping.md",
|
|
165
|
+
"rules.txt",
|
|
166
|
+
"decisions.md",
|
|
167
|
+
"domains.md"
|
|
168
|
+
]) {
|
|
121
169
|
yield* copyFile(legacy, target, `${ModDir}/${index}`, join(pack.specsDir, index));
|
|
122
170
|
}
|
|
123
171
|
return specs + features;
|
|
@@ -229,7 +229,7 @@ const triagePrompt = (pack, failing, specText) => {
|
|
|
229
229
|
/** The spec'd programs: top-level `<NAME>.md` files under the specs dir, indexes aside. */
|
|
230
230
|
const specPrograms = Effect.fn("modernize-verify.specPrograms")(function* (target, specsDir) {
|
|
231
231
|
const paths = yield* matchingFiles(target, `^${specsDir}/[^/]+\\.md$`).pipe(Effect.orElseSucceed(() => []));
|
|
232
|
-
const excluded = new Set(["traceability", "mapping", "README"]);
|
|
232
|
+
const excluded = new Set(["traceability", "mapping", "README", "decisions", "domains"]);
|
|
233
233
|
return paths
|
|
234
234
|
.map((path) => (path.split("/").at(-1) ?? path).replace(/\.md$/, ""))
|
|
235
235
|
.filter((name) => !excluded.has(name))
|
|
@@ -268,10 +268,17 @@ const program = Effect.gen(function* () {
|
|
|
268
268
|
yield* context.events.publish(Info.make({ message: "clean-room wall: no legacy source in the target workspace" }));
|
|
269
269
|
}));
|
|
270
270
|
const rulesText = (yield* files.read(join(input.workDir, pack.specsDir, "rules.txt"))) ?? "";
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
// Everything above `# waived` is the universe; the units below it were
|
|
272
|
+
// waived by a decision (ADR 0015) and are neither expected nor flagged.
|
|
273
|
+
const rulesLines = rulesText.split(/\r?\n/).map((line) => line.trim());
|
|
274
|
+
const waivedAt = rulesLines.indexOf("# waived");
|
|
275
|
+
const universe = (waivedAt < 0 ? rulesLines : rulesLines.slice(0, waivedAt)).filter((line) => line.length > 0);
|
|
276
|
+
const waived = waivedAt < 0 ? [] : rulesLines.slice(waivedAt + 1).filter((l) => l.length > 0);
|
|
277
|
+
if (waived.length > 0) {
|
|
278
|
+
yield* context.events.publish(Info.make({
|
|
279
|
+
message: `${waived.length} rule(s) waived by decision are left out of the universe`
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
275
282
|
if (universe.length === 0) {
|
|
276
283
|
yield* context.events.publish(Info.make({
|
|
277
284
|
message: "no rules.txt in the seeded spec pack — the report will use the vectors' own " +
|
|
@@ -15,10 +15,11 @@ target) the [workshop runbook](fixtures/demo-bank/RUNBOOK.md) rehearses on.
|
|
|
15
15
|
| [`jsp-nextjs`](packs/jsp-nextjs/pack.md) | JSP/Java → Next.js SPA | `nextjs-spa` | no |
|
|
16
16
|
| [`jsp-bff-nextjs`](packs/jsp-bff-nextjs/pack.md) | JSP/Java → Spring BFF + Next.js | `spring-bff` | no |
|
|
17
17
|
|
|
18
|
-
| Flow
|
|
19
|
-
|
|
|
20
|
-
| `convert-page`
|
|
21
|
-
| `convert-
|
|
18
|
+
| Flow | What it does |
|
|
19
|
+
| ----------------- | ---------------------------------------------------------------------------------------- |
|
|
20
|
+
| `convert-page` | Convert ONE extracted page into the Next.js target on its own branch |
|
|
21
|
+
| `convert-feature` | Convert ONE domain feature of the approved `domains.md`: one branch, one merged contract |
|
|
22
|
+
| `convert-all` | Walk the approved domain map (features) or the survey inventory (pages), one branch each |
|
|
22
23
|
|
|
23
24
|
```sh
|
|
24
25
|
llm4ts run modernize-pack-check --pack j2ee-nextjs-spa --repo /path/to/legacy-estate
|
|
@@ -119,6 +119,49 @@ estate once every wave is in.
|
|
|
119
119
|
LLM4TS_WAVE=wave-2 llm4ts run modernize-extract --repo ~/demo/legacy-j2ee
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
### Refine — the pack becomes what the client wants built (optional beat)
|
|
123
|
+
|
|
124
|
+
Extraction says what the legacy does. Refinement (ADR 0015) says what the
|
|
125
|
+
target should become, in two files the audience can read on screen:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
llm4ts refine --repo ~/demo/legacy-j2ee --target ~/demo/nextjs
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Three marks, then run:
|
|
132
|
+
|
|
133
|
+
1. **Mark programs** — `promoQ3`, `oldTransfer`, `testHarness`: `drop`
|
|
134
|
+
("dead, nothing links here"); `login`: `?` with the note "the target has
|
|
135
|
+
an AuthProvider". The proposal reads `~/demo/nextjs` read-only and comes
|
|
136
|
+
back with `provided — src/auth/AuthProvider.tsx`, or an open point if it
|
|
137
|
+
could not find the proof. Say that `defer` is never proposed by the model.
|
|
138
|
+
2. **Deepen a program** — `accountOverview` with the focus "the date-range
|
|
139
|
+
filter on movements is missing; check AccountOverviewServlet". Watch the
|
|
140
|
+
analyst revise the spec (own commit, `deepen accountOverview`), the judge
|
|
141
|
+
score the focus, and the mark stamp `[done <commit>]`.
|
|
142
|
+
3. **Run modernize-refine** — the map lands at
|
|
143
|
+
`docs/modernization/domains.md`: `beneficiaryList` + `beneficiaryEdit`
|
|
144
|
+
become "Beneficiary maintenance" (they share the `/beneficiary` form
|
|
145
|
+
target), the three transfer steps "Wire transfer", header/nav/footer the
|
|
146
|
+
shell; the filler pages are folded only if the model proposed it with
|
|
147
|
+
evidence. `plan.md` is regenerated per feature and `rules.txt` gains a
|
|
148
|
+
`# waived` section listing the dropped pages' units. Answer any open
|
|
149
|
+
point in the menu, rerun, then **Approve the overlays** — and point out
|
|
150
|
+
that the README went back to `- [ ] Approved` the moment refine wrote
|
|
151
|
+
anything.
|
|
152
|
+
|
|
153
|
+
Off stage the same beat is: edit `decisions.md` by hand (its header explains
|
|
154
|
+
the vocabulary) and `LLM4TS_TARGET_REPO=~/demo/nextjs llm4ts run
|
|
155
|
+
modernize-refine --repo ~/demo/legacy-j2ee`. Skip the beat entirely and the
|
|
156
|
+
pipeline behaves exactly as before.
|
|
157
|
+
|
|
158
|
+
Once `domains.md` is approved, Act 2 may convert **one feature instead of
|
|
159
|
+
two pages**: `llm4ts run convert-feature --repo ~/demo/nextjs
|
|
160
|
+
beneficiary-maintenance` lands `beneficiaryList` and `beneficiaryEdit` on one
|
|
161
|
+
`convert/beneficiary-maintenance` branch behind one merged contract, and
|
|
162
|
+
`convert-all` walks the features rather than the pages. Rehearse the timing
|
|
163
|
+
before choosing it on stage: it is the two pages' cost in one run.
|
|
164
|
+
|
|
122
165
|
Extraction runs three pages at once (`LLM4TS_EXTRACT_CONCURRENCY=3` from Act
|
|
123
166
|
0): the pages of a wave are independent, each lands in its own commit holding
|
|
124
167
|
only its four files, and the log interleaves — say so before it starts, then
|
|
@@ -65,3 +65,29 @@ anti-corruption renaming in a Page Spec has something to bite on.
|
|
|
65
65
|
|
|
66
66
|
All data is fictional: EUR accounts, fake IBAN-like numbers in the form
|
|
67
67
|
`IT00 DEMO 0000 ...`, customer "MARIO BIANCHI" / customer id `CUST0042`.
|
|
68
|
+
|
|
69
|
+
## Refinement answer key (ADR 0015)
|
|
70
|
+
|
|
71
|
+
What `modernize-refine` should produce on this estate once the three dead
|
|
72
|
+
pages are marked `drop` and `login` resolves to `provided` by the target's
|
|
73
|
+
AuthProvider:
|
|
74
|
+
|
|
75
|
+
| Domain feature (deterministic cluster) | Programs | Why they cluster |
|
|
76
|
+
| ------------------------------------------------------------ | --------------------------------------------------------- | --------------------------------------------- |
|
|
77
|
+
| Portal shell | `header.jsp`, `nav.jsp`, `footer.jsp` | fragments: targets of `jsp-include` edges |
|
|
78
|
+
| Account overview | `accountOverview.jsp` | its ajax target names no other page |
|
|
79
|
+
| Beneficiary maintenance | `beneficiaryList.jsp`, `beneficiaryEdit.jsp` | both post to `/beneficiary` (`jsp-form-action`) |
|
|
80
|
+
| Wire transfer | `transferStep1.jsp`, `transferStep2.jsp`, `transferConfirm.jsp` | all post to `/transfer` |
|
|
81
|
+
| one singleton each (fold into the shell only by proposal) | `dashboard.jsp`, `settings.jsp`, `profile.jsp`, `messages.jsp`, `help.jsp` | no shared form or ajax target |
|
|
82
|
+
|
|
83
|
+
Waived coverage units after the drops: every `url-pattern`, form action, and
|
|
84
|
+
ajax url captured only from `oldTransfer.jsp`, `promoQ3.jsp`, and
|
|
85
|
+
`testHarness.jsp`; `login.jsp`'s form action once it is `provided`.
|
|
86
|
+
|
|
87
|
+
Feature contracts `convert-feature` writes once the map above is approved:
|
|
88
|
+
`contracts/beneficiary-maintenance.openapi.yaml` unions `GET /beneficiary`
|
|
89
|
+
(list, both pages), `GET /beneficiary?action=edit` and `POST /beneficiary`
|
|
90
|
+
(edit) over the shared `Beneficiary` DTO; `contracts/wire-transfer.openapi.yaml`
|
|
91
|
+
unions the three `POST /transfer` steps — same path, one operation per
|
|
92
|
+
`step`, so the pages must agree on the `TransferDraft` DTO or the map gets a
|
|
93
|
+
conflict open point.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Convert the whole legacy estate: walk the
|
|
1
|
+
// Convert the whole legacy estate: walk the approved domain map (one branch per feature) or the survey inventory (one branch per page) in wave order, progress board, estimated-cost migration report.
|
|
2
2
|
//
|
|
3
3
|
// Runs rooted at the TARGET repository (`--repo <nextjs>`), with
|
|
4
4
|
// LLM4TS_LEGACY_REPO pointing at the extracted legacy repository:
|
|
@@ -24,7 +24,7 @@ import { AdoConfig, makeAzureDevOpsTool } from "@llm4ts/flow/AzureDevOpsTool";
|
|
|
24
24
|
import { BoardItem, composeBoardSync, makeAdoBoardSync, makeLocalBoardSync } from "@llm4ts/flow/BoardSync";
|
|
25
25
|
import { describeFlowError } from "@llm4ts/flow/FlowError";
|
|
26
26
|
import { Info, asReadOnly, coderFromEnv, nodePlainFileStore, nodeProcessExecutor, resolveFlowInput, runFlowMain, runNode, stage } from "@llm4ts/runner";
|
|
27
|
-
import { conversionInventory, convertPage, migrationReport, setupConversion } from "./lib/convert.js";
|
|
27
|
+
import { conversionInventory, convertFeature, convertPage, featureInventory, migrationReport, setupConversion } from "./lib/convert.js";
|
|
28
28
|
const program = Effect.gen(function* () {
|
|
29
29
|
const input = yield* resolveFlowInput("Convert the legacy estate into the destination SPA");
|
|
30
30
|
const coder = coderFromEnv(process.env);
|
|
@@ -57,39 +57,75 @@ const program = Effect.gen(function* () {
|
|
|
57
57
|
yield* context.events.publish(Info.make({ message: `ADO board mirror enabled: ${orgUrl}/${project}` }));
|
|
58
58
|
}
|
|
59
59
|
const board = composeBoardSync(boards);
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// ADR 0012 addendum: an approved domain map makes the feature the
|
|
61
|
+
// unit of delivery; without one the walk is per page as before.
|
|
62
|
+
const features = yield* stage(context.events, "inventory", featureInventory(files, deps.legacy, deps.legacyDir, deps.pack));
|
|
63
|
+
let items;
|
|
64
|
+
if (features !== undefined) {
|
|
65
|
+
yield* context.events.publish(Info.make({
|
|
66
|
+
message: `approved domain map: converting ${features.length} feature(s), one branch each`
|
|
67
|
+
}));
|
|
68
|
+
items = features.map((entry) => ({
|
|
69
|
+
id: entry.feature.id,
|
|
70
|
+
title: entry.feature.name,
|
|
71
|
+
...(entry.wave === undefined ? {} : { wave: entry.wave }),
|
|
72
|
+
detail: `pages: ${entry.feature.programs.join(", ")}`,
|
|
73
|
+
...(entry.disposed ? { skip: "every page disposed by decision" } : {}),
|
|
74
|
+
convert: convertFeature(deps, entry.feature.id)
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const inventory = yield* conversionInventory(files, deps.legacy, deps.legacyDir, deps.pack);
|
|
79
|
+
items = yield* Effect.forEach(inventory, ({ page, wave, disposition }) => Effect.gen(function* () {
|
|
80
|
+
const specPath = join(deps.legacyDir, deps.pack.specsDir, `${page}.md`);
|
|
81
|
+
const missing = (yield* files.read(specPath)) === undefined;
|
|
82
|
+
return {
|
|
83
|
+
id: page,
|
|
84
|
+
title: page,
|
|
85
|
+
...(wave === undefined ? {} : { wave }),
|
|
86
|
+
// A page the decisions overlay disposed of as a whole (ADR 0015)
|
|
87
|
+
// is listed with its disposition, like a page triaged dead.
|
|
88
|
+
...(disposition !== undefined
|
|
89
|
+
? { skip: `${disposition} by decision` }
|
|
90
|
+
: missing
|
|
91
|
+
? { skip: "no extracted spec" }
|
|
92
|
+
: {}),
|
|
93
|
+
convert: convertPage(deps, page)
|
|
94
|
+
};
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
if (items.length === 0) {
|
|
62
98
|
yield* context.events.publish(Info.make({ message: "inventory is empty — extract the legacy estate first" }));
|
|
63
99
|
return;
|
|
64
100
|
}
|
|
65
101
|
// The whole estate lands on the board as planned up front — the
|
|
66
102
|
// breadth view exists from minute one.
|
|
67
|
-
yield* stage(context.events, "board", board.plan(
|
|
68
|
-
id:
|
|
69
|
-
title:
|
|
103
|
+
yield* stage(context.events, "board", board.plan(items.map((item) => BoardItem.make({
|
|
104
|
+
id: item.id,
|
|
105
|
+
title: item.title,
|
|
70
106
|
status: "planned",
|
|
71
|
-
...(wave === undefined ? {} : { wave })
|
|
107
|
+
...(item.wave === undefined ? {} : { wave: item.wave }),
|
|
108
|
+
...(item.detail === undefined ? {} : { detail: item.detail })
|
|
72
109
|
}))));
|
|
73
110
|
const baseBranch = yield* context.git.currentBranch;
|
|
74
111
|
const failFast = process.env.LLM4TS_FAIL_FAST === "1";
|
|
75
|
-
for (const
|
|
112
|
+
for (const item of items) {
|
|
76
113
|
const snapshot = yield* board.snapshot;
|
|
77
|
-
const known = snapshot.items.find((
|
|
114
|
+
const known = snapshot.items.find((candidate) => candidate.id === item.id);
|
|
78
115
|
if (known !== undefined && known.status !== "planned" && known.status !== "failed") {
|
|
79
|
-
yield* context.events.publish(Info.make({ message: `resume: ${
|
|
116
|
+
yield* context.events.publish(Info.make({ message: `resume: ${item.id} is already ${known.status} — skipping` }));
|
|
80
117
|
continue;
|
|
81
118
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
yield* board.skip(page, "no extracted spec");
|
|
119
|
+
if (item.skip !== undefined) {
|
|
120
|
+
yield* board.skip(item.id, item.skip);
|
|
85
121
|
continue;
|
|
86
122
|
}
|
|
87
123
|
const checkpoint = yield* context.git.checkpoint;
|
|
88
|
-
yield* board.start(
|
|
89
|
-
const result = yield* Effect.result(
|
|
124
|
+
yield* board.start(item.id);
|
|
125
|
+
const result = yield* Effect.result(item.convert);
|
|
90
126
|
if (result._tag === "Success") {
|
|
91
127
|
const outcome = result.success;
|
|
92
|
-
yield* board.complete(
|
|
128
|
+
yield* board.complete(item.id, {
|
|
93
129
|
branch: outcome.branch,
|
|
94
130
|
reportPath: outcome.reportPath,
|
|
95
131
|
...(outcome.estimatedTokens === undefined
|
|
@@ -103,15 +139,15 @@ const program = Effect.gen(function* () {
|
|
|
103
139
|
}
|
|
104
140
|
else {
|
|
105
141
|
const reason = describeFlowError(result.failure);
|
|
106
|
-
// A stuck
|
|
142
|
+
// A stuck unit must not sink the walk: reset the working tree,
|
|
107
143
|
// mark the failure, keep going (LLM4TS_FAIL_FAST=1 to stop).
|
|
108
144
|
yield* context.git.rollback(checkpoint);
|
|
109
145
|
yield* context.git.checkout(baseBranch);
|
|
110
|
-
yield* board.fail(
|
|
146
|
+
yield* board.fail(item.id, reason);
|
|
111
147
|
if (failFast) {
|
|
112
148
|
return yield* Effect.fromResult(result);
|
|
113
149
|
}
|
|
114
|
-
yield* context.events.publish(Info.make({ message:
|
|
150
|
+
yield* context.events.publish(Info.make({ message: `${item.id} failed — continuing: ${reason}` }));
|
|
115
151
|
}
|
|
116
152
|
}
|
|
117
153
|
const finalBoard = yield* board.snapshot;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Convert ONE domain feature of the approved domains.md into the destination Next.js SPA: one branch, one merged contract, the port then each page in navigation order.
|
|
2
|
+
//
|
|
3
|
+
// Runs rooted at the TARGET repository (`--repo <nextjs>`), with
|
|
4
|
+
// LLM4TS_LEGACY_REPO pointing at the refined legacy repository whose
|
|
5
|
+
// docs/modernization/domains.md is approved. The task text is the feature id:
|
|
6
|
+
//
|
|
7
|
+
// LLM4TS_LEGACY_REPO=~/estates/demo-bank-legacy \
|
|
8
|
+
// llm4ts run convert-feature --repo ~/estates/demo-bank-nextjs beneficiary-maintenance
|
|
9
|
+
//
|
|
10
|
+
// ADR 0012 addendum: the unit of delivery is the domain feature — its
|
|
11
|
+
// surviving pages share `contracts/<feature>.openapi.yaml` (the deterministic
|
|
12
|
+
// union of their API sections; conflicts are open points in domains.md, never
|
|
13
|
+
// silent merges), one port under src/services/<feature>/, page components
|
|
14
|
+
// and tests per page. Gates and judge as convert-page, plus the feature
|
|
15
|
+
// judged against its contract of record. Branch `convert/<feature>`, no PR.
|
|
16
|
+
import * as Effect from "effect/Effect";
|
|
17
|
+
import { FlowAborted, Info, asReadOnly, coderFromEnv, resolveFlowInput, runFlowMain, runNode } from "@llm4ts/runner";
|
|
18
|
+
import { convertFeature, setupConversion } from "./lib/convert.js";
|
|
19
|
+
const program = Effect.gen(function* () {
|
|
20
|
+
const input = yield* resolveFlowInput("Convert one domain feature into the destination SPA");
|
|
21
|
+
const feature = input.prompt.trim().split(/\s+/)[0] ?? "";
|
|
22
|
+
const coder = coderFromEnv(process.env);
|
|
23
|
+
yield* runNode({
|
|
24
|
+
workDir: input.workDir,
|
|
25
|
+
workspace: input.workspace,
|
|
26
|
+
userPrompt: input.prompt,
|
|
27
|
+
coder,
|
|
28
|
+
reasoning: asReadOnly(coder),
|
|
29
|
+
reviewers: [asReadOnly(coder)],
|
|
30
|
+
environment: process.env
|
|
31
|
+
}, (context) => Effect.gen(function* () {
|
|
32
|
+
if (feature.length === 0) {
|
|
33
|
+
return yield* FlowAborted.make({
|
|
34
|
+
message: "pass the domain feature id to convert, e.g.: llm4ts run convert-feature beneficiary-maintenance"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const deps = yield* setupConversion(context, input, process.env, import.meta.dirname);
|
|
38
|
+
const outcome = yield* convertFeature(deps, feature);
|
|
39
|
+
yield* context.events.publish(Info.make({
|
|
40
|
+
message: `converted feature ${outcome.page} on branch ${outcome.branch} — ` +
|
|
41
|
+
`report at ${outcome.reportPath}` +
|
|
42
|
+
(outcome.estimatedTokens === undefined
|
|
43
|
+
? ""
|
|
44
|
+
: ` (~${outcome.estimatedTokens} tokens, ESTIMATED)`)
|
|
45
|
+
}));
|
|
46
|
+
}));
|
|
47
|
+
});
|
|
48
|
+
runFlowMain(program);
|