@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
|
|
156
|
-
export const
|
|
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.
|
|
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/
|
|
56
|
-
"@llm4ts/
|
|
57
|
-
"@llm4ts/
|
|
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"
|