silvermoon 0.0.2 → 0.1.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,305 @@
1
+ import { inspectAdoption } from "./adoption.js";
2
+ import {
3
+ diagnosticInstruction,
4
+ diagnosticProblem,
5
+ localize,
6
+ resolvedConfiguration,
7
+ summarizeIdeas,
8
+ } from "./dialogue.js";
9
+ import { inspectIdeaLayout } from "./idea-layout.js";
10
+ import { resolveLanguage } from "./language.js";
11
+ import { traceAsync } from "./trace.js";
12
+ import { loadUserConfig } from "./user-config.js";
13
+
14
+ function projectObservation({
15
+ configuration,
16
+ ideas,
17
+ problems,
18
+ root,
19
+ version,
20
+ }) {
21
+ if (version === undefined) {
22
+ return {
23
+ state: "project-setup-required",
24
+ observedThrough: "root",
25
+ root,
26
+ problems,
27
+ };
28
+ }
29
+ if (configuration === undefined) {
30
+ return {
31
+ state: "project-setup-required",
32
+ observedThrough: "version",
33
+ root,
34
+ version,
35
+ problems,
36
+ };
37
+ }
38
+ if (ideas === undefined) {
39
+ return {
40
+ state: "project-setup-required",
41
+ observedThrough: "configuration",
42
+ root,
43
+ version,
44
+ configuration,
45
+ problems,
46
+ };
47
+ }
48
+ return {
49
+ state: "project-setup-required",
50
+ observedThrough: "ideas",
51
+ root,
52
+ version,
53
+ configuration,
54
+ ideas,
55
+ problems,
56
+ };
57
+ }
58
+
59
+ function localizeFinding(finding, language) {
60
+ if (finding.sourceDiagnostic) {
61
+ return {
62
+ priority: finding.priority,
63
+ problem: diagnosticProblem(finding.sourceDiagnostic, language),
64
+ instruction: diagnosticInstruction(finding.sourceDiagnostic, language),
65
+ };
66
+ }
67
+ return {
68
+ priority: finding.priority,
69
+ problem: {
70
+ type: finding.problem.type,
71
+ summary: localize(
72
+ language,
73
+ finding.problem.summary,
74
+ `Silvermoon 发现 ${finding.problem.type}:${finding.problem.summary}`,
75
+ ),
76
+ },
77
+ instruction: localize(
78
+ language,
79
+ finding.instruction,
80
+ `处理 ${finding.problem.type}:${finding.instruction}`,
81
+ ),
82
+ };
83
+ }
84
+
85
+ export function incompleteObservation({
86
+ configuration,
87
+ ideas,
88
+ problem,
89
+ root,
90
+ version,
91
+ }) {
92
+ return projectObservation({
93
+ configuration,
94
+ ideas,
95
+ problems: [problem],
96
+ root,
97
+ version,
98
+ });
99
+ }
100
+
101
+ export function unavailableObservation({ root, version, problem }) {
102
+ return {
103
+ state: "check-unavailable",
104
+ root,
105
+ version,
106
+ problems: [problem],
107
+ };
108
+ }
109
+
110
+ export function repositoryProblemObservation(observation, problems) {
111
+ return {
112
+ ...observation,
113
+ state: "repository-sync-required",
114
+ problems,
115
+ };
116
+ }
117
+
118
+ async function observeSnapshotInternal({
119
+ allowMissingIdeas = false,
120
+ contentRoot,
121
+ gitRoot,
122
+ ideaLanguage,
123
+ projectOnly = false,
124
+ root,
125
+ snapshotTree,
126
+ userHome,
127
+ version,
128
+ }) {
129
+ const adoption = await traceAsync(
130
+ "adoption.inspect",
131
+ {},
132
+ () => inspectAdoption({ contentRoot, root }),
133
+ );
134
+ const user = await traceAsync(
135
+ "user-config.load",
136
+ {},
137
+ () => loadUserConfig({ home: userHome }),
138
+ );
139
+ const userFindings = user.diagnostics.map((diagnostic) => ({
140
+ priority: 30,
141
+ problem: diagnosticProblem(diagnostic),
142
+ instruction: diagnostic.remediation,
143
+ sourceDiagnostic: diagnostic,
144
+ }));
145
+ const baseFindings = [...adoption.findings, ...userFindings]
146
+ .sort((left, right) => left.priority - right.priority);
147
+ const fallbackLanguage = resolveLanguage({
148
+ global: user.config?.preferredLanguage,
149
+ }).tag;
150
+
151
+ if (!adoption.gitReady) {
152
+ const findings = baseFindings.map((finding) =>
153
+ localizeFinding(finding, fallbackLanguage)
154
+ );
155
+ const observation = projectObservation({
156
+ problems: findings.map(({ problem }) => problem),
157
+ root: adoption.root,
158
+ });
159
+ return {
160
+ config: null,
161
+ findings,
162
+ language: fallbackLanguage,
163
+ layout: null,
164
+ observation,
165
+ projectReady: false,
166
+ };
167
+ }
168
+
169
+ if (!adoption.config) {
170
+ const findings = baseFindings.map((finding) =>
171
+ localizeFinding(finding, fallbackLanguage)
172
+ );
173
+ const observation = projectObservation({
174
+ problems: findings.map(({ problem }) => problem),
175
+ root: adoption.root,
176
+ version,
177
+ });
178
+ return {
179
+ config: null,
180
+ findings,
181
+ language: fallbackLanguage,
182
+ layout: null,
183
+ observation,
184
+ projectReady: false,
185
+ };
186
+ }
187
+
188
+ const language = resolveLanguage({
189
+ idea: ideaLanguage,
190
+ project: adoption.config.preferredLanguage,
191
+ global: user.config?.preferredLanguage,
192
+ }).tag;
193
+ const configuration = resolvedConfiguration(adoption.config, language);
194
+ const findings = baseFindings.map((finding) =>
195
+ localizeFinding(finding, language)
196
+ );
197
+ let layout;
198
+ try {
199
+ layout = await traceAsync(
200
+ "idea-layout.inspect",
201
+ {},
202
+ () => inspectIdeaLayout({
203
+ config: adoption.config,
204
+ gitRoot: gitRoot ?? adoption.root,
205
+ root: contentRoot ?? adoption.root,
206
+ snapshotTree,
207
+ }),
208
+ );
209
+ } catch (caught) {
210
+ layout = {
211
+ diagnostics: [{
212
+ code: "layout.inspection-failed",
213
+ level: "error",
214
+ message: `Cannot inspect Silvermoon ideas: ${caught.message}`,
215
+ remediation: "Repair the ideas directory and retry.",
216
+ }],
217
+ ideas: [],
218
+ };
219
+ }
220
+ if (allowMissingIdeas) {
221
+ layout.diagnostics = layout.diagnostics.filter(
222
+ ({ code }) => code !== "layout.ideas.missing",
223
+ );
224
+ }
225
+
226
+ const layoutFindings = layout.diagnostics.map((diagnostic) => ({
227
+ priority: 50,
228
+ problem: diagnosticProblem(diagnostic, language),
229
+ instruction: diagnosticInstruction(diagnostic, language),
230
+ }));
231
+ findings.push(...layoutFindings);
232
+ findings.sort((left, right) => left.priority - right.priority);
233
+
234
+ if (layout.diagnostics.length > 0) {
235
+ const observation = projectObservation({
236
+ configuration,
237
+ problems: findings.map(({ problem }) => problem),
238
+ root: adoption.root,
239
+ version,
240
+ });
241
+ return {
242
+ config: adoption.config,
243
+ findings,
244
+ language,
245
+ layout,
246
+ observation,
247
+ projectReady: false,
248
+ };
249
+ }
250
+
251
+ const ideas = summarizeIdeas(layout.ideas);
252
+ if (findings.length > 0) {
253
+ const observation = projectObservation({
254
+ configuration,
255
+ ideas,
256
+ problems: findings.map(({ problem }) => problem),
257
+ root: adoption.root,
258
+ version,
259
+ });
260
+ return {
261
+ config: adoption.config,
262
+ findings,
263
+ language,
264
+ layout,
265
+ observation,
266
+ projectReady: false,
267
+ };
268
+ }
269
+
270
+ return {
271
+ config: adoption.config,
272
+ findings,
273
+ language,
274
+ layout,
275
+ observation: {
276
+ state: projectOnly
277
+ ? "project-ready"
278
+ : ideas.activeIdeas.length > 0 ? "task-pending" : "idle",
279
+ root: adoption.root,
280
+ version,
281
+ configuration,
282
+ ideas,
283
+ problems: [],
284
+ },
285
+ projectReady: true,
286
+ };
287
+ }
288
+
289
+ export async function observeSnapshot(options) {
290
+ return traceAsync(
291
+ "snapshot.observe",
292
+ {},
293
+ () => observeSnapshotInternal(options),
294
+ );
295
+ }
296
+
297
+ export function withObservationLanguage(observed, ideaLanguage) {
298
+ if (!observed.config || !ideaLanguage) return observed;
299
+ const language = resolveLanguage({ idea: ideaLanguage }).tag;
300
+ const observation = {
301
+ ...observed.observation,
302
+ configuration: resolvedConfiguration(observed.config, language),
303
+ };
304
+ return { ...observed, language, observation };
305
+ }
package/src/trace.js ADDED
@@ -0,0 +1,171 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { randomUUID } from "node:crypto";
3
+ import { constants } from "node:fs";
4
+ import { access, lstat, writeFile } from "node:fs/promises";
5
+ import { dirname, resolve } from "node:path";
6
+
7
+ const TRACE_SCHEMA_VERSION = 1;
8
+ const traceContext = new AsyncLocalStorage();
9
+
10
+ function durationMilliseconds(started) {
11
+ return Math.round(Number(process.hrtime.bigint() - started) / 1_000) / 1_000;
12
+ }
13
+
14
+ function errorName(caught) {
15
+ return caught instanceof Error ? caught.name : typeof caught;
16
+ }
17
+
18
+ class Trace {
19
+ constructor() {
20
+ this.events = [];
21
+ this.nextSequence = 1;
22
+ this.nextSpanId = 1;
23
+ this.traceId = randomUUID();
24
+ }
25
+
26
+ emit(event) {
27
+ this.events.push({
28
+ schemaVersion: TRACE_SCHEMA_VERSION,
29
+ traceId: this.traceId,
30
+ sequence: this.nextSequence,
31
+ timestamp: new Date().toISOString(),
32
+ ...event,
33
+ });
34
+ this.nextSequence += 1;
35
+ }
36
+
37
+ start(name, parentSpanId, attributes) {
38
+ const span = {
39
+ attributes,
40
+ id: String(this.nextSpanId),
41
+ name,
42
+ parentSpanId,
43
+ started: process.hrtime.bigint(),
44
+ };
45
+ this.nextSpanId += 1;
46
+ this.emit({
47
+ event: "span-start",
48
+ spanId: span.id,
49
+ parentSpanId,
50
+ name,
51
+ attributes,
52
+ });
53
+ return span;
54
+ }
55
+
56
+ end(span, status, attributes = {}) {
57
+ this.emit({
58
+ event: "span-end",
59
+ spanId: span.id,
60
+ parentSpanId: span.parentSpanId,
61
+ name: span.name,
62
+ status,
63
+ durationMs: durationMilliseconds(span.started),
64
+ attributes: { ...span.attributes, ...attributes },
65
+ });
66
+ }
67
+
68
+ serialize() {
69
+ return `${this.events.map((event) => JSON.stringify(event)).join("\n")}\n`;
70
+ }
71
+ }
72
+
73
+ async function requireAvailableTracePath(path) {
74
+ const parent = dirname(path);
75
+ const parentMetadata = await lstat(parent);
76
+ if (!parentMetadata.isDirectory()) {
77
+ throw new Error(`Trace parent is not a directory: ${parent}`);
78
+ }
79
+ await access(parent, constants.W_OK);
80
+ try {
81
+ await lstat(path);
82
+ } catch (caught) {
83
+ if (caught.code === "ENOENT") return;
84
+ throw caught;
85
+ }
86
+ throw new Error(`Trace file already exists: ${path}`);
87
+ }
88
+
89
+ export async function traceAsync(name, attributes, callback) {
90
+ const context = traceContext.getStore();
91
+ if (!context) return callback();
92
+
93
+ const span = context.trace.start(name, context.parentSpanId, attributes);
94
+ return traceContext.run(
95
+ { parentSpanId: span.id, trace: context.trace },
96
+ async () => {
97
+ try {
98
+ const result = await callback();
99
+ context.trace.end(span, "ok");
100
+ return result;
101
+ } catch (caught) {
102
+ context.trace.end(span, "error", { errorName: errorName(caught) });
103
+ throw caught;
104
+ }
105
+ },
106
+ );
107
+ }
108
+
109
+ export function traceSync(name, attributes, callback, summarize = () => ({})) {
110
+ const context = traceContext.getStore();
111
+ if (!context) return callback();
112
+
113
+ const span = context.trace.start(name, context.parentSpanId, attributes);
114
+ return traceContext.run(
115
+ { parentSpanId: span.id, trace: context.trace },
116
+ () => {
117
+ try {
118
+ const result = callback();
119
+ const summary = summarize(result);
120
+ context.trace.end(
121
+ span,
122
+ summary.status ?? "ok",
123
+ summary.attributes ?? {},
124
+ );
125
+ return result;
126
+ } catch (caught) {
127
+ context.trace.end(span, "error", { errorName: errorName(caught) });
128
+ throw caught;
129
+ }
130
+ },
131
+ );
132
+ }
133
+
134
+ export async function withTraceFile(file, name, attributes, callback) {
135
+ if (file === undefined) return callback();
136
+
137
+ const path = resolve(file);
138
+ await requireAvailableTracePath(path);
139
+ const trace = new Trace();
140
+ let result;
141
+ let commandError;
142
+ try {
143
+ result = await traceContext.run(
144
+ { parentSpanId: null, trace },
145
+ () => traceAsync(name, attributes, callback),
146
+ );
147
+ } catch (caught) {
148
+ commandError = caught;
149
+ }
150
+
151
+ let traceError;
152
+ try {
153
+ await writeFile(path, trace.serialize(), { encoding: "utf8", flag: "wx" });
154
+ } catch (caught) {
155
+ traceError = caught;
156
+ }
157
+
158
+ if (commandError && traceError) {
159
+ throw new AggregateError(
160
+ [commandError, traceError],
161
+ `Command failed and trace file could not be written: ${path}`,
162
+ );
163
+ }
164
+ if (commandError) throw commandError;
165
+ if (traceError) {
166
+ throw new Error(`Cannot write trace file ${path}: ${traceError.message}`, {
167
+ cause: traceError,
168
+ });
169
+ }
170
+ return result;
171
+ }