@llm4ts/shell 0.16.2 → 0.17.0

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.
@@ -0,0 +1,166 @@
1
+ // Legacy modernization phase -1: load a pack and match its rules against an estate; no LLM.
2
+ //
3
+ // The pack equivalent of the mock provider: a deterministic first success
4
+ // before any paid phase. The pack is loaded exactly as survey and extract load
5
+ // it, then the flow reports what it found and how each rule matches the
6
+ // estate at `--repo`:
7
+ //
8
+ // 1. manifest — name, source, scaffold, specs/features dirs, gates, replay,
9
+ // equivalence policy, judge dimensions, prompt and reviewer sidecars.
10
+ // 2. estate — the files `sources:` selects (minus `exclude:`), the files
11
+ // `programs:` selects, and for every `## Coverage:` and `## Survey:` rule
12
+ // the units it captures, with a sample of each so a regex typo is
13
+ // visible at a glance.
14
+ // 3. verdict — hard failures (the pack does not load, `sources:` or
15
+ // `programs:` match nothing) abort with exit code 1; likely mistakes (a
16
+ // rule capturing no unit, a missing phase prompt, a scaffold path that
17
+ // does not exist, no reviewer lens, no judge dimension) are listed as
18
+ // warnings and the flow exits 0.
19
+ //
20
+ // Run: LLM4TS_PACK=packs/my-pack modernize-pack-check --repo ~/estates/legacy
21
+ import { existsSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import * as Effect from "effect/Effect";
24
+ import { FlowAborted } from "@llm4ts/flow/FlowError";
25
+ import { Info } from "@llm4ts/flow/FlowEvents";
26
+ import { stage } from "@llm4ts/flow/PlanExecution";
27
+ import { coverageUnits, matchingFiles } from "@llm4ts/flow/SpecChecks";
28
+ import { legacySourceWorkspaceLimits, workspaceLimitsFromEnv } from "@llm4ts/flow/Workspace";
29
+ import { mock } from "@llm4ts/runner/Connectors";
30
+ import { resolveFlowInput } from "@llm4ts/runner/FlowArgs";
31
+ import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner";
32
+ import { makeNodeWorkspace } from "@llm4ts/runner/NodeWorkspace";
33
+ import { openPack } from "@llm4ts/runner/Packs";
34
+ /** Prompt sidecars the modernization phases read, with the phase that reads each. */
35
+ const phasePrompts = [
36
+ ["analysis", "extract"],
37
+ ["spec", "extract"],
38
+ ["bdd", "extract"],
39
+ ["plan", "extract"],
40
+ ["implement", "implement"],
41
+ ["review", "review"],
42
+ ["vectors", "verify"]
43
+ ];
44
+ const sampleSize = 5;
45
+ const sample = (units) => units.length === 0
46
+ ? "(none)"
47
+ : units.slice(0, sampleSize).join(", ") +
48
+ (units.length > sampleSize ? ` … +${units.length - sampleSize} more` : "");
49
+ const plural = (count, noun, many = `${noun}s`) => `${count} ${count === 1 ? noun : many}`;
50
+ const program = Effect.gen(function* () {
51
+ const input = yield* resolveFlowInput("Check the pack against the estate");
52
+ yield* runNode({
53
+ workDir: input.workDir,
54
+ workspace: input.workspace,
55
+ userPrompt: input.prompt,
56
+ // This phase makes no model call; the mock seat satisfies the one
57
+ // context shape the runner composes for every flow.
58
+ coder: mock,
59
+ environment: process.env
60
+ }, (context) => Effect.gen(function* () {
61
+ const say = (message) => context.events.publish(Info.make({ message }));
62
+ const warnings = [];
63
+ const failures = [];
64
+ const opened = yield* stage(context.events, "pack", openPack({
65
+ environment: process.env,
66
+ launchDir: input.workspace,
67
+ flowDir: import.meta.dirname
68
+ }));
69
+ const pack = opened.pack;
70
+ const packRoot = join(opened.workspace.root, opened.dir);
71
+ yield* stage(context.events, "manifest", Effect.gen(function* () {
72
+ yield* say(`pack '${pack.name}' (source: ${pack.source}) at ${packRoot}`);
73
+ yield* say(`specs-dir: ${pack.specsDir} · features-dir: ${pack.featuresDir}`);
74
+ if (pack.scaffold === undefined) {
75
+ yield* say("scaffold: (none — seed needs a non-empty target)");
76
+ }
77
+ else {
78
+ const scaffoldRoot = join(packRoot, pack.scaffold);
79
+ yield* say(`scaffold: ${pack.scaffold}`);
80
+ if (!existsSync(scaffoldRoot)) {
81
+ warnings.push(`scaffold '${pack.scaffold}' does not exist at ${scaffoldRoot}`);
82
+ }
83
+ }
84
+ const gates = Object.entries(pack.gates);
85
+ yield* say(gates.length === 0
86
+ ? "gates: (none)"
87
+ : `gates: ${gates.map(([name, command]) => `${name} → ${command.join(" ")}`).join(" · ")}`);
88
+ if (gates.length === 0) {
89
+ warnings.push("## Gates is empty — implement and verify have nothing to enforce");
90
+ }
91
+ yield* say(pack.replay === undefined
92
+ ? "replay: (none — verify cannot run equivalence vectors)"
93
+ : `replay: ${pack.replay.join(" ")} (${pack.equivalence.ordering.toLowerCase()} comparison)`);
94
+ yield* say(`judge: ${plural(pack.judgeDimensions.length, "dimension")}` +
95
+ (pack.judgeDimensions.length === 0
96
+ ? ""
97
+ : ` — ${pack.judgeDimensions.map((d) => `${d.name} (0..${d.maxScore})`).join(", ")}`));
98
+ if (pack.judgeDimensions.length === 0) {
99
+ warnings.push("## Judge has no dimensions — extract's gate cannot score a spec");
100
+ }
101
+ const present = phasePrompts.filter(([name]) => pack.prompt(name) !== undefined);
102
+ const missing = phasePrompts.filter(([name]) => pack.prompt(name) === undefined);
103
+ yield* say(`prompts: ${present.length}/${phasePrompts.length} phase sidecars` +
104
+ (present.length === 0 ? "" : ` — ${present.map(([name]) => name).join(", ")}`));
105
+ for (const [name, phase] of missing) {
106
+ warnings.push(`prompts/${name}.md not found (read by ${phase})`);
107
+ }
108
+ yield* say(`reviewers: ${plural(pack.lenses.length, "lens", "lenses")}` +
109
+ (pack.lenses.length === 0 ? "" : ` — ${pack.lenses.map((l) => l.name).join(", ")}`));
110
+ if (pack.lenses.length === 0) {
111
+ warnings.push("no reviewers/*.md sidecar — review runs without pack lenses");
112
+ }
113
+ yield* say(`lessons: ${pack.lessons === undefined ? "(none yet)" : "present"}`);
114
+ }));
115
+ yield* stage(context.events, "estate", Effect.gen(function* () {
116
+ const repo = yield* makeNodeWorkspace(input.workDir, workspaceLimitsFromEnv(process.env, legacySourceWorkspaceLimits));
117
+ const sourcesRegex = pack.sources ?? ".*";
118
+ const sources = yield* matchingFiles(repo, sourcesRegex, pack.exclude);
119
+ yield* say(`sources: ${plural(sources.length, "file")} match '${sourcesRegex}'` +
120
+ (pack.exclude === undefined ? "" : ` minus '${pack.exclude}'`) +
121
+ ` — ${sample(sources)}`);
122
+ if (sources.length === 0) {
123
+ failures.push(`sources '${sourcesRegex}' matched no file under ${input.workDir}`);
124
+ }
125
+ if (pack.programs === undefined) {
126
+ yield* say("programs: (not set — every source file is a program)");
127
+ }
128
+ else {
129
+ const programs = yield* matchingFiles(repo, pack.programs, pack.exclude);
130
+ yield* say(`programs: ${plural(programs.length, "file")} match '${pack.programs}' — ${sample(programs)}`);
131
+ if (programs.length === 0) {
132
+ failures.push(`programs '${pack.programs}' matched no file under ${input.workDir}`);
133
+ }
134
+ }
135
+ const report = Effect.fn("modernize-pack-check.rules")(function* (kind, rules) {
136
+ if (rules.length === 0) {
137
+ yield* say(`${kind}: (no rules)`);
138
+ return;
139
+ }
140
+ const units = yield* coverageUnits(repo, rules);
141
+ for (const rule of rules) {
142
+ const found = units[rule.name] ?? [];
143
+ yield* say(`${kind} '${rule.name}': ${plural(found.length, "unit")} — ${sample(found)}`);
144
+ if (found.length === 0) {
145
+ warnings.push(`${kind} rule '${rule.name}' captured no unit: files '${rule.files}', unit '${rule.unit}'`);
146
+ }
147
+ }
148
+ });
149
+ yield* report("coverage", pack.coverage);
150
+ yield* report("survey", pack.survey);
151
+ }));
152
+ yield* stage(context.events, "verdict", Effect.gen(function* () {
153
+ for (const warning of warnings) {
154
+ yield* say(`warning: ${warning}`);
155
+ }
156
+ if (failures.length > 0) {
157
+ return yield* FlowAborted.make({
158
+ message: `pack '${pack.name}' failed the check: ${failures.join("; ")}`
159
+ });
160
+ }
161
+ yield* say(`pack '${pack.name}' check passed with ${plural(warnings.length, "warning")} — ` +
162
+ `next: LLM4TS_PACK=${process.env.LLM4TS_PACK ?? opened.dir} modernize-survey --repo ${input.workDir}`);
163
+ }));
164
+ }));
165
+ });
166
+ runFlowMain(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llm4ts/shell",
3
- "version": "0.16.2",
3
+ "version": "0.17.0",
4
4
  "description": "Interactive shell and CLI for llm4ts: flow discovery, run-a-flow, and view",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -50,10 +50,10 @@
50
50
  "dependencies": {
51
51
  "@effect/platform-node": "4.0.0-beta.102",
52
52
  "@effect/platform-node-shared": "4.0.0-beta.102",
53
- "@llm4ts/core": "0.16.2",
54
- "@llm4ts/flow": "0.16.2",
55
- "@llm4ts/modernize": "0.16.2",
56
- "@llm4ts/runner": "0.16.2"
53
+ "@llm4ts/core": "0.17.0",
54
+ "@llm4ts/flow": "0.17.0",
55
+ "@llm4ts/runner": "0.17.0",
56
+ "@llm4ts/modernize": "0.17.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "effect": "4.0.0-beta.102"