@llm4ts/shell 0.6.0 → 0.6.2

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.
@@ -1,368 +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 { stage } from "@llm4ts/flow/PlanExecution"
44
- import { mergeReviewResults } from "@llm4ts/flow/Review"
45
- import { coverage, coverageUnits, features, matchingFiles } from "@llm4ts/flow/SpecChecks"
46
- import {
47
- ProgramArtifacts,
48
- ProgramUnit,
49
- extractProgramsResumably
50
- } from "@llm4ts/modernize/Artifacts"
51
- import { asReadOnly, coderFromEnv } from "@llm4ts/runner/Connectors"
52
- import { resolveFlowInput } from "@llm4ts/runner/FlowArgs"
53
- import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner"
54
- import { nodePlainFileStore } from "@llm4ts/runner/NodePlainFileStore"
55
- import { makeNodeWorkspace } from "@llm4ts/runner/NodeWorkspace"
56
- import { openPack } from "@llm4ts/runner/Packs"
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 mode =
81
- process.env.LLM4TS_BENCH_MODE?.trim() ??
82
- (input.prompt.trim().toLowerCase() === "report" ? "report" : "measure")
83
- const files = nodePlainFileStore
84
- const benchPath = join(input.workspace, BenchFile)
85
- const coder = coderFromEnv(process.env)
86
- const startedAt = new Date()
87
-
88
- if (mode === "report") {
89
- const records = yield* loadBenchRecords(files, benchPath)
90
- if (records.length === 0) {
91
- return yield* FlowAborted.make({ message: `no benchmark records at ${benchPath}` })
92
- }
93
- const projected = Number.parseInt(process.env.LLM4TS_BENCH_PROJECT ?? "", 10)
94
- process.stdout.write(
95
- `${renderBenchReport(records, Number.isFinite(projected) && projected > 0 ? projected : undefined)}\n`
96
- )
97
- return
98
- }
99
-
100
- yield* runNode(
101
- {
102
- workDir: input.workDir,
103
- workspace: input.workspace,
104
- userPrompt: input.prompt,
105
- coder,
106
- reasoning: asReadOnly(coder),
107
- environment: process.env
108
- },
109
- (context) =>
110
- Effect.scoped(
111
- Effect.gen(function* () {
112
- const estate = yield* makeNodeWorkspace(input.workDir)
113
- const { pack } = yield* stage(
114
- context.events,
115
- "pack",
116
- openPack({
117
- environment: process.env,
118
- launchDir: input.workspace,
119
- flowDir: import.meta.dirname
120
- })
121
- )
122
- const modDirAbs = join(input.workDir, ModDir)
123
- // The tap observes the same events the terminal renders, so the
124
- // measurement never needs its own instrumentation inside the flow.
125
- const tap = yield* makeBenchTap(context.events)
126
- const phases: Array<BenchPhase> = []
127
-
128
- const measured = <A, E>(name: string, body: Effect.Effect<A, E>) =>
129
- Effect.gen(function* () {
130
- yield* tap.reset
131
- const began = Date.now()
132
- const value = yield* stage(context.events, name, body)
133
- const observation = yield* tap.get
134
- phases.push(makeBenchPhase(observation, name, Date.now() - began))
135
- return value
136
- })
137
-
138
- const programs = yield* measured(
139
- "inventory",
140
- matchingFiles(estate, pack.programs ?? pack.sources ?? ".*")
141
- )
142
- if (programs.length === 0) {
143
- return yield* FlowAborted.make({
144
- message: `no source units matched the pack's programs/sources regex under ${input.workDir}`
145
- })
146
- }
147
- const units = programs.map((relativePath) =>
148
- ProgramUnit.make({ name: programName(relativePath), sourcePath: relativePath })
149
- )
150
- const system = [
151
- pack.prompt("analysis"),
152
- pack.lessons === undefined
153
- ? undefined
154
- : `Lessons from previous modernization runs — apply them:\n${pack.lessons}`
155
- ]
156
- .filter((part) => part !== undefined)
157
- .join("\n\n")
158
-
159
- /**
160
- * Structured calls report usage only to their caller; the Chat seam
161
- * is what normally publishes `TokensUsed` (ADR 0005). A benchmark
162
- * that skipped this would measure wall-clock and nothing else, so
163
- * every measured call republishes its own usage for the tap.
164
- */
165
- const publishUsage = (agent: string, usage: TokenUsage, model: string | undefined) =>
166
- context.events.publish(
167
- TokensUsed.make({
168
- agent,
169
- usage,
170
- ...(model === undefined ? {} : { model })
171
- })
172
- )
173
-
174
- yield* measured(
175
- "extract",
176
- extractProgramsResumably(
177
- files,
178
- units,
179
- (unit) =>
180
- context.coder
181
- .executeStructuredWithUsage(
182
- [
183
- system,
184
- "",
185
- `Extract the behavioural spec for ONE source unit: ${unit.sourcePath}`,
186
- 'Respond only with JSON: {"spec":"…","feature":"…","traceability":"…","mapping":"…"}.',
187
- pack.prompt("spec") ?? "",
188
- pack.prompt("bdd") ?? ""
189
- ].join("\n"),
190
- ProgramArtifacts,
191
- { type: "object" }
192
- )
193
- .pipe(
194
- Effect.mapError(FlowLlmError.from),
195
- Effect.tap(([, usage, model]) =>
196
- usage === undefined ? Effect.void : publishUsage("coder", usage, model)
197
- ),
198
- Effect.map(([artifacts]) => artifacts)
199
- ),
200
- modDirAbs
201
- )
202
- )
203
-
204
- // The deterministic half of the extraction gate, measured on its own:
205
- // it is the part a slower model cannot make cheaper.
206
- const deterministic = yield* measured(
207
- "gate",
208
- Effect.gen(function* () {
209
- const fragments: Array<string> = []
210
- for (const unit of units) {
211
- const text = yield* files.read(join(modDirAbs, "traceability", `${unit.name}.md`))
212
- if (text !== undefined) {
213
- fragments.push(text)
214
- }
215
- }
216
- const trace = fragments.join("\n\n")
217
- const covered = yield* coverage(estate, pack.coverage, trace)
218
- const wellFormed = yield* features(estate, `${ModDir}/features`)
219
- return mergeReviewResults([covered, wellFormed])
220
- })
221
- )
222
-
223
- /**
224
- * The evaluator has no usage-reporting variant, so the seat it runs
225
- * on is wrapped instead: every structured call it makes reports its
226
- * usage to the tap before returning the value.
227
- */
228
- const metered: LlmServiceShape = {
229
- ...context.reasoning,
230
- executeStructured: (prompt, schema, jsonSchema) =>
231
- context.reasoning.executeStructuredWithUsage(prompt, schema, jsonSchema).pipe(
232
- Effect.tap(([, usage, model]) =>
233
- usage === undefined ? Effect.void : publishUsage("reasoning", usage, model)
234
- ),
235
- Effect.map(([value]) => value)
236
- )
237
- }
238
-
239
- const judged = yield* measured(
240
- "judge",
241
- Effect.gen(function* () {
242
- const packJudge = judge(metered, pack.judgeDimensions)
243
- const scores: Array<BenchScore> = []
244
- let findings = 0
245
- for (const unit of units) {
246
- const spec = (yield* files.read(join(modDirAbs, "specs", `${unit.name}.md`))) ?? ""
247
- const source = (yield* files.read(join(input.workDir, unit.sourcePath))) ?? ""
248
- const scored = yield* packJudge
249
- .evaluate(Sample.make({ response: spec, context: source, query: input.prompt }))
250
- .pipe(Effect.mapError(FlowLlmError.from))
251
- for (const score of scored.scores) {
252
- const max = pack.judgeDimensions.find((d) => d.name === score.name)?.maxScore ?? 2
253
- scores.push(
254
- BenchScore.make({
255
- name: `${unit.name}:${score.name}`,
256
- score: score.score,
257
- max,
258
- reasoning: score.reasoning
259
- })
260
- )
261
- if (score.score < max) {
262
- findings += 1
263
- }
264
- }
265
- }
266
- return { scores, findings }
267
- })
268
- )
269
-
270
- const quality = yield* Effect.gen(function* () {
271
- const specFiles = yield* matchingFiles(estate, `^${ModDir}/specs/.*\\.md$`).pipe(
272
- Effect.orElseSucceed(() => [])
273
- )
274
- const featureFiles = yield* matchingFiles(
275
- estate,
276
- `^${ModDir}/features/.*\\.feature$`
277
- ).pipe(Effect.orElseSucceed(() => []))
278
- let scenarios = 0
279
- for (const path of featureFiles) {
280
- const text = yield* estate.read(path).pipe(Effect.orElseSucceed(() => ""))
281
- scenarios += text
282
- .split(/\r?\n/)
283
- .filter((line) => line.trim().startsWith("Scenario")).length
284
- }
285
- let sourceLines = 0
286
- for (const path of programs) {
287
- const text = yield* estate.read(path).pipe(Effect.orElseSucceed(() => ""))
288
- sourceLines += text.split(/\r?\n/).length
289
- }
290
- const units_ = yield* coverageUnits(estate, pack.coverage)
291
- const malformed = (yield* features(estate, `${ModDir}/features`)).issues.length
292
- return BenchQuality.make({
293
- featureFiles: featureFiles.length,
294
- scenarios,
295
- malformedFeatures: malformed,
296
- specFiles: specFiles.length,
297
- programs: programs.length,
298
- sourceFiles: programs.length,
299
- sourceLines,
300
- testFiles: featureFiles.length,
301
- testLines: scenarios,
302
- deterministicFindings: deterministic.issues.length,
303
- judgeFindings: judged.findings,
304
- gateCleared: deterministic.issues.length === 0 && judged.findings === 0,
305
- gateOpenIssues: deterministic.issues.length + judged.findings,
306
- judgeScores: judged.scores,
307
- // The rule universe is the estate's own coverage units.
308
- coveragePct:
309
- Object.values(units_).flat().length === 0
310
- ? 0
311
- : Math.round(
312
- (Object.values(units_)
313
- .flat()
314
- .filter((unit) => unit.length > 0).length /
315
- Object.values(units_).flat().length) *
316
- 100
317
- )
318
- })
319
- })
320
-
321
- // The fingerprint groups records of the SAME estate so the report can
322
- // compare providers rather than mixing unrelated fixtures.
323
- const fingerprint = reviewFingerprint(
324
- pack.name,
325
- ...[...programs].sort(),
326
- String(quality.sourceLines)
327
- ).slice(0, 16)
328
-
329
- const finishedAt = new Date()
330
- const record = BenchRecord.make({
331
- schemaVersion: CurrentBenchSchema,
332
- runId: `${startedAt.toISOString()}-${process.pid}`,
333
- startedAt: startedAt.toISOString(),
334
- finishedAt: finishedAt.toISOString(),
335
- provider: coder.connectorId.value,
336
- modelRequested: coder.model ?? "(harness default)",
337
- modelsServed: [],
338
- judge: BenchJudge.make({
339
- provider: coder.connectorId.value,
340
- model: coder.model ?? "(harness default)",
341
- fixed: false
342
- }),
343
- llm4tsVersion: packageVersion,
344
- pack: pack.name,
345
- fixtureFingerprint: fingerprint,
346
- machine: machine(),
347
- outcome: quality.gateCleared === true ? "completed" : "gate-open",
348
- totalMs: finishedAt.getTime() - startedAt.getTime(),
349
- phases,
350
- quality
351
- })
352
-
353
- yield* appendBenchRecord(files, benchPath, record)
354
- yield* context.events.publish(
355
- Info.make({
356
- message:
357
- `benchmark appended to ${benchPath} — ${record.totalTokens} token(s) over ` +
358
- `${phases.length} phase(s); report with LLM4TS_BENCH_MODE=report`
359
- })
360
- )
361
- const all = yield* loadBenchRecords(files, benchPath)
362
- process.stdout.write(`${renderBenchReport(all)}\n`)
363
- })
364
- )
365
- )
366
- })
367
-
368
- runFlowMain(program)