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.
- package/README.md +40 -24
- package/README.zh-CN.md +36 -24
- package/docs/core-concepts.md +28 -11
- package/docs/getting-started.md +41 -9
- package/docs/npm-package-releases.md +8 -7
- package/docs/operations.md +60 -23
- package/docs/reference.md +125 -67
- package/docs/repository-tasks.md +29 -23
- package/package.json +3 -3
- package/schema/v1/command-report.schema.json +178 -0
- package/schema/v1/domain-message.schema.json +278 -0
- package/schema/v2/trace-event.schema.json +122 -0
- package/skills/silvermoon/SKILL.md +36 -25
- package/skills/silvermoon/references/adoption.md +8 -9
- package/src/cli.js +63 -9
- package/src/create-idea.js +260 -193
- package/src/dialogue.js +6 -305
- package/src/domain.js +592 -0
- package/src/idea-query.js +230 -0
- package/src/ideas.js +1 -0
- package/src/index.js +76 -31
- package/src/language.js +8 -0
- package/src/list-ideas.js +133 -0
- package/src/response.js +443 -0
- package/src/trace.js +90 -4
- package/src/whatsnext.js +81 -84
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ACTIVE_IDEA_STATES,
|
|
5
|
+
IDEA_STATES,
|
|
6
|
+
isValidUlid,
|
|
7
|
+
} from "./ideas.js";
|
|
8
|
+
|
|
9
|
+
const QUERY_STATES = new Set([...IDEA_STATES, "active"]);
|
|
10
|
+
const SORTS = new Set(["newest", "oldest"]);
|
|
11
|
+
const RFC3339 =
|
|
12
|
+
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-](\d{2}):(\d{2}))$/;
|
|
13
|
+
const CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
14
|
+
|
|
15
|
+
function usageError(message) {
|
|
16
|
+
const error = new Error(message);
|
|
17
|
+
error.exitCode = 2;
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeStates(states, all) {
|
|
22
|
+
if (all !== undefined && typeof all !== "boolean") {
|
|
23
|
+
throw usageError("all must be a boolean");
|
|
24
|
+
}
|
|
25
|
+
if (states !== undefined && all === true) {
|
|
26
|
+
throw usageError("state and all are mutually exclusive");
|
|
27
|
+
}
|
|
28
|
+
if (states === undefined) {
|
|
29
|
+
return all === true ? [...IDEA_STATES] : [...ACTIVE_IDEA_STATES];
|
|
30
|
+
}
|
|
31
|
+
const requested = Array.isArray(states) ? states : [states];
|
|
32
|
+
if (requested.length === 0) {
|
|
33
|
+
throw usageError("state must not be empty");
|
|
34
|
+
}
|
|
35
|
+
const selected = new Set();
|
|
36
|
+
for (const state of requested) {
|
|
37
|
+
if (typeof state !== "string" || !QUERY_STATES.has(state)) {
|
|
38
|
+
throw usageError(
|
|
39
|
+
`state must be one of active, ${IDEA_STATES.join(", ")}; received ${JSON.stringify(state)}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
for (const expanded of state === "active" ? ACTIVE_IDEA_STATES : [state]) {
|
|
43
|
+
selected.add(expanded);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return IDEA_STATES.filter((state) => selected.has(state));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeQueryText(query) {
|
|
50
|
+
if (query === undefined || query === null) return null;
|
|
51
|
+
if (typeof query !== "string" || query.trim().length === 0) {
|
|
52
|
+
throw usageError("query must be a non-empty string");
|
|
53
|
+
}
|
|
54
|
+
return query.trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeTimestamp(value, name) {
|
|
58
|
+
if (value === undefined || value === null) return null;
|
|
59
|
+
if (typeof value !== "string") {
|
|
60
|
+
throw usageError(`${name} must be an RFC 3339 timestamp`);
|
|
61
|
+
}
|
|
62
|
+
const match = RFC3339.exec(value);
|
|
63
|
+
if (!match) throw usageError(`${name} must be an RFC 3339 timestamp with a timezone`);
|
|
64
|
+
const [, year, month, day, hour, minute, second, , zone, offsetHour, offsetMinute] =
|
|
65
|
+
match;
|
|
66
|
+
const numeric = [year, month, day, hour, minute, second].map(Number);
|
|
67
|
+
if (
|
|
68
|
+
numeric[1] < 1
|
|
69
|
+
|| numeric[1] > 12
|
|
70
|
+
|| numeric[2] < 1
|
|
71
|
+
|| numeric[2] > 31
|
|
72
|
+
|| numeric[3] > 23
|
|
73
|
+
|| numeric[4] > 59
|
|
74
|
+
|| numeric[5] > 59
|
|
75
|
+
|| (
|
|
76
|
+
zone !== "Z"
|
|
77
|
+
&& (Number(offsetHour) > 23 || Number(offsetMinute) > 59)
|
|
78
|
+
)
|
|
79
|
+
) {
|
|
80
|
+
throw usageError(`${name} must be a valid RFC 3339 timestamp`);
|
|
81
|
+
}
|
|
82
|
+
const calendar = new Date(0);
|
|
83
|
+
calendar.setUTCFullYear(numeric[0], numeric[1] - 1, numeric[2]);
|
|
84
|
+
calendar.setUTCHours(0, 0, 0, 0);
|
|
85
|
+
if (
|
|
86
|
+
calendar.getUTCFullYear() !== numeric[0]
|
|
87
|
+
|| calendar.getUTCMonth() !== numeric[1] - 1
|
|
88
|
+
|| calendar.getUTCDate() !== numeric[2]
|
|
89
|
+
) {
|
|
90
|
+
throw usageError(`${name} must be a valid RFC 3339 timestamp`);
|
|
91
|
+
}
|
|
92
|
+
const timestamp = Date.parse(value);
|
|
93
|
+
if (!Number.isFinite(timestamp)) {
|
|
94
|
+
throw usageError(`${name} must be a valid RFC 3339 timestamp`);
|
|
95
|
+
}
|
|
96
|
+
return new Date(timestamp).toISOString();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeSort(sort) {
|
|
100
|
+
const value = sort ?? "newest";
|
|
101
|
+
if (typeof value !== "string" || !SORTS.has(value)) {
|
|
102
|
+
throw usageError("sort must be newest or oldest");
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeLimit(limit) {
|
|
108
|
+
if (limit === undefined || limit === null) return null;
|
|
109
|
+
const value = typeof limit === "number"
|
|
110
|
+
? limit
|
|
111
|
+
: typeof limit === "string" && /^[1-9]\d*$/.test(limit)
|
|
112
|
+
? Number(limit)
|
|
113
|
+
: Number.NaN;
|
|
114
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
115
|
+
throw usageError("limit must be a positive integer");
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function normalizeIdeaQuery({
|
|
121
|
+
all,
|
|
122
|
+
createdBefore,
|
|
123
|
+
createdSince,
|
|
124
|
+
limit,
|
|
125
|
+
query,
|
|
126
|
+
sort,
|
|
127
|
+
states,
|
|
128
|
+
} = {}) {
|
|
129
|
+
const normalized = {
|
|
130
|
+
states: normalizeStates(states, all),
|
|
131
|
+
query: normalizeQueryText(query),
|
|
132
|
+
createdSince: normalizeTimestamp(createdSince, "createdSince"),
|
|
133
|
+
createdBefore: normalizeTimestamp(createdBefore, "createdBefore"),
|
|
134
|
+
sort: normalizeSort(sort),
|
|
135
|
+
limit: normalizeLimit(limit),
|
|
136
|
+
};
|
|
137
|
+
if (
|
|
138
|
+
normalized.createdSince !== null
|
|
139
|
+
&& normalized.createdBefore !== null
|
|
140
|
+
&& Date.parse(normalized.createdSince) >= Date.parse(normalized.createdBefore)
|
|
141
|
+
) {
|
|
142
|
+
throw usageError("createdSince must be earlier than createdBefore");
|
|
143
|
+
}
|
|
144
|
+
return normalized;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function ideaCreatedAt(id) {
|
|
148
|
+
if (!isValidUlid(id)) throw new Error(`Cannot decode invalid idea ULID: ${id}`);
|
|
149
|
+
let timestamp = 0;
|
|
150
|
+
for (const character of id.slice(0, 10)) {
|
|
151
|
+
timestamp = timestamp * 32 + CROCKFORD_BASE32.indexOf(character);
|
|
152
|
+
}
|
|
153
|
+
return new Date(timestamp).toISOString();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function markdownText(node) {
|
|
157
|
+
if (node.type === "text" || node.type === "inlineCode") return node.value;
|
|
158
|
+
if (node.type === "image") return node.alt ?? "";
|
|
159
|
+
return (node.children ?? []).map(markdownText).join("");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function extractIdeaTitle(source) {
|
|
163
|
+
if (typeof source !== "string") {
|
|
164
|
+
throw new TypeError("Idea document source must be a string");
|
|
165
|
+
}
|
|
166
|
+
const heading = fromMarkdown(source).children.find(
|
|
167
|
+
(node) => node.type === "heading" && node.depth === 1,
|
|
168
|
+
);
|
|
169
|
+
if (!heading) return undefined;
|
|
170
|
+
const title = markdownText(heading).replaceAll(/\s+/g, " ").trim();
|
|
171
|
+
return title.length === 0 ? undefined : title;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function ideaInventoryItem(idea, title) {
|
|
175
|
+
const item = {
|
|
176
|
+
id: idea.id,
|
|
177
|
+
state: idea.state,
|
|
178
|
+
createdAt: ideaCreatedAt(idea.id),
|
|
179
|
+
};
|
|
180
|
+
if (idea.alias !== undefined) item.alias = idea.alias;
|
|
181
|
+
if (title !== undefined) item.title = title;
|
|
182
|
+
return item;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function compareText(left, right) {
|
|
186
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function queryIdeaInventory(items, input) {
|
|
190
|
+
const query = normalizeIdeaQuery(input);
|
|
191
|
+
const states = new Set(query.states);
|
|
192
|
+
const needle = query.query?.toLowerCase();
|
|
193
|
+
const createdSince = query.createdSince === null
|
|
194
|
+
? null
|
|
195
|
+
: Date.parse(query.createdSince);
|
|
196
|
+
const createdBefore = query.createdBefore === null
|
|
197
|
+
? null
|
|
198
|
+
: Date.parse(query.createdBefore);
|
|
199
|
+
const filtered = items.filter((idea) => {
|
|
200
|
+
if (!states.has(idea.state)) return false;
|
|
201
|
+
if (
|
|
202
|
+
needle !== undefined
|
|
203
|
+
&& ![idea.id, idea.alias, idea.title]
|
|
204
|
+
.filter((value) => value !== undefined)
|
|
205
|
+
.some((value) => value.toLowerCase().includes(needle))
|
|
206
|
+
) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const createdAt = Date.parse(idea.createdAt);
|
|
210
|
+
if (createdSince !== null && createdAt < createdSince) return false;
|
|
211
|
+
return createdBefore === null || createdAt < createdBefore;
|
|
212
|
+
});
|
|
213
|
+
const direction = query.sort === "newest" ? -1 : 1;
|
|
214
|
+
filtered.sort((left, right) => direction * compareText(left.id, right.id));
|
|
215
|
+
const counts = Object.fromEntries(IDEA_STATES.map((state) => [state, 0]));
|
|
216
|
+
for (const idea of filtered) counts[idea.state] += 1;
|
|
217
|
+
const ideas = query.limit === null
|
|
218
|
+
? filtered
|
|
219
|
+
: filtered.slice(0, query.limit);
|
|
220
|
+
return {
|
|
221
|
+
query,
|
|
222
|
+
summary: {
|
|
223
|
+
matched: filtered.length,
|
|
224
|
+
returned: ideas.length,
|
|
225
|
+
truncated: ideas.length < filtered.length,
|
|
226
|
+
counts,
|
|
227
|
+
},
|
|
228
|
+
ideas,
|
|
229
|
+
};
|
|
230
|
+
}
|
package/src/ideas.js
CHANGED
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
diagnosticProblem,
|
|
5
5
|
} from "./dialogue.js";
|
|
6
6
|
import { loadConfig } from "./config.js";
|
|
7
|
+
import { createCommandRun } from "./domain.js";
|
|
7
8
|
import {
|
|
8
9
|
fetchPrimary,
|
|
9
10
|
indexSnapshot,
|
|
@@ -32,21 +33,22 @@ function targetArgument({ commit, remote, staged, worktree }) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function failureReport({
|
|
35
|
-
|
|
36
|
+
runtime,
|
|
36
37
|
outputLanguage,
|
|
37
38
|
problem,
|
|
38
39
|
root,
|
|
39
40
|
version,
|
|
40
41
|
}) {
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
observation: unavailableObservation({
|
|
42
|
+
return runtime.complete(
|
|
43
|
+
unavailableObservation({
|
|
44
44
|
outputLanguage,
|
|
45
45
|
problem,
|
|
46
46
|
root,
|
|
47
47
|
version,
|
|
48
48
|
}),
|
|
49
|
-
|
|
49
|
+
{},
|
|
50
|
+
{ factType: "validation.failed" },
|
|
51
|
+
);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
async function inspectTree({
|
|
@@ -71,8 +73,12 @@ async function inspectTree({
|
|
|
71
73
|
);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
function finishCheck(
|
|
75
|
-
return
|
|
76
|
+
function finishCheck(runtime, observed) {
|
|
77
|
+
return runtime.complete(
|
|
78
|
+
observed.observation,
|
|
79
|
+
{},
|
|
80
|
+
{ factType: "validation.completed" },
|
|
81
|
+
);
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
export async function checkRepository({
|
|
@@ -106,6 +112,7 @@ export async function checkRepository({
|
|
|
106
112
|
language: canonicalLanguage ?? null,
|
|
107
113
|
},
|
|
108
114
|
};
|
|
115
|
+
const runtime = createCommandRun(intention);
|
|
109
116
|
const repository = runGit(requestedRoot, ["rev-parse", "--show-toplevel"]);
|
|
110
117
|
if (!repository.ok) {
|
|
111
118
|
const observed = await observeSnapshot({
|
|
@@ -119,7 +126,7 @@ export async function checkRepository({
|
|
|
119
126
|
? { type: "remote", commit: null }
|
|
120
127
|
: { type: target.type },
|
|
121
128
|
});
|
|
122
|
-
return finishCheck(
|
|
129
|
+
return finishCheck(runtime, observed);
|
|
123
130
|
}
|
|
124
131
|
const repositoryRoot = resolve(repository.stdout);
|
|
125
132
|
|
|
@@ -129,7 +136,7 @@ export async function checkRepository({
|
|
|
129
136
|
head = resolveCommit(repositoryRoot, "HEAD");
|
|
130
137
|
} catch (caught) {
|
|
131
138
|
return failureReport({
|
|
132
|
-
|
|
139
|
+
runtime,
|
|
133
140
|
outputLanguage: fallbackOutputLanguage,
|
|
134
141
|
problem: {
|
|
135
142
|
type: "head-unavailable",
|
|
@@ -148,7 +155,7 @@ export async function checkRepository({
|
|
|
148
155
|
);
|
|
149
156
|
} catch (caught) {
|
|
150
157
|
return failureReport({
|
|
151
|
-
|
|
158
|
+
runtime,
|
|
152
159
|
outputLanguage: fallbackOutputLanguage,
|
|
153
160
|
problem: {
|
|
154
161
|
type: "snapshot-inspection-failed",
|
|
@@ -159,9 +166,7 @@ export async function checkRepository({
|
|
|
159
166
|
});
|
|
160
167
|
}
|
|
161
168
|
if (!bootstrap.config) {
|
|
162
|
-
return {
|
|
163
|
-
intention,
|
|
164
|
-
observation: {
|
|
169
|
+
return runtime.complete({
|
|
165
170
|
state: "project-setup-required",
|
|
166
171
|
observedThrough: "version",
|
|
167
172
|
root: repositoryRoot,
|
|
@@ -170,23 +175,35 @@ export async function checkRepository({
|
|
|
170
175
|
problems: bootstrap.diagnostics.map((diagnostic) =>
|
|
171
176
|
diagnosticProblem(diagnostic, fallbackOutputLanguage)
|
|
172
177
|
),
|
|
173
|
-
}
|
|
174
|
-
};
|
|
178
|
+
});
|
|
175
179
|
}
|
|
176
180
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
181
|
+
const fetched = await runtime.performAction(
|
|
182
|
+
{
|
|
183
|
+
type: "fetch-primary",
|
|
184
|
+
repository: bootstrap.config.primaryRepository,
|
|
185
|
+
branch: bootstrap.config.primaryBranch,
|
|
186
|
+
},
|
|
187
|
+
() => ({
|
|
188
|
+
commit: fetchPrimary(repositoryRoot, bootstrap.config),
|
|
189
|
+
}),
|
|
190
|
+
(caught) => ({
|
|
191
|
+
problem: {
|
|
192
|
+
type: "primary-fetch-failed",
|
|
193
|
+
summary: sanitizeGitMessage(caught.message),
|
|
194
|
+
},
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
if (fetched.status === "failure") {
|
|
182
198
|
return failureReport({
|
|
183
|
-
|
|
199
|
+
runtime,
|
|
184
200
|
outputLanguage: fallbackOutputLanguage,
|
|
185
201
|
root: repositoryRoot,
|
|
186
202
|
version: { type: "remote", commit: null },
|
|
187
|
-
problem:
|
|
203
|
+
problem: fetched.problem,
|
|
188
204
|
});
|
|
189
205
|
}
|
|
206
|
+
const primary = fetched.result.commit;
|
|
190
207
|
try {
|
|
191
208
|
const observed = await withTemporaryWorktree(
|
|
192
209
|
repositoryRoot,
|
|
@@ -202,10 +219,10 @@ export async function checkRepository({
|
|
|
202
219
|
version: { type: "remote", commit: primary },
|
|
203
220
|
}),
|
|
204
221
|
);
|
|
205
|
-
return finishCheck(
|
|
222
|
+
return finishCheck(runtime, observed);
|
|
206
223
|
} catch (caught) {
|
|
207
224
|
return failureReport({
|
|
208
|
-
|
|
225
|
+
runtime,
|
|
209
226
|
outputLanguage: fallbackOutputLanguage,
|
|
210
227
|
problem: {
|
|
211
228
|
type: "snapshot-inspection-failed",
|
|
@@ -224,7 +241,7 @@ export async function checkRepository({
|
|
|
224
241
|
resolvedCommit = resolveCommit(repositoryRoot, revision);
|
|
225
242
|
} catch (caught) {
|
|
226
243
|
return failureReport({
|
|
227
|
-
|
|
244
|
+
runtime,
|
|
228
245
|
outputLanguage: fallbackOutputLanguage,
|
|
229
246
|
problem: {
|
|
230
247
|
type: "commit-unavailable",
|
|
@@ -249,10 +266,10 @@ export async function checkRepository({
|
|
|
249
266
|
version: { type: "commit", commit: resolvedCommit },
|
|
250
267
|
}),
|
|
251
268
|
);
|
|
252
|
-
return finishCheck(
|
|
269
|
+
return finishCheck(runtime, observed);
|
|
253
270
|
} catch (caught) {
|
|
254
271
|
return failureReport({
|
|
255
|
-
|
|
272
|
+
runtime,
|
|
256
273
|
outputLanguage: fallbackOutputLanguage,
|
|
257
274
|
problem: {
|
|
258
275
|
type: "snapshot-inspection-failed",
|
|
@@ -275,10 +292,10 @@ export async function checkRepository({
|
|
|
275
292
|
userHome,
|
|
276
293
|
version: { type: "staged" },
|
|
277
294
|
});
|
|
278
|
-
return finishCheck(
|
|
295
|
+
return finishCheck(runtime, observed);
|
|
279
296
|
} catch (caught) {
|
|
280
297
|
return failureReport({
|
|
281
|
-
|
|
298
|
+
runtime,
|
|
282
299
|
outputLanguage: fallbackOutputLanguage,
|
|
283
300
|
problem: {
|
|
284
301
|
type: "snapshot-inspection-failed",
|
|
@@ -300,10 +317,10 @@ export async function checkRepository({
|
|
|
300
317
|
userHome,
|
|
301
318
|
version: { type: "worktree" },
|
|
302
319
|
});
|
|
303
|
-
return finishCheck(
|
|
320
|
+
return finishCheck(runtime, observed);
|
|
304
321
|
} catch (caught) {
|
|
305
322
|
return failureReport({
|
|
306
|
-
|
|
323
|
+
runtime,
|
|
307
324
|
outputLanguage: fallbackOutputLanguage,
|
|
308
325
|
problem: {
|
|
309
326
|
type: "snapshot-inspection-failed",
|
|
@@ -316,15 +333,43 @@ export async function checkRepository({
|
|
|
316
333
|
}
|
|
317
334
|
|
|
318
335
|
export {
|
|
336
|
+
ACTIVE_IDEA_STATES,
|
|
319
337
|
deriveIdeaState,
|
|
338
|
+
IDEA_STATES,
|
|
320
339
|
isValidUlid,
|
|
321
340
|
parseIdeaStatus,
|
|
322
341
|
serializeIdeaStatus,
|
|
323
342
|
validateIdeaStatus,
|
|
324
343
|
} from "./ideas.js";
|
|
344
|
+
export {
|
|
345
|
+
extractIdeaTitle,
|
|
346
|
+
ideaCreatedAt,
|
|
347
|
+
ideaInventoryItem,
|
|
348
|
+
normalizeIdeaQuery,
|
|
349
|
+
queryIdeaInventory,
|
|
350
|
+
} from "./idea-query.js";
|
|
351
|
+
export { listIdeas } from "./list-ideas.js";
|
|
325
352
|
export { createIdea, generateUlid } from "./create-idea.js";
|
|
326
353
|
export {
|
|
327
354
|
inspectAdoption,
|
|
328
355
|
SILVERMOON_VERSION,
|
|
329
356
|
} from "./adoption.js";
|
|
357
|
+
export {
|
|
358
|
+
CommandRun,
|
|
359
|
+
DOMAIN_MESSAGE_SCHEMA_VERSION,
|
|
360
|
+
DomainInvariantError,
|
|
361
|
+
driveCommand,
|
|
362
|
+
initialInternalObservation,
|
|
363
|
+
projectActions,
|
|
364
|
+
projectIntention,
|
|
365
|
+
projectPublicObservation,
|
|
366
|
+
projectReport,
|
|
367
|
+
reduceObservation,
|
|
368
|
+
replayObservation,
|
|
369
|
+
} from "./domain.js";
|
|
370
|
+
export {
|
|
371
|
+
renderResponse,
|
|
372
|
+
respond,
|
|
373
|
+
} from "./response.js";
|
|
374
|
+
export { TRACE_SCHEMA_VERSION } from "./trace.js";
|
|
330
375
|
export { whatsNext } from "./whatsnext.js";
|
package/src/language.js
CHANGED
|
@@ -3,6 +3,14 @@ export const OUTPUT_LANGUAGES = Object.freeze(["en-US", "zh-CN"]);
|
|
|
3
3
|
|
|
4
4
|
const OUTPUT_LANGUAGE_SET = new Set(OUTPUT_LANGUAGES);
|
|
5
5
|
|
|
6
|
+
export function isChinese(language) {
|
|
7
|
+
return language?.toLowerCase().startsWith("zh") ?? false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function localize(language, english, chinese) {
|
|
11
|
+
return isChinese(language) ? chinese : english;
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
export function canonicalizeLanguageTag(value) {
|
|
7
15
|
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
|
|
8
16
|
throw new Error("language tag must be a non-empty trimmed string");
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { createCommandRun } from "./domain.js";
|
|
5
|
+
import {
|
|
6
|
+
extractIdeaTitle,
|
|
7
|
+
ideaInventoryItem,
|
|
8
|
+
normalizeIdeaQuery,
|
|
9
|
+
queryIdeaInventory,
|
|
10
|
+
} from "./idea-query.js";
|
|
11
|
+
import { localize } from "./language.js";
|
|
12
|
+
import { observeSnapshot } from "./observation.js";
|
|
13
|
+
import { traceAsync } from "./trace.js";
|
|
14
|
+
|
|
15
|
+
async function readInventoryItem(root, idea) {
|
|
16
|
+
const documentPath = idea.worlds.idealRevision.documentPath;
|
|
17
|
+
try {
|
|
18
|
+
const source = await readFile(
|
|
19
|
+
resolve(root, ...documentPath.split("/")),
|
|
20
|
+
"utf8",
|
|
21
|
+
);
|
|
22
|
+
return ideaInventoryItem(idea, extractIdeaTitle(source));
|
|
23
|
+
} catch (caught) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Cannot read idea metadata for ${idea.id} at ${documentPath}: ${caught.message}`,
|
|
26
|
+
{ cause: caught },
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function metadataFailureObservation(observed, caught) {
|
|
32
|
+
const { ideas: _ideas, ...base } = observed.observation;
|
|
33
|
+
const problem = {
|
|
34
|
+
type: "idea-metadata-unavailable",
|
|
35
|
+
summary: localize(
|
|
36
|
+
observed.outputLanguage,
|
|
37
|
+
caught.message,
|
|
38
|
+
`无法读取 idea metadata:${caught.message}`,
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
observation: {
|
|
43
|
+
...base,
|
|
44
|
+
state: "project-setup-required",
|
|
45
|
+
observedThrough: "configuration",
|
|
46
|
+
problems: [problem],
|
|
47
|
+
},
|
|
48
|
+
responseContext: {
|
|
49
|
+
nextSteps: localize(
|
|
50
|
+
observed.outputLanguage,
|
|
51
|
+
"Repair the reported idea document and retry `silvermoon list-ideas`.",
|
|
52
|
+
"修复报告的 idea document 后,重新运行 `silvermoon list-ideas`。",
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function listIdeas({
|
|
59
|
+
all,
|
|
60
|
+
createdBefore,
|
|
61
|
+
createdSince,
|
|
62
|
+
limit,
|
|
63
|
+
query,
|
|
64
|
+
root = process.cwd(),
|
|
65
|
+
sort,
|
|
66
|
+
states,
|
|
67
|
+
userHome,
|
|
68
|
+
} = {}) {
|
|
69
|
+
const normalizedQuery = normalizeIdeaQuery({
|
|
70
|
+
all,
|
|
71
|
+
createdBefore,
|
|
72
|
+
createdSince,
|
|
73
|
+
limit,
|
|
74
|
+
query,
|
|
75
|
+
sort,
|
|
76
|
+
states,
|
|
77
|
+
});
|
|
78
|
+
const intention = {
|
|
79
|
+
command: "list-ideas",
|
|
80
|
+
args: normalizedQuery,
|
|
81
|
+
};
|
|
82
|
+
const runtime = createCommandRun(intention);
|
|
83
|
+
const observed = await observeSnapshot({
|
|
84
|
+
root,
|
|
85
|
+
userHome,
|
|
86
|
+
version: { type: "worktree" },
|
|
87
|
+
});
|
|
88
|
+
if (!observed.projectReady) {
|
|
89
|
+
return runtime.complete(
|
|
90
|
+
observed.observation,
|
|
91
|
+
{ nextSteps: observed.findings.map(({ instruction }) => instruction) },
|
|
92
|
+
{ factType: "ideas.inventory-unavailable" },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let items;
|
|
97
|
+
try {
|
|
98
|
+
items = await traceAsync(
|
|
99
|
+
"ideas.inventory-metadata",
|
|
100
|
+
{ ideaCount: observed.layout.ideas.length },
|
|
101
|
+
() => Promise.all(
|
|
102
|
+
observed.layout.ideas.map((idea) =>
|
|
103
|
+
readInventoryItem(observed.observation.root, idea)
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
} catch (caught) {
|
|
108
|
+
const failure = metadataFailureObservation(observed, caught);
|
|
109
|
+
return runtime.complete(
|
|
110
|
+
failure.observation,
|
|
111
|
+
failure.responseContext,
|
|
112
|
+
{ factType: "ideas.inventory-unavailable" },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const inventory = queryIdeaInventory(items, normalizedQuery);
|
|
117
|
+
const {
|
|
118
|
+
configuration,
|
|
119
|
+
outputLanguage,
|
|
120
|
+
root: repositoryRoot,
|
|
121
|
+
version,
|
|
122
|
+
} = observed.observation;
|
|
123
|
+
return runtime.complete({
|
|
124
|
+
state: "ideas-listed",
|
|
125
|
+
root: repositoryRoot,
|
|
126
|
+
version,
|
|
127
|
+
configuration,
|
|
128
|
+
outputLanguage,
|
|
129
|
+
problems: [],
|
|
130
|
+
summary: inventory.summary,
|
|
131
|
+
ideas: inventory.ideas,
|
|
132
|
+
}, {}, { factType: "ideas.listed" });
|
|
133
|
+
}
|