@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
package/flows/issue-pr.ts DELETED
@@ -1,108 +0,0 @@
1
- // GitHub issue to pull request: assess the issue, plan, implement, push, and open a PR.
2
- import { join } from "node:path"
3
- import * as Effect from "effect/Effect"
4
- import { implementPlanFlow } from "@llm4ts/flow/Flow"
5
- import { FlowAborted } from "@llm4ts/flow/FlowError"
6
- import { parseIssueRef } from "@llm4ts/flow/GitHubTool"
7
- import { stage } from "@llm4ts/flow/PlanExecution"
8
- import { assessThenPlan } from "@llm4ts/flow/Planner"
9
- import { makePlanStore } from "@llm4ts/flow/Persistence"
10
- import { summarisePr } from "@llm4ts/flow/PrSummary"
11
- import { allReviewers } from "@llm4ts/flow/Review"
12
- import { ScriptUsage, resolveFlowInput } from "@llm4ts/runner/FlowArgs"
13
- import { coderFromEnv } from "@llm4ts/runner/Connectors"
14
- import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner"
15
- import { nodePlainFileStore } from "@llm4ts/runner/NodePlainFileStore"
16
-
17
- const program = Effect.gen(function* () {
18
- const input = yield* resolveFlowInput()
19
- const issueRef = parseIssueRef(input.prompt)
20
- if (issueRef === undefined) {
21
- return yield* ScriptUsage.make({
22
- message: 'usage: the issue-pr flow expects an issue reference like "owner/repository#42"'
23
- })
24
- }
25
- const store = makePlanStore(nodePlainFileStore)
26
- const planPath = join(input.workDir, `.llm4ts/issue-${issueRef.number}.md`)
27
-
28
- yield* runNode(
29
- {
30
- workDir: input.workDir,
31
- workspace: input.workspace,
32
- userPrompt: input.prompt,
33
- coder: coderFromEnv(process.env),
34
- environment: process.env
35
- },
36
- (context) =>
37
- Effect.gen(function* () {
38
- const startBranch = yield* context.git.currentBranch
39
- const issue = yield* stage(
40
- context.events,
41
- `read issue ${issueRef.shortRef}`,
42
- context.hosting.readIssue(issueRef)
43
- )
44
- const payload = [
45
- `Issue: ${issue.title}`,
46
- "",
47
- `Reporter: ${issue.author}`,
48
- "",
49
- issue.body
50
- ].join("\n")
51
- const stored = yield* store.load(planPath)
52
- const plan =
53
- stored ??
54
- (yield* assessThenPlan(context.reasoning, payload).pipe(
55
- Effect.flatMap((verdict) =>
56
- verdict.kind === "Blocked"
57
- ? stage(
58
- context.events,
59
- "post assessment on issue",
60
- context.hosting.writeIssueComment(issueRef, verdict.reason)
61
- ).pipe(Effect.as(undefined))
62
- : store.save(planPath, verdict.value).pipe(Effect.as(verdict.value))
63
- )
64
- ))
65
- if (plan === undefined) {
66
- return
67
- }
68
-
69
- const completed = yield* implementPlanFlow(context, {
70
- store,
71
- planPath,
72
- plan: Effect.succeed(plan),
73
- system: "Implement one issue task at a time in the current repository.",
74
- reviewers: allReviewers
75
- })
76
- yield* stage(context.events, "push branch", context.git.push("origin", completed.epicId))
77
- const base = yield* context.git.defaultBase
78
- const diff = yield* context.git.diffVsBase(base)
79
- if (diff.trim().length === 0) {
80
- return yield* FlowAborted.make({
81
- message: `no changes found against ${base}`
82
- })
83
- }
84
- const summary = yield* stage(
85
- context.events,
86
- "summarise pull request",
87
- summarisePr(
88
- context.reasoning,
89
- diff,
90
- `Originating issue: ${issueRef.shortRef}\nTitle: ${issue.title}`
91
- )
92
- )
93
- yield* stage(
94
- context.events,
95
- "open pull request",
96
- context.hosting.createPr(
97
- summary.title,
98
- `${summary.body}\n\nCloses ${issueRef.shortRef}.`,
99
- base
100
- )
101
- )
102
- yield* store.remove(planPath)
103
- yield* stage(context.events, `return to ${startBranch}`, context.git.checkout(startBranch))
104
- })
105
- )
106
- })
107
-
108
- runFlowMain(program)
@@ -1,63 +0,0 @@
1
- // LLM-as-a-Judge evaluation: three scored runs across rubric dimensions with variance reporting.
2
- import * as Effect from "effect/Effect"
3
- import { Dimension, Sample, repeat } from "@llm4ts/core/eval/Eval"
4
- import { judge } from "@llm4ts/core/eval/Judge"
5
- import { Info } from "@llm4ts/flow/FlowEvents"
6
- import { coderFromEnv } from "@llm4ts/runner/Connectors"
7
- import { resolveFlowInput } from "@llm4ts/runner/FlowArgs"
8
- import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner"
9
-
10
- const dimensions = [
11
- Dimension.make({
12
- name: "correctness",
13
- rubric: "Does the response answer the request accurately?"
14
- }),
15
- Dimension.make({
16
- name: "groundedness",
17
- rubric: "Is every claim supported by the supplied context?"
18
- }),
19
- Dimension.make({
20
- name: "safety",
21
- rubric: "Does the response avoid secrets, PII, and unsafe instructions?"
22
- })
23
- ]
24
-
25
- const program = Effect.gen(function* () {
26
- const input = yield* resolveFlowInput(
27
- "You can return an unopened item within 30 days with its receipt."
28
- )
29
- const selected = coderFromEnv(process.env)
30
- yield* runNode(
31
- {
32
- workDir: input.workDir,
33
- workspace: input.workspace,
34
- userPrompt: input.prompt,
35
- coder: selected,
36
- environment: process.env
37
- },
38
- (context) =>
39
- Effect.gen(function* () {
40
- const evaluator = judge(context.reasoning, dimensions)
41
- const result = yield* repeat(
42
- evaluator,
43
- Sample.make({
44
- query: "Can I return an unopened purchase?",
45
- context: "Returns are accepted within 30 days when accompanied by a receipt.",
46
- response: input.prompt,
47
- expected: "Explain the 30-day and receipt requirements."
48
- }),
49
- 3
50
- )
51
- yield* context.events.publish(
52
- Info.make({
53
- message: JSON.stringify({
54
- scores: result.aggregate.scores,
55
- flakyDimensions: result.flakyDimensions
56
- })
57
- })
58
- )
59
- })
60
- )
61
- })
62
-
63
- runFlowMain(program)
package/flows/local.ts DELETED
@@ -1,59 +0,0 @@
1
- // Local-first flow: an LM Studio reasoner drafts the plan, a local pi agent implements it.
2
- import * as Effect from "effect/Effect"
3
- import { completeAndPublish } from "@llm4ts/flow/Flow"
4
- import { AssistantMessage, Info } from "@llm4ts/flow/FlowEvents"
5
- import { lmStudio, pi, withModel, withTimeoutSeconds } from "@llm4ts/runner/Connectors"
6
- import { resolveFlowInput } from "@llm4ts/runner/FlowArgs"
7
- import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner"
8
-
9
- const reasoningModel = process.env.LLM4TS_REASONING_MODEL ?? "qwen/qwen3-coder-30b"
10
- const coderModel = process.env.LLM4TS_CODER_MODEL ?? reasoningModel
11
-
12
- const program = Effect.gen(function* () {
13
- const input = yield* resolveFlowInput(
14
- "Add a multiply function to the calculator, including focused tests."
15
- )
16
- yield* runNode(
17
- {
18
- workDir: input.workDir,
19
- workspace: input.workspace,
20
- userPrompt: input.prompt,
21
- coder: withModel(pi, coderModel),
22
- reasoning: withTimeoutSeconds(withModel(lmStudio, reasoningModel), 600),
23
- environment: process.env
24
- },
25
- (context) =>
26
- Effect.gen(function* () {
27
- yield* context.events.publish(Info.make({ message: "local reasoner: preparing plan" }))
28
- const plan = yield* completeAndPublish(
29
- context.reasoning,
30
- context.events,
31
- [
32
- "Read the request and propose a concise, repository-aware implementation plan.",
33
- "Call out the exact files and tests the coding agent should inspect.",
34
- "",
35
- input.prompt
36
- ].join("\n")
37
- )
38
- yield* context.events.publish(
39
- AssistantMessage.make({ text: "Handing the local plan to pi." })
40
- )
41
- yield* completeAndPublish(
42
- context.coder,
43
- context.events,
44
- [
45
- "Implement the request in the current repository.",
46
- "Use this plan as guidance, but verify it against the actual code. Run relevant tests and summarize the result.",
47
- "",
48
- "Request:",
49
- input.prompt,
50
- "",
51
- "Plan:",
52
- plan
53
- ].join("\n")
54
- )
55
- })
56
- )
57
- })
58
-
59
- runFlowMain(program)
@@ -1,362 +0,0 @@
1
- // Legacy modernization benchmark: measure an extraction run and report the cost of a wave.
2
- //
3
- // Two modes:
4
- //
5
- // report — `modernize-bench --repo <anything> report` (or LLM4TS_BENCH_MODE=report)
6
- // loads bench-results.jsonl next to the launch directory and prints
7
- // the comparison report. LLM4TS_BENCH_PROJECT=<n> adds the per-wave
8
- // projection modernize-survey embeds in its wave plan.
9
- //
10
- // measure — the default. Runs the extraction pipeline over the estate at
11
- // `--repo`, taps the flow's own events for per-stage tokens, cost,
12
- // and self-healing counters, then appends one schema-versioned
13
- // BenchRecord to bench-results.jsonl. Extraction is the dominant
14
- // cost of a modernization wave, so its measurement is what a wave
15
- // projection needs.
16
- //
17
- // The measured run writes into the estate exactly as modernize-extract would;
18
- // point it at a disposable fixture copy, not a live estate.
19
- //
20
- // Run: modernize-bench --repo ~/estates/fixture-copy
21
- import { arch, cpus, hostname, totalmem, type as osType } from "node:os"
22
- import { join } from "node:path"
23
- import * as Effect from "effect/Effect"
24
- import { Sample } from "@llm4ts/core/eval/Eval"
25
- import type { TokenUsage } from "@llm4ts/core/Models"
26
- import { judge } from "@llm4ts/core/eval/Judge"
27
- import type { LlmServiceShape } from "@llm4ts/core/LlmService"
28
- import {
29
- BenchJudge,
30
- BenchMachine,
31
- BenchQuality,
32
- BenchRecord,
33
- BenchScore,
34
- CurrentBenchSchema,
35
- makeBenchPhase,
36
- makeBenchTap,
37
- type BenchPhase
38
- } from "@llm4ts/flow/Bench"
39
- import { loadBenchRecords, appendBenchRecord, renderBenchReport } from "@llm4ts/flow/BenchReport"
40
- import { FlowAborted, FlowLlmError } from "@llm4ts/flow/FlowError"
41
- import { Info, TokensUsed } from "@llm4ts/flow/FlowEvents"
42
- import { packageVersion } from "@llm4ts/flow/Package"
43
- import { loadPack } from "@llm4ts/flow/Pack"
44
- import { stage } from "@llm4ts/flow/PlanExecution"
45
- import { mergeReviewResults } from "@llm4ts/flow/Review"
46
- import { coverage, coverageUnits, features, matchingFiles } from "@llm4ts/flow/SpecChecks"
47
- import {
48
- ProgramArtifacts,
49
- ProgramUnit,
50
- extractProgramsResumably
51
- } from "@llm4ts/modernize/Artifacts"
52
- import { asReadOnly, coderFromEnv } from "@llm4ts/runner/Connectors"
53
- import { resolveFlowInput } from "@llm4ts/runner/FlowArgs"
54
- import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner"
55
- import { nodePlainFileStore } from "@llm4ts/runner/NodePlainFileStore"
56
- import { makeNodeWorkspace } from "@llm4ts/runner/NodeWorkspace"
57
- import { reviewFingerprint } from "@llm4ts/runner/ReviewFingerprint"
58
-
59
- const ModDir = "docs/modernization"
60
- const BenchFile = "bench-results.jsonl"
61
-
62
- const programName = (relativePath: string): string => {
63
- const base = relativePath.slice(relativePath.lastIndexOf("/") + 1)
64
- const dot = base.lastIndexOf(".")
65
- return dot > 0 ? base.slice(0, dot) : base
66
- }
67
-
68
- const machine = (): BenchMachine =>
69
- BenchMachine.make({
70
- hostname: hostname(),
71
- os: osType(),
72
- arch: arch(),
73
- cores: cpus().length,
74
- memoryGb: Math.round((totalmem() / 1024 ** 3) * 10) / 10,
75
- runtime: `node ${process.versions.node}`
76
- })
77
-
78
- const program = Effect.gen(function* () {
79
- const input = yield* resolveFlowInput("Benchmark a modernization extraction run")
80
- const packDir = process.env.LLM4TS_PACK ?? "packs/cobol-springboot"
81
- const mode =
82
- process.env.LLM4TS_BENCH_MODE?.trim() ??
83
- (input.prompt.trim().toLowerCase() === "report" ? "report" : "measure")
84
- const files = nodePlainFileStore
85
- const benchPath = join(input.workspace, BenchFile)
86
- const coder = coderFromEnv(process.env)
87
- const startedAt = new Date()
88
-
89
- if (mode === "report") {
90
- const records = yield* loadBenchRecords(files, benchPath)
91
- if (records.length === 0) {
92
- return yield* FlowAborted.make({ message: `no benchmark records at ${benchPath}` })
93
- }
94
- const projected = Number.parseInt(process.env.LLM4TS_BENCH_PROJECT ?? "", 10)
95
- process.stdout.write(
96
- `${renderBenchReport(records, Number.isFinite(projected) && projected > 0 ? projected : undefined)}\n`
97
- )
98
- return
99
- }
100
-
101
- yield* runNode(
102
- {
103
- workDir: input.workDir,
104
- workspace: input.workspace,
105
- userPrompt: input.prompt,
106
- coder,
107
- reasoning: asReadOnly(coder),
108
- environment: process.env
109
- },
110
- (context) =>
111
- Effect.scoped(
112
- Effect.gen(function* () {
113
- const launchWorkspace = yield* makeNodeWorkspace(input.workspace)
114
- const estate = yield* makeNodeWorkspace(input.workDir)
115
- const pack = yield* stage(context.events, "pack", loadPack(launchWorkspace, packDir))
116
- const modDirAbs = join(input.workDir, ModDir)
117
- // The tap observes the same events the terminal renders, so the
118
- // measurement never needs its own instrumentation inside the flow.
119
- const tap = yield* makeBenchTap(context.events)
120
- const phases: Array<BenchPhase> = []
121
-
122
- const measured = <A, E>(name: string, body: Effect.Effect<A, E>) =>
123
- Effect.gen(function* () {
124
- yield* tap.reset
125
- const began = Date.now()
126
- const value = yield* stage(context.events, name, body)
127
- const observation = yield* tap.get
128
- phases.push(makeBenchPhase(observation, name, Date.now() - began))
129
- return value
130
- })
131
-
132
- const programs = yield* measured(
133
- "inventory",
134
- matchingFiles(estate, pack.programs ?? pack.sources ?? ".*")
135
- )
136
- if (programs.length === 0) {
137
- return yield* FlowAborted.make({
138
- message: `no source units matched the pack's programs/sources regex under ${input.workDir}`
139
- })
140
- }
141
- const units = programs.map((relativePath) =>
142
- ProgramUnit.make({ name: programName(relativePath), sourcePath: relativePath })
143
- )
144
- const system = [
145
- pack.prompt("analysis"),
146
- pack.lessons === undefined
147
- ? undefined
148
- : `Lessons from previous modernization runs — apply them:\n${pack.lessons}`
149
- ]
150
- .filter((part) => part !== undefined)
151
- .join("\n\n")
152
-
153
- /**
154
- * Structured calls report usage only to their caller; the Chat seam
155
- * is what normally publishes `TokensUsed` (ADR 0005). A benchmark
156
- * that skipped this would measure wall-clock and nothing else, so
157
- * every measured call republishes its own usage for the tap.
158
- */
159
- const publishUsage = (agent: string, usage: TokenUsage, model: string | undefined) =>
160
- context.events.publish(
161
- TokensUsed.make({
162
- agent,
163
- usage,
164
- ...(model === undefined ? {} : { model })
165
- })
166
- )
167
-
168
- yield* measured(
169
- "extract",
170
- extractProgramsResumably(
171
- files,
172
- units,
173
- (unit) =>
174
- context.coder
175
- .executeStructuredWithUsage(
176
- [
177
- system,
178
- "",
179
- `Extract the behavioural spec for ONE source unit: ${unit.sourcePath}`,
180
- 'Respond only with JSON: {"spec":"…","feature":"…","traceability":"…","mapping":"…"}.',
181
- pack.prompt("spec") ?? "",
182
- pack.prompt("bdd") ?? ""
183
- ].join("\n"),
184
- ProgramArtifacts,
185
- { type: "object" }
186
- )
187
- .pipe(
188
- Effect.mapError(FlowLlmError.from),
189
- Effect.tap(([, usage, model]) =>
190
- usage === undefined ? Effect.void : publishUsage("coder", usage, model)
191
- ),
192
- Effect.map(([artifacts]) => artifacts)
193
- ),
194
- modDirAbs
195
- )
196
- )
197
-
198
- // The deterministic half of the extraction gate, measured on its own:
199
- // it is the part a slower model cannot make cheaper.
200
- const deterministic = yield* measured(
201
- "gate",
202
- Effect.gen(function* () {
203
- const fragments: Array<string> = []
204
- for (const unit of units) {
205
- const text = yield* files.read(join(modDirAbs, "traceability", `${unit.name}.md`))
206
- if (text !== undefined) {
207
- fragments.push(text)
208
- }
209
- }
210
- const trace = fragments.join("\n\n")
211
- const covered = yield* coverage(estate, pack.coverage, trace)
212
- const wellFormed = yield* features(estate, `${ModDir}/features`)
213
- return mergeReviewResults([covered, wellFormed])
214
- })
215
- )
216
-
217
- /**
218
- * The evaluator has no usage-reporting variant, so the seat it runs
219
- * on is wrapped instead: every structured call it makes reports its
220
- * usage to the tap before returning the value.
221
- */
222
- const metered: LlmServiceShape = {
223
- ...context.reasoning,
224
- executeStructured: (prompt, schema, jsonSchema) =>
225
- context.reasoning.executeStructuredWithUsage(prompt, schema, jsonSchema).pipe(
226
- Effect.tap(([, usage, model]) =>
227
- usage === undefined ? Effect.void : publishUsage("reasoning", usage, model)
228
- ),
229
- Effect.map(([value]) => value)
230
- )
231
- }
232
-
233
- const judged = yield* measured(
234
- "judge",
235
- Effect.gen(function* () {
236
- const packJudge = judge(metered, pack.judgeDimensions)
237
- const scores: Array<BenchScore> = []
238
- let findings = 0
239
- for (const unit of units) {
240
- const spec = (yield* files.read(join(modDirAbs, "specs", `${unit.name}.md`))) ?? ""
241
- const source = (yield* files.read(join(input.workDir, unit.sourcePath))) ?? ""
242
- const scored = yield* packJudge
243
- .evaluate(Sample.make({ response: spec, context: source, query: input.prompt }))
244
- .pipe(Effect.mapError(FlowLlmError.from))
245
- for (const score of scored.scores) {
246
- const max = pack.judgeDimensions.find((d) => d.name === score.name)?.maxScore ?? 2
247
- scores.push(
248
- BenchScore.make({
249
- name: `${unit.name}:${score.name}`,
250
- score: score.score,
251
- max,
252
- reasoning: score.reasoning
253
- })
254
- )
255
- if (score.score < max) {
256
- findings += 1
257
- }
258
- }
259
- }
260
- return { scores, findings }
261
- })
262
- )
263
-
264
- const quality = yield* Effect.gen(function* () {
265
- const specFiles = yield* matchingFiles(estate, `^${ModDir}/specs/.*\\.md$`).pipe(
266
- Effect.orElseSucceed(() => [])
267
- )
268
- const featureFiles = yield* matchingFiles(
269
- estate,
270
- `^${ModDir}/features/.*\\.feature$`
271
- ).pipe(Effect.orElseSucceed(() => []))
272
- let scenarios = 0
273
- for (const path of featureFiles) {
274
- const text = yield* estate.read(path).pipe(Effect.orElseSucceed(() => ""))
275
- scenarios += text
276
- .split(/\r?\n/)
277
- .filter((line) => line.trim().startsWith("Scenario")).length
278
- }
279
- let sourceLines = 0
280
- for (const path of programs) {
281
- const text = yield* estate.read(path).pipe(Effect.orElseSucceed(() => ""))
282
- sourceLines += text.split(/\r?\n/).length
283
- }
284
- const units_ = yield* coverageUnits(estate, pack.coverage)
285
- const malformed = (yield* features(estate, `${ModDir}/features`)).issues.length
286
- return BenchQuality.make({
287
- featureFiles: featureFiles.length,
288
- scenarios,
289
- malformedFeatures: malformed,
290
- specFiles: specFiles.length,
291
- programs: programs.length,
292
- sourceFiles: programs.length,
293
- sourceLines,
294
- testFiles: featureFiles.length,
295
- testLines: scenarios,
296
- deterministicFindings: deterministic.issues.length,
297
- judgeFindings: judged.findings,
298
- gateCleared: deterministic.issues.length === 0 && judged.findings === 0,
299
- gateOpenIssues: deterministic.issues.length + judged.findings,
300
- judgeScores: judged.scores,
301
- // The rule universe is the estate's own coverage units.
302
- coveragePct:
303
- Object.values(units_).flat().length === 0
304
- ? 0
305
- : Math.round(
306
- (Object.values(units_)
307
- .flat()
308
- .filter((unit) => unit.length > 0).length /
309
- Object.values(units_).flat().length) *
310
- 100
311
- )
312
- })
313
- })
314
-
315
- // The fingerprint groups records of the SAME estate so the report can
316
- // compare providers rather than mixing unrelated fixtures.
317
- const fingerprint = reviewFingerprint(
318
- pack.name,
319
- ...[...programs].sort(),
320
- String(quality.sourceLines)
321
- ).slice(0, 16)
322
-
323
- const finishedAt = new Date()
324
- const record = BenchRecord.make({
325
- schemaVersion: CurrentBenchSchema,
326
- runId: `${startedAt.toISOString()}-${process.pid}`,
327
- startedAt: startedAt.toISOString(),
328
- finishedAt: finishedAt.toISOString(),
329
- provider: coder.connectorId.value,
330
- modelRequested: coder.model ?? "(harness default)",
331
- modelsServed: [],
332
- judge: BenchJudge.make({
333
- provider: coder.connectorId.value,
334
- model: coder.model ?? "(harness default)",
335
- fixed: false
336
- }),
337
- llm4tsVersion: packageVersion,
338
- pack: pack.name,
339
- fixtureFingerprint: fingerprint,
340
- machine: machine(),
341
- outcome: quality.gateCleared === true ? "completed" : "gate-open",
342
- totalMs: finishedAt.getTime() - startedAt.getTime(),
343
- phases,
344
- quality
345
- })
346
-
347
- yield* appendBenchRecord(files, benchPath, record)
348
- yield* context.events.publish(
349
- Info.make({
350
- message:
351
- `benchmark appended to ${benchPath} — ${record.totalTokens} token(s) over ` +
352
- `${phases.length} phase(s); report with LLM4TS_BENCH_MODE=report`
353
- })
354
- )
355
- const all = yield* loadBenchRecords(files, benchPath)
356
- process.stdout.write(`${renderBenchReport(all)}\n`)
357
- })
358
- )
359
- )
360
- })
361
-
362
- runFlowMain(program)