@llm4ts/shell 0.13.5 → 0.15.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 (30) hide show
  1. package/flows/convert-all.js +152 -0
  2. package/flows/convert-page.js +56 -0
  3. package/flows/modernize-bench.js +1 -1
  4. package/flows/modernize-extract.js +68 -40
  5. package/flows/modernize-survey.js +0 -0
  6. package/flows/packs/cobol-kafka/prompts/survey-refine.md +7 -0
  7. package/flows/packs/cobol-kafka/prompts/survey-triage.md +8 -0
  8. package/flows/packs/cobol-springboot/prompts/survey-refine.md +7 -0
  9. package/flows/packs/cobol-springboot/prompts/survey-triage.md +8 -0
  10. package/flows/packs/j2ee-nextjs-spa/pack.md +48 -0
  11. package/flows/packs/j2ee-nextjs-spa/patterns/PAT-J2EE-001-jstl-table.md +8 -0
  12. package/flows/packs/j2ee-nextjs-spa/patterns/PAT-J2EE-002-jquery-validation.md +8 -0
  13. package/flows/packs/j2ee-nextjs-spa/patterns/PAT-J2EE-003-session-stepper.md +9 -0
  14. package/flows/packs/j2ee-nextjs-spa/patterns/PAT-J2EE-004-esb-port.md +10 -0
  15. package/flows/packs/j2ee-nextjs-spa/patterns/PAT-J2EE-005-include-shell.md +8 -0
  16. package/flows/packs/j2ee-nextjs-spa/prompts/analysis.md +27 -0
  17. package/flows/packs/j2ee-nextjs-spa/prompts/bdd.md +13 -0
  18. package/flows/packs/j2ee-nextjs-spa/prompts/implement.md +26 -0
  19. package/flows/packs/j2ee-nextjs-spa/prompts/plan.md +17 -0
  20. package/flows/packs/j2ee-nextjs-spa/prompts/review.md +17 -0
  21. package/flows/packs/j2ee-nextjs-spa/prompts/spec.md +34 -0
  22. package/flows/packs/j2ee-nextjs-spa/prompts/survey-refine.md +22 -0
  23. package/flows/packs/j2ee-nextjs-spa/prompts/survey-triage.md +20 -0
  24. package/flows/packs/j2ee-nextjs-spa/reviewers/acl-purity.md +12 -0
  25. package/flows/packs/j2ee-nextjs-spa/reviewers/house-style.md +13 -0
  26. package/flows/packs/jsp-bff-nextjs/prompts/survey-refine.md +22 -0
  27. package/flows/packs/jsp-bff-nextjs/prompts/survey-triage.md +20 -0
  28. package/flows/packs/jsp-nextjs/prompts/survey-refine.md +22 -0
  29. package/flows/packs/jsp-nextjs/prompts/survey-triage.md +20 -0
  30. package/package.json +5 -5
@@ -0,0 +1,152 @@
1
+ // Convert the whole legacy estate: walk the survey inventory in wave order, one branch per page, progress board, estimated-cost migration report.
2
+ //
3
+ // Runs rooted at the TARGET repository (`--repo <nextjs>`), with
4
+ // LLM4TS_LEGACY_REPO pointing at the extracted legacy repository:
5
+ //
6
+ // LLM4TS_LEGACY_REPO=~/estates/demo-bank-legacy \
7
+ // llm4ts run convert-all --repo ~/estates/demo-bank-nextjs
8
+ //
9
+ // Order comes from the approved docs/modernization/wave-plan.md when present,
10
+ // otherwise every extracted spec alphabetically. The board (a BoardSync port)
11
+ // always writes the local files at .llm4ts/convert/board.{json,md}; when
12
+ // LLM4TS_ADO_ORG_URL and LLM4TS_ADO_PROJECT are set, an Azure DevOps
13
+ // work-item mirror is added via the az CLI (LLM4TS_ADO_REPO defaults to the
14
+ // project; auth belongs to az itself — `az devops login` or
15
+ // AZURE_DEVOPS_EXT_PAT, never an llm4ts variable, per ADR 0011).
16
+ // A failing page is marked failed and the walk continues
17
+ // (LLM4TS_FAIL_FAST=1 stops instead); pages already done on the board are
18
+ // skipped, so re-running resumes. Every token/cost figure — including the
19
+ // closing whole-estate projection in docs/conversion/migration-report.md —
20
+ // is an ESTIMATE, and the report says so.
21
+ import { basename, join } from "node:path";
22
+ import * as Effect from "effect/Effect";
23
+ import { AdoConfig, makeAzureDevOpsTool } from "@llm4ts/flow/AzureDevOpsTool";
24
+ import { BoardItem, composeBoardSync, makeAdoBoardSync, makeLocalBoardSync } from "@llm4ts/flow/BoardSync";
25
+ import { describeFlowError } from "@llm4ts/flow/FlowError";
26
+ import { Info } from "@llm4ts/flow/FlowEvents";
27
+ import { stage } from "@llm4ts/flow/PlanExecution";
28
+ import { asReadOnly, coderFromEnv } from "@llm4ts/runner/Connectors";
29
+ import { resolveFlowInput } from "@llm4ts/runner/FlowArgs";
30
+ import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner";
31
+ import { nodePlainFileStore } from "@llm4ts/runner/NodePlainFileStore";
32
+ import { nodeProcessExecutor } from "@llm4ts/runner/NodeProcessExecutor";
33
+ import { conversionInventory, convertPage, migrationReport, setupConversion } from "./lib/convert.ts";
34
+ const program = Effect.gen(function* () {
35
+ const input = yield* resolveFlowInput("Convert the legacy estate into the destination SPA");
36
+ const coder = coderFromEnv(process.env);
37
+ yield* runNode({
38
+ workDir: input.workDir,
39
+ workspace: input.workspace,
40
+ userPrompt: input.prompt,
41
+ coder,
42
+ reasoning: asReadOnly(coder),
43
+ reviewers: [asReadOnly(coder)],
44
+ environment: process.env
45
+ }, (context) => Effect.gen(function* () {
46
+ const deps = yield* setupConversion(context, input, process.env, import.meta.dirname);
47
+ const files = nodePlainFileStore;
48
+ const boardTitle = `Conversion: ${basename(input.workDir)}`;
49
+ const boards = [
50
+ makeLocalBoardSync(files, join(input.workDir, ".llm4ts", "convert"), boardTitle)
51
+ ];
52
+ const orgUrl = process.env.LLM4TS_ADO_ORG_URL?.trim();
53
+ const project = process.env.LLM4TS_ADO_PROJECT?.trim();
54
+ if (orgUrl !== undefined && orgUrl.length > 0 && project !== undefined) {
55
+ // az CLI owns the credentials (`az devops login` / AZURE_DEVOPS_EXT_PAT);
56
+ // no PAT ever passes through llm4ts configuration.
57
+ const ado = makeAzureDevOpsTool(AdoConfig.make({
58
+ orgUrl,
59
+ project,
60
+ repository: process.env.LLM4TS_ADO_REPO?.trim() || project
61
+ }), nodeProcessExecutor, input.workDir, context.events);
62
+ boards.push(yield* makeAdoBoardSync(ado, boardTitle));
63
+ yield* context.events.publish(Info.make({ message: `ADO board mirror enabled: ${orgUrl}/${project}` }));
64
+ }
65
+ const board = composeBoardSync(boards);
66
+ const inventory = yield* stage(context.events, "inventory", conversionInventory(files, deps.legacy, deps.legacyDir, deps.pack));
67
+ if (inventory.length === 0) {
68
+ yield* context.events.publish(Info.make({ message: "inventory is empty — extract the legacy estate first" }));
69
+ return;
70
+ }
71
+ // The whole estate lands on the board as planned up front — the
72
+ // breadth view exists from minute one.
73
+ yield* stage(context.events, "board", board.plan(inventory.map(({ page, wave }) => BoardItem.make({
74
+ id: page,
75
+ title: page,
76
+ status: "planned",
77
+ ...(wave === undefined ? {} : { wave })
78
+ }))));
79
+ const baseBranch = yield* context.git.currentBranch;
80
+ const failFast = process.env.LLM4TS_FAIL_FAST === "1";
81
+ for (const { page } of inventory) {
82
+ const snapshot = yield* board.snapshot;
83
+ const known = snapshot.items.find((item) => item.id === page);
84
+ if (known !== undefined && known.status !== "planned" && known.status !== "failed") {
85
+ yield* context.events.publish(Info.make({ message: `resume: ${page} is already ${known.status} — skipping` }));
86
+ continue;
87
+ }
88
+ const specPath = join(deps.legacyDir, deps.pack.specsDir, `${page}.md`);
89
+ if ((yield* files.read(specPath)) === undefined) {
90
+ yield* board.skip(page, "no extracted spec");
91
+ continue;
92
+ }
93
+ const checkpoint = yield* context.git.checkpoint;
94
+ yield* board.start(page);
95
+ const result = yield* Effect.result(convertPage(deps, page));
96
+ if (result._tag === "Success") {
97
+ const outcome = result.success;
98
+ yield* board.complete(page, {
99
+ branch: outcome.branch,
100
+ reportPath: outcome.reportPath,
101
+ ...(outcome.estimatedTokens === undefined
102
+ ? {}
103
+ : { estimatedTokens: outcome.estimatedTokens }),
104
+ ...(outcome.estimatedCostUsd === undefined
105
+ ? {}
106
+ : { estimatedCostUsd: outcome.estimatedCostUsd })
107
+ });
108
+ yield* context.git.checkout(baseBranch);
109
+ }
110
+ else {
111
+ const reason = describeFlowError(result.failure);
112
+ // A stuck page must not sink the walk: reset the working tree,
113
+ // mark the failure, keep going (LLM4TS_FAIL_FAST=1 to stop).
114
+ yield* context.git.rollback(checkpoint);
115
+ yield* context.git.checkout(baseBranch);
116
+ yield* board.fail(page, reason);
117
+ if (failFast) {
118
+ return yield* Effect.fromResult(result);
119
+ }
120
+ yield* context.events.publish(Info.make({ message: `page ${page} failed — continuing: ${reason}` }));
121
+ }
122
+ }
123
+ const finalBoard = yield* board.snapshot;
124
+ const rows = finalBoard.items.flatMap((item) => item.status === "done" || item.status === "failed" || item.status === "skipped"
125
+ ? [
126
+ {
127
+ page: item.id,
128
+ outcome: item.status,
129
+ ...(item.detail === undefined ? {} : { detail: item.detail }),
130
+ ...(item.estimatedTokens === undefined
131
+ ? {}
132
+ : { estimatedTokens: item.estimatedTokens }),
133
+ ...(item.estimatedCostUsd === undefined
134
+ ? {}
135
+ : { estimatedCostUsd: item.estimatedCostUsd })
136
+ }
137
+ ]
138
+ : []);
139
+ const remaining = finalBoard.items
140
+ .filter((item) => item.status === "planned" || item.status === "failed")
141
+ .map((item) => item.id);
142
+ yield* stage(context.events, "report", files
143
+ .writeAtomic(join(input.workDir, "docs", "conversion", "migration-report.md"), migrationReport(rows, remaining))
144
+ .pipe(Effect.andThen(context.git.commitAll("convert: migration report and board"))));
145
+ yield* context.events.publish(Info.make({
146
+ message: `estate walk complete — ${rows.filter((row) => row.outcome === "done").length} ` +
147
+ `converted, ${remaining.length} remaining; ` +
148
+ "report: docs/conversion/migration-report.md (all figures estimated)"
149
+ }));
150
+ }));
151
+ });
152
+ runFlowMain(program);
@@ -0,0 +1,56 @@
1
+ // Convert ONE legacy J2EE page into the destination Next.js SPA: branch per page, contract-first mocked ACL, judged gates, estimated costs.
2
+ //
3
+ // Runs rooted at the TARGET repository (`--repo <nextjs>`), with
4
+ // LLM4TS_LEGACY_REPO pointing at the extracted legacy repository. The task
5
+ // text is the page name (the program name of its extracted spec):
6
+ //
7
+ // LLM4TS_LEGACY_REPO=~/estates/demo-bank-legacy \
8
+ // llm4ts run convert-page --repo ~/estates/demo-bank-nextjs accountOverview
9
+ //
10
+ // Not clean-room (ADR 0012): the coder gets the schema-validated Page Spec as
11
+ // contract plus bounded legacy source evidence, the destination's own pages as
12
+ // style guide, and a deterministically generated OpenAPI anti-corruption
13
+ // contract. Gates: typecheck+lint+test per task, test+build to finish, then a
14
+ // per-page spec-compliance judge with bounded feedback rounds
15
+ // (LLM4TS_JUDGE_ROUNDS). One branch `convert/<page>` per run, no PR — the
16
+ // branch awaits human review; the conversion report is committed with it.
17
+ // Token/cost figures are ESTIMATES (LLM4TS_ESTIMATE_MODEL,
18
+ // LLM4TS_ESTIMATE_CHARS_PER_TOKEN). Pack: LLM4TS_PACK (default
19
+ // packs/j2ee-nextjs-spa).
20
+ import * as Effect from "effect/Effect";
21
+ import { FlowAborted } from "@llm4ts/flow/FlowError";
22
+ import { Info } from "@llm4ts/flow/FlowEvents";
23
+ import { asReadOnly, coderFromEnv } from "@llm4ts/runner/Connectors";
24
+ import { resolveFlowInput } from "@llm4ts/runner/FlowArgs";
25
+ import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner";
26
+ import { convertPage, setupConversion } from "./lib/convert.ts";
27
+ const program = Effect.gen(function* () {
28
+ const input = yield* resolveFlowInput("Convert one legacy page into the destination SPA");
29
+ const page = input.prompt.trim().split(/\s+/)[0] ?? "";
30
+ const coder = coderFromEnv(process.env);
31
+ yield* runNode({
32
+ workDir: input.workDir,
33
+ workspace: input.workspace,
34
+ userPrompt: input.prompt,
35
+ coder,
36
+ reasoning: asReadOnly(coder),
37
+ reviewers: [asReadOnly(coder)],
38
+ environment: process.env
39
+ }, (context) => Effect.gen(function* () {
40
+ if (page.length === 0) {
41
+ return yield* FlowAborted.make({
42
+ message: "pass the page name to convert, e.g.: llm4ts run convert-page accountOverview"
43
+ });
44
+ }
45
+ const deps = yield* setupConversion(context, input, process.env, import.meta.dirname);
46
+ const outcome = yield* convertPage(deps, page);
47
+ yield* context.events.publish(Info.make({
48
+ message: `converted ${outcome.page} on branch ${outcome.branch} — ` +
49
+ `report at ${outcome.reportPath}` +
50
+ (outcome.estimatedTokens === undefined
51
+ ? ""
52
+ : ` (~${outcome.estimatedTokens} tokens estimated)`)
53
+ }));
54
+ }));
55
+ });
56
+ runFlowMain(program);
@@ -99,7 +99,7 @@ const program = Effect.gen(function* () {
99
99
  phases.push(makeBenchPhase(observation, name, Date.now() - began));
100
100
  return value;
101
101
  });
102
- const programs = yield* measured("inventory", matchingFiles(estate, pack.programs ?? pack.sources ?? ".*"));
102
+ const programs = yield* measured("inventory", matchingFiles(estate, pack.programs ?? pack.sources ?? ".*", pack.exclude));
103
103
  if (programs.length === 0) {
104
104
  return yield* FlowAborted.make({
105
105
  message: `no source units matched the pack's programs/sources regex under ${input.workDir}`
@@ -24,8 +24,13 @@
24
24
  // context is bounded by LLM4TS_CONTEXT_BUDGET (chars;
25
25
  // LLM4TS_JUDGE_SOURCES_LIMIT is the deprecated alias). The analyst is bounded
26
26
  // by LLM4TS_ANALYST_TURNS and LLM4TS_MAX_CLOSURE_FILES.
27
+ // LLM4TS_EXTRACT_CONCURRENCY=<n> extracts and judges n programs at once (default
28
+ // 1): the programs of a wave are independent, each gets a commit scoped to its
29
+ // own files, and a failure lets in-flight programs finish before it surfaces.
27
30
  import { join } from "node:path";
28
31
  import * as Effect from "effect/Effect";
32
+ import * as Ref from "effect/Ref";
33
+ import * as Semaphore from "effect/Semaphore";
29
34
  import { Sample } from "@llm4ts/core/eval/Eval";
30
35
  import { judge } from "@llm4ts/core/eval/Judge";
31
36
  import { budget, capped, withShrink } from "@llm4ts/flow/Context";
@@ -42,7 +47,7 @@ import { cachedReview } from "@llm4ts/flow/ReviewCache";
42
47
  import { coverage, coverageUnits, features, matchingFiles } from "@llm4ts/flow/SpecChecks";
43
48
  import { SurveyGraph, closureFor, surveyGraph } from "@llm4ts/flow/Survey";
44
49
  import { withDraftApproval, requireApproval } from "@llm4ts/modernize/Approval";
45
- import { ProgramArtifacts, ProgramUnit, extractProgramsResumably } from "@llm4ts/modernize/Artifacts";
50
+ import { ProgramArtifacts, ProgramUnit, extractProgramsResumably, programArtifactPaths } from "@llm4ts/modernize/Artifacts";
46
51
  import { asReadOnly, coderFromEnv, withTurnLimit } from "@llm4ts/runner/Connectors";
47
52
  import { resolveFlowInput } from "@llm4ts/runner/FlowArgs";
48
53
  import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner";
@@ -63,6 +68,14 @@ const analystTurns = () => positiveEnvInt("LLM4TS_ANALYST_TURNS", 48);
63
68
  * than this gets a bounded, visible subset rather than an unbounded read.
64
69
  */
65
70
  const maxClosureFiles = () => positiveEnvInt("LLM4TS_MAX_CLOSURE_FILES", 40);
71
+ /**
72
+ * Programs extracted (and judged) at once. The programs of a wave are
73
+ * independent — each analyst reads its source and resolved closure, writes
74
+ * its own four files, and gets its own scoped commit — so this divides wall
75
+ * time without changing tokens or cost. Default 1 keeps the sequential
76
+ * narration; the bound is about the coder seat's quota, not correctness.
77
+ */
78
+ const extractConcurrency = () => positiveEnvInt("LLM4TS_EXTRACT_CONCURRENCY", 1);
66
79
  /** `cobol/ACCTXFR.cbl` → `ACCTXFR`: the program name keying every per-program artifact. */
67
80
  const programName = (relativePath) => {
68
81
  const base = relativePath.slice(relativePath.lastIndexOf("/") + 1);
@@ -203,7 +216,7 @@ const program = Effect.gen(function* () {
203
216
  ]
204
217
  .filter((part) => part !== undefined)
205
218
  .join("\n\n");
206
- const all = yield* stage(context.events, "inventory", matchingFiles(repo, pack.programs ?? pack.sources ?? ".*"));
219
+ const all = yield* stage(context.events, "inventory", matchingFiles(repo, pack.programs ?? pack.sources ?? ".*", pack.exclude));
207
220
  const wave = process.env.LLM4TS_WAVE?.trim();
208
221
  const programs = wave === undefined || wave.length === 0
209
222
  ? all
@@ -239,35 +252,48 @@ const program = Effect.gen(function* () {
239
252
  message: "pack has no '## Survey:' edge regexes — the analyst gets no resolved closure"
240
253
  }))
241
254
  .pipe(Effect.as(SurveyGraph.make({ nodes: [], edges: [] })))
242
- : surveyGraph(repo, pack.sources ?? ".*", pack.coverage, pack.survey));
255
+ : surveyGraph(repo, pack.sources ?? ".*", pack.coverage, pack.survey, {
256
+ ...(pack.exclude === undefined ? {} : { exclude: pack.exclude })
257
+ }));
243
258
  // One structured analyst call per program, resumable per program: a rerun
244
- // skips every program whose spec exists, and each program gets its own commit.
259
+ // skips every program whose spec exists, and each program gets its own
260
+ // commit scoped to its four files. LLM4TS_EXTRACT_CONCURRENCY runs that
261
+ // many programs at once; the commit is the one step serialised.
262
+ const concurrency = extractConcurrency();
245
263
  yield* stage(context.events, "extract", Effect.gen(function* () {
246
- for (const [index, unit] of units.entries()) {
247
- const summary = yield* extractProgramsResumably(files, [unit], (target) => Effect.gen(function* () {
264
+ const started = yield* Ref.make(0);
265
+ const commitLock = yield* Semaphore.make(1);
266
+ if (concurrency > 1) {
267
+ yield* context.events.publish(Info.make({
268
+ message: `extracting up to ${concurrency} program(s) at once (LLM4TS_EXTRACT_CONCURRENCY)`
269
+ }));
270
+ }
271
+ const summary = yield* extractProgramsResumably(files, units, (target) => Effect.gen(function* () {
272
+ const ordinal = yield* Ref.updateAndGet(started, (count) => count + 1);
273
+ yield* context.events.publish(Info.make({
274
+ message: `extracting ${target.sourcePath} (${ordinal}/${units.length})`
275
+ }));
276
+ return yield* structuredAndPublish(context.coder, context.events, `${system}\n\n${programAsk(pack, target.sourcePath, closureFor(graph, target.name, maxClosureFiles()))}`, ProgramArtifacts, programArtifactsJsonSchema, "coder").pipe(
277
+ // A turn-limit trip is the wedged-agent tail, not a
278
+ // failure: keep whatever the analyst already produced.
279
+ Effect.catchIf((error) => error.cause?._tag === "TurnLimitError", (error) => Effect.gen(function* () {
280
+ const existing = yield* files.read(join(modDirAbs, "specs", `${target.name}.md`));
281
+ if (existing === undefined) {
282
+ return yield* Effect.fail(error);
283
+ }
248
284
  yield* context.events.publish(Info.make({
249
- message: `extracting ${target.sourcePath} (${index + 1}/${units.length})`
285
+ message: `turn limit hit on ${target.sourcePath} after its spec was written — keeping the work`
250
286
  }));
251
- return yield* structuredAndPublish(context.coder, context.events, `${system}\n\n${programAsk(pack, target.sourcePath, closureFor(graph, target.name, maxClosureFiles()))}`, ProgramArtifacts, programArtifactsJsonSchema, "coder").pipe(
252
- // A turn-limit trip is the wedged-agent tail, not a
253
- // failure: keep whatever the analyst already produced.
254
- Effect.catchIf((error) => error.cause?._tag === "TurnLimitError", (error) => Effect.gen(function* () {
255
- const existing = yield* files.read(join(modDirAbs, "specs", `${target.name}.md`));
256
- if (existing === undefined) {
257
- return yield* Effect.fail(error);
258
- }
259
- yield* context.events.publish(Info.make({
260
- message: `turn limit hit on ${target.sourcePath} after its spec was written — keeping the work`
261
- }));
262
- return ProgramArtifacts.make({
263
- spec: existing,
264
- feature: "",
265
- traceability: "",
266
- mapping: ""
267
- });
268
- })));
269
- }), modDirAbs);
270
- if (summary.created.length > 0) {
287
+ return ProgramArtifacts.make({
288
+ spec: existing,
289
+ feature: "",
290
+ traceability: "",
291
+ mapping: ""
292
+ });
293
+ })));
294
+ }), modDirAbs, {
295
+ concurrency,
296
+ onCreated: (unit) => Effect.gen(function* () {
271
297
  // Tag the traceability fragment with the cards this program's
272
298
  // source matches — regex-decided, so implementation's playbook
273
299
  // is reproducible.
@@ -278,15 +304,18 @@ const program = Effect.gen(function* () {
278
304
  const fragment = (yield* files.read(fragmentPath)) ?? "";
279
305
  yield* files.writeAtomic(fragmentPath, `${fragment.trimEnd()}\n\nPatterns: ${matched.map((card) => card.id).join(", ")}\n`);
280
306
  }
281
- yield* context.git
282
- .commitAll(`modernize(${pack.name}): spec ${unit.name}`)
283
- .pipe(Effect.asVoid);
284
- }
285
- else {
286
- yield* context.events.publish(Info.make({
287
- message: `resume: specs/${unit.name}.md exists — skipping ${unit.sourcePath}`
288
- }));
289
- }
307
+ // Only this program's files, under one permit: a concurrent
308
+ // sibling's half-written artifacts must never ride along.
309
+ yield* commitLock.withPermit(context.git
310
+ .commitPaths(`modernize(${pack.name}): spec ${unit.name}`, programArtifactPaths(unit, ModDir))
311
+ .pipe(Effect.asVoid));
312
+ })
313
+ });
314
+ if (summary.skipped.length > 0) {
315
+ yield* context.events.publish(Info.make({
316
+ message: `resume: ${summary.skipped.length} program(s) already have a spec — skipped ` +
317
+ summary.skipped.join(", ")
318
+ }));
290
319
  }
291
320
  }));
292
321
  // traceability.md / mapping.md are regenerated from the fragments — fixes
@@ -377,10 +406,9 @@ const program = Effect.gen(function* () {
377
406
  });
378
407
  const covered = yield* coverage(repo, pack.coverage, trace);
379
408
  const wellFormed = yield* features(repo, join(ModDir, "features"));
380
- const judged = [];
381
- for (const unit of units) {
382
- judged.push(yield* judgeProgram(unit));
383
- }
409
+ // Verdicts are per program and cached per program, so they judge
410
+ // under the same bound as extraction; the merge is order-stable.
411
+ const judged = yield* Effect.forEach(units, judgeProgram, { concurrency });
384
412
  return mergeReviewResults([covered, wellFormed, docs, ...judged]);
385
413
  });
386
414
  // One bounded fix turn per sub-bar program (own commit) plus one residual
Binary file
@@ -0,0 +1,7 @@
1
+ Regexes miss links in complex COBOL/JCL sources: dynamic CALLs (CALL WS-PROGRAM
2
+ where the target sits in WORKING-STORAGE), JCL symbolic parameters (EXEC
3
+ PGM=&PGM set earlier in the job or in a PROC), PROC expansions, control cards
4
+ and SYSIN members naming programs, and COPY REPLACING variants. Prioritise jobs
5
+ with fewer outgoing edges than steps, programs nothing references, and copybooks
6
+ with degree 0. Calls to system or runtime services (CEE*, DFH*, IGZ*, SQL
7
+ preprocessor stubs) are not estate edges — note them.
@@ -0,0 +1,8 @@
1
+ Units are COBOL programs, copybooks, and JCL jobs. Copybooks with many callers
2
+ are shared data contracts — migrate them with their first consumer or wrap them.
3
+ Jobs are scheduler entry points: a job nothing calls is still live if a
4
+ scheduler runs it, so it is a "wrap" (keep on the platform, front with an API)
5
+ until every program it steps through has been rewritten, never a "retire" on
6
+ graph evidence alone. Programs with no callers and no job step are the retire
7
+ candidates; give the degree evidence. Batch windows and EOD chains make good
8
+ wave boundaries: keep a job's step programs in the same or an earlier wave.
@@ -0,0 +1,7 @@
1
+ Regexes miss links in complex COBOL/JCL sources: dynamic CALLs (CALL WS-PROGRAM
2
+ where the target sits in WORKING-STORAGE), JCL symbolic parameters (EXEC
3
+ PGM=&PGM set earlier in the job or in a PROC), PROC expansions, control cards
4
+ and SYSIN members naming programs, and COPY REPLACING variants. Prioritise jobs
5
+ with fewer outgoing edges than steps, programs nothing references, and copybooks
6
+ with degree 0. Calls to system or runtime services (CEE*, DFH*, IGZ*, SQL
7
+ preprocessor stubs) are not estate edges — note them.
@@ -0,0 +1,8 @@
1
+ Units are COBOL programs, copybooks, and JCL jobs. Copybooks with many callers
2
+ are shared data contracts — migrate them with their first consumer or wrap them.
3
+ Jobs are scheduler entry points: a job nothing calls is still live if a
4
+ scheduler runs it, so it is a "wrap" (keep on the platform, front with an API)
5
+ until every program it steps through has been rewritten, never a "retire" on
6
+ graph evidence alone. Programs with no callers and no job step are the retire
7
+ candidates; give the degree evidence. Batch windows and EOD chains make good
8
+ wave boundaries: keep a job's step programs in the same or an earlier wave.
@@ -0,0 +1,48 @@
1
+ # Pack: j2ee-nextjs-spa
2
+
3
+ source: jsp
4
+ scaffold: ../../fixtures/scaffolds/nextjs-spa
5
+ sources: .*\.(jsp|java|xml)
6
+ programs: .*\.jsp
7
+ specs-dir: docs/modernization/specs
8
+ features-dir: docs/modernization/features
9
+ programFiles: (?:src/app/<NAME>(?:/.*)?|src/services/<NAME>(?:/.*)?|contracts/<NAME>\.openapi\.yaml|tests/<NAME>\..*)
10
+
11
+ ## Gates
12
+
13
+ - typecheck: pnpm typecheck
14
+ - lint: pnpm lint
15
+ - test: pnpm test
16
+ - build: pnpm build
17
+
18
+ ## Judge
19
+
20
+ - completeness (0..2): Every screen, servlet mapping (web.xml url-pattern), form field, navigation path, validation rule (client AND server side), user-facing message, session attribute, and API/ESB call in the JSP/servlet source is captured in the spec and BDD scenarios. Score 2 only if nothing material is missing.
21
+ - faithfulness (0..2): Every statement is grounded in the source: validation thresholds, exact message texts, redirect targets, session behaviour, DTO field names, and which ESB service each endpoint wraps match the code, and nothing is invented. Score 2 only if fully source-grounded.
22
+ - testability (0..2): Scenarios are concrete user journeys — specific accounts, amounts, and the exact message or destination screen expected; no vague language ("shows an error", "handled gracefully"). Score 2 only if every scenario is directly encodable as a test.
23
+ - pagespec (0..2): The spec contains exactly one ```json pagespec fenced block; its forms, apiCalls (with esbService), dtos (legacy→domain rename table), navigation, and sessionState agree with the prose and with the source; domain names are business language, never legacy abbreviations. Score 2 only if the block is present, consistent, and complete.
24
+
25
+ ## Coverage: servlet-url
26
+
27
+ files: .*web\.xml
28
+ unit: <url-pattern>([^<]+)</url-pattern>
29
+
30
+ ## Coverage: jsp-form
31
+
32
+ files: .*\.jsp
33
+ unit: action="([^"]+)"
34
+
35
+ ## Coverage: jsp-ajax
36
+
37
+ files: .*\.jsp
38
+ unit: url:\s*['"]([^'"]+)['"]
39
+
40
+ ## Survey: jsp-include
41
+
42
+ files: .*\.jsp
43
+ unit: <jsp:include page="([^"]+)"
44
+
45
+ ## Survey: servlet-class
46
+
47
+ files: .*web\.xml
48
+ unit: <servlet-class>[a-z.]*\.([A-Za-z0-9]+)</servlet-class>
@@ -0,0 +1,8 @@
1
+ ---
2
+ match: <c:forEach|<%[^=]*for\s*\(
3
+ ---
4
+ A JSTL/scriptlet row loop becomes the destination DataTable component with
5
+ declarative columns: the loop body's cells map to column render functions, and
6
+ status-code-to-label mappings move into a typed lookup beside the page. The
7
+ data comes from a port call in an effect on mount — never fetched in the
8
+ component body, never re-sorted client-side unless the legacy page sorted.
@@ -0,0 +1,8 @@
1
+ ---
2
+ match: \$\(['"]#|validate\.js|onsubmit=
3
+ ---
4
+ jQuery/inline validation becomes the house Form component's validation map:
5
+ one entry per spec rule, message text VERBATIM, evaluated in the spec's order
6
+ (first-failure-wins). Where the legacy client and server rules diverged, the
7
+ spec's per-rule enforcedAt says which set is contract — implement the union,
8
+ flag any contradiction as an open question rather than silently choosing.
@@ -0,0 +1,9 @@
1
+ ---
2
+ match: HttpSession|session\.setAttribute|session\.getAttribute
3
+ ---
4
+ An HttpSession-carried multi-step flow becomes explicit client state driven by
5
+ the destination Stepper: one state object typed after the spec's sessionState
6
+ (the legacy draft DTO, domain-renamed), owned by the flow's top-level page,
7
+ passed down per step. Refresh/back semantics are explicit — the legacy app got
8
+ them free from the session; the SPA must decide and the spec's navigation
9
+ section says what to preserve. Never scatter the draft across components.
@@ -0,0 +1,10 @@
1
+ ---
2
+ match: Esb[A-Za-z]+Service|ESB_[A-Z_]+
3
+ ---
4
+ Every EsbXxxService wrapper the servlet called becomes one port operation in
5
+ the page's anti-corruption contract: the OpenAPI operation carries the domain
6
+ name, the port interface mirrors it, and the mock adapter returns
7
+ contract-shaped fixtures. The ESB routine name lands in the contract's
8
+ description (the future B4F team's pointer), never in code identifiers. If the
9
+ servlet combined two ESB calls, the port exposes the page's need (one
10
+ operation), and the composition note goes in the contract description.
@@ -0,0 +1,8 @@
1
+ ---
2
+ match: <jsp:include|<%@\s*include
3
+ ---
4
+ header/nav/footer jsp:include shells are ALREADY provided by the destination
5
+ app shell (layout + PageLayout) — a converted page never re-renders chrome.
6
+ Map the legacy page body into PageLayout's slots, and turn nav.jsp links into
7
+ the routes of pages that exist (converted or exemplar); links to not-yet-
8
+ converted pages stay out of nav rather than dangling.
@@ -0,0 +1,27 @@
1
+ You are a legacy-web reverse-engineering analyst on a bank modernization. Extract
2
+ the COMPLETE observable behaviour of one J2EE/JSP page into a Page Spec precise
3
+ enough that a converter who prefers the spec (but may consult the source) can
4
+ rebuild it inside an existing Next.js SPA.
5
+
6
+ How to read the app:
7
+
8
+ - Start from WEB-INF/web.xml: every url-pattern is an entry point and must be
9
+ accounted for.
10
+ - Servlets carry the behaviour: request-parameter validation (exact rules AND
11
+ exact error message texts), session reads/writes, redirects vs forwards, and
12
+ every ESB service call behind each endpoint (the EsbXxxService wrappers name
13
+ the routine — record it; ALL business logic lives behind those services and
14
+ must NEVER be reimplemented client-side).
15
+ - JSPs carry the screens: what is displayed per row, status-code-to-label
16
+ mappings, forms (fields, hidden fields, where they submit), links between
17
+ screens, and every jQuery $.ajax call.
18
+ - Client-side jQuery validation and server-side servlet validation often
19
+ DIVERGE — record each rule with where it is enforced (client, server, both);
20
+ the divergences are findings, not noise.
21
+ - Hidden fields and HttpSession attributes are STATE the SPA must own
22
+ explicitly — document what flows through them.
23
+ - DTO classes use abbreviated legacy names (acctNo, curBal): propose a domain
24
+ rename for every field — the anti-corruption table.
25
+
26
+ Never invent behaviour; message texts and thresholds are contract — record them
27
+ verbatim. Genuinely ambiguous behaviour goes in "Open questions", not guesses.
@@ -0,0 +1,13 @@
1
+ Write Gherkin .feature files encoding the spec as executable user journeys —
2
+ these become the component tests of the converted page.
3
+
4
+ - One feature per screen/flow.
5
+ - User vocabulary, not servlet vocabulary: "When she submits a transfer of
6
+ 3000.00, Then she is asked to confirm", not "doPost forwards to confirm.jsp".
7
+ - Error messages are asserted VERBATIM — they are contract.
8
+ - Concrete values everywhere: real account numbers, amounts, statuses from the
9
+ fixture data.
10
+ - Cover: the happy path, EVERY validation rule and its message (noting whether
11
+ the legacy app enforced it client-side, server-side, or both), threshold
12
+ boundaries, confirmation flows, and the state that must survive navigation
13
+ (what the session or hidden fields carried).
@@ -0,0 +1,26 @@
1
+ You convert one legacy J2EE page into an existing client-only Next.js SPA, one
2
+ task at a time. The Page Spec is the contract; the legacy source excerpts you
3
+ are given are evidence for disambiguation — prefer the spec, and when you must
4
+ lean on the source, keep domain names, never legacy abbreviations.
5
+
6
+ Non-negotiables:
7
+
8
+ - Read the destination repo's CONTRIBUTING.md and the existing pages under
9
+ src/app/ FIRST and imitate them: same design-system components, same form
10
+ handling, same port/adapter shape, same test style. "Code the new like what
11
+ we have."
12
+ - ALL business logic stays behind the service port — the mock adapter fakes
13
+ transport, never rules. If the legacy page computed something server-side
14
+ (fees, limits, validation of business state), that is a port operation, not
15
+ client code.
16
+ - Components never call fetch; only adapters touch transport. Pages depend on
17
+ the port through the registry, so the mock swaps for the real gateway later
18
+ without touching the page.
19
+ - Validation error messages match the spec VERBATIM, in the spec's order.
20
+ - Session attributes and hidden-field flows become explicit client state
21
+ (the Stepper pattern for multi-step flows) — never re-derive a value the
22
+ legacy app carried through the session.
23
+ - Legacy DTO names must not appear anywhere in the new code — use the spec's
24
+ domain renames. Money is exact decimals, never floats.
25
+ - TypeScript strict, no `any`, no type assertions; typecheck, lint, test, and
26
+ build must all stay green.
@@ -0,0 +1,17 @@
1
+ Derive the conversion task list for ONE page from its Page Spec. The
2
+ destination is an existing Next.js SPA with a design system, an AuthProvider,
3
+ and a port/adapter service convention — imitate it, never fight it.
4
+
5
+ - Task 1: the anti-corruption service layer — the typed port interface under
6
+ src/services/<page>/port.ts matching the OpenAPI contract at
7
+ contracts/<page>.openapi.yaml (domain names only), a mock adapter under
8
+ src/services/<page>/mock.ts returning contract-shaped fixture data, and the
9
+ registry wiring. No page code yet.
10
+ - Task 2: the page component(s) under src/app/<page>/ using ONLY the
11
+ destination design-system components and the port — forms, validation with
12
+ VERBATIM messages, navigation, and explicit state for anything the legacy
13
+ app carried in the session or hidden fields.
14
+ - Task 3: component tests under tests/<page>.page.test.tsx in the house test
15
+ style — spec'd fields render, spec'd validations fire with their exact
16
+ messages, the port is called with contract-shaped payloads. Nothing else.
17
+ - Each task names the spec rules and scenarios it covers.
@@ -0,0 +1,17 @@
1
+ You review a finished conversion increment: a page ported from a J2EE/JSP app
2
+ into an existing Next.js SPA with a design system and a port/adapter service
3
+ convention. Judge the implementation against the Page Spec and the destination
4
+ house rules, not your own taste.
5
+
6
+ Look specifically for:
7
+
8
+ - Message drift: validation texts that differ from the spec's verbatim
9
+ messages, or rules evaluated out of the spec's order.
10
+ - ACL leaks: fetch outside adapters, legacy DTO names in new code, business
11
+ logic implemented client-side, a mock adapter embedding rules.
12
+ - Contract drift: port methods that do not match the generated OpenAPI
13
+ contract's operations and shapes.
14
+ - State regressions: session-carried or hidden-field state the legacy app had
15
+ that the SPA lost (multi-step drafts, confirmation data across steps).
16
+ - House drift: hand-rolled UI where a design-system component exists, ad-hoc
17
+ styling, tests outside the house style.
@@ -0,0 +1,34 @@
1
+ The spec markdown for a page has two mandatory parts:
2
+
3
+ 1. Prose sections: Screen (layout, displayed data, status label mappings),
4
+ Forms (each field: name, label, type, required; each validation rule with
5
+ its VERBATIM message and where it is enforced — client, server, or both),
6
+ Navigation (inbound links, outbound links/redirects, step order for
7
+ multi-step flows), API (each endpoint: method, path, request/response
8
+ fields, and the ESB service it wraps), Session state, Anti-corruption
9
+ renames (every legacy DTO field → proposed domain name), Open questions.
10
+
11
+ 2. EXACTLY ONE fenced block starting with ```json pagespec containing a JSON
12
+ object with this shape (all names in domain language where marked):
13
+
14
+ {
15
+ "page": "<program name, e.g. accountOverview>",
16
+ "route": "<legacy url-pattern>",
17
+ "title": "<screen title>",
18
+ "complexity": "low" | "medium" | "high",
19
+ "forms": [{ "name", "action", "fields": [{ "name", "label", "type",
20
+ "required", "validations": [{ "rule", "message", "enforcedAt":
21
+ "client"|"server"|"both" }] }] }],
22
+ "dtos": [{ "legacyName", "domainName", "fields": [{ "legacyName",
23
+ "domainName", "type" }] }],
24
+ "apiCalls": [{ "operation": "<domain verb, e.g. listAccounts>",
25
+ "method", "path", "esbService", "request": [{ "legacyName",
26
+ "domainName", "type" }], "response": [{ ... }] }],
27
+ "navigation": { "inbound": [], "outbound": [], "steps": [] },
28
+ "sessionState": ["<what the session carries and why>"],
29
+ "openQuestions": []
30
+ }
31
+
32
+ The block is machine-validated downstream: malformed JSON or a missing block
33
+ fails the page. The JSON and the prose must agree — the JSON is the contract,
34
+ the prose is the evidence.
@@ -0,0 +1,22 @@
1
+ This is a J2EE web application: units are JSP pages and fragments, servlets and
2
+ supporting Java classes, and deployment descriptors. Regexes miss most of the
3
+ links such an application establishes indirectly:
4
+
5
+ - WEB-INF/web.xml: every servlet-mapping ties a url-pattern to a servlet; every
6
+ servlet-class names a Java unit; welcome-file and error-page entries name
7
+ JSPs. A JSP form action, link, or redirect that targets a url-pattern is an
8
+ edge from the JSP to the servlet behind it.
9
+ - Servlets: request.getRequestDispatcher(...).forward/include and
10
+ response.sendRedirect(...) name the JSP or url-pattern they hand off to;
11
+ new/injected service and DAO classes are outgoing edges to those units.
12
+ - JSPs: static includes (<%@ include file="…" %>), dynamic includes
13
+ (<jsp:include page="…">), <jsp:forward>, <a href="…">, <form action="…">,
14
+ jQuery $.ajax / $.get / $.post url targets, and JSTL <c:import>/<c:url>.
15
+ Resolve a path to the unit whose file name (without extension) matches.
16
+ - Tag libraries and shared layout fragments (header, footer, navigation) are
17
+ units only when they are files in the inventory.
18
+
19
+ Prioritise JSPs with zero incoming edges (are they reached through web.xml or
20
+ a redirect?), servlets nothing maps to, and fragments nothing includes. Calls to
21
+ the ESB, application-server services, and third-party libraries are not estate
22
+ edges — note them.
@@ -0,0 +1,20 @@
1
+ Units are JSP pages, JSP fragments (headers, footers, navigation, included
2
+ partials), servlets, DTO/service classes, and web.xml. Judge them as a web
3
+ estate, not a batch one:
4
+
5
+ - A page reached through a web.xml url-pattern, a link, a form action, or a
6
+ redirect is live even when the regex graph shows few callers — check the
7
+ refine notes before calling it dead. A page nothing reaches and nothing
8
+ includes is the retire candidate; give the evidence.
9
+ - Fragments with many includers are layout, not business logic: they are
10
+ migrated with the first page that needs them (same wave) rather than as a
11
+ wave of their own, and never "retire" while a live page includes them.
12
+ - Servlets and service classes that only front an ESB or backend service are
13
+ "wrap" candidates: the modernized application keeps calling the same
14
+ service through a port, and their business rules must not be re-implemented.
15
+ - web.xml and other descriptors are configuration, not migration units:
16
+ disposition "wrap" with a one-line rationale.
17
+
18
+ Waves are user journeys: keep a page, the servlet it posts to, and the
19
+ fragments it includes together; simple read-only screens first, then CRUD
20
+ screens, then multi-step session-backed flows last.
@@ -0,0 +1,12 @@
1
+ You review one conversion increment for anti-corruption discipline. Judge the
2
+ diff against the Page Spec, not your taste. Flag as Critical:
3
+
4
+ - Any fetch/XMLHttpRequest/axios call outside a service adapter module.
5
+ - Any legacy DTO field name (acctNo-style abbreviations) in page code, port
6
+ types, or tests — the spec's domain renames are mandatory.
7
+ - Business logic in the client: fee/limit/eligibility calculations, rules the
8
+ legacy servlet or ESB service owned, or a mock adapter that embeds rules
9
+ instead of returning contract-shaped data.
10
+ - A page importing a mock adapter directly instead of going through the
11
+ registry/port seam.
12
+ - Port methods that do not match the OpenAPI contract's operations and shapes.
@@ -0,0 +1,13 @@
1
+ You review one conversion increment for destination-repo fidelity — the new
2
+ page must read as if the resident team wrote it. Judge the diff against the
3
+ destination's CONTRIBUTING.md and its existing pages. Flag as Major:
4
+
5
+ - Hand-rolled UI where a design-system component exists (raw <table> instead
6
+ of DataTable, raw <input>+<label> instead of Field, ad-hoc wizard state
7
+ instead of Stepper).
8
+ - Styling outside the token/component classes: inline style objects, new CSS
9
+ files, hard-coded colors.
10
+ - Form handling that bypasses the house Form component's validation map.
11
+ - Tests that diverge from the house style (different render helpers, missing
12
+ AuthProvider wrapper, snapshot tests where the exemplars assert behaviour).
13
+ - Auth handled ad hoc instead of through useAuth()/AuthProvider.
@@ -0,0 +1,22 @@
1
+ This is a J2EE web application: units are JSP pages and fragments, servlets and
2
+ supporting Java classes, and deployment descriptors. Regexes miss most of the
3
+ links such an application establishes indirectly:
4
+
5
+ - WEB-INF/web.xml: every servlet-mapping ties a url-pattern to a servlet; every
6
+ servlet-class names a Java unit; welcome-file and error-page entries name
7
+ JSPs. A JSP form action, link, or redirect that targets a url-pattern is an
8
+ edge from the JSP to the servlet behind it.
9
+ - Servlets: request.getRequestDispatcher(...).forward/include and
10
+ response.sendRedirect(...) name the JSP or url-pattern they hand off to;
11
+ new/injected service and DAO classes are outgoing edges to those units.
12
+ - JSPs: static includes (<%@ include file="…" %>), dynamic includes
13
+ (<jsp:include page="…">), <jsp:forward>, <a href="…">, <form action="…">,
14
+ jQuery $.ajax / $.get / $.post url targets, and JSTL <c:import>/<c:url>.
15
+ Resolve a path to the unit whose file name (without extension) matches.
16
+ - Tag libraries and shared layout fragments (header, footer, navigation) are
17
+ units only when they are files in the inventory.
18
+
19
+ Prioritise JSPs with zero incoming edges (are they reached through web.xml or
20
+ a redirect?), servlets nothing maps to, and fragments nothing includes. Calls to
21
+ the ESB, application-server services, and third-party libraries are not estate
22
+ edges — note them.
@@ -0,0 +1,20 @@
1
+ Units are JSP pages, JSP fragments (headers, footers, navigation, included
2
+ partials), servlets, DTO/service classes, and web.xml. Judge them as a web
3
+ estate, not a batch one:
4
+
5
+ - A page reached through a web.xml url-pattern, a link, a form action, or a
6
+ redirect is live even when the regex graph shows few callers — check the
7
+ refine notes before calling it dead. A page nothing reaches and nothing
8
+ includes is the retire candidate; give the evidence.
9
+ - Fragments with many includers are layout, not business logic: they are
10
+ migrated with the first page that needs them (same wave) rather than as a
11
+ wave of their own, and never "retire" while a live page includes them.
12
+ - Servlets and service classes that only front an ESB or backend service are
13
+ "wrap" candidates: the modernized application keeps calling the same
14
+ service through a port, and their business rules must not be re-implemented.
15
+ - web.xml and other descriptors are configuration, not migration units:
16
+ disposition "wrap" with a one-line rationale.
17
+
18
+ Waves are user journeys: keep a page, the servlet it posts to, and the
19
+ fragments it includes together; simple read-only screens first, then CRUD
20
+ screens, then multi-step session-backed flows last.
@@ -0,0 +1,22 @@
1
+ This is a J2EE web application: units are JSP pages and fragments, servlets and
2
+ supporting Java classes, and deployment descriptors. Regexes miss most of the
3
+ links such an application establishes indirectly:
4
+
5
+ - WEB-INF/web.xml: every servlet-mapping ties a url-pattern to a servlet; every
6
+ servlet-class names a Java unit; welcome-file and error-page entries name
7
+ JSPs. A JSP form action, link, or redirect that targets a url-pattern is an
8
+ edge from the JSP to the servlet behind it.
9
+ - Servlets: request.getRequestDispatcher(...).forward/include and
10
+ response.sendRedirect(...) name the JSP or url-pattern they hand off to;
11
+ new/injected service and DAO classes are outgoing edges to those units.
12
+ - JSPs: static includes (<%@ include file="…" %>), dynamic includes
13
+ (<jsp:include page="…">), <jsp:forward>, <a href="…">, <form action="…">,
14
+ jQuery $.ajax / $.get / $.post url targets, and JSTL <c:import>/<c:url>.
15
+ Resolve a path to the unit whose file name (without extension) matches.
16
+ - Tag libraries and shared layout fragments (header, footer, navigation) are
17
+ units only when they are files in the inventory.
18
+
19
+ Prioritise JSPs with zero incoming edges (are they reached through web.xml or
20
+ a redirect?), servlets nothing maps to, and fragments nothing includes. Calls to
21
+ the ESB, application-server services, and third-party libraries are not estate
22
+ edges — note them.
@@ -0,0 +1,20 @@
1
+ Units are JSP pages, JSP fragments (headers, footers, navigation, included
2
+ partials), servlets, DTO/service classes, and web.xml. Judge them as a web
3
+ estate, not a batch one:
4
+
5
+ - A page reached through a web.xml url-pattern, a link, a form action, or a
6
+ redirect is live even when the regex graph shows few callers — check the
7
+ refine notes before calling it dead. A page nothing reaches and nothing
8
+ includes is the retire candidate; give the evidence.
9
+ - Fragments with many includers are layout, not business logic: they are
10
+ migrated with the first page that needs them (same wave) rather than as a
11
+ wave of their own, and never "retire" while a live page includes them.
12
+ - Servlets and service classes that only front an ESB or backend service are
13
+ "wrap" candidates: the modernized application keeps calling the same
14
+ service through a port, and their business rules must not be re-implemented.
15
+ - web.xml and other descriptors are configuration, not migration units:
16
+ disposition "wrap" with a one-line rationale.
17
+
18
+ Waves are user journeys: keep a page, the servlet it posts to, and the
19
+ fragments it includes together; simple read-only screens first, then CRUD
20
+ screens, then multi-step session-backed flows last.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/shell",
3
- "version": "0.13.5",
3
+ "version": "0.15.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",
@@ -49,10 +49,10 @@
49
49
  ],
50
50
  "dependencies": {
51
51
  "@effect/platform-node": "4.0.0-beta.102",
52
- "@llm4ts/core": "0.13.5",
53
- "@llm4ts/modernize": "0.13.5",
54
- "@llm4ts/flow": "0.13.5",
55
- "@llm4ts/runner": "0.13.5"
52
+ "@llm4ts/core": "0.15.0",
53
+ "@llm4ts/flow": "0.15.0",
54
+ "@llm4ts/modernize": "0.15.0",
55
+ "@llm4ts/runner": "0.15.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "effect": "4.0.0-beta.102"