@llm4ts/shell 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,7 @@ import { FlowEvents } from "@llm4ts/flow/FlowEvents";
11
11
  import { ReviewIssue } from "@llm4ts/flow/Review";
12
12
  import { cachedReview } from "@llm4ts/flow/ReviewCache";
13
13
  import { FlowLlmError, Info, ReviewResult, makeChat, reviewFingerprint } from "@llm4ts/runner";
14
+ import { packageVersion } from "@llm4ts/flow/Package";
14
15
  export const ModDir = "docs/modernization";
15
16
  export const positiveEnvInt = (name, fallback) => {
16
17
  const raw = Number.parseInt(process.env[name] ?? "", 10);
@@ -152,11 +153,19 @@ export const globalFixAsk = (issues) => [
152
153
  "from the fragments — do not edit them directly. Fix the findings in place, then stop:",
153
154
  issueLines(issues)
154
155
  ].join("\n");
155
- /** The spec pack README; `notes` records what a refinement changed after the gate passed. */
156
- export const readmeFor = (pack, verdict, notes = []) => [
156
+ /** The `Written by llm4ts X.Y.Z` stamp of a README, or undefined for a pack older than the stamp. */
157
+ export const readmeVersion = (readme) => /^Written by llm4ts (\S+)\.$/m.exec(readme)?.[1];
158
+ /**
159
+ * The spec pack README; `notes` records what a refinement or an upgrade
160
+ * changed after the gate passed. Every writer stamps the llm4ts version it
161
+ * ran as, so a pack extracted by an older release can be recognised and
162
+ * checked (`modernize-pack-upgrade`) before a newer release continues it.
163
+ */
164
+ export const readmeFor = (pack, verdict, notes = [], version = packageVersion) => [
157
165
  `# Modernization spec pack — ${pack.name}`,
158
166
  "",
159
167
  `Extracted by the modernize-extract flow. Gate verdict: ${verdict}.`,
168
+ `Written by llm4ts ${version}.`,
160
169
  "",
161
170
  "- specs/ — behavioural specs, one per program",
162
171
  "- features/ — BDD acceptance scenarios",
@@ -0,0 +1,246 @@
1
+ // Continue a modernization an OLDER llm4ts extracted: check its spec pack against the current pack rules and spec schema, re-index it, and mark what must be re-extracted (no LLM).
2
+ //
3
+ // Runs rooted at the LEGACY repository (`--repo <legacy>`) holding a
4
+ // docs/modernization/ pack written by any earlier release (the README of a
5
+ // pack older than 2.2.0 carries no `Written by llm4ts` stamp). Deterministic:
6
+ //
7
+ // 1. Every program with a spec is checked for its four artifacts, the
8
+ // feature file's Gherkin shape, and — when the pack declares
9
+ // `spec-schema:` — a decodable pagespec block under the CURRENT schema
10
+ // (2.0.0 made `esbService` identifier-only, which fails older blocks).
11
+ // 2. traceability.md, mapping.md, and rules.txt are regenerated from the
12
+ // fragments under the current pack's coverage rules; units the current
13
+ // rules capture that no fragment covers are reported (the closing
14
+ // modernize-extract run, or a deepen, closes them).
15
+ // 3. The README is rewritten with the current version stamp and an
16
+ // upgrade note, its approval reset: a pack another release touched is
17
+ // re-approved by a human before seed.
18
+ // 4. With LLM4TS_MARK_DEEPEN=1 every incompatible program gets a `## Deepen`
19
+ // mark in decisions.md ("regenerate the artifacts under the current
20
+ // schema …"), so `modernize-refine` re-extracts exactly those with the
21
+ // current prompts and judge, one commit each. Without it the marks are
22
+ // printed for a human to paste.
23
+ //
24
+ // Exit 0 whether or not findings exist — the findings ARE the result; the
25
+ // commit records them. Pack: LLM4TS_PACK as for modernize-extract.
26
+ import { join } from "node:path";
27
+ import * as Effect from "effect/Effect";
28
+ import { ProgramUnit, programArtifactPaths } from "@llm4ts/flow/Artifacts";
29
+ import { withDraftApproval } from "@llm4ts/flow/Approval";
30
+ import { DeepenMark, Decisions, parseDecisions, renderDecisions, scenarioTitles, waivedUnits } from "@llm4ts/flow/Decisions";
31
+ import { packageVersion } from "@llm4ts/flow/Package";
32
+ import { coverageReport, coverageUnits, features, matchingFiles, specSchemaIssues } from "@llm4ts/flow/SpecChecks";
33
+ import { legacySourceWorkspaceLimits, workspaceLimitsFromEnv } from "@llm4ts/flow/Workspace";
34
+ import { FlowAborted, Info, makeNodeWorkspace, mock, nodePlainFileStore, openPack, resolveFlowInput, runFlowMain, runNode, stage } from "@llm4ts/runner";
35
+ import { ModDir, programName, readmeFor, readmeVersion } from "./lib/modernize-extract.js";
36
+ const program = Effect.gen(function* () {
37
+ const input = yield* resolveFlowInput("Check a spec pack an older llm4ts extracted against the current release");
38
+ const files = nodePlainFileStore;
39
+ const modDirAbs = join(input.workDir, ModDir);
40
+ const markDeepen = process.env.LLM4TS_MARK_DEEPEN?.trim() === "1";
41
+ yield* runNode({
42
+ workDir: input.workDir,
43
+ workspace: input.workspace,
44
+ userPrompt: input.prompt,
45
+ // No model call: the mock seat satisfies the one context shape the
46
+ // runner composes for every flow.
47
+ coder: mock,
48
+ environment: process.env
49
+ }, (context) => Effect.gen(function* () {
50
+ const say = (message) => context.events.publish(Info.make({ message }));
51
+ const repo = yield* makeNodeWorkspace(input.workDir, workspaceLimitsFromEnv(process.env, legacySourceWorkspaceLimits));
52
+ const opened = yield* stage(context.events, "pack", openPack({
53
+ environment: process.env,
54
+ launchDir: input.workspace,
55
+ flowDir: import.meta.dirname
56
+ }));
57
+ const pack = opened.pack;
58
+ const readme = yield* files.read(join(modDirAbs, "README.md"));
59
+ if (readme === undefined) {
60
+ return yield* FlowAborted.make({
61
+ message: `no spec pack under ${ModDir} — nothing to upgrade`
62
+ });
63
+ }
64
+ const previous = readmeVersion(readme) ?? "an llm4ts older than 2.2.0 (no version stamp)";
65
+ yield* say(`spec pack written by ${previous}; checking it as llm4ts ${packageVersion}`);
66
+ const specPaths = yield* repo
67
+ .discover(`${ModDir}/specs/*.md`)
68
+ .pipe(Effect.orElseSucceed(() => []));
69
+ const names = [...specPaths]
70
+ .map((path) => path.split("/").at(-1) ?? path)
71
+ .filter((file) => file.endsWith(".md") && file !== "README.md")
72
+ .map((file) => file.slice(0, -".md".length))
73
+ .sort();
74
+ if (names.length === 0) {
75
+ return yield* FlowAborted.make({ message: `no specs under ${ModDir}/specs` });
76
+ }
77
+ const sources = yield* matchingFiles(repo, pack.programs ?? pack.sources ?? ".*", pack.exclude);
78
+ const sourceOf = new Map(sources.map((path) => [programName(path), path]));
79
+ const units = names.map((name) => ProgramUnit.make({ name, sourcePath: sourceOf.get(name) ?? "" }));
80
+ // ---- 1. Per-program artifacts under the current rules ----------------------
81
+ const findings = [];
82
+ const fragments = new Map();
83
+ const scenarios = new Map();
84
+ yield* stage(context.events, "check", Effect.gen(function* () {
85
+ const specs = [];
86
+ for (const unit of units) {
87
+ const [specPath, featurePath, tracePath, mappingPath] = programArtifactPaths(unit, modDirAbs);
88
+ const spec = yield* files.read(specPath);
89
+ specs.push({ name: unit.name, markdown: spec });
90
+ if (unit.sourcePath.length === 0) {
91
+ findings.push({
92
+ program: unit.name,
93
+ problem: "no legacy source matches the current pack's programs regex",
94
+ deepen: false
95
+ });
96
+ }
97
+ const feature = yield* files.read(featurePath);
98
+ if (feature === undefined) {
99
+ findings.push({ program: unit.name, problem: "feature file missing", deepen: true });
100
+ }
101
+ else {
102
+ scenarios.set(unit.name, new Set(scenarioTitles(feature)));
103
+ }
104
+ const trace = yield* files.read(tracePath);
105
+ if (trace === undefined || trace.trim().length === 0) {
106
+ findings.push({
107
+ program: unit.name,
108
+ problem: "traceability fragment missing",
109
+ deepen: true
110
+ });
111
+ }
112
+ else {
113
+ fragments.set(unit.name, trace);
114
+ }
115
+ if ((yield* files.read(mappingPath)) === undefined) {
116
+ findings.push({
117
+ program: unit.name,
118
+ problem: "mapping fragment missing",
119
+ deepen: true
120
+ });
121
+ }
122
+ }
123
+ for (const issue of yield* specSchemaIssues(pack.specSchema, specs)) {
124
+ const name = /^judge\[([^\]]+)\]/.exec(issue.title)?.[1] ?? "?";
125
+ findings.push({
126
+ program: name,
127
+ problem: `pagespec block does not decode under the current schema: ${issue.description.split(". ")[0] ?? ""}`,
128
+ deepen: true
129
+ });
130
+ }
131
+ const wellFormed = yield* features(repo, join(ModDir, "features"));
132
+ for (const issue of wellFormed.issues) {
133
+ const stem = (issue.file?.split("/").at(-1) ?? "").replace(/\.feature$/, "");
134
+ const name = names.find((candidate) => candidate.toLowerCase() === stem) ?? stem;
135
+ findings.push({ program: name, problem: issue.description, deepen: true });
136
+ }
137
+ }));
138
+ // ---- 2. Indexes and rules.txt under the current pack --------------------------
139
+ let uncovered = [];
140
+ yield* stage(context.events, "reindex", Effect.gen(function* () {
141
+ for (const [fragmentDir, index] of [
142
+ ["traceability", "traceability.md"],
143
+ ["mapping", "mapping.md"]
144
+ ]) {
145
+ const parts = [];
146
+ for (const unit of units) {
147
+ const text = yield* files.read(join(modDirAbs, fragmentDir, `${unit.name}.md`));
148
+ if (text !== undefined && text.trim().length > 0) {
149
+ parts.push(`===== ${unit.name} =====\n${text.trimEnd()}`);
150
+ }
151
+ }
152
+ if (parts.length > 0) {
153
+ yield* files.writeAtomic(join(modDirAbs, index), parts.join("\n\n") + "\n");
154
+ }
155
+ }
156
+ const decisionsText = yield* files.read(join(modDirAbs, "decisions.md"));
157
+ const decisions = decisionsText === undefined
158
+ ? Decisions.empty()
159
+ : yield* parseDecisions(decisionsText, `${ModDir}/decisions.md`);
160
+ const unitsByRule = yield* coverageUnits(repo, pack.coverage);
161
+ const allUnits = [...new Set(Object.values(unitsByRule).flat())].sort();
162
+ const waived = waivedUnits(decisions, { fragments, scenarios });
163
+ if (allUnits.length > 0) {
164
+ yield* files.writeAtomic(join(modDirAbs, "rules.txt"), [
165
+ ...allUnits,
166
+ ...(waived.length === 0
167
+ ? []
168
+ : [
169
+ "# waived",
170
+ ...waived.map((entry) => `${entry.unit} — waived by ${entry.by}`)
171
+ ])
172
+ ].join("\n") + "\n");
173
+ }
174
+ const trace = (yield* files.read(join(modDirAbs, "traceability.md"))) ?? "";
175
+ const report = yield* coverageReport(repo, pack.coverage, trace, {
176
+ waived: new Set(waived.map((entry) => entry.unit))
177
+ });
178
+ uncovered = report.result.issues.map((issue) => issue.title);
179
+ }));
180
+ // ---- 3. README stamp, 4. deepen marks -------------------------------------------
181
+ const toDeepen = [...new Set(findings.filter((f) => f.deepen).map((f) => f.program))];
182
+ const markLines = toDeepen.map((name) => `- ${name}: regenerate the artifacts under the current llm4ts ${packageVersion} schema and prompts — ` +
183
+ findings
184
+ .filter((f) => f.program === name && f.deepen)
185
+ .map((f) => f.problem)
186
+ .join("; "));
187
+ if (markDeepen && toDeepen.length > 0) {
188
+ yield* stage(context.events, "mark", Effect.gen(function* () {
189
+ const text = yield* files.read(join(modDirAbs, "decisions.md"));
190
+ const decisions = text === undefined
191
+ ? Decisions.empty()
192
+ : yield* parseDecisions(text, `${ModDir}/decisions.md`);
193
+ const already = new Set(decisions.pendingDeepen.map((mark) => mark.program));
194
+ const added = toDeepen
195
+ .filter((name) => !already.has(name))
196
+ .map((name) => DeepenMark.make({
197
+ program: name,
198
+ focus: markLines
199
+ .find((line) => line.startsWith(`- ${name}: `))
200
+ ?.slice(name.length + 4) ?? ""
201
+ }));
202
+ yield* files.writeAtomic(join(modDirAbs, "decisions.md"), renderDecisions(Decisions.make({
203
+ ...decisions,
204
+ deepen: [...decisions.deepen, ...added],
205
+ approved: false
206
+ })));
207
+ yield* say(`${added.length} deepen mark(s) written to ${ModDir}/decisions.md`);
208
+ }));
209
+ }
210
+ const verdict = /Gate verdict: (.+?)\.\n/.exec(readme)?.[1] ?? "UNKNOWN — extracted by an older release";
211
+ const priorNotes = (/Refined after the gate passed:\n((?:- .*\n)+)/.exec(readme)?.[1] ?? "")
212
+ .split("\n")
213
+ .filter((line) => line.startsWith("- "))
214
+ .map((line) => line.slice(2));
215
+ const note = `upgraded from ${previous} to llm4ts ${packageVersion}: ${findings.length} finding(s), ` +
216
+ `${uncovered.length} uncovered unit(s) under the current rules` +
217
+ (markDeepen && toDeepen.length > 0
218
+ ? `, ${toDeepen.length} program(s) marked for deepen`
219
+ : "");
220
+ yield* files.writeAtomic(join(modDirAbs, "README.md"), withDraftApproval(readmeFor(pack, verdict, [...priorNotes, note])));
221
+ yield* stage(context.events, "commit", context.git
222
+ .commitAll(`modernize(${pack.name}): pack upgrade check as llm4ts ${packageVersion}`)
223
+ .pipe(Effect.asVoid));
224
+ // ---- Report -------------------------------------------------------------------
225
+ for (const finding of findings) {
226
+ yield* say(`finding: ${finding.program} — ${finding.problem}`);
227
+ }
228
+ for (const title of uncovered.slice(0, 20)) {
229
+ yield* say(`uncovered under the current rules: ${title}`);
230
+ }
231
+ if (uncovered.length > 20) {
232
+ yield* say(`… and ${uncovered.length - 20} more uncovered unit(s)`);
233
+ }
234
+ if (toDeepen.length > 0 && !markDeepen) {
235
+ yield* say(`${toDeepen.length} program(s) need re-extraction — rerun with LLM4TS_MARK_DEEPEN=1, or add under '## Deepen' in ${ModDir}/decisions.md:\n` +
236
+ markLines.join("\n"));
237
+ }
238
+ yield* say(findings.length === 0 && uncovered.length === 0
239
+ ? `pack is compatible with llm4ts ${packageVersion} — review ${ModDir}/README.md, flip '- [x] Approved', then continue with modernize-refine or modernize-seed`
240
+ : `upgrade check done — ${findings.length} finding(s); ` +
241
+ (markDeepen && toDeepen.length > 0
242
+ ? "run modernize-refine to re-extract the marked programs"
243
+ : "mark the programs to re-extract, then run modernize-refine"));
244
+ }));
245
+ });
246
+ runFlowMain(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/shell",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Interactive shell and CLI for llm4ts: flow discovery, run-a-flow, and view",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -52,9 +52,9 @@
52
52
  "dependencies": {
53
53
  "@effect/platform-node": "4.0.0-rc.115",
54
54
  "@effect/platform-node-shared": "4.0.0-rc.115",
55
- "@llm4ts/core": "2.2.0",
56
- "@llm4ts/flow": "2.2.0",
57
- "@llm4ts/runner": "2.2.0"
55
+ "@llm4ts/flow": "2.2.1",
56
+ "@llm4ts/runner": "2.2.1",
57
+ "@llm4ts/core": "2.2.1"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "effect": "4.0.0-rc.115"