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.
@@ -0,0 +1,443 @@
1
+ import {
2
+ DEFAULT_LANGUAGE,
3
+ localize,
4
+ resolveOutputLanguage,
5
+ } from "./language.js";
6
+
7
+ const BLOCKED_STATES = new Set([
8
+ "check-unavailable",
9
+ "idea-create-failed",
10
+ "phase-guidance-invalid",
11
+ "project-setup-required",
12
+ "repository-preparation-required",
13
+ "repository-sync-required",
14
+ ]);
15
+
16
+ function clone(value) {
17
+ return value === undefined ? undefined : structuredClone(value);
18
+ }
19
+
20
+ function responseLanguage(intention, observation) {
21
+ return observation.outputLanguage
22
+ ?? resolveOutputLanguage({
23
+ content: observation.configuration?.preferredLanguage,
24
+ override: intention.args?.language ?? undefined,
25
+ }).tag
26
+ ?? DEFAULT_LANGUAGE;
27
+ }
28
+
29
+ function normalizeNextSteps(nextSteps) {
30
+ if (nextSteps === undefined || nextSteps === null || nextSteps === "") {
31
+ return [];
32
+ }
33
+ const values = Array.isArray(nextSteps) ? nextSteps : [nextSteps];
34
+ return values.map((step) =>
35
+ typeof step === "string"
36
+ ? { type: "instruction", text: step }
37
+ : clone(step)
38
+ );
39
+ }
40
+
41
+ function observationDetails(observation) {
42
+ const details = {};
43
+ if (observation.root !== undefined) details.root = observation.root;
44
+ if (observation.version !== undefined) details.version = clone(observation.version);
45
+ if (observation.configuration !== undefined) {
46
+ details.primary = {
47
+ repository: observation.configuration.primaryRepository,
48
+ branch: observation.configuration.primaryBranch,
49
+ };
50
+ details.contentLanguage = observation.configuration.preferredLanguage;
51
+ }
52
+ return details;
53
+ }
54
+
55
+ function blockedSummary(command, observation, language) {
56
+ const summaries = {
57
+ "check-unavailable": localize(
58
+ language,
59
+ "The requested repository snapshot could not be validated.",
60
+ "无法验证请求的 repository snapshot。",
61
+ ),
62
+ "idea-create-failed": localize(
63
+ language,
64
+ "The repository was ready, but creating the idea failed.",
65
+ "repository 已就绪,但创建 idea 失败。",
66
+ ),
67
+ "phase-guidance-invalid": localize(
68
+ language,
69
+ "The current phase guidance is invalid.",
70
+ "当前阶段 guidance 无效。",
71
+ ),
72
+ "project-setup-required": localize(
73
+ language,
74
+ "The project requires Silvermoon setup.",
75
+ "项目需要完成 Silvermoon 整备。",
76
+ ),
77
+ "repository-preparation-required": localize(
78
+ language,
79
+ "The local repository must be prepared before creating an idea.",
80
+ "创建 idea 前必须先整备本地 repository。",
81
+ ),
82
+ "repository-sync-required": localize(
83
+ language,
84
+ "The repository must be synchronized before this command can continue.",
85
+ "继续此命令前必须先同步 repository。",
86
+ ),
87
+ };
88
+ return summaries[observation.state] ?? localize(
89
+ language,
90
+ `Command ${command} is blocked.`,
91
+ `命令 ${command} 当前受阻。`,
92
+ );
93
+ }
94
+
95
+ function validationResponse(intention, observation, language) {
96
+ const valid = observation.state === "project-ready";
97
+ return {
98
+ kind: "validation-result",
99
+ language,
100
+ summary: valid
101
+ ? localize(
102
+ language,
103
+ "The requested Silvermoon snapshot is valid.",
104
+ "请求的 Silvermoon snapshot 验证通过。",
105
+ )
106
+ : blockedSummary(intention.command, observation, language),
107
+ validation: {
108
+ target: clone(intention.args.target),
109
+ valid,
110
+ ...(observation.version === undefined
111
+ ? {}
112
+ : { version: clone(observation.version) }),
113
+ },
114
+ problems: clone(observation.problems ?? []),
115
+ };
116
+ }
117
+
118
+ function dialogueResponse(intention, internalObservation, language) {
119
+ const observation = internalObservation.observation;
120
+ const context = internalObservation.responseContext ?? {};
121
+ const base = {
122
+ language,
123
+ details: observationDetails(observation),
124
+ };
125
+ const nextSteps = normalizeNextSteps(context.nextSteps);
126
+
127
+ if (BLOCKED_STATES.has(observation.state)) {
128
+ return {
129
+ ...base,
130
+ kind: "blocked",
131
+ summary: blockedSummary(intention.command, observation, language),
132
+ problems: clone(observation.problems ?? []),
133
+ nextSteps,
134
+ ...(observation.selectedIdea === undefined
135
+ ? {}
136
+ : { idea: clone(observation.selectedIdea) }),
137
+ };
138
+ }
139
+
140
+ if (intention.command === "create-idea" && observation.state === "idea-created") {
141
+ return {
142
+ ...base,
143
+ kind: "idea-created",
144
+ summary: localize(
145
+ language,
146
+ `Created idea ${observation.createdIdea.id}.`,
147
+ `已创建 idea ${observation.createdIdea.id}。`,
148
+ ),
149
+ createdIdea: clone(observation.createdIdea),
150
+ nextSteps,
151
+ ...(observation.guidance === undefined
152
+ ? {}
153
+ : { guidance: clone(observation.guidance) }),
154
+ };
155
+ }
156
+
157
+ if (intention.command === "whats-next") {
158
+ if (observation.state === "navigation-ready") {
159
+ const choices = clone(observation.ideas?.activeIdeas ?? []);
160
+ return {
161
+ ...base,
162
+ kind: "choice-required",
163
+ summary: choices.length === 0
164
+ ? localize(
165
+ language,
166
+ "No active ideas are available.",
167
+ "当前没有 active idea。",
168
+ )
169
+ : localize(
170
+ language,
171
+ `${choices.length} active idea(s) are available.`,
172
+ `当前有 ${choices.length} 个 active idea。`,
173
+ ),
174
+ choices,
175
+ nextSteps,
176
+ };
177
+ }
178
+ if (observation.state === "idea-not-found") {
179
+ return {
180
+ ...base,
181
+ kind: "choice-required",
182
+ summary: localize(
183
+ language,
184
+ `Idea ${intention.args.idea} was not found.`,
185
+ `未找到 idea ${intention.args.idea}。`,
186
+ ),
187
+ choices: clone(observation.candidates ?? []),
188
+ problems: clone(observation.problems ?? []),
189
+ nextSteps,
190
+ };
191
+ }
192
+ if (observation.state === "idea-selected") {
193
+ const idea = clone(observation.selectedIdea);
194
+ return {
195
+ ...base,
196
+ kind: "next-steps",
197
+ summary: localize(
198
+ language,
199
+ `Continue idea ${idea.alias ?? idea.id}; its state is ${idea.state}.`,
200
+ `继续推进 idea ${idea.alias ?? idea.id};当前状态为 ${idea.state}。`,
201
+ ),
202
+ idea,
203
+ nextSteps,
204
+ ...(observation.guidance === undefined
205
+ ? {}
206
+ : { guidance: clone(observation.guidance) }),
207
+ };
208
+ }
209
+ }
210
+
211
+ if (intention.command === "list-ideas" && observation.state === "ideas-listed") {
212
+ return {
213
+ ...base,
214
+ kind: "idea-list",
215
+ summary: localize(
216
+ language,
217
+ `Matched ${observation.summary.matched} idea(s) and returned ${observation.summary.returned}.`,
218
+ `匹配 ${observation.summary.matched} 个 idea,返回 ${observation.summary.returned} 个。`,
219
+ ),
220
+ query: clone(intention.args),
221
+ inventory: clone(observation.summary),
222
+ items: clone(observation.ideas),
223
+ };
224
+ }
225
+
226
+ throw new Error(
227
+ `Cannot produce a response for ${intention.command}:${observation.state}`,
228
+ );
229
+ }
230
+
231
+ export function respond(intention, internalObservation) {
232
+ if (!intention || !internalObservation?.observation) {
233
+ throw new Error("Response requires an intention and a final observation");
234
+ }
235
+ const language = responseLanguage(intention, internalObservation.observation);
236
+ if (intention.command === "check") {
237
+ return validationResponse(
238
+ intention,
239
+ internalObservation.observation,
240
+ language,
241
+ );
242
+ }
243
+ return dialogueResponse(intention, internalObservation, language);
244
+ }
245
+
246
+ function codeSpan(value) {
247
+ const text = String(value);
248
+ let longest = 0;
249
+ for (const [run] of text.matchAll(/`+/g)) {
250
+ longest = Math.max(longest, run.length);
251
+ }
252
+ const fence = "`".repeat(longest + 1);
253
+ return longest === 0 ? `${fence}${text}${fence}` : `${fence} ${text} ${fence}`;
254
+ }
255
+
256
+ function renderVersion(version) {
257
+ if (!version) return null;
258
+ if (version.type === "commit" || version.type === "remote") {
259
+ return version.commit === null
260
+ ? version.type
261
+ : `${version.type} ${version.commit}`;
262
+ }
263
+ return version.type;
264
+ }
265
+
266
+ function renderGuidance(guidance, language) {
267
+ const content = guidance.content
268
+ .split("\n")
269
+ .map((line) => line.length === 0 ? ">" : `> ${line}`)
270
+ .join("\n");
271
+ return [
272
+ `### ${localize(language, "Project phase guidance", "项目阶段 guidance")}`,
273
+ "",
274
+ `- ${localize(language, "source", "来源")}: ${localize(
275
+ language,
276
+ "repository-owned additive guidance",
277
+ "repository-owned 追加 guidance",
278
+ )}`,
279
+ `- ${localize(language, "phase", "阶段")}: ${codeSpan(guidance.phase)}`,
280
+ `- ${localize(language, "path", "路径")}: ${codeSpan(guidance.path)}`,
281
+ `- ${localize(language, "content revision", "内容 revision")}: ${codeSpan(guidance.contentRevision)}`,
282
+ "",
283
+ content,
284
+ ].join("\n");
285
+ }
286
+
287
+ function responseTitle(response) {
288
+ const titles = {
289
+ blocked: ["Blocked", "受阻"],
290
+ "choice-required": ["Choose what to continue", "选择要继续的工作"],
291
+ "idea-created": ["Idea created", "已创建 idea"],
292
+ "idea-list": ["Ideas", "Ideas"],
293
+ "next-steps": ["Next steps", "下一步"],
294
+ "validation-result": ["Check", "检查"],
295
+ };
296
+ const [english, chinese] = titles[response.kind] ?? ["Result", "结果"];
297
+ return localize(response.language, english, chinese);
298
+ }
299
+
300
+ export function renderResponse(response) {
301
+ const language = response.language ?? DEFAULT_LANGUAGE;
302
+ const lines = [`## ${responseTitle(response)}`, "", response.summary];
303
+
304
+ if (response.inventory) {
305
+ const counts = Object.entries(response.inventory.counts)
306
+ .map(([state, count]) => `${state}=${count}`)
307
+ .join(", ");
308
+ lines.push(
309
+ "",
310
+ `- ${localize(language, "States", "状态")}: ${response.query.states
311
+ .map(codeSpan)
312
+ .join(", ")}`,
313
+ );
314
+ if (response.query.query !== null) {
315
+ lines.push(
316
+ `- ${localize(language, "Query", "查询")}: ${codeSpan(response.query.query)}`,
317
+ );
318
+ }
319
+ if (response.query.createdSince !== null) {
320
+ lines.push(
321
+ `- ${localize(language, "Created since", "创建时间下界")}: ${codeSpan(response.query.createdSince)}`,
322
+ );
323
+ }
324
+ if (response.query.createdBefore !== null) {
325
+ lines.push(
326
+ `- ${localize(language, "Created before", "创建时间上界")}: ${codeSpan(response.query.createdBefore)}`,
327
+ );
328
+ }
329
+ lines.push(
330
+ `- ${localize(language, "Sort", "排序")}: ${codeSpan(response.query.sort)}`,
331
+ `- ${localize(language, "Limit", "上限")}: ${response.query.limit === null
332
+ ? localize(language, "none", "无")
333
+ : codeSpan(response.query.limit)}`,
334
+ `- ${localize(language, "Matched", "匹配")}: ${response.inventory.matched}`,
335
+ `- ${localize(language, "Returned", "返回")}: ${response.inventory.returned}`,
336
+ `- ${localize(language, "Truncated", "已截断")}: ${response.inventory.truncated
337
+ ? localize(language, "yes", "是")
338
+ : localize(language, "no", "否")}`,
339
+ `- ${localize(language, "Counts", "计数")}: ${counts}`,
340
+ );
341
+ }
342
+
343
+ if (response.validation) {
344
+ const target = response.validation.target.type === "commit"
345
+ ? `${response.validation.target.type} ${response.validation.target.revision}`
346
+ : response.validation.target.type;
347
+ lines.push(
348
+ "",
349
+ `- ${localize(language, "Target", "目标")}: ${codeSpan(target)}`,
350
+ );
351
+ const version = renderVersion(response.validation.version);
352
+ if (version) {
353
+ lines.push(`- ${localize(language, "Version", "版本")}: ${codeSpan(version)}`);
354
+ }
355
+ lines.push(
356
+ `- ${localize(language, "Result", "结果")}: ${response.validation.valid
357
+ ? localize(language, "valid", "通过")
358
+ : localize(language, "invalid or unavailable", "未通过或无法验证")}`,
359
+ );
360
+ }
361
+
362
+ if (response.idea) {
363
+ lines.push(
364
+ "",
365
+ `- ${localize(language, "Idea", "Idea")}: ${codeSpan(response.idea.id)}`,
366
+ `- ${localize(language, "State", "状态")}: ${codeSpan(response.idea.state)}`,
367
+ );
368
+ if (response.idea.alias !== undefined) {
369
+ lines.push(
370
+ `- ${localize(language, "Alias", "Alias")}: ${codeSpan(response.idea.alias)}`,
371
+ );
372
+ }
373
+ }
374
+
375
+ if (response.createdIdea) {
376
+ lines.push(
377
+ "",
378
+ `- ${localize(language, "ID", "ID")}: ${codeSpan(response.createdIdea.id)}`,
379
+ `- ${localize(language, "Path", "路径")}: ${codeSpan(response.createdIdea.path)}`,
380
+ `- ${localize(language, "State", "状态")}: ${codeSpan(response.createdIdea.state)}`,
381
+ );
382
+ }
383
+
384
+ if (response.choices?.length > 0) {
385
+ lines.push(
386
+ "",
387
+ `### ${localize(language, "Available ideas", "可用 ideas")}`,
388
+ "",
389
+ );
390
+ for (const idea of response.choices) {
391
+ const alias = idea.alias === undefined ? "" : ` (${idea.alias})`;
392
+ lines.push(`- ${codeSpan(idea.id)}${alias} ${idea.state}`);
393
+ }
394
+ }
395
+
396
+ if (response.items) {
397
+ lines.push("", `### ${localize(language, "Results", "结果")}`, "");
398
+ if (response.items.length === 0) {
399
+ lines.push(localize(language, "No ideas matched.", "没有匹配的 idea。"));
400
+ } else {
401
+ for (const idea of response.items) {
402
+ const details = [
403
+ `${localize(language, "State", "状态")}=${codeSpan(idea.state)}`,
404
+ `${localize(language, "Created", "创建时间")}=${codeSpan(idea.createdAt)}`,
405
+ ];
406
+ if (idea.alias !== undefined) {
407
+ details.push(
408
+ `${localize(language, "Alias", "Alias")}=${codeSpan(idea.alias)}`,
409
+ );
410
+ }
411
+ if (idea.title !== undefined) {
412
+ details.push(
413
+ `${localize(language, "Title", "标题")}=${codeSpan(idea.title)}`,
414
+ );
415
+ }
416
+ lines.push(`- ${codeSpan(idea.id)}: ${details.join("; ")}`);
417
+ }
418
+ }
419
+ }
420
+
421
+ if (response.problems?.length > 0) {
422
+ lines.push(
423
+ "",
424
+ `### ${localize(language, "Issues to address", "需要处理的问题")}`,
425
+ "",
426
+ );
427
+ for (const problem of response.problems) {
428
+ lines.push(`- [${problem.type}] ${problem.summary}`);
429
+ }
430
+ }
431
+
432
+ if (response.nextSteps?.length > 0) {
433
+ lines.push("", `### ${localize(language, "Next steps", "下一步")}`, "");
434
+ for (const step of response.nextSteps) {
435
+ lines.push(step.text);
436
+ }
437
+ }
438
+
439
+ if (response.guidance !== undefined) {
440
+ lines.push("", renderGuidance(response.guidance, language));
441
+ }
442
+ return lines.join("\n");
443
+ }
package/src/trace.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import { randomUUID } from "node:crypto";
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import { constants } from "node:fs";
4
4
  import { access, lstat, writeFile } from "node:fs/promises";
5
5
  import { dirname, resolve } from "node:path";
6
6
 
7
- const TRACE_SCHEMA_VERSION = 1;
7
+ export const TRACE_SCHEMA_VERSION = 2;
8
8
  const traceContext = new AsyncLocalStorage();
9
9
 
10
10
  function durationMilliseconds(started) {
@@ -44,6 +44,7 @@ class Trace {
44
44
  };
45
45
  this.nextSpanId += 1;
46
46
  this.emit({
47
+ channel: "telemetry",
47
48
  event: "span-start",
48
49
  spanId: span.id,
49
50
  parentSpanId,
@@ -55,6 +56,7 @@ class Trace {
55
56
 
56
57
  end(span, status, attributes = {}) {
57
58
  this.emit({
59
+ channel: "telemetry",
58
60
  event: "span-end",
59
61
  spanId: span.id,
60
62
  parentSpanId: span.parentSpanId,
@@ -86,7 +88,12 @@ async function requireAvailableTracePath(path) {
86
88
  throw new Error(`Trace file already exists: ${path}`);
87
89
  }
88
90
 
89
- export async function traceAsync(name, attributes, callback) {
91
+ export async function traceAsync(
92
+ name,
93
+ attributes,
94
+ callback,
95
+ summarize = () => ({}),
96
+ ) {
90
97
  const context = traceContext.getStore();
91
98
  if (!context) return callback();
92
99
 
@@ -96,7 +103,12 @@ export async function traceAsync(name, attributes, callback) {
96
103
  async () => {
97
104
  try {
98
105
  const result = await callback();
99
- context.trace.end(span, "ok");
106
+ const summary = summarize(result);
107
+ context.trace.end(
108
+ span,
109
+ summary.status ?? "ok",
110
+ summary.attributes ?? {},
111
+ );
100
112
  return result;
101
113
  } catch (caught) {
102
114
  context.trace.end(span, "error", { errorName: errorName(caught) });
@@ -106,6 +118,80 @@ export async function traceAsync(name, attributes, callback) {
106
118
  );
107
119
  }
108
120
 
121
+ function hash(value) {
122
+ return createHash("sha256")
123
+ .update(JSON.stringify(value))
124
+ .digest("hex");
125
+ }
126
+
127
+ function projectDomainMessage(message) {
128
+ const base = {
129
+ channel: "domain",
130
+ event: message.type,
131
+ messageSequence: message.sequence,
132
+ };
133
+ if (message.type === "intention.accepted") {
134
+ return {
135
+ ...base,
136
+ command: message.intention.command,
137
+ argumentNames: Object.keys(message.intention.args ?? {}).sort(),
138
+ intentionHash: hash(message.intention),
139
+ };
140
+ }
141
+ if (message.type === "observation.fact") {
142
+ return {
143
+ ...base,
144
+ factType: message.fact.type,
145
+ progress: message.fact.progress,
146
+ state: message.fact.observation.state,
147
+ problemCount: message.fact.observation.problems?.length ?? 0,
148
+ problemTypes: (message.fact.observation.problems ?? [])
149
+ .map(({ type }) => type),
150
+ ...(message.fact.observation.selectedIdea?.id === undefined
151
+ ? {}
152
+ : { selectedIdeaId: message.fact.observation.selectedIdea.id }),
153
+ ...(message.fact.observation.createdIdea?.id === undefined
154
+ ? {}
155
+ : { createdIdeaId: message.fact.observation.createdIdea.id }),
156
+ };
157
+ }
158
+ if (message.type === "action.requested") {
159
+ return {
160
+ ...base,
161
+ actionId: message.actionId,
162
+ actionType: message.action.type,
163
+ };
164
+ }
165
+ if (message.type === "action.finished") {
166
+ return {
167
+ ...base,
168
+ actionId: message.actionId,
169
+ actionType: message.actionType,
170
+ status: message.status,
171
+ ...(message.problem?.type === undefined
172
+ ? {}
173
+ : { problemType: message.problem.type }),
174
+ resultFields: Object.keys(message.result ?? {}).sort(),
175
+ };
176
+ }
177
+ if (message.type === "response.created") {
178
+ return {
179
+ ...base,
180
+ kind: message.kind,
181
+ responseHash: message.metadata.hash,
182
+ nextStepCount: message.metadata.nextStepCount,
183
+ itemCount: message.metadata.itemCount,
184
+ };
185
+ }
186
+ throw new Error(`Cannot trace unknown domain message type: ${message.type}`);
187
+ }
188
+
189
+ export function emitDomainMessage(message) {
190
+ const context = traceContext.getStore();
191
+ if (!context) return;
192
+ context.trace.emit(projectDomainMessage(message));
193
+ }
194
+
109
195
  export function traceSync(name, attributes, callback, summarize = () => ({})) {
110
196
  const context = traceContext.getStore();
111
197
  if (!context) return callback();