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.
- package/README.md +168 -0
- package/README.zh-CN.md +19 -4
- package/docs/core-concepts.md +14 -12
- package/docs/getting-started.md +53 -27
- package/docs/maintaining.md +11 -2
- package/docs/operations.md +77 -30
- package/docs/reference.md +197 -37
- package/docs/repository-tasks.md +25 -13
- package/package.json +2 -2
- package/schema/v1/idea-status.schema.json +3 -3
- package/skills/silvermoon/SKILL.md +142 -187
- package/skills/silvermoon/references/adoption.md +34 -20
- package/src/adoption.js +136 -260
- package/src/cli.js +86 -113
- package/src/create-idea.js +222 -110
- package/src/dialogue.js +303 -0
- package/src/git.js +172 -97
- package/src/idea-layout.js +59 -123
- package/src/index.js +203 -170
- package/src/observation.js +305 -0
- package/src/trace.js +171 -0
- package/src/whatsnext.js +529 -378
package/src/idea-layout.js
CHANGED
|
@@ -2,15 +2,9 @@ import { lstat, readFile, readdir } from "node:fs/promises";
|
|
|
2
2
|
import { relative, resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { deriveIdeaState, isValidUlid, parseIdeaStatus } from "./ideas.js";
|
|
5
|
-
import {
|
|
5
|
+
import { inspectTreePaths, worktreeSnapshot } from "./git.js";
|
|
6
6
|
import { IDEAS_ROOT, ideaPaths } from "./layout.js";
|
|
7
7
|
|
|
8
|
-
const REVISION_BINDINGS = {
|
|
9
|
-
approvedRevision: "idealRevision",
|
|
10
|
-
implementationAcceptedRevision: "implementationRevision",
|
|
11
|
-
deploymentAcceptedRevision: "deploymentRevision",
|
|
12
|
-
};
|
|
13
|
-
|
|
14
8
|
function displayPath(root, path) {
|
|
15
9
|
return relative(root, path).replaceAll("\\", "/");
|
|
16
10
|
}
|
|
@@ -28,91 +22,6 @@ async function metadata(path) {
|
|
|
28
22
|
}
|
|
29
23
|
}
|
|
30
24
|
|
|
31
|
-
function validateRevisionObjects(root, statusPath, status, diagnostics) {
|
|
32
|
-
for (const key of Object.keys(REVISION_BINDINGS)) {
|
|
33
|
-
if (!status[key]) continue;
|
|
34
|
-
const type = runGit(root, ["cat-file", "-t", status[key]]);
|
|
35
|
-
if (!type.ok || type.stdout !== "tree") {
|
|
36
|
-
diagnostics.push(error(
|
|
37
|
-
"idea.revision.invalid-object",
|
|
38
|
-
`${statusPath}#${key}`,
|
|
39
|
-
`${key} must resolve to a Git tree object: ${status[key]}`,
|
|
40
|
-
"Use the current corresponding world tree object ID from Git.",
|
|
41
|
-
));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function validateRevisionHistory(root, commit, idea, diagnostics) {
|
|
47
|
-
for (const [statusKey, revisionKey] of Object.entries(REVISION_BINDINGS)) {
|
|
48
|
-
const revision = idea.status[statusKey];
|
|
49
|
-
if (!revision) continue;
|
|
50
|
-
const history = runGit(root, [
|
|
51
|
-
"log",
|
|
52
|
-
"--reverse",
|
|
53
|
-
"--format=%H",
|
|
54
|
-
"-S",
|
|
55
|
-
`${statusKey}: ${revision}`,
|
|
56
|
-
commit,
|
|
57
|
-
"--",
|
|
58
|
-
idea.statusPath,
|
|
59
|
-
]);
|
|
60
|
-
const evidenceCommit = history.ok
|
|
61
|
-
? history.stdout.split(/\r?\n/).filter(Boolean)[0]
|
|
62
|
-
: null;
|
|
63
|
-
if (!evidenceCommit) {
|
|
64
|
-
const shallow = runGit(root, ["rev-parse", "--is-shallow-repository"]);
|
|
65
|
-
diagnostics.push(error(
|
|
66
|
-
shallow.ok && shallow.stdout === "true"
|
|
67
|
-
? "history.incomplete"
|
|
68
|
-
: "idea.revision.history-missing",
|
|
69
|
-
`${idea.statusPath}#${statusKey}`,
|
|
70
|
-
`Primary history does not prove when ${statusKey} recorded ${revision}.`,
|
|
71
|
-
"Fetch complete primary history or repair the decision through a forward commit.",
|
|
72
|
-
));
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
const tree = runGit(root, ["rev-parse", `${evidenceCommit}:${idea.worlds[revisionKey].path}`]);
|
|
76
|
-
if (!tree.ok || tree.stdout !== revision) {
|
|
77
|
-
diagnostics.push(error(
|
|
78
|
-
"idea.revision.history-mismatch",
|
|
79
|
-
`${idea.statusPath}#${statusKey}`,
|
|
80
|
-
`${statusKey} was not bound to its world tree in commit ${evidenceCommit}.`,
|
|
81
|
-
"Record the decision only for the corresponding world tree in the same candidate commit.",
|
|
82
|
-
));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function validateCandidateRevisions(root, baseRevision, idea, diagnostics, objectIdLength) {
|
|
88
|
-
let previous = {};
|
|
89
|
-
if (baseRevision) {
|
|
90
|
-
const previousSource = runGit(root, ["show", `${baseRevision}:${idea.statusPath}`]);
|
|
91
|
-
if (previousSource.ok) {
|
|
92
|
-
try {
|
|
93
|
-
previous = parseIdeaStatus(previousSource.stdout + "\n", { objectIdLength });
|
|
94
|
-
} catch {
|
|
95
|
-
previous = {};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
for (const [statusKey, revisionKey] of Object.entries(REVISION_BINDINGS)) {
|
|
100
|
-
if (
|
|
101
|
-
idea.status[statusKey] === previous[statusKey] ||
|
|
102
|
-
idea.status[statusKey] === undefined
|
|
103
|
-
) continue;
|
|
104
|
-
const expected = idea.revisions[revisionKey];
|
|
105
|
-
if (idea.status[statusKey] !== expected) {
|
|
106
|
-
diagnostics.push(error(
|
|
107
|
-
"idea.revision.candidate-mismatch",
|
|
108
|
-
`${idea.statusPath}#${statusKey}`,
|
|
109
|
-
`${statusKey} must equal the corresponding world tree in the same candidate snapshot.`,
|
|
110
|
-
`Use ${expected} for ${revisionKey} or omit the stale mutation.`,
|
|
111
|
-
));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
25
|
async function requireDirectory(root, path, diagnostics) {
|
|
117
26
|
const absolute = resolve(root, path);
|
|
118
27
|
const value = await metadata(absolute);
|
|
@@ -184,23 +93,25 @@ async function rejectSymlinks(root, path, diagnostics) {
|
|
|
184
93
|
}
|
|
185
94
|
}
|
|
186
95
|
|
|
187
|
-
function
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
96
|
+
function objectIdLength(tree) {
|
|
97
|
+
if (/^[0-9a-f]{40}$/.test(tree)) return 40;
|
|
98
|
+
if (/^[0-9a-f]{64}$/.test(tree)) return 64;
|
|
99
|
+
throw new Error(`Unsupported Git tree object ID: ${tree}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function requiredTree(objects, path) {
|
|
103
|
+
const inspected = objects.get(path);
|
|
104
|
+
if (!inspected || inspected.type !== "tree" || inspected.object === null) {
|
|
105
|
+
throw new Error(`${path} does not resolve to a Git tree`);
|
|
192
106
|
}
|
|
193
|
-
return
|
|
107
|
+
return inspected.object;
|
|
194
108
|
}
|
|
195
109
|
|
|
196
110
|
export async function inspectIdeaLayout({
|
|
197
|
-
baseRevision,
|
|
198
111
|
config: _config,
|
|
199
|
-
historyCommit,
|
|
200
112
|
root,
|
|
201
113
|
gitRoot = root,
|
|
202
114
|
snapshotTree,
|
|
203
|
-
validateCandidate = false,
|
|
204
115
|
}) {
|
|
205
116
|
const diagnostics = [];
|
|
206
117
|
const ideasRoot = resolve(root, IDEAS_ROOT);
|
|
@@ -228,9 +139,12 @@ export async function inspectIdeaLayout({
|
|
|
228
139
|
};
|
|
229
140
|
}
|
|
230
141
|
|
|
231
|
-
let
|
|
142
|
+
let resolvedSnapshotTree;
|
|
143
|
+
let repositoryObjectIdLength;
|
|
232
144
|
try {
|
|
233
|
-
|
|
145
|
+
resolvedSnapshotTree = snapshotTree
|
|
146
|
+
?? worktreeSnapshot(gitRoot, { paths: [IDEAS_ROOT] }).tree;
|
|
147
|
+
repositoryObjectIdLength = objectIdLength(resolvedSnapshotTree);
|
|
234
148
|
} catch (caught) {
|
|
235
149
|
return {
|
|
236
150
|
diagnostics: [error(
|
|
@@ -246,7 +160,30 @@ export async function inspectIdeaLayout({
|
|
|
246
160
|
const aliases = new Map();
|
|
247
161
|
const ideas = [];
|
|
248
162
|
const caseNames = new Map();
|
|
249
|
-
|
|
163
|
+
const entries = (await readdir(ideasRoot, { withFileTypes: true }))
|
|
164
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
165
|
+
const ideaIds = new Set(
|
|
166
|
+
entries.filter((entry) => isValidUlid(entry.name)).map((entry) => entry.name),
|
|
167
|
+
);
|
|
168
|
+
const worldPaths = [...ideaIds].flatMap((id) => {
|
|
169
|
+
const paths = ideaPaths(id);
|
|
170
|
+
return [paths.idealPath, paths.innerPath, paths.outerPath];
|
|
171
|
+
});
|
|
172
|
+
let worldObjects;
|
|
173
|
+
try {
|
|
174
|
+
worldObjects = inspectTreePaths(gitRoot, resolvedSnapshotTree, worldPaths);
|
|
175
|
+
} catch (caught) {
|
|
176
|
+
return {
|
|
177
|
+
diagnostics: [error(
|
|
178
|
+
"idea.revision.unavailable",
|
|
179
|
+
IDEAS_ROOT,
|
|
180
|
+
caught.message,
|
|
181
|
+
"Ensure every world can be represented as a Git tree.",
|
|
182
|
+
)],
|
|
183
|
+
ideas: [],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
for (const entry of entries) {
|
|
250
187
|
const folderPath = resolve(ideasRoot, entry.name);
|
|
251
188
|
const folderMetadata = await lstat(folderPath);
|
|
252
189
|
const relativePath = displayPath(root, folderPath);
|
|
@@ -282,7 +219,9 @@ export async function inspectIdeaLayout({
|
|
|
282
219
|
}
|
|
283
220
|
|
|
284
221
|
const paths = ideaPaths(entry.name);
|
|
285
|
-
|
|
222
|
+
const children = (await readdir(folderPath, { withFileTypes: true }))
|
|
223
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
224
|
+
for (const child of children) {
|
|
286
225
|
if (
|
|
287
226
|
child.name === "status.yaml" ||
|
|
288
227
|
child.name === "ledger.md" ||
|
|
@@ -331,7 +270,7 @@ export async function inspectIdeaLayout({
|
|
|
331
270
|
try {
|
|
332
271
|
status = parseIdeaStatus(
|
|
333
272
|
await readFile(resolve(root, paths.statusPath), "utf8"),
|
|
334
|
-
{ objectIdLength },
|
|
273
|
+
{ objectIdLength: repositoryObjectIdLength },
|
|
335
274
|
);
|
|
336
275
|
} catch (caught) {
|
|
337
276
|
diagnostics.push(error(
|
|
@@ -351,6 +290,14 @@ export async function inspectIdeaLayout({
|
|
|
351
290
|
));
|
|
352
291
|
}
|
|
353
292
|
if (status.alias !== undefined) {
|
|
293
|
+
if (ideaIds.has(status.alias)) {
|
|
294
|
+
diagnostics.push(error(
|
|
295
|
+
"idea.alias.id-collision",
|
|
296
|
+
`${paths.statusPath}#alias`,
|
|
297
|
+
`Alias ${status.alias} collides with an idea ULID.`,
|
|
298
|
+
"Choose an alias that is distinct from every idea ULID.",
|
|
299
|
+
));
|
|
300
|
+
}
|
|
354
301
|
const aliasOwner = aliases.get(status.alias);
|
|
355
302
|
if (aliasOwner) {
|
|
356
303
|
diagnostics.push(error(
|
|
@@ -374,15 +321,9 @@ export async function inspectIdeaLayout({
|
|
|
374
321
|
let revisions;
|
|
375
322
|
try {
|
|
376
323
|
revisions = {
|
|
377
|
-
idealRevision:
|
|
378
|
-
implementationRevision:
|
|
379
|
-
|
|
380
|
-
paths.innerPath,
|
|
381
|
-
),
|
|
382
|
-
deploymentRevision: resolveTree(
|
|
383
|
-
{ gitRoot, root, snapshotTree },
|
|
384
|
-
paths.outerPath,
|
|
385
|
-
),
|
|
324
|
+
idealRevision: requiredTree(worldObjects, paths.idealPath),
|
|
325
|
+
implementationRevision: requiredTree(worldObjects, paths.innerPath),
|
|
326
|
+
deploymentRevision: requiredTree(worldObjects, paths.outerPath),
|
|
386
327
|
};
|
|
387
328
|
} catch (caught) {
|
|
388
329
|
diagnostics.push(error(
|
|
@@ -394,23 +335,22 @@ export async function inspectIdeaLayout({
|
|
|
394
335
|
continue;
|
|
395
336
|
}
|
|
396
337
|
|
|
397
|
-
validateRevisionObjects(gitRoot, paths.statusPath, status, diagnostics);
|
|
398
338
|
const worlds = {
|
|
399
339
|
idealRevision: {
|
|
400
340
|
name: "Ideal World",
|
|
401
|
-
displayName: "
|
|
341
|
+
displayName: "理想世界",
|
|
402
342
|
path: paths.idealPath,
|
|
403
343
|
documentPath: paths.ideaDocumentPath,
|
|
404
344
|
},
|
|
405
345
|
implementationRevision: {
|
|
406
346
|
name: "Inner World",
|
|
407
|
-
displayName: "
|
|
347
|
+
displayName: "主体世界",
|
|
408
348
|
path: paths.innerPath,
|
|
409
349
|
documentPath: paths.implementationDocumentPath,
|
|
410
350
|
},
|
|
411
351
|
deploymentRevision: {
|
|
412
352
|
name: "Outer World",
|
|
413
|
-
displayName: "
|
|
353
|
+
displayName: "现实世界",
|
|
414
354
|
path: paths.outerPath,
|
|
415
355
|
documentPath: paths.deploymentDocumentPath,
|
|
416
356
|
},
|
|
@@ -428,10 +368,6 @@ export async function inspectIdeaLayout({
|
|
|
428
368
|
worlds,
|
|
429
369
|
};
|
|
430
370
|
if (status.alias !== undefined) idea.alias = status.alias;
|
|
431
|
-
if (validateCandidate) {
|
|
432
|
-
validateCandidateRevisions(gitRoot, baseRevision, idea, diagnostics, objectIdLength);
|
|
433
|
-
}
|
|
434
|
-
if (historyCommit) validateRevisionHistory(gitRoot, historyCommit, idea, diagnostics);
|
|
435
371
|
ideas.push(idea);
|
|
436
372
|
}
|
|
437
373
|
|