silvermoon 0.2.1 → 0.3.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/src/index.js CHANGED
@@ -1,20 +1,20 @@
1
1
  import { resolve } from "node:path";
2
2
 
3
+ import { REPOSITORY_SKILL_PATH } from "./adoption.js";
3
4
  import {
4
5
  diagnosticProblem,
5
6
  } from "./dialogue.js";
6
- import { loadConfig } from "./config.js";
7
+ import { loadConfigSnapshot } from "./config.js";
7
8
  import { createCommandRun } from "./domain.js";
8
9
  import {
9
10
  fetchPrimary,
10
11
  indexSnapshot,
11
- resolveCommit,
12
+ resolveSnapshot,
12
13
  runGit,
13
14
  sanitizeGitMessage,
14
15
  worktreeSnapshot,
15
- withTemporaryTree,
16
- withTemporaryWorktree,
17
16
  } from "./git.js";
17
+ import { createGitSnapshotFileSystem } from "./git-snapshot.js";
18
18
  import {
19
19
  canonicalizeOutputLanguage,
20
20
  DEFAULT_LANGUAGE,
@@ -23,6 +23,24 @@ import {
23
23
  observeSnapshot,
24
24
  unavailableObservation,
25
25
  } from "./observation.js";
26
+ import {
27
+ CONFIG_PATH,
28
+ GUIDANCE_ROOT,
29
+ IDEAS_ROOT,
30
+ } from "./layout.js";
31
+
32
+ function preloadCheckBlob({ name }) {
33
+ return (
34
+ name === CONFIG_PATH
35
+ || name === "package.json"
36
+ || name.startsWith(`${REPOSITORY_SKILL_PATH}/`)
37
+ || name.startsWith(`${GUIDANCE_ROOT}/`)
38
+ || (
39
+ name.startsWith(`${IDEAS_ROOT}/`)
40
+ && name.endsWith("/status.yaml")
41
+ )
42
+ );
43
+ }
26
44
 
27
45
  function targetArgument({ commit, remote, staged, worktree }) {
28
46
  if (remote) return { type: "remote" };
@@ -59,18 +77,23 @@ async function inspectTree({
59
77
  userHome,
60
78
  version,
61
79
  }) {
62
- return withTemporaryTree(gitRoot, tree, (contentRoot) =>
63
- observeSnapshot({
64
- contentRoot,
65
- gitRoot,
66
- outputLanguage,
67
- root,
68
- projectOnly: true,
69
- snapshotTree: tree,
70
- userHome,
71
- version,
72
- })
73
- );
80
+ const filesystem = createGitSnapshotFileSystem({
81
+ gitRoot,
82
+ preload: preloadCheckBlob,
83
+ root,
84
+ tree,
85
+ });
86
+ return observeSnapshot({
87
+ contentRoot: root,
88
+ filesystem,
89
+ gitRoot,
90
+ outputLanguage,
91
+ root,
92
+ projectOnly: true,
93
+ snapshotTree: tree,
94
+ userHome,
95
+ version,
96
+ });
74
97
  }
75
98
 
76
99
  function finishCheck(runtime, observed) {
@@ -133,7 +156,7 @@ export async function checkRepository({
133
156
  if (remote) {
134
157
  let head;
135
158
  try {
136
- head = resolveCommit(repositoryRoot, "HEAD");
159
+ head = resolveSnapshot(repositoryRoot, "HEAD");
137
160
  } catch (caught) {
138
161
  return failureReport({
139
162
  runtime,
@@ -148,11 +171,10 @@ export async function checkRepository({
148
171
  }
149
172
  let bootstrap;
150
173
  try {
151
- bootstrap = await withTemporaryWorktree(
152
- repositoryRoot,
153
- head,
154
- (contentRoot) => loadConfig({ root: contentRoot }),
155
- );
174
+ bootstrap = await loadConfigSnapshot({
175
+ gitRoot: repositoryRoot,
176
+ tree: head.tree,
177
+ });
156
178
  } catch (caught) {
157
179
  return failureReport({
158
180
  runtime,
@@ -205,20 +227,15 @@ export async function checkRepository({
205
227
  }
206
228
  const primary = fetched.result.commit;
207
229
  try {
208
- const observed = await withTemporaryWorktree(
209
- repositoryRoot,
210
- primary,
211
- (contentRoot, tree) => observeSnapshot({
212
- contentRoot,
213
- gitRoot: repositoryRoot,
214
- outputLanguage: canonicalLanguage,
215
- root: repositoryRoot,
216
- projectOnly: true,
217
- snapshotTree: tree,
218
- userHome,
219
- version: { type: "remote", commit: primary },
220
- }),
221
- );
230
+ const snapshot = resolveSnapshot(repositoryRoot, primary);
231
+ const observed = await inspectTree({
232
+ gitRoot: repositoryRoot,
233
+ outputLanguage: canonicalLanguage,
234
+ root: repositoryRoot,
235
+ tree: snapshot.tree,
236
+ userHome,
237
+ version: { type: "remote", commit: primary },
238
+ });
222
239
  return finishCheck(runtime, observed);
223
240
  } catch (caught) {
224
241
  return failureReport({
@@ -236,9 +253,9 @@ export async function checkRepository({
236
253
 
237
254
  if (commit !== undefined || (!staged && !worktree)) {
238
255
  const revision = commit ?? "HEAD";
239
- let resolvedCommit;
256
+ let snapshot;
240
257
  try {
241
- resolvedCommit = resolveCommit(repositoryRoot, revision);
258
+ snapshot = resolveSnapshot(repositoryRoot, revision);
242
259
  } catch (caught) {
243
260
  return failureReport({
244
261
  runtime,
@@ -252,20 +269,14 @@ export async function checkRepository({
252
269
  });
253
270
  }
254
271
  try {
255
- const observed = await withTemporaryWorktree(
256
- repositoryRoot,
257
- resolvedCommit,
258
- (contentRoot, tree) => observeSnapshot({
259
- contentRoot,
260
- gitRoot: repositoryRoot,
261
- outputLanguage: canonicalLanguage,
262
- root: repositoryRoot,
263
- projectOnly: true,
264
- snapshotTree: tree,
265
- userHome,
266
- version: { type: "commit", commit: resolvedCommit },
267
- }),
268
- );
272
+ const observed = await inspectTree({
273
+ gitRoot: repositoryRoot,
274
+ outputLanguage: canonicalLanguage,
275
+ root: repositoryRoot,
276
+ tree: snapshot.tree,
277
+ userHome,
278
+ version: { type: "commit", commit: snapshot.commit },
279
+ });
269
280
  return finishCheck(runtime, observed);
270
281
  } catch (caught) {
271
282
  return failureReport({
@@ -276,7 +287,7 @@ export async function checkRepository({
276
287
  summary: sanitizeGitMessage(caught.message),
277
288
  },
278
289
  root: repositoryRoot,
279
- version: { type: "commit", commit: resolvedCommit },
290
+ version: { type: "commit", commit: snapshot.commit },
280
291
  });
281
292
  }
282
293
  }
package/src/language.js CHANGED
@@ -40,6 +40,10 @@ export function canonicalizeOutputLanguage(value) {
40
40
  caught.exitCode = 2;
41
41
  throw caught;
42
42
  }
43
+ canonical = {
44
+ en: "en-US",
45
+ zh: "zh-CN",
46
+ }[canonical.toLowerCase()] ?? canonical;
43
47
  if (!OUTPUT_LANGUAGE_SET.has(canonical)) {
44
48
  const error = new Error(
45
49
  `unsupported output language: ${canonical}; expected ${OUTPUT_LANGUAGES.join(" or ")}`,
package/src/list-ideas.js CHANGED
@@ -1,71 +1,129 @@
1
- import { readFile } from "node:fs/promises";
2
- import { resolve } from "node:path";
3
-
4
1
  import { createCommandRun } from "./domain.js";
5
2
  import {
6
- extractIdeaTitle,
7
3
  ideaInventoryItem,
8
4
  normalizeIdeaQuery,
9
5
  queryIdeaInventory,
10
6
  } from "./idea-query.js";
11
- import { localize } from "./language.js";
7
+ import {
8
+ metadataFailureObservation,
9
+ readIdeaInventoryItem,
10
+ } from "./idea-metadata.js";
11
+ import { canonicalizeOutputLanguage } from "./language.js";
12
12
  import { observeSnapshot } from "./observation.js";
13
13
  import { traceAsync } from "./trace.js";
14
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
- }
15
+ function withoutTitleQuery(query, { limit = query.limit } = {}) {
16
+ return { ...query, limit, query: null };
29
17
  }
30
18
 
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],
19
+ function matchesIdentity(item, needle) {
20
+ return [item.id, item.alias]
21
+ .filter((value) => value !== undefined)
22
+ .some((value) => value.toLowerCase().includes(needle));
23
+ }
24
+
25
+ async function queryObservedInventory({
26
+ ideas,
27
+ metadataReader,
28
+ normalizedQuery,
29
+ root,
30
+ }) {
31
+ const ideasById = new Map(ideas.map((idea) => [idea.id, idea]));
32
+ const baseItems = ideas.map((idea) => ideaInventoryItem(idea));
33
+ const candidates = queryIdeaInventory(
34
+ baseItems,
35
+ withoutTitleQuery(normalizedQuery, { limit: null }),
36
+ ).ideas;
37
+
38
+ return traceAsync(
39
+ "ideas.inventory-metadata",
40
+ {
41
+ candidateCount: candidates.length,
42
+ ideaCount: ideas.length,
47
43
  },
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
- ),
44
+ async () => {
45
+ const loadedItems = new Map();
46
+ let queryCandidateCount = 0;
47
+ let titleReadCount = 0;
48
+
49
+ const readItems = async (items) => {
50
+ const unread = items.filter(({ id }) => !loadedItems.has(id));
51
+ titleReadCount += unread.length;
52
+ const loaded = await Promise.all(unread.map(({ id }) => {
53
+ const idea = ideasById.get(id);
54
+ if (idea === undefined) {
55
+ throw new Error(`Missing observed idea metadata for ${id}`);
56
+ }
57
+ return metadataReader(root, idea);
58
+ }));
59
+ for (const item of loaded) loadedItems.set(item.id, item);
60
+ };
61
+
62
+ let matchingItems = candidates;
63
+ if (normalizedQuery.query !== null) {
64
+ const needle = normalizedQuery.query.toLowerCase();
65
+ const directMatchIds = new Set(
66
+ candidates
67
+ .filter((item) => matchesIdentity(item, needle))
68
+ .map(({ id }) => id),
69
+ );
70
+ const titleCandidates = candidates.filter(
71
+ ({ id }) => !directMatchIds.has(id),
72
+ );
73
+ queryCandidateCount = titleCandidates.length;
74
+ await readItems(titleCandidates);
75
+ matchingItems = candidates
76
+ .filter(({ id }) =>
77
+ directMatchIds.has(id)
78
+ || loadedItems.get(id)?.title?.toLowerCase().includes(needle)
79
+ )
80
+ .map((item) => loadedItems.get(item.id) ?? item);
81
+ }
82
+
83
+ const selected = queryIdeaInventory(
84
+ matchingItems,
85
+ withoutTitleQuery(normalizedQuery),
86
+ );
87
+ await readItems(selected.ideas);
88
+ const inventory = {
89
+ summary: selected.summary,
90
+ ideas: selected.ideas.map(({ id }) => {
91
+ const item = loadedItems.get(id);
92
+ if (item === undefined) {
93
+ throw new Error(`Missing selected idea metadata for ${id}`);
94
+ }
95
+ return item;
96
+ }),
97
+ };
98
+ return { inventory, queryCandidateCount, titleReadCount };
54
99
  },
55
- };
100
+ ({ inventory, queryCandidateCount, titleReadCount }) => ({
101
+ attributes: {
102
+ matchedCount: inventory.summary.matched,
103
+ queryCandidateCount,
104
+ returnedCount: inventory.summary.returned,
105
+ titleReadCount,
106
+ },
107
+ }),
108
+ );
56
109
  }
57
110
 
58
111
  export async function listIdeas({
59
112
  all,
60
113
  createdBefore,
61
114
  createdSince,
115
+ language,
62
116
  limit,
117
+ metadataReader = readIdeaInventoryItem,
63
118
  query,
64
119
  root = process.cwd(),
65
120
  sort,
66
121
  states,
67
122
  userHome,
68
123
  } = {}) {
124
+ const canonicalLanguage = language === undefined
125
+ ? undefined
126
+ : canonicalizeOutputLanguage(language);
69
127
  const normalizedQuery = normalizeIdeaQuery({
70
128
  all,
71
129
  createdBefore,
@@ -77,11 +135,15 @@ export async function listIdeas({
77
135
  });
78
136
  const intention = {
79
137
  command: "list-ideas",
80
- args: normalizedQuery,
138
+ args: {
139
+ ...normalizedQuery,
140
+ language: canonicalLanguage ?? null,
141
+ },
81
142
  };
82
143
  const runtime = createCommandRun(intention);
83
144
  const observed = await observeSnapshot({
84
145
  root,
146
+ outputLanguage: canonicalLanguage,
85
147
  userHome,
86
148
  version: { type: "worktree" },
87
149
  });
@@ -93,19 +155,20 @@ export async function listIdeas({
93
155
  );
94
156
  }
95
157
 
96
- let items;
158
+ let queried;
97
159
  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
- );
160
+ queried = await queryObservedInventory({
161
+ ideas: observed.layout.ideas,
162
+ metadataReader,
163
+ normalizedQuery,
164
+ root: observed.observation.root,
165
+ });
107
166
  } catch (caught) {
108
- const failure = metadataFailureObservation(observed, caught);
167
+ const failure = metadataFailureObservation(
168
+ observed,
169
+ caught,
170
+ "silvermoon list-ideas",
171
+ );
109
172
  return runtime.complete(
110
173
  failure.observation,
111
174
  failure.responseContext,
@@ -113,7 +176,7 @@ export async function listIdeas({
113
176
  );
114
177
  }
115
178
 
116
- const inventory = queryIdeaInventory(items, normalizedQuery);
179
+ const { inventory } = queried;
117
180
  const {
118
181
  configuration,
119
182
  outputLanguage,
@@ -132,6 +132,7 @@ export function repositoryProblemObservation(observation, problems) {
132
132
  async function observeSnapshotInternal({
133
133
  allowMissingIdeas = false,
134
134
  contentRoot,
135
+ filesystem,
135
136
  gitRoot,
136
137
  ideaLanguage,
137
138
  outputLanguage: requestedOutputLanguage,
@@ -147,7 +148,12 @@ async function observeSnapshotInternal({
147
148
  const adoption = await traceAsync(
148
149
  "adoption.inspect",
149
150
  {},
150
- () => inspectAdoption({ contentRoot, root }),
151
+ () => inspectAdoption({
152
+ contentRoot,
153
+ filesystem,
154
+ repositoryRoot: gitRoot,
155
+ root,
156
+ }),
151
157
  );
152
158
  const user = await traceAsync(
153
159
  "user-config.load",
@@ -233,6 +239,7 @@ async function observeSnapshotInternal({
233
239
  {},
234
240
  () => inspectIdeaLayout({
235
241
  config: adoption.config,
242
+ filesystem,
236
243
  gitRoot: gitRoot ?? adoption.root,
237
244
  root: contentRoot ?? adoption.root,
238
245
  snapshotTree,
@@ -257,6 +264,7 @@ async function observeSnapshotInternal({
257
264
 
258
265
  const guidanceDiagnostics = projectOnly
259
266
  ? (await inspectAllGuidance({
267
+ filesystem,
260
268
  gitRoot: gitRoot ?? adoption.root,
261
269
  snapshotTree,
262
270
  })).diagnostics
package/src/repository.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { createHash } from "node:crypto";
2
- import { spawnSync } from "node:child_process";
3
2
 
4
3
  const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
5
4
  const ENCODED_SEPARATOR = /%(?:2f|5c)/i;
@@ -58,6 +57,7 @@ export function validBranchName(value) {
58
57
  typeof value !== "string" ||
59
58
  value.length === 0 ||
60
59
  value === "@" ||
60
+ value === "HEAD" ||
61
61
  value.startsWith("-") ||
62
62
  value.startsWith("refs/") ||
63
63
  value.startsWith("remotes/") ||
@@ -74,13 +74,7 @@ export function validBranchName(value) {
74
74
  const structurallyValid = value
75
75
  .split("/")
76
76
  .every((component) => component && !component.startsWith(".") && !component.endsWith(".lock"));
77
- if (!structurallyValid) return false;
78
-
79
- const checked = spawnSync("git", ["check-ref-format", "--branch", value], {
80
- encoding: "utf8",
81
- windowsHide: true,
82
- });
83
- return checked.status === 0;
77
+ return structurallyValid;
84
78
  }
85
79
 
86
80
  export function effectiveSourceRepository(config, record) {