@llm4ts/shell 0.6.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.
@@ -1,266 +0,0 @@
1
- // Legacy modernization phase 2: seed the target repository from the APPROVED spec pack.
2
- //
3
- // Runs rooted at the TARGET repository (`--repo <target>`), reading the spec
4
- // pack out of the legacy repository (`LLM4TS_LEGACY_REPO`). Deliberately
5
- // deterministic — no LLM calls:
6
- //
7
- // 1. Refuses to run until a human has flipped `- [x] Approved` in the legacy
8
- // repo's docs/modernization/README.md (extract writes it unchecked).
9
- // 2. An empty target gets the pack's scaffold; a non-empty one is used as-is.
10
- // 3. Specs, traceability, mapping, and rules.txt land in the pack's
11
- // specs-dir; .feature files land in its features-dir. Legacy SOURCE never
12
- // crosses — that is the clean-room wall the later phases enforce.
13
- // 4. The proposed plan is re-parsed (a hard validation) and materialized at
14
- // docs/modernization/plan.md for modernize-implement to resume.
15
- // 5. The provenance manifest (docs/modernization/provenance.json) records the
16
- // clean-room receipt: spec file hashes, gate verdict digests, the pack,
17
- // the llm4ts version, the extraction seats, and LLM4TS_APPROVER when set.
18
- //
19
- // Run: LLM4TS_LEGACY_REPO=~/estates/meridian-legacy \
20
- // modernize-seed --repo ~/services/meridian-transfers
21
- import { join } from "node:path"
22
- import * as Effect from "effect/Effect"
23
- import * as Schema from "effect/Schema"
24
- import { FlowAborted, PersistenceError } from "@llm4ts/flow/FlowError"
25
- import { Info } from "@llm4ts/flow/FlowEvents"
26
- import { packageVersion } from "@llm4ts/flow/Package"
27
- import { parsePlan } from "@llm4ts/flow/Plan"
28
- import { stage } from "@llm4ts/flow/PlanExecution"
29
- import { Provenance, makeProvenanceStore } from "@llm4ts/flow/Provenance"
30
- import { matchingFiles } from "@llm4ts/flow/SpecChecks"
31
- import type { WorkspaceShape } from "@llm4ts/flow/Workspace"
32
- import { requireApproval } from "@llm4ts/modernize/Approval"
33
- import { coderFromEnv } from "@llm4ts/runner/Connectors"
34
- import { resolveFlowInput } from "@llm4ts/runner/FlowArgs"
35
- import { runFlowMain, runNode } from "@llm4ts/runner/FlowRunner"
36
- import { nodePlainFileStore } from "@llm4ts/runner/NodePlainFileStore"
37
- import { makeNodeWorkspace } from "@llm4ts/runner/NodeWorkspace"
38
- import { openPack } from "@llm4ts/runner/Packs"
39
-
40
- const ModDir = "docs/modernization"
41
- const skipped = new Set([".git", "target", "node_modules", "dist"])
42
-
43
- const Seats = Schema.Record(Schema.String, Schema.String)
44
-
45
- const isLitter = (path: string): boolean => path.split("/").some((segment) => skipped.has(segment))
46
-
47
- /** Copies every text file under `from` into `into`, skipping build and VCS litter. */
48
- const copyTree = Effect.fn("modernize-seed.copyTree")(function* (
49
- source: WorkspaceShape,
50
- target: WorkspaceShape,
51
- from: string,
52
- into: string
53
- ) {
54
- const paths = yield* source.discover(`${from}/**`).pipe(Effect.orElseSucceed(() => []))
55
- let copied = 0
56
- for (const path of paths) {
57
- if (isLitter(path)) {
58
- continue
59
- }
60
- const contents = yield* source.read(path).pipe(Effect.orElseSucceed(() => undefined))
61
- if (contents === undefined) {
62
- continue
63
- }
64
- yield* target.write(join(into, path.slice(from.length + 1)), contents)
65
- copied += 1
66
- }
67
- return copied
68
- })
69
-
70
- /** Copies one file when it exists; false when the source is absent. */
71
- const copyFile = Effect.fn("modernize-seed.copyFile")(function* (
72
- source: WorkspaceShape,
73
- target: WorkspaceShape,
74
- from: string,
75
- into: string
76
- ) {
77
- const contents = yield* source.read(from).pipe(Effect.orElseSucceed(() => undefined))
78
- if (contents === undefined) {
79
- return false
80
- }
81
- yield* target.write(into, contents)
82
- return true
83
- })
84
-
85
- const program = Effect.gen(function* () {
86
- const input = yield* resolveFlowInput("Seed the target repository from the approved spec pack")
87
- const legacyRepo = process.env.LLM4TS_LEGACY_REPO?.trim()
88
- const files = nodePlainFileStore
89
-
90
- yield* runNode(
91
- {
92
- workDir: input.workDir,
93
- workspace: input.workspace,
94
- userPrompt: input.prompt,
95
- // This phase makes no model call; the seat is wired only because the
96
- // runner composes one context shape for every flow.
97
- coder: coderFromEnv(process.env),
98
- environment: process.env
99
- },
100
- (context) =>
101
- Effect.gen(function* () {
102
- if (legacyRepo === undefined || legacyRepo.length === 0) {
103
- return yield* FlowAborted.make({
104
- message:
105
- "set LLM4TS_LEGACY_REPO to the legacy repository holding the approved spec pack"
106
- })
107
- }
108
- const legacy = yield* makeNodeWorkspace(legacyRepo)
109
- const target = yield* makeNodeWorkspace(input.workDir)
110
- const opened = yield* stage(
111
- context.events,
112
- "pack",
113
- openPack({
114
- environment: process.env,
115
- launchDir: input.workspace,
116
- flowDir: import.meta.dirname
117
- })
118
- )
119
- const pack = opened.pack
120
- const specPackRoot = join(legacyRepo, ModDir)
121
-
122
- yield* stage(
123
- context.events,
124
- "approval",
125
- requireApproval(files, join(specPackRoot, "README.md"))
126
- )
127
-
128
- yield* stage(
129
- context.events,
130
- "scaffold",
131
- Effect.gen(function* () {
132
- const existing = (yield* target.discover().pipe(Effect.orElseSucceed(() => []))).filter(
133
- (path) => !path.startsWith(".git/")
134
- )
135
- if (existing.length > 0) {
136
- yield* context.events.publish(
137
- Info.make({ message: "target repo is not empty — using it as-is" })
138
- )
139
- return
140
- }
141
- if (pack.scaffold === undefined) {
142
- return yield* FlowAborted.make({
143
- message: `target repo is empty and pack '${pack.name}' has no scaffold`
144
- })
145
- }
146
- // The scaffold path is relative to the pack directory.
147
- const scaffoldRoot = join(pack.dir, pack.scaffold)
148
- const scaffold = yield* makeNodeWorkspace(join(opened.workspace.root, scaffoldRoot))
149
- const paths = (yield* scaffold.discover()).filter((path) => !isLitter(path))
150
- for (const path of paths) {
151
- yield* target.write(path, yield* scaffold.read(path))
152
- }
153
- yield* context.events.publish(
154
- Info.make({ message: `scaffolded ${paths.length} file(s) from ${scaffoldRoot}` })
155
- )
156
- })
157
- )
158
-
159
- const seeded = yield* stage(
160
- context.events,
161
- "seed",
162
- Effect.gen(function* () {
163
- const specs = yield* copyTree(legacy, target, `${ModDir}/specs`, pack.specsDir)
164
- // A spec pack that contributes no specs means the wrong legacy repo,
165
- // or an extraction that never wrote any. Seeding an empty target and
166
- // reporting success would push that discovery minutes downstream.
167
- if (specs === 0) {
168
- return yield* FlowAborted.make({
169
- message:
170
- `no specs found under ${legacyRepo}/${ModDir}/specs — ` +
171
- "check LLM4TS_LEGACY_REPO and that extraction wrote its spec pack"
172
- })
173
- }
174
- const features = yield* copyTree(legacy, target, `${ModDir}/features`, pack.featuresDir)
175
- for (const index of ["traceability.md", "mapping.md", "rules.txt"]) {
176
- yield* copyFile(legacy, target, `${ModDir}/${index}`, join(pack.specsDir, index))
177
- }
178
- return specs + features
179
- })
180
- )
181
-
182
- const plan = yield* stage(
183
- context.events,
184
- "plan",
185
- Effect.gen(function* () {
186
- const text = yield* files.read(join(specPackRoot, "plan.md"))
187
- if (text === undefined) {
188
- return yield* PersistenceError.make({
189
- message: "spec pack has no plan.md — did extraction clear its gate?"
190
- })
191
- }
192
- // Re-parsing is the hard validation: a malformed plan fails here,
193
- // not minutes into the implementation phase.
194
- const parsed = yield* parsePlan(text)
195
- yield* target.write(`${ModDir}/plan.md`, parsed.render)
196
- return parsed
197
- })
198
- )
199
-
200
- yield* stage(
201
- context.events,
202
- "provenance",
203
- Effect.gen(function* () {
204
- const provenance = makeProvenanceStore(files)
205
- const specFiles = yield* matchingFiles(target, `^${pack.specsDir}/.*\\.md$`)
206
- const specs = yield* provenance.hashFiles(input.workDir, specFiles)
207
- const gateFiles = yield* matchingFiles(legacy, `^${ModDir}/gate/.*\\.json$`).pipe(
208
- Effect.orElseSucceed(() => [])
209
- )
210
- const verdicts = yield* provenance.hashFiles(legacyRepo, gateFiles)
211
- const seatsText = yield* files
212
- .read(join(specPackRoot, "seats.json"))
213
- .pipe(Effect.orElseSucceed(() => undefined))
214
- // A pre-seats spec pack, or a malformed sidecar, degrades to no
215
- // recorded seats rather than failing the seed.
216
- const seats =
217
- seatsText === undefined
218
- ? {}
219
- : yield* Effect.try({
220
- try: () => Schema.decodeUnknownSync(Schema.fromJsonString(Seats))(seatsText),
221
- catch: () => undefined
222
- }).pipe(Effect.orElseSucceed((): Readonly<Record<string, string>> => ({})))
223
- const approver = process.env.LLM4TS_APPROVER?.trim()
224
- yield* provenance.write(
225
- join(input.workDir, ModDir, "provenance.json"),
226
- Provenance.make({
227
- schema: 1,
228
- pack: pack.name,
229
- llm4tsVersion: packageVersion,
230
- createdAt: new Date().toISOString(),
231
- ...(approver === undefined || approver.length === 0
232
- ? {}
233
- : { approvedBy: approver }),
234
- seats,
235
- specs,
236
- gateVerdicts: Object.fromEntries(
237
- Object.entries(verdicts).map(([path, hash]) => [
238
- (path.split("/").at(-1) ?? path).replace(/\.json$/, ""),
239
- hash
240
- ])
241
- ),
242
- fixSpecs: []
243
- })
244
- )
245
- })
246
- )
247
-
248
- yield* stage(
249
- context.events,
250
- "commit",
251
- context.git
252
- .commitAll(
253
- `modernize(${pack.name}): seed specs, features, plan, and provenance (${seeded} file(s))`
254
- )
255
- .pipe(Effect.asVoid)
256
- )
257
- yield* context.events.publish(
258
- Info.make({
259
- message: `target seeded (plan ${plan.epicId}) — run modernize-implement with the same --repo`
260
- })
261
- )
262
- })
263
- )
264
- })
265
-
266
- runFlowMain(program)
Binary file