@llm4ts/shell 0.5.0 → 0.6.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.
Files changed (168) hide show
  1. package/dist/Cli.d.ts +2 -2
  2. package/dist/Cli.d.ts.map +1 -1
  3. package/dist/Cli.js +8 -4
  4. package/dist/Cli.js.map +1 -1
  5. package/dist/FlowCatalog.d.ts.map +1 -1
  6. package/dist/FlowCatalog.js +6 -2
  7. package/dist/FlowCatalog.js.map +1 -1
  8. package/flows/fixtures/scaffolds/kafka-streams-service/README.md +12 -0
  9. package/flows/fixtures/scaffolds/kafka-streams-service/pom.xml +49 -0
  10. package/flows/fixtures/scaffolds/kafka-streams-service/scripts/replay.sh +10 -0
  11. package/flows/fixtures/scaffolds/kafka-streams-service/src/main/java/com/meridian/scaffold/Application.java +29 -0
  12. package/flows/fixtures/scaffolds/kafka-streams-service/src/test/java/com/meridian/scaffold/ApplicationTests.java +21 -0
  13. package/flows/fixtures/scaffolds/kafka-streams-service/src/test/resources/features/.gitkeep +0 -0
  14. package/flows/fixtures/scaffolds/nextjs-spa/README.md +6 -0
  15. package/flows/fixtures/scaffolds/nextjs-spa/app/layout.js +11 -0
  16. package/flows/fixtures/scaffolds/nextjs-spa/app/page.js +7 -0
  17. package/flows/fixtures/scaffolds/nextjs-spa/docs/specs/.gitkeep +0 -0
  18. package/flows/fixtures/scaffolds/nextjs-spa/features/.gitkeep +0 -0
  19. package/flows/fixtures/scaffolds/nextjs-spa/next.config.js +7 -0
  20. package/flows/fixtures/scaffolds/nextjs-spa/package.json +15 -0
  21. package/flows/fixtures/scaffolds/nextjs-spa/tests/smoke.test.js +6 -0
  22. package/flows/fixtures/scaffolds/spring-bff/README.md +9 -0
  23. package/flows/fixtures/scaffolds/spring-bff/frontend/app/layout.js +11 -0
  24. package/flows/fixtures/scaffolds/spring-bff/frontend/app/page.js +7 -0
  25. package/flows/fixtures/scaffolds/spring-bff/frontend/next.config.js +7 -0
  26. package/flows/fixtures/scaffolds/spring-bff/frontend/package.json +15 -0
  27. package/flows/fixtures/scaffolds/spring-bff/frontend/tests/smoke.test.js +6 -0
  28. package/flows/fixtures/scaffolds/spring-bff/pom.xml +57 -0
  29. package/flows/fixtures/scaffolds/spring-bff/scripts/test.sh +6 -0
  30. package/flows/fixtures/scaffolds/spring-bff/src/main/java/com/meridian/bff/Application.java +12 -0
  31. package/flows/fixtures/scaffolds/spring-bff/src/main/resources/application.yaml +12 -0
  32. package/flows/fixtures/scaffolds/spring-bff/src/test/java/com/meridian/bff/ApplicationTests.java +12 -0
  33. package/flows/fixtures/scaffolds/spring-bff/src/test/resources/features/.gitkeep +0 -0
  34. package/flows/fixtures/scaffolds/spring-boot-service/README.md +7 -0
  35. package/flows/fixtures/scaffolds/spring-boot-service/pom.xml +53 -0
  36. package/flows/fixtures/scaffolds/spring-boot-service/scripts/replay.sh +9 -0
  37. package/flows/fixtures/scaffolds/spring-boot-service/src/main/java/com/meridian/scaffold/Application.java +12 -0
  38. package/flows/fixtures/scaffolds/spring-boot-service/src/main/resources/application.yaml +12 -0
  39. package/flows/fixtures/scaffolds/spring-boot-service/src/test/java/com/meridian/scaffold/ApplicationTests.java +12 -0
  40. package/flows/fixtures/scaffolds/spring-boot-service/src/test/resources/features/.gitkeep +0 -0
  41. package/flows/implement.js +29 -0
  42. package/flows/issue-pr.js +71 -0
  43. package/flows/judge-suite.js +48 -0
  44. package/flows/local.js +40 -0
  45. package/flows/modernize-bench.js +263 -0
  46. package/flows/modernize-extract.js +454 -0
  47. package/flows/modernize-implement.js +249 -0
  48. package/flows/modernize-review.js +237 -0
  49. package/flows/modernize-seed.js +189 -0
  50. package/flows/modernize-survey.js +0 -0
  51. package/flows/modernize-verify.js +435 -0
  52. package/flows/packs/ace-integration/lessons.md +6 -0
  53. package/flows/packs/ace-integration/pack.md +29 -0
  54. package/flows/packs/ace-integration/prompts/analysis.md +21 -0
  55. package/flows/packs/ace-integration/prompts/bdd.md +13 -0
  56. package/flows/packs/ace-integration/prompts/implement.md +17 -0
  57. package/flows/packs/ace-integration/prompts/plan.md +12 -0
  58. package/flows/packs/ace-integration/prompts/review.md +17 -0
  59. package/flows/packs/ace-integration/prompts/spec.md +29 -0
  60. package/flows/packs/ace-integration/reviewers/routing-fidelity.md +19 -0
  61. package/flows/packs/ace-integration/reviewers/traceability.md +13 -0
  62. package/flows/packs/ace-kafka/lessons.md +6 -0
  63. package/flows/packs/ace-kafka/pack.md +36 -0
  64. package/flows/packs/ace-kafka/prompts/analysis.md +21 -0
  65. package/flows/packs/ace-kafka/prompts/bdd.md +13 -0
  66. package/flows/packs/ace-kafka/prompts/implement.md +34 -0
  67. package/flows/packs/ace-kafka/prompts/plan.md +12 -0
  68. package/flows/packs/ace-kafka/prompts/review.md +17 -0
  69. package/flows/packs/ace-kafka/prompts/spec.md +31 -0
  70. package/flows/packs/ace-kafka/prompts/vectors.md +21 -0
  71. package/flows/packs/ace-kafka/reviewers/event-contracts.md +9 -0
  72. package/flows/packs/ace-kafka/reviewers/routing-fidelity.md +19 -0
  73. package/flows/packs/ace-kafka/reviewers/traceability.md +13 -0
  74. package/flows/packs/cobol-kafka/lessons.md +3 -0
  75. package/flows/packs/cobol-kafka/pack.md +51 -0
  76. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-001-record-to-event.md +7 -0
  77. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-002-job-step-topology.md +6 -0
  78. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-003-checkpoint-idempotence.md +7 -0
  79. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-004-eod-totals-window.md +7 -0
  80. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-005-reject-topic.md +6 -0
  81. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-006-keyed-state-store.md +7 -0
  82. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-007-outbox.md +7 -0
  83. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-008-partition-key.md +7 -0
  84. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-009-batch-window-cutoff.md +7 -0
  85. package/flows/packs/cobol-kafka/patterns/PAT-STREAM-010-replay-backfill.md +7 -0
  86. package/flows/packs/cobol-kafka/prompts/analysis.md +23 -0
  87. package/flows/packs/cobol-kafka/prompts/bdd.md +15 -0
  88. package/flows/packs/cobol-kafka/prompts/implement.md +47 -0
  89. package/flows/packs/cobol-kafka/prompts/plan.md +13 -0
  90. package/flows/packs/cobol-kafka/prompts/review.md +17 -0
  91. package/flows/packs/cobol-kafka/prompts/spec.md +28 -0
  92. package/flows/packs/cobol-kafka/prompts/vectors.md +21 -0
  93. package/flows/packs/cobol-kafka/reviewers/cobol-fidelity.md +16 -0
  94. package/flows/packs/cobol-kafka/reviewers/pattern-conformance.md +10 -0
  95. package/flows/packs/cobol-kafka/reviewers/traceability.md +13 -0
  96. package/flows/packs/cobol-springboot/lessons.md +3 -0
  97. package/flows/packs/cobol-springboot/pack.md +51 -0
  98. package/flows/packs/cobol-springboot/prompts/analysis.md +23 -0
  99. package/flows/packs/cobol-springboot/prompts/bdd.md +15 -0
  100. package/flows/packs/cobol-springboot/prompts/implement.md +31 -0
  101. package/flows/packs/cobol-springboot/prompts/plan.md +13 -0
  102. package/flows/packs/cobol-springboot/prompts/review.md +17 -0
  103. package/flows/packs/cobol-springboot/prompts/spec.md +28 -0
  104. package/flows/packs/cobol-springboot/prompts/vectors.md +22 -0
  105. package/flows/packs/cobol-springboot/reviewers/cobol-fidelity.md +16 -0
  106. package/flows/packs/cobol-springboot/reviewers/pattern-conformance.md +10 -0
  107. package/flows/packs/cobol-springboot/reviewers/traceability.md +13 -0
  108. package/flows/packs/jsp-bff-nextjs/lessons.md +6 -0
  109. package/flows/packs/jsp-bff-nextjs/pack.md +30 -0
  110. package/flows/packs/jsp-bff-nextjs/prompts/analysis.md +20 -0
  111. package/flows/packs/jsp-bff-nextjs/prompts/bdd.md +10 -0
  112. package/flows/packs/jsp-bff-nextjs/prompts/implement.md +17 -0
  113. package/flows/packs/jsp-bff-nextjs/prompts/plan.md +12 -0
  114. package/flows/packs/jsp-bff-nextjs/prompts/review.md +16 -0
  115. package/flows/packs/jsp-bff-nextjs/prompts/spec.md +27 -0
  116. package/flows/packs/jsp-bff-nextjs/reviewers/bff-boundary.md +18 -0
  117. package/flows/packs/jsp-bff-nextjs/reviewers/traceability.md +13 -0
  118. package/flows/packs/jsp-nextjs/lessons.md +6 -0
  119. package/flows/packs/jsp-nextjs/pack.md +29 -0
  120. package/flows/packs/jsp-nextjs/prompts/analysis.md +21 -0
  121. package/flows/packs/jsp-nextjs/prompts/bdd.md +12 -0
  122. package/flows/packs/jsp-nextjs/prompts/implement.md +17 -0
  123. package/flows/packs/jsp-nextjs/prompts/plan.md +14 -0
  124. package/flows/packs/jsp-nextjs/prompts/review.md +16 -0
  125. package/flows/packs/jsp-nextjs/prompts/spec.md +26 -0
  126. package/flows/packs/jsp-nextjs/reviewers/traceability.md +13 -0
  127. package/flows/packs/jsp-nextjs/reviewers/ux-fidelity.md +16 -0
  128. package/flows/patterns/PAT-COBOL-001-comp3-money.md +7 -0
  129. package/flows/patterns/PAT-COBOL-002-redefines.md +7 -0
  130. package/flows/patterns/PAT-COBOL-003-88-levels.md +7 -0
  131. package/flows/patterns/PAT-COBOL-004-perform-thru.md +7 -0
  132. package/flows/patterns/PAT-COBOL-005-file-status.md +6 -0
  133. package/flows/patterns/PAT-COBOL-006-sqlcode.md +7 -0
  134. package/flows/patterns/PAT-COBOL-007-validation-order.md +7 -0
  135. package/flows/patterns/PAT-COBOL-008-zoned-decimal.md +6 -0
  136. package/flows/patterns/PAT-COBOL-009-occurs-table.md +6 -0
  137. package/flows/patterns/PAT-COBOL-010-copybook-record.md +7 -0
  138. package/flows/patterns/PAT-COBOL-011-batch-counters.md +6 -0
  139. package/flows/patterns/PAT-COBOL-012-commit-frequency.md +7 -0
  140. package/flows/patterns/PAT-COBOL-013-abend.md +6 -0
  141. package/flows/patterns/PAT-COBOL-014-reject-file.md +6 -0
  142. package/flows/patterns/PAT-COBOL-015-accept-date.md +6 -0
  143. package/flows/patterns/PAT-COBOL-016-string-building.md +6 -0
  144. package/flows/patterns/PAT-COBOL-017-cursor-loop.md +6 -0
  145. package/flows/patterns/PAT-COBOL-018-yn-switches.md +6 -0
  146. package/flows/patterns/PAT-COBOL-019-shared-accumulators.md +6 -0
  147. package/flows/patterns/PAT-COBOL-020-tiered-fees.md +7 -0
  148. package/flows/patterns/PAT-COBOL-021-overdraft-floor.md +7 -0
  149. package/flows/patterns/PAT-COBOL-022-jcl-job-net.md +6 -0
  150. package/flows/patterns/PAT-COBOL-023-sort-step.md +6 -0
  151. package/flows/patterns/PAT-COBOL-024-fixed-width-records.md +6 -0
  152. package/flows/patterns/PAT-COBOL-025-audit-rows.md +6 -0
  153. package/flows/sdd.js +114 -0
  154. package/package.json +5 -4
  155. package/src/Cli.ts +13 -4
  156. package/src/FlowCatalog.ts +8 -2
  157. package/flows/implement.ts +0 -38
  158. package/flows/issue-pr.ts +0 -108
  159. package/flows/judge-suite.ts +0 -63
  160. package/flows/local.ts +0 -59
  161. package/flows/modernize-bench.ts +0 -362
  162. package/flows/modernize-extract.ts +0 -633
  163. package/flows/modernize-implement.ts +0 -333
  164. package/flows/modernize-review.ts +0 -343
  165. package/flows/modernize-seed.ts +0 -259
  166. package/flows/modernize-survey.ts +0 -0
  167. package/flows/modernize-verify.ts +0 -611
  168. package/flows/sdd.ts +0 -181
@@ -1,633 +0,0 @@
1
- // Legacy modernization phase 1: reverse-engineer the estate into a judged, human-approved spec pack.
2
- //
3
- // Runs rooted at the LEGACY repository (`--repo <legacy>`), after modernize-survey's
4
- // wave plan is approved. Extraction is PER PROGRAM and resumable
5
- // (`extractProgramsResumably` skips every program whose spec already exists —
6
- // delete `specs/<NAME>.md` to re-extract one program): each program gets a
7
- // structured analyst call producing its four artifacts — `specs/<NAME>.md`,
8
- // `features/<name>.feature`, `traceability/<NAME>.md`, `mapping/<NAME>.md` —
9
- // and its own commit. The estate-wide indexes (`traceability.md`, `mapping.md`)
10
- // are regenerated deterministically from the fragments before every gate round.
11
- //
12
- // The gate is layered and nothing auto-approves: deterministic `SpecChecks`
13
- // (every legacy coverage unit must appear in the traceability matrix; features
14
- // must be well-formed Gherkin; both indexes present), then an LLM-as-a-Judge
15
- // pass per program against the pack's rubrics with full marks required.
16
- // Findings feed one bounded fix round per sub-bar program plus one estate-wide
17
- // residual turn; a still-dirty pack is committed as an explicit DRAFT and the
18
- // flow halts for human triage. Even a clean pack only gets an unchecked
19
- // `- [ ] Approved` marker in `docs/modernization/README.md`.
20
- //
21
- // Pack: LLM4TS_PACK=<dir> (default packs/cobol-springboot, resolved against the
22
- // launch dir). LLM4TS_WAVE=<name> scopes the run to one wave of the approved
23
- // plan. Judge context is bounded by LLM4TS_JUDGE_SOURCES_LIMIT (chars).
24
- import { join } from "node:path"
25
- import * as Effect from "effect/Effect"
26
- import { Sample, type EvalResult } from "@llm4ts/core/eval/Eval"
27
- import { judge } from "@llm4ts/core/eval/Judge"
28
- import type { JsonSchema } from "@llm4ts/core/Models"
29
- import { FlowAborted, FlowLlmError } from "@llm4ts/flow/FlowError"
30
- import { Info } from "@llm4ts/flow/FlowEvents"
31
- import { makeChat } from "@llm4ts/flow/Chat"
32
- import { loadPack, type Pack } from "@llm4ts/flow/Pack"
33
- import { loadPatternCards, matchingPatternCards } from "@llm4ts/flow/Patterns"
34
- import { stage } from "@llm4ts/flow/PlanExecution"
35
- import { defaultPlanInstructions, planFrom } from "@llm4ts/flow/Planner"
36
- import { ReviewIssue, ReviewResult, mergeReviewResults } from "@llm4ts/flow/Review"
37
- import { cachedReview } from "@llm4ts/flow/ReviewCache"
38
- import { coverage, coverageUnits, features, matchingFiles } from "@llm4ts/flow/SpecChecks"
39
- import { withDraftApproval, requireApproval } from "@llm4ts/modernize/Approval"
40
- import {
41
- ProgramArtifacts,
42
- ProgramUnit,
43
- extractProgramsResumably
44
- } from "@llm4ts/modernize/Artifacts"
45
- import { asReadOnly, coderFromEnv } from "@llm4ts/runner/Connectors"
46
- import { resolveFlowInput } from "@llm4ts/runner/FlowArgs"
47
- import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner"
48
- import { reviewFingerprint } from "@llm4ts/runner/ReviewFingerprint"
49
- import { nodePlainFileStore } from "@llm4ts/runner/NodePlainFileStore"
50
- import { makeNodeWorkspace } from "@llm4ts/runner/NodeWorkspace"
51
-
52
- const ModDir = "docs/modernization"
53
- const MaxRounds = 3
54
-
55
- const judgeSourcesLimit = (): number => {
56
- const raw = Number.parseInt(process.env.LLM4TS_JUDGE_SOURCES_LIMIT ?? "", 10)
57
- return Number.isFinite(raw) && raw > 0 ? raw : 400_000
58
- }
59
-
60
- /** Past the limit keep head + tail so entry points and trailing rules stay visible. */
61
- const capText = (text: string, limit: number): string => {
62
- if (text.length <= limit) {
63
- return text
64
- }
65
- const head = Math.floor((limit * 3) / 4)
66
- return `${text.slice(0, head)}\n\n… [truncated] …\n\n${text.slice(text.length - (limit - head))}`
67
- }
68
-
69
- /** `cobol/ACCTXFR.cbl` → `ACCTXFR`: the program name keying every per-program artifact. */
70
- const programName = (relativePath: string): string => {
71
- const base = relativePath.slice(relativePath.lastIndexOf("/") + 1)
72
- const dot = base.lastIndexOf(".")
73
- return dot > 0 ? base.slice(0, dot) : base
74
- }
75
-
76
- /** The `- PROG` entries of `## Wave: <name>` in the survey's wave plan. */
77
- const wavePrograms = (planText: string, wave: string): ReadonlyArray<string> => {
78
- const lines = planText.split(/\r?\n/)
79
- const start = lines.findIndex((line) => line.trim() === `## Wave: ${wave}`)
80
- if (start < 0) {
81
- return []
82
- }
83
- const section: Array<string> = []
84
- for (const line of lines.slice(start + 1)) {
85
- if (line.trim().startsWith("## ")) {
86
- break
87
- }
88
- if (line.trim().startsWith("- ")) {
89
- section.push(line.trim().slice(2).trim())
90
- }
91
- }
92
- return section
93
- }
94
-
95
- const programArtifactsJsonSchema: JsonSchema = {
96
- type: "object",
97
- properties: {
98
- spec: { type: "string" },
99
- feature: { type: "string" },
100
- traceability: { type: "string" },
101
- mapping: { type: "string" }
102
- },
103
- required: ["spec", "feature", "traceability", "mapping"]
104
- }
105
-
106
- const programAsk = (pack: Pack, relativePath: string): string =>
107
- [
108
- `Extract the behavioural spec for ONE source unit of this repository: ${relativePath}`,
109
- "",
110
- "Read the source file and anything it references (copybooks, includes, called programs)",
111
- `for context, but spec ONLY ${relativePath} and do not modify legacy sources.`,
112
- "",
113
- 'Respond only with JSON: {"spec":"…","feature":"…","traceability":"…","mapping":"…"} where:',
114
- "",
115
- `- "spec" — the behavioural spec for ${relativePath}, as Markdown.`,
116
- pack.prompt("spec") ?? "",
117
- "",
118
- `- "feature" — BDD scenarios encoding that spec, as a well-formed Gherkin .feature file`,
119
- " (Feature: header, Scenario: blocks, Given/When/Then steps).",
120
- pack.prompt("bdd") ?? "",
121
- "",
122
- `- "traceability" — EVERY source unit of ${relativePath} (each COBOL paragraph, each JCL`,
123
- " step) on its own line, mapped to the spec rules/scenarios that cover it:",
124
- " `<UNIT-NAME> — <refs>`. Unit names verbatim as they appear in the source.",
125
- "",
126
- `- "mapping" — data & interface mapping for ${relativePath}: tables/record layouts → target`,
127
- " entities; files/screens/queues → target service contracts."
128
- ].join("\n")
129
-
130
- /** Sub-bar judge dimensions as Critical review issues, titled with their program. */
131
- const judgeIssues = (
132
- pack: Pack,
133
- scored: {
134
- readonly scores: ReadonlyArray<{
135
- readonly name: string
136
- readonly score: number
137
- readonly reasoning: string
138
- }>
139
- },
140
- program: string
141
- ): ReviewResult => {
142
- const issues = scored.scores.flatMap((score) => {
143
- const maxScore = pack.judgeDimensions.find((d) => d.name === score.name)?.maxScore ?? 2
144
- return score.score < maxScore
145
- ? [
146
- ReviewIssue.make({
147
- severity: "Critical",
148
- title: `judge[${program}]: ${score.name} scored ${score.score}`,
149
- description: score.reasoning
150
- })
151
- ]
152
- : []
153
- })
154
- return ReviewResult.make({ issues, summary: `judge:${program}` })
155
- }
156
-
157
- const judgeIssueProgram = (issue: ReviewIssue): string | undefined =>
158
- /^judge\[([^\]]+)\]: /.exec(issue.title)?.[1]
159
-
160
- const issueLines = (issues: ReadonlyArray<ReviewIssue>): string =>
161
- issues.map((issue) => `- [${issue.severity}] ${issue.title}: ${issue.description}`).join("\n")
162
-
163
- const programFixAsk = (
164
- name: string,
165
- relativePath: string,
166
- issues: ReadonlyArray<ReviewIssue>
167
- ): string =>
168
- [
169
- `The spec pack for ONE program did not clear its quality gate: ${name} (source: ${relativePath}).`,
170
- `Fix these findings by editing ONLY this program's files — ${ModDir}/specs/${name}.md,`,
171
- `${ModDir}/features/${name.toLowerCase()}.feature, ${ModDir}/traceability/${name}.md,`,
172
- `${ModDir}/mapping/${name}.md — against the source at ${relativePath}. Then stop:`,
173
- issueLines(issues)
174
- ].join("\n")
175
-
176
- const globalFixAsk = (issues: ReadonlyArray<ReviewIssue>): string =>
177
- [
178
- "The spec pack did not clear its estate-wide quality gate. Fix these findings by editing the",
179
- `per-program files under ${ModDir}/ (specs/, features/, traceability/<PROGRAM>.md,`,
180
- `mapping/<PROGRAM>.md). ${ModDir}/traceability.md and ${ModDir}/mapping.md are REGENERATED`,
181
- "from the fragments — do not edit them directly. Fix the findings in place, then stop:",
182
- issueLines(issues)
183
- ].join("\n")
184
-
185
- const readmeFor = (pack: Pack, verdict: string): string =>
186
- [
187
- `# Modernization spec pack — ${pack.name}`,
188
- "",
189
- `Extracted by the modernize-extract flow. Gate verdict: ${verdict}.`,
190
- "",
191
- "- specs/ — behavioural specs, one per program",
192
- "- features/ — BDD acceptance scenarios",
193
- "- traceability.md — source-unit → spec coverage matrix (generated from traceability/)",
194
- "- mapping.md — data & interface mapping (generated from mapping/)",
195
- "- rules.txt — every coverage unit, one per line (the rule universe verification reports against)",
196
- "- plan.md — proposed implementation tasks",
197
- "",
198
- "Review everything, then flip the marker below and run the seed phase."
199
- ].join("\n")
200
-
201
- const program = Effect.gen(function* () {
202
- const input = yield* resolveFlowInput(
203
- "Extract the complete behavioural spec pack for this legacy estate"
204
- )
205
- const packDir = process.env.LLM4TS_PACK ?? "packs/cobol-springboot"
206
- const coder = coderFromEnv(process.env)
207
- const files = nodePlainFileStore
208
- const modDirAbs = join(input.workDir, ModDir)
209
-
210
- yield* runNode(
211
- {
212
- workDir: input.workDir,
213
- workspace: input.workspace,
214
- userPrompt: input.prompt,
215
- coder,
216
- reasoning: asReadOnly(coder),
217
- environment: process.env
218
- },
219
- (context) =>
220
- Effect.gen(function* () {
221
- const launchWorkspace = yield* makeNodeWorkspace(input.workspace)
222
- const repo = yield* makeNodeWorkspace(input.workDir)
223
- const pack = yield* stage(context.events, "pack", loadPack(launchWorkspace, packDir))
224
- yield* stage(
225
- context.events,
226
- "branch",
227
- context.git.checkoutOrCreate("modernize/spec-pack").pipe(Effect.asVoid)
228
- )
229
- const system = [
230
- pack.prompt("analysis"),
231
- pack.lessons === undefined
232
- ? undefined
233
- : `Lessons from previous modernization runs — apply them:\n${pack.lessons}`
234
- ]
235
- .filter((part) => part !== undefined)
236
- .join("\n\n")
237
-
238
- const all = yield* stage(
239
- context.events,
240
- "inventory",
241
- matchingFiles(repo, pack.programs ?? pack.sources ?? ".*")
242
- )
243
- const wave = process.env.LLM4TS_WAVE?.trim()
244
- const programs =
245
- wave === undefined || wave.length === 0
246
- ? all
247
- : yield* Effect.gen(function* () {
248
- yield* stage(
249
- context.events,
250
- "wave approval",
251
- requireApproval(files, join(modDirAbs, "wave-plan.md"))
252
- )
253
- const planText = (yield* files.read(join(modDirAbs, "wave-plan.md"))) ?? ""
254
- const names = new Set(wavePrograms(planText, wave))
255
- const scoped = all.filter((rel) => names.has(programName(rel)))
256
- if (scoped.length === 0) {
257
- return yield* FlowAborted.make({
258
- message: `wave '${wave}' matches no programs in ${ModDir}/wave-plan.md`
259
- })
260
- }
261
- return scoped
262
- })
263
- if (programs.length === 0) {
264
- return yield* FlowAborted.make({
265
- message: `no source units matched the pack's programs/sources regex under ${input.workDir}`
266
- })
267
- }
268
-
269
- const units = programs.map((rel) =>
270
- ProgramUnit.make({ name: programName(rel), sourcePath: rel })
271
- )
272
-
273
- // Pattern cards are selected deterministically from the SOURCE, never by
274
- // the model: implementation later injects exactly the cards cited here.
275
- const cards = [
276
- ...(yield* loadPatternCards(launchWorkspace, `${packDir}/patterns`)),
277
- ...(yield* loadPatternCards(launchWorkspace, "patterns"))
278
- ]
279
-
280
- // One structured analyst call per program, resumable per program: a rerun
281
- // skips every program whose spec exists, and each program gets its own commit.
282
- yield* stage(
283
- context.events,
284
- "extract",
285
- Effect.gen(function* () {
286
- for (const [index, unit] of units.entries()) {
287
- const summary = yield* extractProgramsResumably(
288
- files,
289
- [unit],
290
- (target) =>
291
- Effect.gen(function* () {
292
- yield* context.events.publish(
293
- Info.make({
294
- message: `extracting ${target.sourcePath} (${index + 1}/${units.length})`
295
- })
296
- )
297
- return yield* context.coder
298
- .executeStructured(
299
- `${system}\n\n${programAsk(pack, target.sourcePath)}`,
300
- ProgramArtifacts,
301
- programArtifactsJsonSchema
302
- )
303
- .pipe(
304
- Effect.mapError(FlowLlmError.from),
305
- // A turn-limit trip is the wedged-agent tail, not a
306
- // failure: keep whatever the analyst already produced.
307
- Effect.catchIf(
308
- (error) => error.cause?._tag === "TurnLimitError",
309
- (error) =>
310
- Effect.gen(function* () {
311
- const existing = yield* files.read(
312
- join(modDirAbs, "specs", `${target.name}.md`)
313
- )
314
- if (existing === undefined) {
315
- return yield* Effect.fail(error)
316
- }
317
- yield* context.events.publish(
318
- Info.make({
319
- message: `turn limit hit on ${target.sourcePath} after its spec was written — keeping the work`
320
- })
321
- )
322
- return ProgramArtifacts.make({
323
- spec: existing,
324
- feature: "",
325
- traceability: "",
326
- mapping: ""
327
- })
328
- })
329
- )
330
- )
331
- }),
332
- modDirAbs
333
- )
334
- if (summary.created.length > 0) {
335
- // Tag the traceability fragment with the cards this program's
336
- // source matches — regex-decided, so implementation's playbook
337
- // is reproducible.
338
- const source = yield* files.read(join(input.workDir, unit.sourcePath))
339
- const matched = source === undefined ? [] : matchingPatternCards(source, cards)
340
- if (matched.length > 0) {
341
- const fragmentPath = join(modDirAbs, "traceability", `${unit.name}.md`)
342
- const fragment = (yield* files.read(fragmentPath)) ?? ""
343
- yield* files.writeAtomic(
344
- fragmentPath,
345
- `${fragment.trimEnd()}\n\nPatterns: ${matched.map((card) => card.id).join(", ")}\n`
346
- )
347
- }
348
- yield* context.git
349
- .commitAll(`modernize(${pack.name}): spec ${unit.name}`)
350
- .pipe(Effect.asVoid)
351
- } else {
352
- yield* context.events.publish(
353
- Info.make({
354
- message: `resume: specs/${unit.name}.md exists — skipping ${unit.sourcePath}`
355
- })
356
- )
357
- }
358
- }
359
- })
360
- )
361
-
362
- // traceability.md / mapping.md are regenerated from the fragments — fixes
363
- // belong in the fragments, never the indexes.
364
- const rebuildIndexes = Effect.gen(function* () {
365
- for (const [fragmentDir, index] of [
366
- ["traceability", "traceability.md"],
367
- ["mapping", "mapping.md"]
368
- ] as const) {
369
- const parts: Array<string> = []
370
- for (const unit of units) {
371
- const text = yield* files.read(join(modDirAbs, fragmentDir, `${unit.name}.md`))
372
- if (text !== undefined && text.trim().length > 0) {
373
- parts.push(`===== ${unit.name} =====\n${text.trimEnd()}`)
374
- }
375
- }
376
- if (parts.length > 0) {
377
- yield* files.writeAtomic(join(modDirAbs, index), parts.join("\n\n") + "\n")
378
- }
379
- }
380
- })
381
-
382
- const writeRules = Effect.gen(function* () {
383
- const unitsByRule = yield* coverageUnits(repo, pack.coverage)
384
- const allUnits = [...new Set(Object.values(unitsByRule).flat())].sort()
385
- if (allUnits.length > 0) {
386
- yield* files.writeAtomic(join(modDirAbs, "rules.txt"), allUnits.join("\n") + "\n")
387
- }
388
- })
389
-
390
- // Commit the (ungated) draft: extraction is the expensive step, and a gate
391
- // failure or crash must not cost it.
392
- yield* stage(
393
- context.events,
394
- "draft",
395
- rebuildIndexes.pipe(
396
- Effect.andThen(writeRules),
397
- Effect.andThen(
398
- context.git
399
- .commitAll(`modernize(${pack.name}): spec pack draft (ungated)`)
400
- .pipe(Effect.asVoid)
401
- )
402
- )
403
- )
404
-
405
- const packJudge = judge(context.reasoning, pack.judgeDimensions)
406
- const limit = judgeSourcesLimit()
407
-
408
- /**
409
- * An empty structured response that survives the in-run retries is
410
- * usually DETERMINISTIC (context overflow), so repeating the same
411
- * prompt cannot succeed — retry at half, then quarter context instead.
412
- */
413
- const judgeWithShrink = (spec: string, feature: string, source: string) => {
414
- const attempt = (
415
- cap: number,
416
- rest: ReadonlyArray<number>
417
- ): Effect.Effect<EvalResult, FlowLlmError> =>
418
- packJudge
419
- .evaluate(
420
- Sample.make({
421
- response: capText(`${spec}\n\n${feature}`, cap),
422
- context: capText(source, cap),
423
- query: input.prompt
424
- })
425
- )
426
- .pipe(
427
- Effect.mapError(FlowLlmError.from),
428
- Effect.catchIf(
429
- (error) => rest.length > 0 && error.message.includes("empty response"),
430
- (error) => {
431
- const [next, ...remaining] = rest
432
- return context.events
433
- .publish(
434
- Info.make({
435
- message: `judge returned empty at cap ${cap} chars — shrinking to ${next ?? cap}: ${error.message}`
436
- })
437
- )
438
- .pipe(Effect.andThen(attempt(next ?? cap, remaining)))
439
- }
440
- )
441
- )
442
- return attempt(limit, [Math.floor(limit / 2), Math.floor(limit / 4)])
443
- }
444
-
445
- /**
446
- * Judging is resumable per program: the verdict persists under
447
- * `gate/<NAME>.json`, fingerprinted over the source, spec, feature, and
448
- * rubric it judged. Unchanged content reuses the stored verdict with NO
449
- * model call, so a crash or quota death re-judges only what changed.
450
- * Delete `gate/` to force a full re-judge.
451
- */
452
- const judgeProgram = (unit: ProgramUnit) =>
453
- Effect.gen(function* () {
454
- const spec = (yield* files.read(join(modDirAbs, "specs", `${unit.name}.md`))) ?? ""
455
- const feature =
456
- (yield* files.read(
457
- join(modDirAbs, "features", `${unit.name.toLowerCase()}.feature`)
458
- )) ?? ""
459
- const source = (yield* files.read(join(input.workDir, unit.sourcePath))) ?? ""
460
- const rubric = pack.judgeDimensions
461
- .map(
462
- (dimension) => `${dimension.name} (0..${dimension.maxScore}): ${dimension.rubric}`
463
- )
464
- .join("\n")
465
- return yield* cachedReview(
466
- files,
467
- join(modDirAbs, "gate", `${unit.name}.json`),
468
- reviewFingerprint(source, spec, feature, rubric),
469
- context.events.publish(Info.make({ message: `judging ${unit.name}` })).pipe(
470
- Effect.andThen(judgeWithShrink(spec, feature, source)),
471
- Effect.map((scored) => judgeIssues(pack, scored, unit.name))
472
- )
473
- )
474
- })
475
-
476
- const gateEvaluate = Effect.gen(function* () {
477
- yield* rebuildIndexes
478
- const trace = (yield* files.read(join(modDirAbs, "traceability.md"))) ?? ""
479
- const mapping = (yield* files.read(join(modDirAbs, "mapping.md"))) ?? ""
480
- const docs = ReviewResult.make({
481
- issues: [
482
- ...(trace.trim().length === 0
483
- ? [
484
- ReviewIssue.make({
485
- severity: "Critical",
486
- title: "missing traceability",
487
- description: `no ${ModDir}/traceability/ fragments were written`
488
- })
489
- ]
490
- : []),
491
- ...(mapping.trim().length === 0
492
- ? [
493
- ReviewIssue.make({
494
- severity: "Critical",
495
- title: "missing mapping",
496
- description: `no ${ModDir}/mapping/ fragments were written`
497
- })
498
- ]
499
- : [])
500
- ],
501
- summary: "docs"
502
- })
503
- const covered = yield* coverage(repo, pack.coverage, trace)
504
- const wellFormed = yield* features(repo, join(ModDir, "features"))
505
- const judged: Array<ReviewResult> = []
506
- for (const unit of units) {
507
- judged.push(yield* judgeProgram(unit))
508
- }
509
- return mergeReviewResults([covered, wellFormed, docs, ...judged])
510
- })
511
-
512
- // One bounded fix turn per sub-bar program (own commit) plus one residual
513
- // estate-wide turn, then re-evaluate — up to MaxRounds evaluations.
514
- const fixOnce = (result: ReviewResult) =>
515
- Effect.gen(function* () {
516
- const relOf = new Map(units.map((unit) => [unit.name, unit.sourcePath]))
517
- const scoped = new Map<string, Array<ReviewIssue>>()
518
- const global: Array<ReviewIssue> = []
519
- for (const issue of result.issues) {
520
- const name = judgeIssueProgram(issue)
521
- if (name !== undefined && relOf.has(name)) {
522
- const bucket = scoped.get(name) ?? []
523
- bucket.push(issue)
524
- scoped.set(name, bucket)
525
- } else {
526
- global.push(issue)
527
- }
528
- }
529
- const turn = (ask: string, commitMessage: string) =>
530
- Effect.gen(function* () {
531
- const chat = yield* makeChat(context.coder, { system })
532
- yield* chat.ask(ask).pipe(
533
- Effect.asVoid,
534
- // A wedged agent that trips its turn limit mid-fix still wrote
535
- // something; re-evaluate what landed instead of failing.
536
- Effect.catchIf(
537
- (error) => error._tag === "Llm" && error.cause?._tag === "TurnLimitError",
538
- () =>
539
- context.events.publish(
540
- Info.make({
541
- message:
542
- "turn limit hit during a fix turn — re-evaluating what was written"
543
- })
544
- )
545
- )
546
- )
547
- yield* context.git.commitAll(commitMessage).pipe(Effect.asVoid)
548
- })
549
- for (const [name, issues] of [...scoped.entries()].sort()) {
550
- yield* context.events.publish(
551
- Info.make({ message: `fixing ${name} — ${issues.length} finding(s)` })
552
- )
553
- const relativePath = relOf.get(name)
554
- if (relativePath !== undefined) {
555
- yield* turn(
556
- programFixAsk(name, relativePath, issues),
557
- `modernize(${pack.name}): gate fixes ${name}`
558
- )
559
- }
560
- }
561
- if (global.length > 0) {
562
- yield* context.events.publish(
563
- Info.make({ message: `fixing estate-wide findings — ${global.length}` })
564
- )
565
- yield* turn(globalFixAsk(global), `modernize(${pack.name}): gate fixes (estate-wide)`)
566
- }
567
- })
568
-
569
- let result = yield* stage(context.events, "gate", gateEvaluate)
570
- for (let round = 2; round <= MaxRounds && result.issues.length > 0; round += 1) {
571
- yield* stage(context.events, `gate fixes (round ${round})`, fixOnce(result))
572
- result = yield* stage(context.events, `gate (round ${round})`, gateEvaluate)
573
- }
574
-
575
- if (result.issues.length === 0) {
576
- yield* stage(
577
- context.events,
578
- "plan",
579
- Effect.gen(function* () {
580
- const specTexts: Array<string> = []
581
- for (const unit of units) {
582
- const spec = yield* files.read(join(modDirAbs, "specs", `${unit.name}.md`))
583
- if (spec !== undefined) {
584
- specTexts.push(spec)
585
- }
586
- }
587
- const plan = yield* planFrom(
588
- context.reasoning,
589
- capText(specTexts.join("\n\n"), limit),
590
- `${defaultPlanInstructions}\n\n${pack.prompt("plan") ?? ""}`
591
- )
592
- yield* files.writeAtomic(join(modDirAbs, "plan.md"), plan.render)
593
- })
594
- )
595
- }
596
-
597
- const verdict =
598
- result.issues.length === 0
599
- ? "PASSED — pending human approval"
600
- : `DRAFT — ${result.issues.length} open issue(s)`
601
- yield* stage(
602
- context.events,
603
- "commit",
604
- files
605
- .writeAtomic(join(modDirAbs, "README.md"), withDraftApproval(readmeFor(pack, verdict)))
606
- .pipe(
607
- Effect.andThen(
608
- context.git
609
- .commitAll(`modernize(${pack.name}): spec pack (${verdict})`)
610
- .pipe(Effect.asVoid)
611
- )
612
- )
613
- )
614
-
615
- if (result.issues.length > 0) {
616
- return yield* FlowAborted.make({
617
- message:
618
- `extraction gate not cleared after ${MaxRounds} round(s) — spec pack committed as draft:\n` +
619
- result.issues.map((issue) => `- ${issue.title}`).join("\n")
620
- })
621
- }
622
- yield* context.events.publish(
623
- Info.make({
624
- message:
625
- `spec pack ready — review ${ModDir}/README.md, set '- [x] Approved', ` +
626
- "then run the seed phase"
627
- })
628
- )
629
- })
630
- )
631
- })
632
-
633
- runFlowMain(program)