silvermoon 0.1.3 → 0.2.0-rc.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.
@@ -3,12 +3,11 @@ import { lstat, mkdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
4
 
5
5
  import {
6
- createEnvelope,
7
6
  diagnosticProblem,
8
7
  dialogueReadyObservation,
9
8
  localize,
10
- outcome,
11
9
  } from "./dialogue.js";
10
+ import { createCommandRun } from "./domain.js";
12
11
  import { inspectPhaseGuidance } from "./guidance.js";
13
12
  import {
14
13
  DEPLOYMENT_TEMPLATE,
@@ -110,10 +109,7 @@ async function ensureDirectoryPath(root, relativePath, operations) {
110
109
  }
111
110
  return created;
112
111
  } catch (caught) {
113
- caught.cleanup = await removeCreatedDirectories(
114
- created,
115
- operations.removeDirectory,
116
- );
112
+ caught.createdDirectories = created;
117
113
  throw caught;
118
114
  }
119
115
  }
@@ -126,32 +122,41 @@ function cleanupSummary(results) {
126
122
  + preserved.map(({ path }) => path).join(", ");
127
123
  }
128
124
 
129
- function failureEnvelope({
130
- caught,
131
- intention,
125
+ function cleanupResult(results) {
126
+ return {
127
+ removed: results.filter(({ status }) => status === "removed").length,
128
+ preserved: results.filter(({ status }) => status.startsWith("preserved")).length,
129
+ items: results,
130
+ };
131
+ }
132
+
133
+ function failureReport({
134
+ cleanup,
135
+ problem: actionProblem,
132
136
  observation,
133
- outcomes,
134
- cleanup = [],
137
+ runtime,
135
138
  language,
136
139
  }) {
137
- outcomes.push(outcome(
138
- "create-idea-scaffold",
139
- "failure",
140
- localize(
141
- language,
142
- `Idea creation failed: ${caught.message} ${cleanupSummary(cleanup)}`,
143
- `创建 idea 失败:${caught.message} ${cleanupSummary(cleanup)}`,
144
- ),
145
- ));
146
- return createEnvelope(
147
- intention,
148
- dialogueReadyObservation(observation, "idea-create-failed"),
149
- outcomes,
150
- localize(
140
+ const problem = {
141
+ type: "idea-scaffold-failed",
142
+ summary: localize(
151
143
  language,
152
- "Preserve any reported paths, resolve the filesystem error, and retry `silvermoon create-idea`.",
153
- "保留报告中的路径,解决文件系统错误后,再运行 `silvermoon create-idea`。",
144
+ `Idea creation failed: ${actionProblem.summary} ${cleanupSummary(cleanup.items)}`,
145
+ `创建 idea 失败:${actionProblem.summary} ${cleanupSummary(cleanup.items)}`,
154
146
  ),
147
+ };
148
+ return runtime.complete(
149
+ dialogueReadyObservation(observation, "idea-create-failed", {
150
+ cleanup,
151
+ problems: [...observation.problems, problem],
152
+ }),
153
+ {
154
+ nextSteps: localize(
155
+ language,
156
+ "Preserve any reported paths, resolve the filesystem error, and retry `silvermoon create-idea`.",
157
+ "保留报告中的路径,解决文件系统错误后,再运行 `silvermoon create-idea`。",
158
+ ),
159
+ },
155
160
  );
156
161
  }
157
162
 
@@ -163,102 +168,57 @@ function projectCreationObservation(observation) {
163
168
  return project;
164
169
  }
165
170
 
166
- export async function createIdea({
167
- generateId = generateUlid,
168
- guidanceReader = inspectPhaseGuidance,
169
- language,
170
- operations = {},
171
- root = process.cwd(),
172
- userHome,
173
- } = {}) {
174
- const requestedRoot = resolve(root);
175
- const canonicalLanguage = language === undefined
176
- ? undefined
177
- : canonicalizeLanguageTag(language);
178
- const intention = {
179
- command: "create-idea",
180
- args: { language: canonicalLanguage ?? null },
181
- };
182
- const outcomes = [];
183
- const observed = await observeSnapshot({
184
- allowMissingIdeas: true,
185
- ideaLanguage: canonicalLanguage,
186
- root: requestedRoot,
187
- userHome,
188
- version: { type: "worktree" },
189
- });
190
- const recheckCommand = canonicalLanguage === undefined
191
- ? "silvermoon create-idea"
192
- : `silvermoon create-idea --language ${canonicalLanguage}`;
193
- if (!observed.projectReady) {
194
- return createEnvelope(
195
- intention,
196
- projectCreationObservation(observed.observation),
197
- outcomes,
198
- projectInstructions(
199
- observed,
200
- observed.observation.root,
201
- observed.outputLanguage,
202
- recheckCommand,
203
- ),
204
- );
205
- }
206
- const repositoryRoot = observed.observation.root;
207
- const readiness = await assessIdeaCreationReadiness({
208
- observed,
209
- outcomes,
210
- recheckCommand,
211
- root: repositoryRoot,
212
- });
213
- if (!readiness.ready) {
214
- return createEnvelope(
215
- intention,
216
- dialogueReadyObservation(
217
- readiness.observation,
218
- "repository-preparation-required",
219
- ),
220
- outcomes,
221
- readiness.instructions,
222
- );
223
- }
171
+ function withCleanupPlan(caught, cleanupPlan) {
172
+ const error = caught instanceof Error ? caught : new Error(String(caught));
173
+ error.cleanupPlan = cleanupPlan;
174
+ return error;
175
+ }
224
176
 
225
- const inspectedGuidance = await guidanceReader({
226
- gitRoot: repositoryRoot,
227
- phase: "preparing",
228
- snapshotTree: readiness.head,
229
- });
230
- if (inspectedGuidance.state === "invalid") {
231
- return createEnvelope(
232
- intention,
233
- dialogueReadyObservation(
234
- readiness.observation,
235
- "phase-guidance-invalid",
236
- {
237
- problems: inspectedGuidance.diagnostics.map((diagnostic) =>
238
- diagnosticProblem(diagnostic, observed.outputLanguage)
239
- ),
240
- },
241
- ),
242
- outcomes,
243
- phaseGuidanceInstructions(
244
- inspectedGuidance.diagnostics,
245
- repositoryRoot,
246
- observed.outputLanguage,
247
- recheckCommand,
248
- ),
249
- );
177
+ async function cleanupScaffold(cleanupPlan, operations) {
178
+ const read = operations.readFile ?? readFile;
179
+ const remove = operations.rm ?? rm;
180
+ const removeDirectory = operations.rmdir ?? rmdir;
181
+ const results = [];
182
+ for (const [path, source] of [...cleanupPlan.cleanupFiles].reverse()) {
183
+ try {
184
+ results.push(await removeOwnedFile(path, source, read, remove));
185
+ } catch (caught) {
186
+ results.push({
187
+ path,
188
+ status: "preserved",
189
+ error: caught.message,
190
+ });
191
+ }
250
192
  }
193
+ results.push(...await removeCreatedDirectories(
194
+ cleanupPlan.createdDirectories,
195
+ removeDirectory,
196
+ ));
197
+ if (cleanupPlan.collisionPath) {
198
+ results.push({
199
+ path: cleanupPlan.collisionPath,
200
+ status: "preserved-existing",
201
+ });
202
+ }
203
+ results.push(...await removeCreatedDirectories(
204
+ cleanupPlan.rootDirectories,
205
+ removeDirectory,
206
+ ));
207
+ return cleanupResult(results);
208
+ }
251
209
 
210
+ async function createScaffold({
211
+ canonicalLanguage,
212
+ generateId,
213
+ operations,
214
+ repositoryRoot,
215
+ }) {
252
216
  const inspect = operations.lstat ?? lstat;
253
217
  const makeDirectory = operations.mkdir ?? mkdir;
254
- const read = operations.readFile ?? readFile;
255
- const remove = operations.rm ?? rm;
256
- const removeDirectory = operations.rmdir ?? rmdir;
257
218
  const write = operations.writeFile ?? writeFile;
258
219
  const fileOperations = {
259
220
  inspect,
260
221
  makeDirectory,
261
- removeDirectory,
262
222
  };
263
223
  let rootDirectories;
264
224
  try {
@@ -268,13 +228,11 @@ export async function createIdea({
268
228
  fileOperations,
269
229
  );
270
230
  } catch (caught) {
271
- return failureEnvelope({
272
- caught,
273
- intention,
274
- observation: readiness.observation,
275
- outcomes,
276
- cleanup: caught.cleanup,
277
- language: observed.outputLanguage,
231
+ throw withCleanupPlan(caught, {
232
+ cleanupFiles: [],
233
+ collisionPath: null,
234
+ createdDirectories: [],
235
+ rootDirectories: caught.createdDirectories ?? [],
278
236
  });
279
237
  }
280
238
 
@@ -291,17 +249,11 @@ export async function createIdea({
291
249
  folder = resolve(repositoryRoot, paths.ideaPath);
292
250
  if (await pathExists(folder, inspect)) continue;
293
251
  } catch (caught) {
294
- const cleanup = await removeCreatedDirectories(
252
+ throw withCleanupPlan(caught, {
253
+ cleanupFiles: [],
254
+ collisionPath: null,
255
+ createdDirectories: [],
295
256
  rootDirectories,
296
- removeDirectory,
297
- );
298
- return failureEnvelope({
299
- caught,
300
- intention,
301
- observation: readiness.observation,
302
- outcomes,
303
- cleanup,
304
- language: observed.outputLanguage,
305
257
  });
306
258
  }
307
259
 
@@ -345,74 +297,189 @@ export async function createIdea({
345
297
  throw caught;
346
298
  }
347
299
  }
348
- outcomes.push(outcome(
349
- "create-idea-scaffold",
350
- "success",
351
- localize(
352
- observed.outputLanguage,
353
- `Created idea ${id} at ${paths.ideaPath}.`,
354
- `已在 ${paths.ideaPath} 创建 idea ${id}。`,
355
- ),
356
- ));
357
- return createEnvelope(
358
- intention,
359
- dialogueReadyObservation(readiness.observation, "idea-created", {
360
- createdIdea: { id, path: paths.ideaPath, state: "preparing" },
361
- ...(inspectedGuidance.guidance === undefined
362
- ? {}
363
- : { guidance: inspectedGuidance.guidance }),
364
- }),
365
- outcomes,
366
- localize(
367
- observed.outputLanguage,
368
- `Use ${observed.contentLanguage} for natural-language content while describing the requested Ideal World in ${paths.ideaDocumentPath}; keep the stable IDs and placeholders in ${paths.implementationDocumentPath}, ${paths.deploymentDocumentPath}, and ${paths.ledgerPath} synchronized.`,
369
- `使用 ${observed.contentLanguage} 在 ${paths.ideaDocumentPath} 中描述请求的理想契约,并保持 ${paths.implementationDocumentPath}、${paths.deploymentDocumentPath} 与 ${paths.ledgerPath} 中的稳定 ID 和占位同步。`,
370
- ),
371
- );
300
+ return {
301
+ createdIdea: {
302
+ id,
303
+ path: paths.ideaPath,
304
+ state: "preparing",
305
+ },
306
+ paths,
307
+ };
372
308
  } catch (caught) {
373
- const cleanup = [];
374
- for (const [path, source] of [...cleanupFiles].reverse()) {
375
- try {
376
- cleanup.push(await removeOwnedFile(path, source, read, remove));
377
- } catch (cleanupError) {
378
- cleanup.push({
379
- path,
380
- status: "preserved",
381
- error: cleanupError.message,
382
- });
383
- }
384
- }
385
- cleanup.push(...await removeCreatedDirectories(
309
+ throw withCleanupPlan(caught, {
310
+ cleanupFiles,
311
+ collisionPath,
386
312
  createdDirectories,
387
- removeDirectory,
388
- ));
389
- if (collisionPath) {
390
- cleanup.push({ path: collisionPath, status: "preserved-existing" });
391
- }
392
- cleanup.push(...await removeCreatedDirectories(
393
313
  rootDirectories,
394
- removeDirectory,
395
- ));
396
- return failureEnvelope({
397
- caught,
398
- intention,
399
- observation: readiness.observation,
400
- outcomes,
401
- cleanup,
402
- language: observed.outputLanguage,
403
314
  });
404
315
  }
405
316
  }
406
317
 
407
- const cleanup = await removeCreatedDirectories(rootDirectories, removeDirectory);
408
- return failureEnvelope({
409
- caught: new Error(
410
- `Could not allocate a unique idea id after ${MAX_ID_ATTEMPTS} attempts.`,
411
- ),
412
- intention,
413
- observation: readiness.observation,
414
- outcomes,
415
- cleanup,
416
- language: observed.outputLanguage,
318
+ throw withCleanupPlan(
319
+ new Error(`Could not allocate a unique idea id after ${MAX_ID_ATTEMPTS} attempts.`),
320
+ {
321
+ cleanupFiles: [],
322
+ collisionPath: null,
323
+ createdDirectories: [],
324
+ rootDirectories,
325
+ },
326
+ );
327
+ }
328
+
329
+ export async function createIdea({
330
+ generateId = generateUlid,
331
+ guidanceReader = inspectPhaseGuidance,
332
+ language,
333
+ operations = {},
334
+ root = process.cwd(),
335
+ userHome,
336
+ } = {}) {
337
+ const requestedRoot = resolve(root);
338
+ const canonicalLanguage = language === undefined
339
+ ? undefined
340
+ : canonicalizeLanguageTag(language);
341
+ const intention = {
342
+ command: "create-idea",
343
+ args: { language: canonicalLanguage ?? null },
344
+ };
345
+ const runtime = createCommandRun(intention);
346
+ const observed = await observeSnapshot({
347
+ allowMissingIdeas: true,
348
+ ideaLanguage: canonicalLanguage,
349
+ root: requestedRoot,
350
+ userHome,
351
+ version: { type: "worktree" },
417
352
  });
353
+ runtime.observe(projectCreationObservation(observed.observation), {
354
+ factType: "project.snapshot",
355
+ });
356
+ const recheckCommand = canonicalLanguage === undefined
357
+ ? "silvermoon create-idea"
358
+ : `silvermoon create-idea --language ${canonicalLanguage}`;
359
+ if (!observed.projectReady) {
360
+ return runtime.complete(
361
+ projectCreationObservation(observed.observation),
362
+ {
363
+ nextSteps: projectInstructions(
364
+ observed,
365
+ observed.observation.root,
366
+ observed.outputLanguage,
367
+ recheckCommand,
368
+ ),
369
+ },
370
+ );
371
+ }
372
+ const repositoryRoot = observed.observation.root;
373
+ const readiness = await assessIdeaCreationReadiness({
374
+ observed,
375
+ runtime,
376
+ recheckCommand,
377
+ root: repositoryRoot,
378
+ });
379
+ if (!readiness.ready) {
380
+ return runtime.complete(
381
+ dialogueReadyObservation(
382
+ readiness.observation,
383
+ "repository-preparation-required",
384
+ ),
385
+ { nextSteps: readiness.instructions },
386
+ );
387
+ }
388
+
389
+ const inspectedGuidance = await guidanceReader({
390
+ gitRoot: repositoryRoot,
391
+ phase: "preparing",
392
+ snapshotTree: readiness.head,
393
+ });
394
+ if (inspectedGuidance.state === "invalid") {
395
+ return runtime.complete(
396
+ dialogueReadyObservation(
397
+ readiness.observation,
398
+ "phase-guidance-invalid",
399
+ {
400
+ problems: inspectedGuidance.diagnostics.map((diagnostic) =>
401
+ diagnosticProblem(diagnostic, observed.outputLanguage)
402
+ ),
403
+ },
404
+ ),
405
+ {
406
+ nextSteps: phaseGuidanceInstructions(
407
+ inspectedGuidance.diagnostics,
408
+ repositoryRoot,
409
+ observed.outputLanguage,
410
+ recheckCommand,
411
+ ),
412
+ },
413
+ );
414
+ }
415
+
416
+ const completion = await runtime.performAction(
417
+ {
418
+ type: "create-idea-scaffold",
419
+ contentLanguage: observed.contentLanguage,
420
+ },
421
+ () => createScaffold({
422
+ canonicalLanguage,
423
+ generateId,
424
+ operations,
425
+ repositoryRoot,
426
+ }),
427
+ (caught) => ({
428
+ problem: {
429
+ type: "idea-scaffold-failed",
430
+ summary: caught.message,
431
+ },
432
+ internal: { cleanupPlan: caught.cleanupPlan },
433
+ }),
434
+ );
435
+ if (completion.status === "failure") {
436
+ const cleanupCompletion = await runtime.performAction(
437
+ { type: "remove-owned-creation-paths" },
438
+ () => cleanupScaffold(
439
+ completion.internal.cleanupPlan,
440
+ operations,
441
+ ),
442
+ (caught) => ({
443
+ problem: {
444
+ type: "idea-cleanup-failed",
445
+ summary: caught.message,
446
+ },
447
+ }),
448
+ );
449
+ const cleanup = cleanupCompletion.status === "success"
450
+ ? cleanupCompletion.result
451
+ : {
452
+ removed: 0,
453
+ preserved: 1,
454
+ items: [{
455
+ path: repositoryRoot,
456
+ status: "preserved",
457
+ error: cleanupCompletion.problem.summary,
458
+ }],
459
+ };
460
+ return failureReport({
461
+ cleanup,
462
+ problem: completion.problem,
463
+ observation: readiness.observation,
464
+ runtime,
465
+ language: observed.outputLanguage,
466
+ });
467
+ }
468
+
469
+ const { createdIdea, paths } = completion.result;
470
+ return runtime.complete(
471
+ dialogueReadyObservation(readiness.observation, "idea-created", {
472
+ createdIdea,
473
+ ...(inspectedGuidance.guidance === undefined
474
+ ? {}
475
+ : { guidance: inspectedGuidance.guidance }),
476
+ }),
477
+ {
478
+ nextSteps: localize(
479
+ observed.outputLanguage,
480
+ `Use ${observed.contentLanguage} for natural-language content while describing the requested Ideal World in ${paths.ideaDocumentPath}; keep the stable IDs and placeholders in ${paths.implementationDocumentPath}, ${paths.deploymentDocumentPath}, and ${paths.ledgerPath} synchronized.`,
481
+ `使用 ${observed.contentLanguage} 在 ${paths.ideaDocumentPath} 中描述请求的理想契约,并保持 ${paths.implementationDocumentPath}、${paths.deploymentDocumentPath} 与 ${paths.ledgerPath} 中的稳定 ID 和占位同步。`,
482
+ ),
483
+ },
484
+ );
418
485
  }